教程一 · 第 6 章:Resource 数据驱动、动态实例化、存档
前 5 章解决了”场景里有什么、怎么动、怎么通信”。本章解决两个工程问题:数据怎么组织(10 种武器、50 种敌人难道写 50 个类?)和进度怎么持久化(关掉游戏再打开还在)。
6.1 Resource:Godot 被低估的最强概念
Resource(资源)= 可序列化为文件、可在 Inspector 里编辑、可复用的数据对象。 纹理是 Resource、音频是 Resource、PackedScene(场景文件)也是 Resource。而你可以自定义 Resource 类型——这是 Godot 数据驱动开发的核心。
自定义一个武器数据
scripts/resources/WeaponData.cs:
using Godot;
[GlobalClass] // 关键:让 Godot 认识这个类型,Inspector 里才能创建和选择public partial class WeaponData : Resource{ [Export] public string WeaponName = "New Weapon"; [Export] public int Damage = 10;[Export] public float FireInterval = 0.3f; [Export] public float BulletSpeed = 800f; [Export] public Texture2D Icon = null!; [Export] public PackedScene BulletScene = null!;}在文件系统面板右键 → 新建资源 → 选 WeaponData,存成 weapons/pistol.tres;再建 shotgun.tres、rifle.tres。双击每个 .tres,在检查器里填不同数值。没有新类,没有 if-else,只有数据资产。
.tres是文本格式资源(Git 可 diff,开发期用);.res是二进制(发布后体积小)。场景文件同理:.tscn是文本。
使用它
public partial class Weapon : Node2D{ [Export] public WeaponData Data = null!; // 拖入 pistol.tres private float _cooldown;
public override void _Process(double delta) { _cooldown -= (float)delta; if (Input.IsActionPressed("fire") && _cooldown <= 0f) { Fire(); _cooldown = Data.FireInterval; } }
private void Fire() { var bullet = Data.BulletScene.Instantiate<Bullet>(); bullet.GlobalPosition = GlobalPosition; bullet.Setup(Vector2.Right, Data.BulletSpeed, Data.Damage); GetTree().CurrentScene.AddChild(bullet); // 加进当前场景,而不是武器自己 }}换武器 = 换一个拖入的 .tres 文件。做 50 种敌人、20 个关卡配置,全是这个套路。逻辑写一遍,数据做成资产——这是新手脚本堆砌和正经项目之间最大的思维差距。
Resource 的共享语义(也是坑)
Resource 默认按引用共享:100 个敌人引用同一个 enemy_stats.tres,运行时改一份,全部变。两种利用方式:
- 想让所有敌人同时狂暴?改共享 Resource,天然”全局数据”
- 想要实例独立数据?代码里
stats.Duplicate()复制一份,或每个敌人单独挂 .tres
6.2 动态实例化:子弹、敌人、特效
前面 Pong 的球是预先摆在场景里的。真实游戏里子弹要在开火瞬间生成:
完整模板
public partial class Bullet : Area2D{ private Vector2 _velocity; private int _damage;
// 用普通方法做"构造参数":Godot 节点不能用 new 传参(要先 Instantiate 再加树) public void Setup(Vector2 direction, float speed, int damage) { _velocity = direction.Normalized() * speed; _damage = damage; }
public override void _PhysicsProcess(double delta) { GlobalPosition += _velocity * (float)delta; }
public void OnTimerTimeout() => QueueFree(); // 配个 Timer 子节点,5 秒自毁}生成方记住三步顺序:
var bullet = _bulletScene.Instantiate<Bullet>(); // ① 实例化(还不在树里)bullet.Setup(dir, speed, damage); // ② 注入数据(此时 _Ready 还没跑)GetTree().CurrentScene.AddChild(bullet); // ③ 加进场景树(_Ready 此刻才执行)⚠️ 常见错误:先 AddChild 再 Setup,_Ready 读到的还是默认值。顺序是实例化 → 注入 → 入树。
对象池(性能进阶)
Instantiate 有开销,弹幕游戏每秒几百发子弹时用对象池:预先实例化一批,用时从池里取(移出池节点/重设位置),不用时 RemoveChild 回池而不是 QueueFree。新手阶段先不用做,等 Instantiate 出现在性能分析器顶部再优化(第 8 章讲调试器)。
6.3 节点查找在动态生成下的正确姿势
子弹生成在 CurrentScene 下,敌人怎么找玩家?回顾第 5 章决策树:用组。
AddToGroup("player");
// Enemy 或子弹需要时(_Ready 缓存,不要每帧找)private Player _player = null!;public override void _Ready(){ await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); _player = GetTree().GetFirstNodeInGroup("player") as Player;}6.4 存档:三种层次
层次 1:Godot Resource 存二进制/文本(最简单)
把存档数据定义成 Resource:
[GlobalClass]public partial class SaveData : Resource{ [Export] public int Level = 1; [Export] public int Coins = 0; [Export] public float MaxHp = 100f; [Export] public string PlayerName = ""; [Export] public Godot.Collections.Array<bool> LevelsCleared = new();}存:
var data = new SaveData { Level = 3, Coins = 42, MaxHp = 80f };string path = "user://save.tres"; // user:// 是 Godot 托管的用户目录,跨平台ResourceSaver.Save(data, path);读:
if (ResourceLoader.Exists(path)){ var data = ResourceLoader.Load<SaveData>(path); GD.Print(data.Coins);}- 优点:三行代码、类型安全、Inspector 可看
- 缺点:改类结构后旧存档可能失配;玩家可以随手改 .tres 文本作弊;不适合存世界中每个物件的状态
user://的物理位置:Windows%APPDATA%/Godot/app_userdata/项目名,Linux~/.local/share/godot/app_userdata/项目名,macOS 在~/Library/Application Support
层次 2:JSON(可读、可迁移、可控)
public class SaveData{ public int Level { get; set; } public int Coins { get; set; } public Vector2 Position { get; set; } // System.Text.Json 不能直接序列化 Godot Vector2}⚠️ Godot 结构体(Vector2、Color)和 System.Text.Json 不来电,用普通 DTO(float PosX/PosY)转换,或用 JsonUtility:
var godotObj = new Godot.Collections.Dictionary{ ["level"] = 3, ["pos"] = new Vector2(10, 20) // Godot 自己的 JSON 类认识这些类型};string json = Json.Stringify(godotObj);
var parsed = Json.ParseString(json).AsGodotDictionary();实操推荐:用 Godot 的 Json.Stringify + Dictionary 写,或用 NuGet 装 Newtonsoft.Json(配置一个转换器处理 Vector2)。中小项目层次 1 足够。
层次 3:“收集—存储—恢复”完整模式
真正难的不是写文件,是存什么、怎么恢复。给需要持久化的节点统一接口:
public interface ISaveable{ string SaveId { get; } Godot.Collections.Dictionary Save(); void Load(Godot.Collections.Dictionary data);}敌人、箱子、门实现它,SaveManager 遍历组 "saveable" 收集字典、写 JSON;读档时按 SaveId 匹配恢复。玩家位置、已捡物品(存”已移除的节点 id 集合”比存”场上所有物品”聪明)、开关状态都能覆盖。这是正式项目的做法,理解思路即可,需要时回来实现。
存档安全
- 本地存档没有真正的防作弊,只能提高门槛:发布版存
.res二进制 + 加 HMAC 校验(密钥在代码里,破解成本高一点) - Steam 云存档等以后上平台再管
- 多存档槽:
save_01.tres、save_02.tres……
6.5 配置数据放哪:一张选型表
| 数据 | 载体 |
|---|---|
| 武器/敌人/物品数值 | 自定义 Resource(.tres) |
| 游戏平衡常量(重力、暴击率) | [Export] 在场景上,或单独 balance.cfg Resource |
| 本地化文本 | Godot 翻译系统(CSV/PO) |
| 按键绑定 | InputMap 运行时修改 + 存 user:// |
| 玩家进度/设置 | SaveData Resource 或 JSON |
| 大量结构化数值(对话树、任务表) | JSON/CSV 导入,或插件(如对话插件) |
| 环境变量/密钥 | 不要进 Git,配置文件 + .gitignore |
6.6 一个贯穿例子:给平台游戏加”武器系统”作业
目标:把本章知识用一遍。
- 建
WeaponData(名字、伤害、射速、弹速)+ 手枪/霰弹两个 .tres Weapon.cs挂在 Player 子节点,[Export] WeaponData,按 J 开火bullet.tscn:Area2D + CollisionShape2D + 小圆 Polygon2D,飞行 + 碰墙 QueueFree + 命中敌人调TakeDamage- 敌人
enemy.tscn:CharacterBody2D 左右巡逻(用Mathf.Sin(Time.GetTicksMsec())或两个 Marker2D 之间 MoveToward),HP 用[Export],归零发Died信号并 QueueFree - 金币数和当前关卡写进
SaveData,按 F5 快速存、F9 快速读
6.7 本章金句
- 逻辑写一遍,数据做成资产
- 实例化三步走:实例化 → 注入 → 入树
- 销毁节点用
QueueFree(),不是置 null user://是唯一该写存档的地方- 存档设计先想”恢复时怎么对上号”,再想存什么
第 7 章回到玩家可见的东西:UI 系统与游戏流程(主菜单、暂停、血条、设置界面、场景过渡)。
《Godot C# 零基础》系列导航
⬅️ 上一篇:Godot C# 零基础教程 05:信号与节点通信 📑 返回系列总目录(第 01 篇) ➡️ 下一篇:Godot C# 零基础教程 07:UI 与游戏流程