Godot 4 GDScript 教程 / 24 - 资源系统(Resource)
24 - 资源系统(Resource)
Resource 是 Godot 中最核心的基类之一。几乎所有游戏数据——纹理、音频、网格、脚本、场景——都是 Resource 的子类。理解资源系统是高效使用 Godot 的关键。
Resource 类详解
| 方法 | 说明 |
|---|---|
load(path) | 从文件加载资源(延迟加载) |
preload(path) | 编译时预加载资源 |
ResourceLoader.load(path, ...) | 高级加载(带缓存/线程控制) |
ResourceSaver.save(resource, path) | 保存资源到文件 |
resource.duplicate() | 复制资源(深/浅拷贝) |
resource.duplicate_deep() | 深拷贝资源及其子资源 |
💡 资源是引用计数的。只要有任何变量持有资源引用,它就不会被释放。这对于纹理和音频的内存管理至关重要。
自定义资源
基本自定义资源
# weapon_data.gd
class_name WeaponData
extends Resource
@export var weapon_name: String = "未命名武器"
@export var damage: int = 10
@export var attack_speed: float = 1.0
@export var range: float = 2.0
@export var icon: Texture2D
@export var attack_animation: StringName = &"attack"
@export_group("音效")
@export var swing_sound: AudioStream
@export var hit_sound: AudioStream
@export var crit_sound: AudioStream
@export_group("特殊效果")
@export var particle_effect: PackedScene
@export var status_effect: StatusEffect
func get_dps() -> float:
return damage * attack_speed
func _to_string() -> String:
return "WeaponData(%s, DMG:%d, SPD:%.1f)" % [weapon_name, damage, attack_speed]
在编辑器中创建资源
- 在文件系统面板右键 → 新建资源
- 搜索
WeaponData - 在 Inspector 中编辑属性
- 保存为
.tres文件
# 使用自定义资源
var sword = preload("res://data/weapons/sword.tres")
print(sword.weapon_name) # "铁剑"
print(sword.get_dps()) # 15.0
资源嵌套
# armor_data.gd
class_name ArmorData
extends Resource
@export var armor_name: String
@export var defense: int
@export var slot: ArmorSlot
enum ArmorSlot { HEAD, CHEST, LEGS, FEET, HANDS }
# character_stats.gd
class_name CharacterStats
extends Resource
@export var base_health: int = 100
@export var base_attack: int = 10
@export var base_defense: int = 5
@export var equipped_weapon: WeaponData
@export var equipped_armor: Array[ArmorData] = []
@export var skills: Array[SkillData] = []
func get_total_defense() -> int:
var total = base_defense
for armor in equipped_armor:
total += armor.defense
return total
⚠️ 注意:自定义资源类文件不能放在 res://addons/ 以外的自动加载路径中。通常放在 res://data/ 或 res://resources/ 下。
资源加载:load vs preload
# preload — 编译时加载,脚本解析时就加载完成
# 适用于已知的、必定使用的资源
const SWORD_SCENE = preload("res://weapons/sword.tscn")
const TEXTURE_ATLAS = preload("res://sprites/atlas.webp")
# load — 运行时按需加载
# 适用于可选资源或延迟加载
func load_weapon(weapon_id: String):
var path = "res://data/weapons/%s.tres" % weapon_id
if ResourceLoader.exists(path):
var weapon: WeaponData = load(path)
return weapon
return null
# ResourceLoader.load — 高级加载
func load_large_texture_async():
var request_id = ResourceLoader.load_threaded_request(
"res://textures/large_terrain.webp",
"", # type hint (可选)
true # use_sub_threads
)
# 轮询加载状态
while true:
var progress = []
var status = ResourceLoader.load_threaded_get_status(
"res://textures/large_terrain.webp", progress
)
if status == ResourceLoader.THREAD_LOAD_LOADED:
var texture = ResourceLoader.load_threaded_get(
"res://textures/large_terrain.webp"
)
return texture
elif status == ResourceLoader.THREAD_LOAD_FAILED:
push_error("资源加载失败")
return null
await get_tree().process_frame
| 加载方式 | 时机 | 适用场景 |
|---|---|---|
preload | 编译时 | 启动必须的资源 |
load | 运行时同步 | 按需加载小资源 |
load_threaded_request | 运行时异步 | 大型资源(纹理/音频) |
💡
preload在游戏启动时就加载,会增加启动时间。对于大量资源,建议使用load或异步加载。
资源保存
# 保存自定义资源到文件
func save_game_data(data: SaveData, slot: int):
var path = "user://saves/save_%d.tres" % slot
var error = ResourceSaver.save(data, path)
if error != OK:
push_error("保存失败: " + error_string(error))
else:
print("保存成功: ", path)
# 加载存档
func load_game_data(slot: int) -> SaveData:
var path = "user://saves/save_%d.tres" % slot
if not ResourceLoader.exists(path):
return null
return ResourceLoader.load(path)
# 保存为 .res 格式(二进制,更小但不可读)
func save_binary(data: Resource, path: String):
ResourceSaver.save(data, path, ResourceSaver.FLAG_COMPRESS)
.tres vs .res 格式
| 格式 | 特点 | 适用场景 |
|---|---|---|
.tres | 文本格式,可读,可 Git diff | 开发阶段、配置文件 |
.res | 二进制格式,体积小,加载快 | 发布版本、大型资源 |
.tres.remap | Godot 自动生成的重映射文件 | 导出后自动转换 |
资源缓存机制
# Godot 自动缓存已加载的资源
var texture1 = load("res://icon.svg")
var texture2 = load("res://icon.svg")
print(texture1 == texture2) # true — 同一资源对象
# 清除缓存(慎用)
func clear_specific_resource(path: String):
# 不直接支持清除单个资源缓存
# 但可以通过以下方式强制重新加载
var resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
# 检查资源是否被缓存
func is_cached(path: String) -> bool:
return ResourceLoader.has_cached(path)
# 强制重新加载
func reload_resource(path: String) -> Resource:
return ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_REPLACE)
⚠️ 注意:在开发编辑器插件时,如果修改了资源文件但编辑器显示旧版本,可以使用 CACHE_MODE_IGNORE 强制重新加载。
运行时资源生成
# 动态生成纹理
func generate_gradient_texture(width: int, height: int,
color_a: Color, color_b: Color) -> ImageTexture:
var image = Image.create(width, height, false, Image.FORMAT_RGBA8)
for y in range(height):
for x in range(width):
var t = float(x) / float(width)
var color = color_a.lerp(color_b, t)
image.set_pixel(x, y, color)
var texture = ImageTexture.create_from_image(image)
return texture
# 动态生成网格
func generate_terrain_mesh(heightmap: PackedFloat32Array,
size: int, height_scale: float) -> ArrayMesh:
var surface_tool = SurfaceTool.new()
surface_tool.begin(Mesh.PRIMITIVE_TRIANGLES)
for z in range(size - 1):
for x in range(size - 1):
var i0 = z * size + x
var i1 = z * size + x + 1
var i2 = (z + 1) * size + x
var i3 = (z + 1) * size + x + 1
var v0 = Vector3(x, heightmap[i0] * height_scale, z)
var v1 = Vector3(x + 1, heightmap[i1] * height_scale, z)
var v2 = Vector3(x, heightmap[i2] * height_scale, z + 1)
var v3 = Vector3(x + 1, heightmap[i3] * height_scale, z + 1)
# 三角形 1
surface_tool.add_vertex(v0)
surface_tool.add_vertex(v2)
surface_tool.add_vertex(v1)
# 三角形 2
surface_tool.add_vertex(v1)
surface_tool.add_vertex(v2)
surface_tool.add_vertex(v3)
surface_tool.generate_normals()
surface_tool.generate_tangents()
return surface_tool.commit()
# 动态生成音频
func generate_sine_wave(frequency: float, duration: float,
sample_rate: int = 44100) -> AudioStreamWAV:
var num_samples = int(sample_rate * duration)
var data = PackedByteArray()
for i in range(num_samples):
var t = float(i) / float(sample_rate)
var sample = sin(2.0 * PI * frequency * t)
var sample_int = int(sample * 32767)
# 16-bit PCM, little-endian
data.append(sample_int & 0xFF)
data.append((sample_int >> 8) & 0xFF)
var stream = AudioStreamWAV.new()
stream.format = AudioStreamWAV.FORMAT_16_BITS
stream.mix_rate = sample_rate
stream.stereo = false
stream.data = data
return stream
资源版本管理
# 版本化资源格式
class_name VersionedResource
extends Resource
const CURRENT_VERSION = 3
@export var version: int = CURRENT_VERSION
@export var data: Dictionary = {}
# 自动迁移旧版本
func migrate():
while version < CURRENT_VERSION:
match version:
1:
# v1 → v2: 重命名字段
if data.has("hp"):
data["health"] = data["hp"]
data.erase("hp")
version = 2
2:
# v2 → v3: 新增字段
if not data.has("mana"):
data["mana"] = 0
version = 3
print("资源已迁移到版本 ", CURRENT_VERSION)
💡 对于线上游戏,始终要设计好资源的前向兼容和版本迁移方案,避免老玩家存档损坏。
资源导入流程自定义
# custom_importer.gd
@tool
extends EditorImportPlugin
func _get_importer_name() -> String:
return "my_game.tileset"
func _get_visible_name() -> String:
return "Tileset Data"
func _get_recognized_extensions() -> PackedStringArray:
return PackedStringArray(["tileset"])
func _get_save_extension() -> String:
return "tres"
func _get_resource_type() -> String:
return "Resource"
func _get_preset_count() -> int:
return 1
func _get_preset_name(preset_index: int) -> String:
return "默认"
func _get_import_options(path: String, preset_index: int) -> Array[Dictionary]:
return [
{
"name": "tile_size",
"default_value": Vector2i(32, 32),
"property_hint": PROPERTY_HINT_TYPE_STRING,
},
{
"name": "auto_tile",
"default_value": true,
}
]
func _import(source_file: str, save_path: str, options: Dictionary,
platform_variants: Array, gen_files: Array) -> Error:
# 读取源文件
var file = FileAccess.open(source_file, FileAccess.READ)
if not file:
return FileAccess.get_open_error()
var json = JSON.new()
var error = json.parse(file.get_as_text())
if error != OK:
return error
var data = json.data
# 创建资源
var resource = TilesetData.new()
resource.tile_size = options.get("tile_size", Vector2i(32, 32))
resource.tiles = data.get("tiles", [])
# 保存为 .tres
return ResourceSaver.save(resource, save_path + ".tres")
资源最佳实践
实践指南
| 最佳实践 | 说明 |
|---|---|
| 使用类型提示 | var weapon: WeaponData = preload(...) 获得自动补全 |
| 资源复用 | 同一资源在多处引用,避免重复加载 |
| 分离数据与逻辑 | Resource 存数据,Node 存逻辑 |
使用 @export | 让资源可在 Inspector 中编辑 |
| 避免循环引用 | Resource A 引用 B,B 引用 A 会导致问题 |
| 使用类名 | class_name WeaponData 可以全局引用 |
游戏开发场景:装备数据系统
# equipment_system.gd
extends Node
var equipped: Dictionary = {} # slot -> EquipmentData
var inventory: Array[EquipmentData] = []
signal equipment_changed(slot: StringName, old_item: EquipmentData, new_item: EquipmentData)
func equip(item: EquipmentData) -> bool:
var slot = item.slot_name
var old = equipped.get(slot)
equipped[slot] = item
inventory.erase(item)
if old:
inventory.append(old)
equipment_changed.emit(slot, old, item)
return true
func unequip(slot: StringName) -> bool:
if not equipped.has(slot):
return false
var item = equipped[slot]
equipped.erase(slot)
inventory.append(item)
equipment_changed.emit(slot, item, null)
return true
func get_total_stats() -> Dictionary:
var stats = {"attack": 0, "defense": 0, "speed": 0}
for item in equipped.values():
stats.attack += item.attack_bonus
stats.defense += item.defense_bonus
stats.speed += item.speed_bonus
return stats