强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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]

在编辑器中创建资源

  1. 在文件系统面板右键 → 新建资源
  2. 搜索 WeaponData
  3. 在 Inspector 中编辑属性
  4. 保存为 .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.remapGodot 自动生成的重映射文件导出后自动转换

资源缓存机制

# 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

💡 扩展阅读