第一部分 10:Resource 资源系统

2026-05-19
6127 字约 21 分钟
...

这一章讲 Godot 里另一个特别重要的东西:Resource。

前面我们一直在讲 Node:

Node 是场景里的功能对象

这一章要开始理解 Resource:

Resource 是被节点使用的数据和资源

Godot 官方文档对这俩区分得很清楚:Node 提供功能,比如显示 Sprite、模拟物理、布局 UI;Resource 是数据容器,它自己不做事情,而是被节点使用。 比如图片、脚本、动画、场景、材质、TileSet 都属于资源。(Godot Engine documentation)

1. Resource 是什么

Resource 可以理解成 Godot 里的“资源数据”。

它可以是:

图片
音频
字体
动画
材质
Shader
TileSet
PackedScene
脚本
自定义配置
物品数据
技能数据
天气数据
NPC 数据

一句话:

Resource 主要负责保存数据,Node 负责使用这些数据做事情。

比如:

Sprite2D 是 Node
Texture2D 是 Resource

AudioStreamPlayer 是 Node
AudioStream 是 Resource

TileMapLayer 是 Node
TileSet 是 Resource

AnimationPlayer 是 Node
Animation 是 Resource

实例化出来的 Player 是 Node
player.tscn 加载后得到的 PackedScene 是 Resource

2. 前端类比

在前端里,你有:

组件
图片
JSON 配置
CSS 文件
字体
音频
接口数据

Godot 里可以类比成:

Node ≈ 组件实例 / DOM 元素 / 运行时对象
Resource ≈ 图片、JSON、配置、字体、样式、音频、组件模板

比如前端:

<img src="/player.png" />

这里:

img 标签是界面对象
player.png 是资源

Godot 里:

Sprite2D 是节点
player.png 导入后是 Texture2D 资源

3. Node 和 Resource 的区别

这是本章最核心的地方。

Node:
场景树里的对象,有生命周期,可以进入 SceneTree,可以运行脚本,可以有父子关系。

Resource:
数据容器,不在场景树里,通常没有位置、没有父子节点关系,主要被 Node 引用和使用。

对比表:

类型是否在 SceneTree 里是否有位置是否负责行为例子
Node是可能有是Sprite2D、Player、Camera2D、Button
Resource否没有通常不负责Texture2D、TileSet、PackedScene、AudioStream

比如:

Player CharacterBody2D
├── BodySprite Sprite2D
└── CollisionShape2D

这里都是 Node。

但它们会引用 Resource:

BodySprite.texture → Texture2D
CollisionShape2D.shape → Shape2D
AnimationPlayer.animations → Animation

4. 一个最简单例子:Sprite2D 和 Texture2D

假设你有一张图片:

res://assets/player.png

Godot 导入后,它可以作为 Texture2D 使用。

你的场景里有:

Player CharacterBody2D
└── BodySprite Sprite2D

代码:

@onready var body_sprite: Sprite2D = $BodySprite

func _ready() -> void:
    body_sprite.texture = preload("res://assets/player.png")

这里:

BodySprite 是 Sprite2D 节点
player.png 加载出来是 Texture2D 资源
texture 属性用来引用这个资源

Sprite2D 负责显示。

Texture2D 负责提供图片数据。

5. Resource 文件常见后缀

Godot 里你会看到很多资源文件。

常见:

.png      图片资源,导入后可作为 Texture2D
.webp     图片资源
.ogg      音频资源
.wav      音频资源
.ttf      字体资源
.tscn     场景资源,文本格式
.scn      场景资源,二进制格式
.tres     通用 Resource,文本格式
.res      通用 Resource,二进制格式
.gd       脚本资源
.gdshader Shader 资源

Godot 官方文档说明,Resource 可以保存为更适合版本控制的文本文件 *.tres,导出游戏时 Godot 会把资源序列化成二进制资源以提升速度和压缩效果。(Godot Engine documentation)

6. .tres 和 .res 的区别

.tres

.tres 是文本格式资源。

优点:

可读性更好
适合 Git 版本管理
方便 diff
开发阶段常用

比如:

res://resources/items/potion.tres

.res

.res 是二进制格式资源。

优点:

体积和加载性能可能更好
不适合人工阅读
导出时常见

入门阶段你优先记:

自己创建的自定义资源,通常保存为 .tres

比如:

ItemData.tres
SkillData.tres
WeatherData.tres

7. PackedScene:场景也是一种 Resource

这个特别重要。

.tscn 场景文件加载后,不是直接变成 Node,而是先得到一个 PackedScene 资源。

官方文档也明确说:当你 preload("res://my_scene.tscn") 后,得到的是一个 packed scene resource,不是节点;要创建真正的节点,需要调用 PackedScene.instantiate()。(Godot Engine documentation)

例子:

const SLIME_SCENE = preload("res://scenes/enemies/slime.tscn")

func spawn_slime(pos: Vector2) -> void:
    var slime = SLIME_SCENE.instantiate()
    add_child(slime)
    slime.global_position = pos

这里的流程是:

slime.tscn 文件
↓ preload
PackedScene 资源
↓ instantiate()
真正的 Slime 节点实例
↓ add_child()
进入 SceneTree

