mcpField
R
resource-pattern

resource-pattern

github.com/jame581开发889181 次安装4 天前更新
代码仓库

关于 resource-pattern

面向 Godot 4.3+ 游戏开发者的数据建模技能,介绍用 Resource 保存可编辑、可复用的配置与游戏数据,并讲解共享状态和实例副本的处理。

主要功能

  1. 01自定义 Resource 与 Inspector 编辑
  2. 02数据驱动的游戏配置
  3. 03Resource 集合与加载
  4. 04共享资源与实例副本管理
  5. 05Resource 保存与反序列化

适用场景

  1. 01定义物品、敌人或能力数据
  2. 02让设计师在 Inspector 中调整配置
  3. 03跨场景复用数据资源
  4. 04保存强类型游戏数据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jame581/godotprompter --skill resource-pattern

安装后在 Claude Code 中输入/resource-pattern确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
collections.md
configuration-pattern.md
editor-integration.md
saving-resources.md
sharing-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 示例替代方案
物品定义包含名称、图标、价值的 ItemDataDictionary(会失去类型安全性)
敌人配置包含生命值、速度、伤害的 EnemyStatsNode 上的导出变量(无法复用)
角色属性包含基础数值的 CharacterStats自动加载(全局状态,难以测试)
能力定义包含冷却时间、消耗、效果的 AbilityData硬编码常量
关卡元数据包含音乐、时限、目标的 LevelConfigJSON(无法与编辑器集成)
音频/视觉主题包含调色板、字体的 UIThemeDataTheme Resource(理念相同,且为内置类型)
对话树引用下一句对话的 DialogueLineJSON(没有类型检查)

只要需要在检视器中编辑 + 使用类型化数据 + 跨场景共享,就可以使用自定义 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 文件

评论

支持 Markdown