关于 resource-pattern
面向 Godot 4.3+ 游戏开发者的数据建模技能,介绍用 Resource 保存可编辑、可复用的配置与游戏数据,并讲解共享状态和实例副本的处理。
主要功能
- 01自定义 Resource 与 Inspector 编辑
- 02数据驱动的游戏配置
- 03Resource 集合与加载
- 04共享资源与实例副本管理
- 05Resource 保存与反序列化
适用场景
- 01定义物品、敌人或能力数据
- 02让设计师在 Inspector 中调整配置
- 03跨场景复用数据资源
- 04保存强类型游戏数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jame581/ godotprompter --skill resource-pattern安装后在 Claude Code 中输入/resource-pattern确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/collections.mdconfiguration-pattern.mdeditor-integration.mdsaving-resources.mdsharing-vs-unique.md详细介绍
Godot 4.3+ 中的 Resource 模式
Resource 是 Godot 内置的数据容器。可将其用于配置、物品定义、角色属性,以及任何需要存储在场景树之外的数据。所有示例均适用于 Godot 4.3+,不使用已弃用的 API。
相关技能: inventory-system 用于基于 Resource 的物品定义,save-load 用于 Resource 序列化,component-system 用于数据驱动的组件配置,ability-system 用于基于此模式构建的 Resource 能力定义。
1. 什么是 Resource
Resource 是一种引用计数数据对象:
- 以
.tres(文本)或.res(二进制)文件形式保存在磁盘上 - 可直接在 Godot 检视器中编辑
- 默认情况下只加载一次并共享——每个加载相同路径的节点都会获取同一个内存对象
- 可嵌套在其他 Resource 和 PackedScene 中
- 只有在仍有对象引用它时,场景切换后才会继续存在(例如由自动加载或静态变量引用)——它是
RefCounted,因此最后一个引用消失时,它也会被释放
由于 Resource 默认共享,因此非常适合存储只读数据(物品定义、音频设置、能力蓝图)。对于每个实例可变的状态,请调用 duplicate()(或设置 resource_local_to_scene)——参见第 8 节。
2. 何时使用 Resource
| 用例 | Resource 示例 | 替代方案 |
|---|---|---|
| 物品定义 | 包含名称、图标、价值的 ItemData | Dictionary(会失去类型安全性) |
| 敌人配置 | 包含生命值、速度、伤害的 EnemyStats | Node 上的导出变量(无法复用) |
| 角色属性 | 包含基础数值的 CharacterStats | 自动加载(全局状态,难以测试) |
| 能力定义 | 包含冷却时间、消耗、效果的 AbilityData | 硬编码常量 |
| 关卡元数据 | 包含音乐、时限、目标的 LevelConfig | JSON(无法与编辑器集成) |
| 音频/视觉主题 | 包含调色板、字体的 UIThemeData | Theme Resource(理念相同,且为内置类型) |
| 对话树 | 引用下一句对话的 DialogueLine | JSON(没有类型检查) |
只要需要在检视器中编辑 + 使用类型化数据 + 跨场景共享,就可以使用自定义 Resource。
3. 基本自定义 Resource
GDScript
# item_data.gd
class_name ItemData
extends Resource
enum ItemType { WEAPON, ARMOUR, CONSUMABLE, QUEST }
@export var name: String = ""
@export var description: String = ""
@export var icon: Texture2D
@export var value: int = 0
@export var item_type: ItemType = ItemType.CONSUMABLE
在编辑器中创建实例:右键单击 FileSystem 面板 → New Resource → 选择 ItemData。填写检视器字段,并保存为 res://data/items/health_potion.tres。
在运行时加载:
var potion: ItemData = load("res://data/items/health_potion.tres")
print(potion.name) # "Health Potion"
print(potion.value) # 50
C#
// ItemData.cs
using Godot;
[GlobalClass]
public partial class ItemData : Resource
{
public enum ItemType { Weapon, Armour, Consumable, Quest }
[Export] public string Name { get; set; } = "";
[Export] public string Description { get; set; } = "";
[Export] public Texture2D Icon { get; set; }
[Export] public int Value { get; set; } = 0;
[Export] public ItemType Type { get; set; } = ItemType.Consumable;
}
在 C# 中,必须使用
[GlobalClass],以便编辑器识别该类并在 New Resource 中显示它。
var potion = GD.Load<ItemData>("res://data/items/health_potion.tres");
GD.Print(potion.Name); // "Health Potion"
GD.Print(potion.Value); // 50
4. 编辑器集成
使用 class_name 和 @icon,让自定义 Resource 在检视器中成为一等对象(仅当 setter 必须在编辑器中运行时才添加 @tool)——它们会显示在 Resource 选择器中,可通过右键单击“New Resource”创建,并显示自定义图标。@export_group 和 @export_subgroup 可用于整理属性。
完整的 GDScript + C# 模式(包含
class_name、@icon、@export_group)请参阅 references/editor-integration.md。
5. 将 Resource 用作配置
最适合 Resource 的场景是数据驱动的游戏内容。战利品表、敌人属性、能力定义和物品目录都可以成为自定义 Resource。设计师可在检视器中调整 .tres 文件;程序员则负责接入加载器。这样可以避免 JSON 模式松散、解析依赖字符串的问题。
完整示例 LootTable + DropEntry(GDScript + C#)请参阅 references/configuration-pattern.md。
6. Resource 集合
@export var entries: Array[Entry] = [] 可在检视器中显示类型化数组——拖放多个 Resource 文件即可。对于启动时加载的集合,请使用 ResourcePreloader。若要在运行时发现资源文件夹中的资源,请遍历 DirAccess。
有关类型化数组导出(保留 v1.6.0 C# 一致性)、
ResourcePreloader设置和目录遍历加载器模式,请参阅 references/collections.md。
7. 资源与节点
| 方面 | 资源 | 节点 |
|---|---|---|
| 用途 | 数据存储和配置 | 行为、渲染、物理、输入 |
| 场景树 | 不在树中 | 位于场景树中 |
| 生命周期钩子 | 没有树回调(_init、_setup_local_to_scene) | _ready、_process、_physics_process 等 |
| 共享 | 默认共享(相同路径 = 相同对象) | 每个实例相互独立 |
| 序列化 | 保存为 .tres / .res,可在 Inspector 中编辑 | 保存在 .tscn 中 |
| 信号 | 支持 | 支持 |
| 适用场景 | 物品数据、属性、配置、技能蓝图 | 玩家、敌人、UI 控件、摄像机 |
| 不适用场景 | 任何需要逐帧更新或查询场景的内容 | 运行时不会变化的静态数据 |
经验法则: 如果它没有行为,也不需要存在于场景树中,就将它做成 Resource。如果它需要移动、渲染、接收输入或运行逐帧逻辑,就将它做成 Node。
8. 共享与独立
默认情况下,Resource 按引用共享。两个场景引用 res://items/sword.tres 时,会看到同一个实例——修改一个就会同时修改另一个。对于实例独有的状态,请使用 .duplicate()(浅复制)或 .duplicate(true)(深复制——受版本限制,详见参考文档)。
完整模式请参见 references/sharing-vs-unique.md(保留了 v1.6.0 C# 对应实现)。
9. 保存自定义资源
ResourceSaver.save(resource, path) 会将资源写入 .tres(文本)或 .res(二进制)文件。当你需要强类型时,可用于保存游戏存档(其架构在代码中定义为 Resource 子类),而不必手动编写 JSON。
完整的保存 / 加载模式(GDScript + C#)和安全注意事项请参见 references/saving-resources.md(切勿从不可信来源加载
.tres——其中可能会执行嵌入的 GDScript)。
10. 反模式
可变的共享 Resource 未使用 duplicate()——意外共享状态
# BAD — all enemies share the same EnemyStats object.
# Damaging one enemy damages all of them.
class_name Enemy
extends CharacterBody2D
@export var stats: EnemyStats # loaded from .tres, shared
func take_damage(amount: int) -> void:
stats.health -= amount # mutates the shared Resource!
# GOOD — each enemy owns its own copy.
func _ready() -> void:
stats = stats.duplicate() # now safe to mutate
在 Resource 中编写游戏逻辑
# BAD — Resources have no scene tree access, no _process, no signals from nodes.
class_name EnemyStats
extends Resource
func update_health_regen(delta: float) -> void:
# Can't call get_tree(), can't read Input, can't access nodes.
# This logic belongs in a Node.
health = min(health + regen_rate * delta, max_health)
# GOOD — keep logic in Nodes, data in Resources.
# enemy.gd
func _process(delta: float) -> void:
_current_health = minf(_current_health + stats.regen_rate * delta, stats.max_health)
巨型单体 Resource
# BAD — one Resource holds everything; impossible to reuse parts.
class_name GameConfig
extends Resource
@export var player_health: int
@export var player_speed: float
@export var enemy_goblin_health: int
@export var enemy_goblin_speed: float
@export var enemy_troll_health: int
# ... 200 more properties
# GOOD — small focused Resources, composed together.
class_name PlayerConfig
extends Resource
@export var health: int = 100
@export var speed: float = 200.0
class_name EnemyConfig
extends Resource
@export var health: int = 50
@export var speed: float = 80.0
// ❌ Anti-pattern: mutating a shared Resource — accidental shared state
// All enemies share the same EnemyStats object loaded from the .tres file.
// Damaging one enemy damages all of them.
[GlobalClass]
public partial class Enemy : CharacterBody2D
{
[Export] public EnemyStats Stats; // loaded from .tres, shared by default
public void TakeDamage(int amount)
{
Stats.Health -= amount; // mutates the shared Resource — affects every Enemy!
}
}
// ✅ Correct: duplicate before mutating so each instance owns its own copy.
public partial class EnemyGood : CharacterBody2D
{
[Export] public EnemyStats Stats;
public override void _Ready()
{
Stats = (EnemyStats)Stats.Duplicate(); // now safe to mutate independently
}
public void TakeDamage(int amount)
{
Stats.Health -= amount; // only affects this instance
}
}
// ❌ Anti-pattern: game logic inside a Resource
// Resources have no scene-tree access, no _Process, and cannot call GetTree() or read Input.
[GlobalClass]
public partial class EnemyStatsBad : Resource
{
[Export] public float Health { get; set; }
[Export] public float MaxHealth { get; set; }
[Export] public float RegenRate { get; set; }
// This logic belongs in a Node, not a Resource.
public void UpdateHealthRegen(double delta)
{
// Cannot call GetTree(), cannot access nodes, cannot read Input.
Health = Mathf.Min(Health + RegenRate * (float)delta, MaxHealth);
}
}
// ✅ Correct: keep logic in Nodes, data in Resources.
public partial class EnemyCorrect : CharacterBody2D
{
[Export] public EnemyStats Stats;
private float _currentHealth;
public override void _Ready()
{
Stats = (EnemyStats)Stats.Duplicate();
_currentHealth = Stats.MaxHealth;
}
public override void _Process(double delta)
{
_currentHealth = Mathf.Min(_currentHealth + Stats.RegenRate * (float)delta, Stats.MaxHealth);
}
}
// ❌ Anti-pattern: giant monolithic Resource
// One Resource holds everything; impossible to reuse individual pieces.
[GlobalClass]
public partial class GameConfigBad : Resource
{
[Export] public int PlayerHealth { get; set; }
[Export] public float PlayerSpeed { get; set; }
[Export] public int EnemyGoblinHealth { get; set; }
[Export] public float EnemyGoblinSpeed { get; set; }
[Export] public int EnemyTrollHealth { get; set; }
// ... 200 more properties
}
// ✅ Correct: small focused Resources, composed together.
[GlobalClass]
public partial class PlayerConfig : Resource
{
[Export] public int Health { get; set; } = 100;
[Export] public float Speed { get; set; } = 200.0f;
}
[GlobalClass]
public partial class EnemyConfig : Resource
{
[Export] public int Health { get; set; } = 50;
[Export] public float Speed { get; set; } = 80.0f;
}
11. 检查清单
- Resource 子类使用
class_name,以便编辑器能在 New Resource 中找到它 - C# 类具有
[GlobalClass]属性 - 所有可在 Inspector 中编辑的字段都使用
@export/[Export] - 对可由设计师调整范围的数值字段使用
@export_range - 对包含许多字段的 Resource,使用
@export_group和@export_category来整理 Inspector 布局 - 在
_ready()中对每个实例独有的可变 Resource 使用duplicate() - 只读的共享 Resource(定义、蓝图)不进行复制
- 游戏逻辑(逐帧更新、场景查询)放在 Node 中,而不是 Resource 中
- 将大型数据集拆分为职责单一的专用 Resource
- 开发期间使用
.tres;发布生产数据时考虑使用.res - 检查
ResourceSaver.save()的返回值,并使用push_error()报告错误 - 绝不从不可信的外部来源加载
.tres/.res文件