8. PackedScene 和 Node 的区别

这个一定要分清。

const PLAYER_SCENE = preload("res://scenes/player/player.tscn")

这里 PLAYER_SCENE 是:

PackedScene Resource

它还不是玩家。

你不能直接:

PLAYER_SCENE.global_position = Vector2(100, 100)

因为它不是 Node。

你要:

var player = PLAYER_SCENE.instantiate()
add_child(player)
player.global_position = Vector2(100, 100)

这时 player 才是 Node。

9. preload 和 load

preload

const SLIME_SCENE = preload("res://scenes/enemies/slime.tscn")

preload 在脚本加载时提前加载资源。

适合:

固定会用到的资源
常用敌人场景
玩家场景
常用音效
常用 UI 场景

例子:

const HIT_SOUND = preload("res://assets/audio/hit.ogg")
const CHEST_SCENE = preload("res://scenes/items/chest.tscn")

load

var map_scene = load("res://scenes/maps/forest.tscn")

load 在代码运行到这一行时才加载资源。

适合:

根据条件动态加载
根据地图名加载地图
根据配置加载物品图标
不一定会用到的资源

例子:

func load_map(map_name: String) -> PackedScene:
    return load("res://scenes/maps/%s.tscn" % map_name)

10. ResourceLoader

load() 背后其实就是资源加载。

Godot 也提供了 ResourceLoader 单例,用来从文件系统加载资源文件,并把它们转换成引擎可用的格式。(Godot Engine documentation)

入门阶段可以先不用直接写 ResourceLoader,知道有它就行。

常见:

var texture: Texture2D = load("res://assets/player.png")

更明确一点:

var scene: PackedScene = load("res://scenes/player.tscn")

11. ResourceSaver

和加载相对的是保存。

Godot 提供 ResourceSaver 用来把资源保存到文件系统,可以保存成文本格式,比如 .tres、.tscn,也可以保存成二进制格式,比如 .res、.scn。(Godot Engine documentation)

比如:

var item_data = ItemData.new()
item_data.id = "potion"
item_data.display_name = "生命药水"

ResourceSaver.save(item_data, "res://resources/items/potion.tres")

不过普通游戏开发早期,你更多是在编辑器里创建 .tres,不一定需要代码保存资源。

12. 常见内置 Resource 类型

Texture2D

图片纹理。

常被这些节点使用:

Sprite2D.texture
TextureRect.texture
TileSet 里的图块图片

例子:

$Sprite2D.texture = preload("res://assets/player.png")

AudioStream

音频资源。

常被:

AudioStreamPlayer
AudioStreamPlayer2D
AudioStreamPlayer3D

使用。

例子:

$AudioStreamPlayer.stream = preload("res://assets/audio/click.ogg")
$AudioStreamPlayer.play()

Font

字体资源。

常用于 UI 文本。

Label
Button
RichTextLabel
Theme

Shape2D

碰撞形状资源。

比如:

RectangleShape2D
CircleShape2D
CapsuleShape2D

常被:

CollisionShape2D.shape

使用。

TileSet

瓦片集资源。

被:

TileMapLayer.tile_set

使用。

Animation

动画资源。

被:

AnimationPlayer

使用。

Shader / ShaderMaterial

着色器和材质资源。

用于:

水面效果
雾气效果
雨雪效果
角色受伤闪白
UI 特效

PackedScene

场景资源。

用于:

实例化玩家
实例化敌人
实例化子弹
实例化 UI
实例化特效

13. Resource 是共享引用

这个非常重要。

如果多个节点引用同一个 Resource,它们可能共享同一份数据。

比如两个 Sprite2D 用同一张 Texture2D:

SpriteA.texture → player.png
SpriteB.texture → player.png

这没问题。

图片资源本来就应该共享。

但自定义 Resource 如果会在运行时修改,就要小心。

假设多个敌人都引用同一个 EnemyStats.tres:

Slime1.stats → slime_stats.tres
Slime2.stats → slime_stats.tres
Slime3.stats → slime_stats.tres

如果你运行时修改:

stats.health -= 10

你可能不是只改了当前敌人的数据,而是在改共享资源数据。

这就很容易出 bug。

所以记住:

Resource 适合做配置模板
运行时状态最好放在 Node 自己的变量里

14. 配置数据和运行时状态要分开

这是做游戏架构特别重要的一点。

配置数据

比如:

最大血量
移动速度
攻击力
物品名称
物品图标
技能冷却
天气颜色

这些适合放 Resource。

运行时状态

比如:

当前血量
当前坐标
是否死亡
是否打开
当前冷却剩余时间
玩家当前金币
当前任务进度

这些通常放 Node 或存档数据里。

15. 错误例子:把当前血量放 Resource 里

不推荐:

# EnemyStats.gd
class_name EnemyStats
extends Resource

@export var max_health: int = 30
@export var current_health: int = 30

如果所有 Slime 都用同一个 slime_stats.tres,那 current_health 就可能被共享。

更推荐:

# EnemyStats.gd
class_name EnemyStats
extends Resource

@export var max_health: int = 30
@export var move_speed: float = 80.0
@export var attack_damage: int = 5

Enemy 节点:

class_name Enemy
extends CharacterBody2D

