第一部分 10:Resource 资源系统

2026-05-19
612721 分钟
...

这一章讲 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.textureTexture2D
CollisionShape2D.shapeShape2D
AnimationPlayer.animationsAnimation

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.textureplayer.png
SpriteB.textureplayer.png

这没问题。

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

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

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

Slime1.statsslime_stats.tres
Slime2.statsslime_stats.tres
Slime3.statsslime_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 NodeTexture2D Resource
TileMapLayer NodeTileSet Resource
AnimationPlayer NodeAnimation Resource
CollisionShape2D NodeShape2D 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 官方文档也把节点和场景放在一起讲:多个节点组成树状结构后...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录