第一部分 10:Resource 资源系统
这一章讲 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。如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录