@export var stats: EnemyStats

var current_health: int

func _ready() -> void:
    current_health = stats.max_health

这样:

stats.max_health 是配置
current_health 是当前敌人自己的运行时状态

16. 自定义 Resource 是什么

自定义 Resource 就是你自己定义的数据类型。

比如你做 RPG,会特别需要这些:

ItemData
SkillData
EnemyStats
NPCData
QuestData
WeatherData
SeasonData
DialogueData
CardData

它们本质上是可在 Inspector 编辑、可保存为 .tres 的数据对象。

官方 Resources 教程也说明,要让自定义资源类出现在 Create Resource 界面,需要给 GDScript 提供 class_name。(Godot Engine documentation)

17. 自定义 Resource 基础写法

比如做一个物品数据:

# item_data.gd
class_name ItemData
extends Resource

@export var id: String
@export var display_name: String
@export_multiline var description: String
@export var icon: Texture2D
@export var price: int = 0
@export var stackable: bool = true
@export var max_stack: int = 99

这里:

class_name ItemData
让它成为可识别类型

extends Resource
表示它是一个资源,不是节点

@export
让字段可以在 Inspector 里编辑

之后你可以基于 ItemData 创建多个资源文件:

res://resources/items/potion.tres
res://resources/items/iron_sword.tres
res://resources/items/herb.tres

这些 .tres 不是普通资源文件,而是创建时选择了 ItemData 类型的资源文件。

所以每一个都是一份具体的 ItemData 数据。

18. ItemData 用在哪里

背包物品格子可以引用它。

class_name InventorySlot
extends Control

@export var item_data: ItemData

@onready var icon_rect: TextureRect = $Icon
@onready var name_label: Label = $NameLabel

func _ready() -> void:
    if item_data == null:
        return

    icon_rect.texture = item_data.icon
    name_label.text = item_data.display_name

这样你不用在代码里写死:

name_label.text = "生命药水"
icon_rect.texture = preload("res://assets/items/potion.png")

而是让资源配置来驱动 UI。

19. 为什么自定义 Resource 很适合做配置

因为它可以:

在 Inspector 里编辑
保存成 .tres 文件
被多个节点引用
被数组、字典、其他 Resource 引用
类型清晰
比散落的 Dictionary 更好维护

比如你如果用 Dictionary 写物品:

var potion = {
    "id": "potion",
    "name": "生命药水",
    "price": 50,
    "icon": preload("res://assets/items/potion.png")
}

短期可以。

但物品多了以后会乱。

用 Resource:

potion.tres
iron_sword.tres
wood.tres
magic_scroll.tres

就更像正式游戏项目。

20. 自定义 SkillData

技能配置:

# skill_data.gd
class_name SkillData
extends Resource

@export var id: String
@export var display_name: String
@export_multiline var description: String
@export var icon: Texture2D

@export var cooldown: float = 1.0
@export var mana_cost: int = 0
@export var damage: int = 0
@export var range: float = 64.0

@export var effect_scene: PackedScene

技能系统使用:

class_name SkillUser
extends Node

@export var skills: Array[SkillData] = []

func use_skill(index: int) -> void:
    if index < 0 or index >= skills.size():
        return

    var skill := skills[index]
    print("使用技能:", skill.display_name)

    if skill.effect_scene != null:
        var effect = skill.effect_scene.instantiate()
        get_tree().current_scene.add_child(effect)

这里 SkillData 只保存数据。

真正释放技能、生成特效、扣蓝,是节点逻辑做的。

21. 自定义 EnemyStats

敌人配置:

# enemy_stats.gd
class_name EnemyStats
extends Resource

@export var id: String
@export var display_name: String
@export var max_health: int = 30
@export var move_speed: float = 80.0
@export var attack_damage: int = 5
@export var exp_reward: int = 10
@export var drop_items: Array[ItemData] = []

敌人节点:

class_name Enemy
extends CharacterBody2D

@export var stats: EnemyStats

var health: int

func _ready() -> void:
    if stats == null:
        push_warning("Enemy 缺少 stats 配置")
        return

    health = stats.max_health

func take_damage(amount: int) -> void:
    health -= amount

    if health <= 0:
        die()

func die() -> void:
    print(stats.display_name, "死亡,获得经验:", stats.exp_reward)
    queue_free()

这样你可以创建:

slime_stats.tres
bat_stats.tres
wolf_stats.tres
goblin_stats.tres

然后不同 Enemy 场景引用不同 stats。

22. 自定义 WeatherData

天气配置:

# weather_data.gd
class_name WeatherData
extends Resource

@export var id: String
@export var display_name: String
@export var world_modulate: Color = Color(1, 1, 1)
@export var fog_density: float = 0.0
@export var rain_intensity: float = 0.0
@export var snow_intensity: float = 0.0
@export var ambient_sound: AudioStream

WeatherManager:

class_name WeatherManager
extends Node

signal weather_changed(weather_data: WeatherData)

@export var current_weather: WeatherData

func set_weather(weather_data: WeatherData) -> void:
    current_weather = weather_data
    weather_changed.emit(current_weather)
    get_tree().call_group("weather_affected", "apply_weather", current_weather)

World:

extends Node2D

