FEATURED · 精选文章

Godot建造游戏开发:类型化字典、世界存档与右键移除实现

发布时间 / 2026/9/4 21:53:24
来源 / 创域科博编辑部
栏目 / 资讯中心
Godot建造游戏开发:类型化字典、世界存档与右键移除实现 在 Godot 的建造类游戏开发中“摆放方块”只是第一步真正让世界变得可玩的是三件事世界能被保存、数据能被高效管理、方块能被再次移除。这一篇是系列教程的 Ep 2.5 补充篇我把它定位成一个很实用的“功能缝合包”在已有的 Godot 项目中加入世界存档用类型化字典来管理方块数据同时实现一个鼠标右键移除方块的工具。无论你是正在做类似 Minecraft 的体素世界还是做了一个基于网格的建造小游戏这套流程都值得完整过一遍。即使暂时不太理解类型化字典的底层原理也能照做跑通然后在实战里慢慢体会它的好处。1. 背景与核心概念1.1 为什么需要“世界存档”很多新手在完成了“左键放置方块”之后会面临一个尴尬情况游戏看似能玩了但只要关闭窗口再打开所有玩家辛辛苦苦建造的内容全部消失。一个没有存档的游戏就像记事本不能保存文件一样只能算一个演示 Demo。世界存档的本质就是把游戏运行时内存中的状态转换成一种可以在磁盘上长期保存的数据格式。我们需要从中选出真正需要保存的内容。在体素或网格建造类游戏里世界状态的灵魂是每个格子上的方块类型。假设一个世界只有 3 个方块位置分别是(0,1,0)、(1,2,3)、(4,0,2)那么存档只需要回答三个问题有几个方块分别站在哪些网格坐标上每个坐标上的方块是什么类型把这些信息存到文件里游戏启动时再读取文件、重新创建方块节点世界就恢复了。1.2 类型化字典给字典加上“约束”Godot 的 GDScript 里有三种最常用的集合类数组、字典、信号。字典相当于一个“码表”左边是键右边是值例如键坐标Vector3i(1, 2, 3)值方块类型1由于每个方块都需要记录“位置 - 类型”的映射关系字典几乎是建造游戏里最合适的数据结构。而“类型化字典”是 Godot 4.x 提供的一种增强写法它在字典的基础上增加了类型的限制。普通字典就像一个“什么都能装的箱子”而类型化字典则像带隔断的收纳盒你事先规定了键必须是哪种类型、值必须是哪种类型放错了就会报错或无法通过编辑器的检查。用代码表示这就是一个类型化字典var blocks: Dictionary[Vector3i, int] {}它表示blocks的键必须是Vector3i类型值必须是int类型不能混淆。虽然看起来只是多了个尖括号但在项目变大后能减少大量低级错误。1.3 右键移除让对象可以修改一个只能放置不能移除的工具箱是不完整的。玩家摆错位置怎么办想改造建筑怎么办我们需要一个“拆除”操作。放在第一人称建造类游戏里最自然的交互就是鼠标左键放置方块鼠标右键移除方块。移除操作要做的其实也不复杂从相机视线方向发射一条射线。检测射线是否撞到了某个方块。如果撞到了就从世界中删除该方块节点并从字典数据里删掉对应记录。这里有一个容易被忽略的重点场景里的方块节点只是一层“表现”而真正决定世界状态的是内存中的字典数据。所以右键移除不只是把场景里的节点删掉还要同步删除字典中的记录。如果不删字典下次保存存档又会出现已经拆除的方块。1.4 一个简单的数据关系图为了便于理解我们可以用文字把整个体系的关系画出来世界容器 WorldController ├── 方块节点MeshInstance3D / StaticBody3D表示画面里的方块 └── 字典数据 blocks记录所有方块的坐标与类型作为数据真相 存放遍历字典 - 写成 JSON 文件 读取解析 JSON 文件 - 重新生成方块节点 移除射线击中方块 - 删除节点 - 删除字典里对应的键1.5 本集适合谁这篇教程适合刚刚看完 Godot 基础教程想要做一个完整建造类游戏的开发者。已经实现了“放置方块”功能但还不会做存档和移除功能的同学。想要理解 GDScript 字典和类型化字典区别的初学者。在阅读本文前建议先掌握 Godot 的基础场景结构、节点添加、简单的_ready()回调等知识。不需要完全精通射线检测我会在对应小节里把步骤拆开讲清楚。2. 环境准备与项目结构2.1 Godot 版本说明本文的代码以Godot 4.x的 GDScript 语法为主。类型化字典Dictionary[KeyType, ValueType]这类写法建议使用Godot 4.2 或更高版本。如果你的项目还在使用 Godot 3.x或者 4.0、4.1 等早期版本可以把类型化字典退化成普通字典代码核心逻辑不受影响。关于版本需要提醒一句不同小版本的 API 会有细微差异如果你的编辑器报错位置和本文不一致优先查看当前项目的 Godot 版本再对照官方文档确认对应函数名称。2.2 示例项目的场景结构为了使本教程可复现我假设你已经有一个最基础的“第一人称建造”项目场景树类似下面这样World (Node3D) ├── Player (CharacterBody3D) │ └── Camera3D ├── BlockContainer (Node3D) └── InteractionController (Node3D)在这个结构里World是整个游戏世界的根节点我建议把存档逻辑挂在这里。BlockContainer专门用来存放所有已经生成的方块节点方便统一管理。InteractionController负责监听鼠标事件并执行射线检测。如果你之前的项目结构不同也没有关系重点理解三个核心模块方块节点如何生成并挂载。方块数据如何记录。玩家交互如何调用删除。2.3 方块场景 Block.tscn为了让右键射线的碰撞检测正常工作方块场景需要由三部分组成StaticBody3D静态刚体提供碰撞供射线检测。MeshInstance3D可见的方块外观。CollisionShape3D碰撞盒形状。同时需要为根节点挂一个脚本方便存储坐标和类型 ID。下面是我在项目中使用的Block.gd# 文件路径res://scripts/Block.gd extends StaticBody3D class_name WorldBlock # 记录方块所在网格坐标 var grid_pos : Vector3i.ZERO # 记录方块类型 ID例如 1草方块2石块 var type_id : 0这里面的关键是把方块所在格子的坐标记录在节点身上。当射线碰撞到这个方块时我们可以直接通过block.grid_pos拿到坐标再用坐标去删除数据。3. 类型化字典从一个普通字典说起3.1 字典的基础用法在 GDScript 中字典用花括号创建var player_data : { name: Alice, hp: 100, level: 3 }通过键来取值print(player_data[name]) # 输出 Alice print(player_data.hp) # 输出 100字典也可以使用点语法修改或新增键值player_data[hp] 80 player_data[exp] 1200删除键player_data.erase(exp)检查键是否存在if player_data.has(name): print(存在 name 键)如果只是写小型工具脚本普通字典用起来很方便。但随着数据规模变大问题也开始暴露。3.2 普通字典的问题如果项目中用普通字典记录方块通常会这样写var blocks : {}之后向里面添加数据时可以这样放进去blocks[Vector3i(1, 2, 3)] 1看似没问题但如果某天手误blocks[1,2,3] 1 # 键是字符串不是 Vector3i不会立刻报错数据也能存进字典。但后续遍历时的处理逻辑会变得混乱因为键的类型不稳定有时是Vector3i有时是String很难排查。另外当你尝试用点语法拉取编辑器自动补全时普通字典就像一团“混沌”编辑器无法告诉你键的类型。由于 GDScript 是动态语言很多错误要等到运行时才暴露。3.3 类型化字典的声明与用法类型化字典的语法是这样的var blocks: Dictionary[Vector3i, int] {}一旦声明之后编辑器就会检查所有键必须是Vector3i所有值必须是int否则会提示类型不匹配。还可以声明更常见的类型# 字符串键 - 整数 var scores: Dictionary[String, int] {} scores[gold] 100 # 坐标键 - 节点对象 var block_nodes: Dictionary[Vector3i, StaticBody3D] {} block_nodes[Vector3i(0, 0, 0)] some_block_node # 字符串键 - 布尔值 var settings: Dictionary[String, bool] {} settings[audio_enabled] true类型化字典的优势主要有三个编译期给更多提示减少运行时报错。在编辑器中更容易自动补全。代码可读性更高一看声明就知道字典里存的是什么。有一点需要清楚类型化字典并不会带来非常明显的性能提升它的主要价值是让代码更严谨和可维护。在游戏开发里“能少一个隐藏 Bug”本身就很有意义。3.4 直接对类型化字典序列化是不行的在世界存档这个场景里我们最关心的问题是如何把Dictionary[Vector3i, int]这种结构保存到文件。看到这里你可能会想直接用JSON.stringify(blocks)不就行了让我们来看实际情况。JSON 格式有一个前提对象的键必须是字符串。但 GDScript 的字典键可以是任意类型比如:整数作为键Vector3i这种内置结构体作为键其他对象作为键当你尝试直接序列化时会遇到两类问题编辑器并不确定键能否被 JSON 正确转成字符串。即使能转成字符串读取的时候也很难恢复成原来的Vector3i。因此正确的做法是在存档写入前自行把Vector3i键转成约定的字符串格式在读取存档时再把字符串解析回Vector3i。3.5 一个可运行的字典小示例为了检验你的 Godot 环境是否支持类型化字典可以新建一个脚本在_ready()里写# 文件路径res://scripts/test_typed_dict.gd extends Node func _ready() - void: var dict: Dictionary[String, int] {} dict[apple] 3 dict[pear] 2 for key: String in dict: print(key, : , dict[key])运行后可以输出apple: 3 pear: 2注意for key: String in dict这种写法也用到了类型标注这是 Godot 4.x 给 for 循环提供的类型推断写法。4. 存档格式设计方块世界的“档案表”4.1 存档格式选择Godot 支持多种持久化方式包括ResourceSaver保存资源。ConfigFile保存配置类数据。FileAccess 自定义文本格式。FileAccess JSON 序列化。在 Godot 游戏开发中JSON 是一种很直观、容易检查和调试的格式。因此世界存档保存的路径通常选择user://下比如user://world_save.json。user://是 Godot 提供给项目专属数据目录的虚拟路径不同系统会自动映射到对应的用户数据目录。举个简单例子你可以在 Godot 输出面板中打印绝对路径print(ProjectSettings.globalize_path(user://))4.2 推荐存档结构因为 JSON 键必须是字符串所以我推荐使用x,y,z作为键方块类型 ID 作为值{ version: 1, blocks: { 0,1,0: 1, 1,2,3: 2, 4,0,2: 1 } }这个结构有两个优点可读性好能直接用文本编辑器查看。结构清晰方便以后扩展玩家位置、世界种子、日期等字段。其中version字段非常重要。随着游戏功能发展存档格式可能变化。如果将来增加了其他数据并根据版本号做迁移就能避免旧存档读取失败。4.3 为什么坐标要用字符串拼接而不是直接转成数组字符串Vector3i(1, 2, 3)如果直接调用str()在 Godot 4 里的输出形式类似(1, 2, 3)这种格式包含括号和逗号空格解析起来虽然也能做但容易受格式差异影响。自定义的1,2,3格式则更稳定、更容易编写解析代码。将位置转换为字符串的代码可以封装成一个辅助函数func _pos_to_key(pos: Vector3i) - String: return %d,%d,%d % [pos.x, pos.y, pos.z]反过来解析func _key_to_pos(key: String) - Vector3i: var parts : key.split(,) if parts.size() ! 3: return Vector3i.ZERO return Vector3i(int(parts[0]), int(parts[1]), int(parts[2]))5. 世界存档完整实现在这一节里我们开始写完整的存档逻辑。这个脚本可以挂在World根节点上内部持有blocks字典。5.1 WorldController 完整脚本为了避免逻辑混乱这里我把功能集中在一个管理器脚本中。实际项目中你可以继续拆分成多个类但初学阶段保持简单更容易理解。# 文件路径res://scripts/world_controller.gd extends Node3D class_name WorldController # 方块场景在 Inspector 中指定 export var block_scene: PackedScene # 方块父节点用来统一管理方块 export var block_container: Node3D # 核心数据位置 - 方块类型 ID var blocks: Dictionary[Vector3i, int] {} const SAVE_PATH : user://world_save.json const SAVE_VERSION : 1 func _ready() - void: if block_container null: block_container $BlockContainer # 如果存档存在就读取否则创建初始世界 if not load_world(): _create_default_world() # 在某个网格坐标上放置方块 func place_block(pos: Vector3i, type_id: int, save_now : true) - void: if blocks.has(pos): return if block_scene null: push_error(block_scene 未设置) var block: WorldBlock block_scene.instantiate() block_container.add_child(block) block.grid_pos pos block.type_id type_id block.position Vector3(pos) blocks[pos] type_id if save_now: save_world() # 从世界移除指定位置的方块 func remove_block(pos: Vector3i, save_now : true) - void: if not blocks.has(pos): return for child in block_container.get_children(): if child is WorldBlock and child.grid_pos pos: child.queue_free() break blocks.erase(pos) if save_now: save_world() # 清空世界中所有已生成的方块节点和数据 func _clear_all_blocks() - void: blocks.clear() for child in block_container.get_children(): child.queue_free() # 保存当前世界到存档文件 func save_world() - void: var save_data : { version: SAVE_VERSION, blocks: {} } for pos: Vector3i in blocks: var key : %d,%d,%d % [pos.x, pos.y, pos.z] save_data[blocks][key] blocks[pos] var file : FileAccess.open(SAVE_PATH, FileAccess.WRITE) if file null: push_error(无法打开存档文件: %s错误码: %d % [SAVE_PATH, FileAccess.get_open_error()]) return # 第二个参数使用 \t 可以格式化 JSON方便阅读 file.store_string(JSON.stringify(save_data, \t)) file.close() print(世界已保存, SAVE_PATH) # 从存档文件中读取世界 func load_world() - bool: if not FileAccess.file_exists(SAVE_PATH): return false var file : FileAccess.open(SAVE_PATH, FileAccess.READ) if file null: return false var content : file.get_as_text() file.close() var parsed JSON.parse_string(content) if not (parsed is Dictionary): push_error(存档解析失败文件内容不是合法 JSON) return false var save_data: Dictionary parsed # 读取存档前先清除当前场景中的所有方块 _clear_all_blocks() var block_data: Dictionary save_data.get(blocks, {}) for key: String in block_data: var parts : key.split(,) if parts.size() ! 3: continue var pos : Vector3i(int(parts[0]), int(parts[1]), int(parts[2])) var type_id : int(block_data[key]) # 从存档恢复时不再触发自动保存防止刚读档就写文件 place_block(pos, type_id, false) print(世界加载成功共恢复方块, blocks.size()) return true # 当没有存档时创建一个简单的出生点平台 func _create_default_world() - void: for x in range(-2, 3): for z in range(-2, 3): place_block(Vector3i(x, 0, z), 1, false) # 在中间放一块石头作为标记 place_block(Vector3i(0, 1, 0), 2, false) save_world()5.2 代码中几个关键设计点place_block和remove_block都带了一个save_now参数默认是true。这样普通鼠标操作会立即保存而在创建默认世界或加载存档时我们传false来避免频繁写磁盘。sava_world里的循环使用for pos: Vector3i in blocks依靠类型化字典的声明能保证键是Vector3i。遍历时把键转换成字符串var key : %d,%d,%d % [pos.x, pos.y, pos.z]读取存档时先调用_clear_all_blocks()清空内存字典和场景节点再通过place_block恢复方块。这样可以避免重复加载的情况比如按了手动读档后又把旧存档叠加到新世界上面。5.3 为什么要把位置转成 Vector3 赋值block.position Vector3(pos)是一个比较简洁的写法Vector3(Vector3i(1, 2, 3)) # 结果等价于 Vector3(1.0, 2.0, 3.0)场景节点位置使用浮点数而网格坐标是整数两者相乘的映射关系是 1 个格子等于 1 米。如果你的方块尺寸不是 1x1x1你需要自己定义坐标转换函数而不应该直接把整数坐标赋给 position。5.4 运行与测试存档创建好世界后运行项目。如果一开始没有任何存档_create_default_world()会生成一片平台和一个石块。当游戏关闭时数据已经保存在user://world_save.json。为了验证可以在 Godot 编辑器中关闭游戏再重新运行。此时_ready()中的load_world()会检测到存档并恢复方块。还可以在外部文本编辑器打开存档文件查看内容。如果你已经放置过许多方块文件内容像是这样{ version: 1, blocks: { -2,0,-2: 1, -2,0,-1: 1, -2,0,0: 1, 0,1,0: 2 } }6. 右键移除工具实现6.1 InteractionController 的定位我们单独写一个脚本来管理交互。这个脚本运行在InteractionController节点上它会监听鼠标输入发射射线然后调用WorldController的remove_block。# 文件路径res://scripts/interaction_controller.gd extends Node3D export var world: WorldController export var camera: Camera3D # 最大可拆除距离 export var max_reach : 10.0 # 用于在 UI 打开时暂时关闭交互 export var interaction_enabled : true func _unhandled_input(event: InputEvent) - void: if not interaction_enabled: return if event is InputEventMouseButton and event.button_index MOUSE_BUTTON_RIGHT and event.pressed: _remove_block_from_cursor() # 防止其他节点再次响应这个事件 get_viewport().set_input_as_handled() func _remove_block_from_cursor() - void: if world null: return if camera null: return var mouse_pos : get_viewport().get_mouse_position() var from : camera.project_ray_origin(mouse_pos) var to : from camera.project_ray_normal(mouse_pos) * max_reach var query : PhysicsRayQueryParameters3D.create(from, to) query.collide_with_bodies true query.collide_with_areas false var hit : get_world_3d().direct_space_state.intersect_ray(query) if hit.is_empty(): return var block : hit.get(collider) as WorldBlock if block null: return # 调用世界控制器删除方块默认会立即保存 world.remove_block(block.grid_pos)6.2 为什么监听 _unhandled_input 而不是 _input_input会在所有 Node 之前收到事件包括 UI 控件。在 Godot 里如果一个按钮消费了鼠标点击事件后续节点就不应该再响应它。使用_unhandled_input的时机要晚于 UI 处理如果 UI 控件已经消费了事件那么_unhandled_input不会再被调用。所以在“点击 Button 打开菜单”时菜单所在 UI 会先处理右键不会导致玩家站在菜单前还能直接拆除方块。需要提醒的是_unhandled_input的准确触发时机取决于控件是否标记了鼠标过滤器。如果你希望 UI 左侧弹出的面板不拦截右键也可以给控件设置mouse_filter MOUSE_FILTER_IGNORE但这不是本集重点。6.3 射线检测详解射线检测的核心原理是从相机位置向鼠标方向发射一条直线。这条直线与场景中的碰撞体发生碰撞。返回离相机最近的碰撞结果。project_ray_origin的作用是把鼠标的二维坐标转换成世界空间中的起始点也就是相机所在位置。project_ray_normal会返回一个方向向量从相机前方指向鼠标位置。from camera.project_ray_normal(mouse_pos) * max_reach表示射线终点。因为射线不能无限延伸我们限制最大拆除距离为 10。intersect_ray返回一个带有position、normal、collider等信息的字典。其中collider是我们关心的碰撞体对象也就是WorldBlock节点。这里有一个易错点block ! null的判断要注意如果射线打到了玩家或者其他碰撞体这时直接不处理。6.4 一个谨慎的处理坐标从节点身上读取你可能想过另一种做法在射线击中的位置hit.position上做取整然后删除对应坐标的方块。例如var hit_pos: Vector3 hit.position world.remove_block(Vector3i(hit_pos.floor()))这种方法原理上也行但容易出现边界偏差射线击中方块边缘时取整的结果可能落到方块侧面相邻的格子里导致误删旁边方块。相比之下从被击中节点身上读取grid_pos更可靠。这也是我们把坐标存在方块节点上的价值碰撞到哪个方块就删除哪个方块不存在位置换算偏差。6.5 与左键放置工具的融合如果你的项目中已经有了一个“左键放置方块”的脚本可以让它与新的右键移除工具放在同一个InteractionController中也可以拆成两个脚本。推荐最简洁的方式在同一个_unhandled_input中根据MOUSE_BUTTON_LEFT和MOUSE_BUTTON_RIGHT分流func _unhandled_input(event: InputEvent) - void: if not interaction_enabled: return if event is InputEventMouseButton and event.pressed: match event.button_index: MOUSE_BUTTON_LEFT: _place_block_from_cursor() get_viewport().set_input_as_handled() MOUSE_BUTTON_RIGHT: _remove_block_from_cursor() get_viewport().set_input_as_handled()左键放置通常还需要先计算射线碰撞到的格子位置然后偏移到相邻的空格这块内容比较复杂。在这篇文章中我们把重点放在右键移除和存档上。7. 完整验证流程7.1 场景连线在使用前如果你在InteractionController中通过export暴露了world和camera记得在编辑器 Inspector 面板里拖拽引用world拖到World根节点。camera拖到玩家身上的Camera3D。同样普通字典方块场景需要在WorldController的block_scene和block_container上做好引用。7.2 操作测试流程如下运行项目。如果第一次进入会在玩家脚下生成一个小平台。移动鼠标对准某个方块。按下鼠标右键方块消失。再次关闭游戏并运行会发现被拆除的方块没有恢复而其他方块依然存在。如果从外部打开存档文件也能看到对应坐标已经不在blocks列表中。7.3 预期运行结果在没有更多粒子特效的前提下控制台应当依次打印出类似内容世界加载成功共恢复方块42或者当你手动放置或删除方块时世界已保存user://world_save.json这些打印日志方便你确认存档是否被写入。8. 常见问题与排查思路在实际开发中新手经常会在存档或右键工具上遇到各种问题。我按照真实 Debug 经验整理了下面几种情况问题现象可能原因解决思路类型化字典语法报错项目使用的 Godot 版本低于 4.2改为普通字典var blocks : {}运行后世界是空的没有默认平台加载逻辑读到了旧的空存档删除user://world_save.json后重新运行右键点击没有反应没设置world或camera引用检查export变量是否在 Inspector 中赋值右键点击被 UI 拦截UI 控件消费了鼠标事件使用_unhandled_input或调整控件的mouse_filter删除方块后重新读档方块又出现删除后没有调用save_world确认remove_block默认的save_nowtrue射线总是打不到方块方块没有碰撞体为 Block 场景添加CollisionShape3D确认大小覆盖方块手工编辑 JSON 后读档失败JSON 格式错误或键不符合x,y,z用 JSON 校验工具检查格式并逐项对比约定格式存档恢复时方块重叠读档前没有清理旧方块加载开始时调用_clear_all_blocks()8.1 存档文件在哪个位置如果找不到world_save.json大概率是因为对user://路径没有概念。你可以在任意一个脚本中执行print(ProjectSettings.globalize_path(user://))打印出来的路径就是当前项目的专属用户目录不同系统在不同位置。8.2 JSON 被格式化后为什么解析出来顺序不同在 JSON 里对象的键顺序可能不会和写入时保持一致。因此在读档循环中不需要对顺序做任何假设只需要以键作为定位依据即可。8.3 一定要在每次删除时保存吗本教程为了方便演示让每次放置或删除都触发save_world()。这在方块数量少时完全没压力。但如果以后要做大世界频繁写整个存档会导致卡顿。工程上可以选择每 30 秒自动存档一次。按快捷键手动保存。玩家离开游戏时保存一次。放置或删除后设置一个“脏标记”批量保存。不要为了“保险”而盲目每帧调用写文件磁盘 IO 是很昂贵的操作。9. 最佳实践与工程建议9.1 数据与表现分离把整个世界的数据保存在blocks字典里而不是反过来通过遍历场景节点来还原世界状态。场景里的块节点只负责被看见、被碰撞、被删除。这样即使你重新设计了方块外观存档格式也可以保持不变同样存档丢失后场景也能随时重建。9.2 使用版本号保留迁移空间任何时候都建议给存档文件增加version字段。将来如果增加“箱子中的物品”“方块额外属性”等数据你可以根据版本号做不同的加载处理不需要让新代码兼容所有旧格式通过迁移脚本就可以解决。9.3 类型化字典是好习惯但别把它当银弹类型化字典能够约束类型但不代表数据内容一定正确。比如你记录了Vector3i(100, 100, 100)这个位置在世界上可能已经超出边界。所以游戏逻辑仍需要自己检查数值范围和坐标合法性。另外一个细节是GDScript 中字典的键必须具有可哈希性。普通整数、字符串、Vector3i都可以作为键但某些复杂对象不一定可以。如果遇到“无法把对象作为键”一类的错误优先考虑用字符串或者整数 ID 来代替。9.4 导出变量让场景配置更灵活在上面代码中block_scene、block_container、camera等都使用了export。让美术或策划同事直接在 Inspector 中拖拽而不是把路径写死在代码里可以提高协作效率。不过在初始化时如果没设置你要记得做空引用判断避免直接空指针。9.5 关于保存路径的安全意识当你把存档路径改成user://world_save.json时游戏本身运行在自己的专属目录下没有系统级写入风险。但在做任何涉及文件的操作前请记住三条底线不直接覆盖重要原文件必要时先备份。先写临时文件测试无误后再替换正式文件。在别人电脑上运行时不要假设磁盘可写或权限足够。9.6 后期可以继续完善的方向这篇文章实现的是一个完整的可运行功能但离正式商业项目还有一段距离。后续可以做这些扩展存档增加玩家位置、朝向、背包数据。使用二进制或压缩格式减少存档体积。添加手动读档存档界面。实现区域分块只保存被修改过的区块。将vector key改用整数编码以提升性能例如把(x,y,z)编码成一个整数。这些扩展并不是必须在第一版就全部写完但数据结构上留好了version和独立字典后面接起来会比较顺手。9.7 对新手补充一句如果你正在做一个弹幕游戏、横版过关或者 RPG本文中的很多技巧同样是通用的。字典是一种内存中的组织方式存档是一种持久化的组织方式输入交互是玩家与游戏世界沟通的入口。无论画面是 2D 还是 3D只要涉及“很多个对象需要被记录”你就能用这些思路改造出合适的解决方案。10. 收尾与下一步到这里我们已经完成了三件事世界可以被保存成 JSON 文件方块数据使用类型化字典管理玩家可以用鼠标右键把方块从场景和存档中同步移除。你不再需要在每次关闭 Godot 前拍照记录自己搭了什么因为存档会记住整个世界。下一步你可以继续给世界增加更多方块类型给不同类型的方块提供不同材质也可以把左键放置功能从“只能放床边砖”升级成有多种物品可以选择。当你在编辑器里体验到自己放上去、自己拆下来、存档还始终准确的时候就会发现一套清晰的数据管理方案比华丽的画面更能降低开发成本。如果你在实测中遇到了问题不要着急优先级最高的事情是确认三个引用链路场景节点引用是否正确方块碰撞体是否存在存档文件内容是否符合预期。想清楚“数据存在哪里、节点在哪里显示、事件从哪里触发”这一类功能以后再也不会成为你的拦路虎。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