func apply_weather(weather_data: WeatherData) -> void:
    modulate = weather_data.world_modulate

这样你可以创建:

sunny.tres
rainy.tres
snowy.tres
foggy.tres

每个天气一份配置。

23. 自定义 DialogueData

对话配置:

# dialogue_data.gd
class_name DialogueData
extends Resource

@export var speaker_name: String
@export var portrait: Texture2D
@export var lines: Array[String] = []

NPC:

class_name NPC
extends CharacterBody2D

@export var dialogue: DialogueData

func interact() -> void:
    if dialogue == null:
        return

    print("开始对话:", dialogue.speaker_name)

DialogBox:

class_name DialogBox
extends Control

@onready var name_label: Label = $NameLabel
@onready var text_label: Label = $TextLabel
@onready var portrait_rect: TextureRect = $Portrait

var dialogue: DialogueData
var index: int = 0

func show_dialogue(value: DialogueData) -> void:
    dialogue = value
    index = 0
    visible = true

    name_label.text = dialogue.speaker_name
    portrait_rect.texture = dialogue.portrait
    text_label.text = dialogue.lines[index]

这种写法比把所有对话写死在 NPC 脚本里好多了。

24. 自定义 CardData

你之前说还想加卡牌元素,那卡牌就特别适合用 Resource。

# card_data.gd
class_name CardData
extends Resource

enum CardType {
    ATTACK,
    DEFENSE,
    SKILL,
    ITEM
}

@export var id: String
@export var display_name: String
@export_multiline var description: String
@export var icon: Texture2D
@export var type: CardType
@export var cost: int = 0
@export var damage: int = 0
@export var block: int = 0
@export var effect_scene: PackedScene

然后你可以创建:

slash.tres
fireball.tres
shield.tres
heal.tres

卡牌 UI:

class_name CardView
extends Control

@export var card_data: CardData

@onready var name_label: Label = $NameLabel
@onready var desc_label: Label = $DescriptionLabel
@onready var icon_rect: TextureRect = $Icon

func _ready() -> void:
    refresh()

func refresh() -> void:
    if card_data == null:
        return

    name_label.text = card_data.display_name
    desc_label.text = card_data.description
    icon_rect.texture = card_data.icon

25. Resource 里可以引用其他 Resource

比如 SkillData 里引用 Texture2D、PackedScene:

@export var icon: Texture2D
@export var effect_scene: PackedScene

EnemyStats 里引用 ItemData 数组:

@export var drop_items: Array[ItemData] = []

WeatherData 里引用 AudioStream:

@export var ambient_sound: AudioStream

这就是 Resource 很强的地方:

资源可以嵌套资源
配置可以引用图片、音效、场景、其他配置

官方文档也提到,Resource 可以递归序列化 sub-Resources,所以可以设计更复杂的数据结构。(Godot Engine documentation)

26. @export var resource: ResourceType

常见写法:

@export var item_data: ItemData
@export var skill_data: SkillData
@export var enemy_stats: EnemyStats
@export var weather_data: WeatherData
@export var scene: PackedScene
@export var icon: Texture2D
@export var sound: AudioStream

这样 Inspector 里就能拖资源。

比如:

@export var bullet_scene: PackedScene

然后你可以把:

res://scenes/projectiles/fireball.tscn

拖进去。

代码:

func shoot() -> void:
    var bullet = bullet_scene.instantiate()
    get_tree().current_scene.add_child(bullet)

27. Resource 和 Dictionary 怎么选

Dictionary 适合什么

适合快速、临时、动态的数据:

存档中间数据
网络数据
简单配置
调试数据
运行时统计

比如:

var save_data := {
    "gold": 100,
    "inventory": ["potion", "key"],
    "position": [100, 200]
}

Resource 适合什么

适合稳定、可编辑、可复用的数据:

物品配置
技能配置
敌人配置
天气配置
NPC 对话
任务配置
卡牌配置
商店商品配置

如果一份数据需要在 Inspector 里反复编辑,优先考虑 Resource。

28. Resource 和 JSON 怎么选

JSON 适合

大量外部数据
策划表导出
跨工具共享
玩家可改配置
服务器下发配置

Resource 适合

Godot 编辑器内维护
需要拖图片、场景、音频资源
需要类型提示
需要和 Inspector 深度结合

比如物品配置如果需要图标:

@export var icon: Texture2D

Resource 特别方便,因为你能直接拖 Godot 资源进去。

JSON 里只能写路径:

{
  "icon": "res://assets/items/potion.png"
}

然后你还要自己 load。

29. 资源路径 res:// 和 user://

res://

项目资源路径。

res://assets/player.png
res://scenes/player.tscn
res://resources/items/potion.tres

开发时项目里的文件通常用 res://。

user://

用户数据路径。

适合保存:

存档
设置
截图
运行时生成文件

比如:

const SAVE_PATH = "user://save.json"

简单记:

res:// 是游戏自带资源
user:// 是玩家本地数据

30. Resource 不一定是文件

Resource 可以是磁盘文件,也可以是内存里 new 出来的对象。

比如:

var item := ItemData.new()
item.id = "temp_item"
item.display_name = "临时物品"

这个 item 是 Resource,但它还没有保存成 .tres 文件。

如果要保存:

ResourceSaver.save(item, "res://resources/items/temp_item.tres")

普通项目里更多是编辑器创建 .tres,但运行时创建也可以。

31. 创建自定义 Resource 文件的流程

假设你写好了:

# item_data.gd
class_name ItemData
extends Resource

@export var id: String
@export var display_name: String
@export var icon: Texture2D
@export var price: int

创建 .tres:

1. 在 FileSystem 面板里右键
2. New Resource
3. 搜索 ItemData
4. 创建
5. 保存为 potion.tres
6. 在 Inspector 里填写字段

如果搜不到 ItemData,通常检查:

脚本有没有 class_name ItemData
脚本有没有 extends Resource
Godot 有没有重新加载脚本
脚本有没有报错

32. Resource 的 Inspector 编辑体验

自定义 Resource 里的 @export 字段会显示在 Inspector。

比如:

@export var display_name: String
@export_multiline var description: String
@export var icon: Texture2D
@export_range(0, 9999) var price: int = 0

Inspector 里会出现:

Display Name
Description
Icon
Price

如果是 Texture2D,能拖图片。

如果是 PackedScene,能拖场景。

如果是 AudioStream,能拖音频。

这就是 Godot Resource 配置流特别舒服的地方。

33. @export_multiline

多行文本用:

@export_multiline var description: String

适合:

物品描述
技能描述
NPC 对话
任务描述
卡牌效果文本

比如:

@export_multiline var description: String = "恢复 30 点生命。"

34. @export_range

数值范围:

@export_range(0, 9999) var price: int = 0
@export_range(0, 100) var drop_rate: float = 10.0
@export_range(0, 10) var cooldown: float = 1.0

适合:

价格
概率
速度
冷却
音量
强度
透明度

35. @export_enum

可以导出枚举式选择。

@export_enum("common", "rare", "epic", "legendary")
var rarity: String = "common"

物品例子:

class_name ItemData
extends Resource

@export var id: String
@export var display_name: String

@export_enum("material", "consumable", "weapon", "armor", "quest")
var item_type: String = "material"

@export_enum("common", "rare", "epic", "legendary")
var rarity: String = "common"

这样 Inspector 里可以下拉选择。

36. 用 enum 写得更类型化

也可以用 GDScript enum:

class_name ItemData
extends Resource

enum ItemType {
    MATERIAL,
    CONSUMABLE,
    WEAPON,
    ARMOR,
    QUEST
}

enum Rarity {
    COMMON,
    RARE,
    EPIC,
    LEGENDARY
}

@export var id: String
@export var display_name: String
@export var item_type: ItemType = ItemType.MATERIAL
@export var rarity: Rarity = Rarity.COMMON

这样代码里判断更稳:

if item_data.item_type == ItemData.ItemType.CONSUMABLE:
    use_consumable(item_data)

37. Resource 继承

自定义 Resource 也可以继承。

比如基础物品:

# item_data.gd
class_name ItemData
extends Resource

@export var id: String
@export var display_name: String
@export var icon: Texture2D
@export_multiline var description: String

消耗品:

# consumable_item_data.gd
class_name ConsumableItemData
extends ItemData

@export var heal_amount: int = 0
@export var mana_amount: int = 0

武器:

# weapon_item_data.gd
class_name WeaponItemData
extends ItemData

@export var attack_damage: int = 0
@export var attack_speed: float = 1.0

使用时:

func use_item(item: ItemData) -> void:
    if item is ConsumableItemData:
        use_consumable(item as ConsumableItemData)
    elif item is WeaponItemData:
        equip_weapon(item as WeaponItemData)

这就有点像 TypeScript 的接口/类继承味道。

38. Resource 里能不能写方法

可以。

Resource 不只是死数据,也可以有方法。

比如:

class_name ItemData
extends Resource

@export var id: String
@export var display_name: String
@export var price: int = 0

func get_display_text() -> String:
    return display_name + " - " + str(price) + "G"

但注意:

Resource 里的方法适合处理和数据强相关的简单逻辑
复杂游戏行为还是放 Node 或系统里

比如物品价格显示,可以放 Resource。

但“使用药水后给玩家加血、播放音效、更新 UI”最好放 Inventory / Player / ItemSystem。

39. Resource 和 Manager 配合

资源负责数据。

Manager 负责逻辑。

比如:

ItemData:
物品名称、图标、描述、价格

Inventory:
玩家拥有哪些物品

ItemSystem:
如何使用物品

HUD:
如何显示物品

不要让 ItemData 直接操作 Player。

不推荐:

func use(player: Player) -> void:
    player.health += 30
    player.hud.update()

更推荐:

class_name ConsumableItemData
extends ItemData

@export var heal_amount: int = 30

然后 ItemSystem:

func use_item(item: ItemData, target: Player) -> void:
    if item is ConsumableItemData:
        var consumable := item as ConsumableItemData
        target.heal(consumable.heal_amount)

这样数据和行为更清楚。

40. RPG 项目推荐资源目录

你可以这样规划:

res://resources/
├── items/
│   ├── potion.tres
│   ├── herb.tres
│   └── iron_sword.tres
├── skills/
│   ├── fireball.tres
│   ├── heal.tres
│   └── slash.tres
├── enemies/
│   ├── slime_stats.tres
│   ├── bat_stats.tres
│   └── wolf_stats.tres
├── weather/
│   ├── sunny.tres
│   ├── rainy.tres
│   ├── snowy.tres
│   └── foggy.tres
├── dialogues/
│   ├── villager_intro.tres
│   └── merchant_intro.tres
├── quests/
│   ├── first_quest.tres
│   └── find_sword.tres
└── cards/
    ├── slash.tres
    ├── guard.tres
    └── fireball.tres

脚本目录:

res://scripts/resources/
├── item_data.gd
├── skill_data.gd
├── enemy_stats.gd
├── weather_data.gd
├── dialogue_data.gd
├── quest_data.gd
└── card_data.gd

场景目录:

res://scenes/
├── player/
├── enemies/
├── ui/
├── world/
└── effects/

这样结构会很清楚:

scripts/resources 放资源类型定义
resources 放具体配置数据
scenes 放节点场景
assets 放原始图片音频素材

41. assets 和 resources 的区别

这不是 Godot 强制要求,是项目组织习惯。

assets

放原始素材:

图片
音频
字体
shader
tileset 图片
角色立绘
图标

比如:

res://assets/items/potion.png
res://assets/audio/click.ogg
res://assets/tilesets/forest_tiles.png

resources

放 Godot Resource 配置:

ItemData
SkillData
EnemyStats
WeatherData
QuestData

比如:

res://resources/items/potion.tres
res://resources/weather/rainy.tres

一个 potion.tres 可以引用:

res://assets/items/potion.png

42. TileSet 也是 Resource

你前面一直在学 TileMap。

这里要把关系串起来:

TileMapLayer 是 Node
TileSet 是 Resource
Tile Texture 是 Texture2D Resource

结构:

World Node2D
└── Ground TileMapLayer

Ground 这个 TileMapLayer 有一个属性:

tile_set

它引用一个 TileSet 资源。

TileSet 里又引用瓦片图片:

forest_tiles.png

所以:

TileMapLayer 负责在场景里画地图
TileSet 负责定义有哪些 tile、碰撞、地形规则
Texture2D 负责提供瓦片图片

43. Animation 也是 Resource

AnimationPlayer 是节点。

它里面的每段动画是 Resource。

比如:

AnimationPlayer Node
├── idle_down Animation
├── walk_down Animation
├── open Animation
└── die Animation

你在 Animation 面板里编辑的 keyframe,本质上会保存为动画数据。

所以:

AnimationPlayer 负责播放动画
Animation 负责保存动画轨道和关键帧数据

44. Shape2D 也是 Resource

CollisionShape2D 是节点。

它的 shape 是资源。

比如:

CollisionShape2D.shape = RectangleShape2D

这意味着:

CollisionShape2D 负责把碰撞形状放到场景里
RectangleShape2D 负责保存矩形大小数据

所以你在 Inspector 里给 CollisionShape2D 选择 RectangleShape2D,本质上就是给它分配一个 Shape2D Resource。

45. Material / Shader 也是 Resource

比如你后面做:

水面波纹
雾气
雨天变暗
雪地闪光
角色受伤闪白
UI 发光

会用到:

Shader
ShaderMaterial
CanvasItemMaterial

关系是:

Sprite2D 是 Node
ShaderMaterial 是 Resource
Shader 是 Resource

节点负责显示,材质和 Shader 负责显示效果。

46. Resource 的本地化副本:local_to_scene

你以后可能会遇到一个问题:

多个场景实例共享同一个资源,改一个影响全部。

Godot Resource 有 resource_local_to_scene 这类机制,可以让场景实例使用独立资源副本。

入门阶段你先不用深究,只记一句:

Resource 默认偏共享;如果运行时要修改资源本身,要小心影响其他引用者。

更安全的初期做法是:

Resource 存模板配置
Node 存运行时状态

这样大部分共享问题都能避开。

47. 什么时候不要用 Resource

不是所有数据都要用 Resource。

不适合 Resource 的:

玩家当前血量
玩家当前坐标
临时输入状态
临时攻击冷却
当前对话行数
当前正在交互的对象
怪物当前追踪目标
UI 当前打开关闭状态

这些是运行时状态,通常放在 Node 变量里。

比如:

var current_health: int
var current_target: Node2D
var input_locked: bool
var current_dialog_index: int

48. 什么时候适合用 Resource

适合 Resource 的:

物品基础信息
技能基础信息
敌人基础属性
天气配置
季节配置
任务配置
NPC 对话配置
卡牌配置
商店商品配置
角色成长曲线
音效配置
地图配置

一句话:

可复用、可编辑、偏静态的数据,适合 Resource。

49. Resource 和 Scene 怎么配合

一个完整敌人可能这样设计:

Enemy.tscn
├── Enemy CharacterBody2D
├── Sprite2D
├── CollisionShape2D
└── AnimationPlayer

Enemy.gd:

class_name Enemy
extends CharacterBody2D

@export var stats: EnemyStats

EnemyStats:

slime_stats.tres

包含:

名称:史莱姆
最大血量:30
速度:60
伤害:5
掉落:草药

这样:

Enemy.tscn 负责结构和行为
slime_stats.tres 负责数据

如果你想复用同一个 Enemy 场景做不同怪物,也可以实例化后设置不同 stats。

50. Resource 和 UI 怎么配合

比如物品卡片 UI:

ItemCard Control
├── Icon TextureRect
├── NameLabel Label
└── DescriptionLabel Label

ItemCard.gd:

class_name ItemCard
extends Control

@export var item_data: ItemData

@onready var icon: TextureRect = $Icon
@onready var name_label: Label = $NameLabel
@onready var description_label: Label = $DescriptionLabel

func _ready() -> void:
    refresh()

func refresh() -> void:
    if item_data == null:
        return

    icon.texture = item_data.icon
    name_label.text = item_data.display_name
    description_label.text = item_data.description

这样 UI 不需要知道药水、铁剑、草药分别是什么。

它只要拿到 ItemData 就能显示。

51. Resource 和生成系统怎么配合

比如生成掉落物。

DropItem.tscn:

DropItem Area2D
├── Sprite2D
└── CollisionShape2D

DropItem.gd:

class_name DropItem
extends Area2D

@export var item_data: ItemData

@onready var sprite: Sprite2D = $Sprite2D

func _ready() -> void:
    if item_data != null:
        sprite.texture = item_data.icon

func collect() -> ItemData:
    queue_free()
    return item_data

敌人死亡:

@export var drop_item_scene: PackedScene
@export var drop_item_data: ItemData

func die() -> void:
    if drop_item_scene != null and drop_item_data != null:
        var drop: DropItem = drop_item_scene.instantiate()
        get_tree().current_scene.add_child(drop)

        drop.global_position = global_position
        drop.item_data = drop_item_data

    queue_free()

这里:

DropItem.tscn 是掉落物场景模板
ItemData 是掉落的具体物品数据

52. Resource 和存档的关系

一般不直接把整个 Resource 存进存档。

更常见是存资源 ID。

比如 ItemData:

@export var id: String = "potion"

背包存档:

{
    "items": [
        {
            "id": "potion",
            "amount": 3
        },
        {
            "id": "iron_sword",
            "amount": 1
        }
    ]
}

读档时,根据 ID 找回 ItemData。

你可以做一个 ItemDatabase:

class_name ItemDatabase
extends Node

@export var items: Array[ItemData] = []

var item_map: Dictionary = {}

func _ready() -> void:
    for item in items:
        item_map[item.id] = item

func get_item(id: String) -> ItemData:
    return item_map.get(id)

这样存档轻量,资源仍然由 Godot 管理。

53. 简单 ItemDatabase

ItemDatabase.gd:

class_name ItemDatabase
extends Node

@export var items: Array[ItemData] = []

var item_map: Dictionary = {}

func _ready() -> void:
    build_map()

func build_map() -> void:
    item_map.clear()

    for item in items:
        if item == null:
            continue

        if item.id == "":
            push_warning("发现没有 id 的 ItemData")
            continue

        item_map[item.id] = item

func get_item(id: String) -> ItemData:
    return item_map.get(id)

Inspector 里把所有物品 .tres 拖到 items 数组里。

以后:

var potion = item_database.get_item("potion")

54. 更完整的物品系统思路

Resource:

ItemData 保存物品配置

Database:

ItemDatabase 根据 id 管理所有 ItemData

Inventory:

Inventory 保存玩家当前拥有的物品 id 和数量

UI:

InventoryPanel 根据 ItemData 显示图标、名字、描述

存档:

只保存 item_id 和 amount

这是比较稳的结构。

55. Resource 常见错误

错误 1:把 Resource 当 Node 用

错误:

var scene = preload("res://scenes/slime.tscn")
scene.global_position = Vector2(100, 100)

正确:

var slime = scene.instantiate()
add_child(slime)
slime.global_position = Vector2(100, 100)

错误 2:忘记 instantiate

错误:

var slime = preload("res://scenes/slime.tscn")
add_child(slime)

slime 是 PackedScene,不是 Node。

正确:

var slime_scene = preload("res://scenes/slime.tscn")
var slime = slime_scene.instantiate()
add_child(slime)

错误 3:把运行时状态放共享 Resource

不推荐:

@export var current_health: int = 30

放在 EnemyStats 里。

推荐:

@export var max_health: int = 30

放 Resource。

var current_health: int

放 Enemy 节点。

错误 4:Resource 缺 class_name,创建时搜不到

自定义资源要:

class_name ItemData
extends Resource

没有 class_name,创建资源时不方便在编辑器里找到。

错误 5:路径写错

preload("res://resource/item/potion.tres")

但真实路径是:

res://resources/items/potion.tres

路径错了就加载失败。

错误 6:资源引用为空没有判断

不推荐:

sprite.texture = item_data.icon

如果 item_data 是 null,会报错。

更稳:

if item_data == null:
    return

sprite.texture = item_data.icon

56. Resource 使用速查

# 加载图片
var texture: Texture2D = preload("res://assets/player.png")

# 给 Sprite2D 设置图片
$Sprite2D.texture = texture

# 加载场景
const SLIME_SCENE: PackedScene = preload("res://scenes/slime.tscn")

# 实例化场景
var slime = SLIME_SCENE.instantiate()

# 加入场景树
add_child(slime)

# 创建自定义 Resource
var item = ItemData.new()

# 保存 Resource
ResourceSaver.save(item, "res://resources/items/item.tres")

# 导出 Resource 引用
@export var item_data: ItemData

# 导出场景资源
@export var enemy_scene: PackedScene

# 导出图片资源
@export var icon: Texture2D

# 导出音频资源
@export var sound: AudioStream

57. 这一章你先记住这些

Resource 是数据容器。
Node 是场景树里的功能对象。
Sprite2D 是 Node,Texture2D 是 Resource。
TileMapLayer 是 Node,TileSet 是 Resource。
AnimationPlayer 是 Node,Animation 是 Resource。
CollisionShape2D 是 Node,Shape2D 是 Resource。
.tscn 加载后是 PackedScene Resource。
PackedScene 要 instantiate 后才会变成 Node。
自定义 Resource 很适合做物品、技能、敌人、天气、任务、卡牌配置。
Resource 适合放配置模板,不适合放运行时状态。
.tres 是文本资源,适合开发和版本管理。

58. 最重要的一句话

Node 负责“做事”,Resource 负责“提供数据”。

再压缩一下:

玩家节点会移动,但玩家配置是 Resource。
地图节点会绘制,但瓦片集是 Resource。
UI 节点会显示,但物品数据是 Resource。
敌人节点会战斗,但敌人属性模板是 Resource。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
godot2026-07-27
Godot 4 常用 UI 节点详解
Godot 4 常用 UI 节点详解 在 Godot 4 中,UI 系统基于 Control(控件) 节点构建。所有 UI 节点都继承自 Control,形成一棵完整的 UI 树。与游戏引擎中常见的"Canvas + DOM"模式不同,Godot 的 UI 系统是声明式的——你在场景中搭好节点树,引擎自动完成布局计算。 一、布局系统:Container 家族 Container 是 Godot UI 的 骨架 。它决定了子节点的大小和位...
学习
godot2026-07-09
Godot 4 自动地形系统(AutoTileSet / Terrains)完全指南
前言 在 2D 游戏开发中,地形瓦片(tile)的拼接是一个绕不开的问题。想象一下:你有一片草地、一条河流、一段平台——如果每一块边缘、角落、过渡区域都要手动选择对应的瓦片图,工作量将是巨大的。 自动地形系统 就是为了解决这个问题而生的。 一、什么是自动地形(Autotiling)? 自动地形的核心思想很简单: 你只管画,引擎帮你选对瓦片 。 当你在 TileMap 上绘制地形时,引擎会自动检测每个瓦片的上下左右邻居,然后根据预设的规则...
学习
godot2026-06-25
Godot 中 zindex 和 ysort 的区别总结
在 Godot 2D 游戏开发中,角色、树木、怪物、地面、技能特效、UI 都需要正确的显示顺序。比如角色走到树前面时,角色应该挡住树;角色走到树后面时,树又应该挡住角色。 这种显示顺序主要和两个概念有关: 和 。 其中 用来手动控制图层顺序, 用来根据物体的 Y 坐标自动排序。 一句话理解 是手动分层。 是根据 Y 坐标自动排序。 简单来说: | 属性 | 作用 | 适合场景 | | | | | | | 数值越大,显示越靠前 | 地面、...
学习
godot2026-06-11
Tileset 资源图的标准和规范
一、什么是 Tileset 资源图 Tileset,中文通常叫“图块资源图”或“瓦片图”,是 2D 游戏中非常常见的一种地图资源组织方式。 简单来说,Tileset 就是把很多小图块按照固定尺寸排列在一张图片里。游戏引擎会按照固定的格子大小去切割这张图片,然后把每个小格子当成一个独立的地图块使用。 比如一个 32×32 像素的 Tileset 中,每一个 tile 都是 32×32 像素。地图编辑器或游戏引擎会按照 32×32 的网格,...
学习
godot2026-05-29
用户角色精灵图制作角色的完整流程
在 2D RPG 游戏里,角色通常不是用一张单独图片完成的,而是用一张“角色精灵图”来做。 所谓角色精灵图,通常是一张包含多个动作帧的大图。比如角色向下走有 4 帧,向左走有 4 帧,向右走有 4 帧,向上走有 4 帧。Godot 会根据这些帧不断切换图片,看起来角色就动起来了。 这篇文章主要介绍:拿到一张角色精灵图之后,如何在 Godot 中把它做成一个可以正常移动、播放动画、和地图产生遮挡关系的角色。 一、先理解角色精灵图是什么 角...
学习
godot2026-05-26
Godot 节点系统详细介绍
Godot 里最核心的东西不是“类”,也不是“组件”,而是 节点 Node 。 你可以把 Godot 的节点理解成: 在前端里,一个页面是由很多 DOM 元素组成的; 在 Godot 里,一个游戏场景是由很多 Node 节点组成的。 比如一个玩家角色,可能不是一个单独对象,而是这样的结构: 这里的 是根节点,下面挂着显示图片、播放动画、碰撞检测、摄像机、音效等子节点。 Godot 官方文档也把节点和场景放在一起讲:多个节点组成树状结构后...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录