文章 / Godot 技术实践(一):用场景树把游戏当成可组合的程序 / Godot 技术实践(三):用资源和组合把项目结构撑住

Godot 技术实践(三):用资源和组合把项目结构撑住

区分 Resource 与 Node、PackedScene 实例化和 Autoload 的适用边界,用组合而不是巨型脚本撑住中型 Godot 项目。

2026年9月1日10 min2936 words
Godot 技术实践(三):用资源和组合把项目结构撑住
图 1-1 cover.webp

角色能跑、关卡能摆之后,Godot 项目会进入第二种失控:数据写进节点、全局写成 Autoload、复制场景当“不同敌人”。表面上都能玩,但调数值要打开十几个 .tscn,改掉血要搜遍脚本。这一篇把边界划清楚:什么该是节点,什么该是资源,什么才值得进全局。

系列导航:上一篇:用状态机把 2D 角色做成可维护的循环|下一篇:从可玩到可发布的调试、性能与导出

一、Node 是运行时,Resource 是数据

节点活在场景树里,有 _process、进入退出树、父子关系。资源不进树,它是可以被多处引用的数据对象:数值、曲线、对话条目、掉落表、PackedScene 本身。

一个直接的对比:

问题放进节点放进 Resource
这一帧怎么移动是否
最大生命、移速、击退力度可以 @export,但敌人一多就该抽出来是
动画怎么播是片段名可以当数据
三只史莱姆共享同一套数值复制节点容易分叉同一份 .tres

资源像货架上的蓝图,节点是被摆进关卡的实例

class_name ActorStats
extends Resource

@export var max_health := 3
@export var speed := 180.0
@export var jump_velocity := -400.0
@export var knockback := Vector2(180, -220)

角色和敌人都只持有一份 ActorStats:

@export var stats: ActorStats

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

史莱姆 A 和史莱姆 B 引用同一资源时,改一处数值,两处生效。需要“精英史莱姆”时,复制资源而不是复制整棵角色树。官方对 Resource 的说明也强调:它是引用,不是节点的替代品。

自定义资源记得在脚本第一行写 class_name,并打开“资源”面板保存为 .tres。不要把大型数值表写进场景根节点的导出字段里,那会让关卡文件和平衡数据绑死。

二、用 PackedScene 生成,而不是在代码里搭树

运行时创建子弹、伤害数字、掉落物,用场景实例化:

@export var bullet_scene: PackedScene

func fire(towards: Vector2) -> void:
    var bullet := bullet_scene.instantiate() as Node2D
    bullet.global_position = $Muzzle.global_position
    bullet.rotation = towards.angle()
    get_tree().current_scene.add_child(bullet)

几个实践细节:

  1. instantiate() 之后先设属性,再 add_child()。 子节点 _ready() 发生在进树时;进树前把 direction、damage 赋好,监听方读到的就是最终值。
  2. 子弹不要挂在玩家脚下。 玩家 queue_free() 时会带走子节点。放到当前关卡、专门的 YSort 或 BulletContainer。
  3. 对象池是优化,不是默认。 先保证生命周期清楚,出现 GC 尖刺或频繁 instantiate 卡顿再池化。
  4. 导出 PackedScene,不要写死路径字符串。 preload("res://...") 适合确定的内部依赖;关卡可配置的内容用 @export,方便策划在检查器里换。

场景实例化是组合的运行时形态。编辑器里拖进去的 Player.tscn,和代码里 instantiate() 的子弹,是同一件事。

三、Autoload 只放真正全局的东西

Autoload 很香,也最容易变成“什么都往里扔的单例”。适合进 Autoload 的通常是:

  • 跨关卡仍然存在的进度、解锁、设置;
  • 音频总线控制、场景切换器;
  • 输入重绑定后的查询入口。

不适合进 Autoload 的是:

  • 当前关卡里的玩家引用;
  • 这一波敌人列表;
  • 某个 UI 弹窗的开关。

反例:

# 不要这样做
Game.player.take_damage(1)

关卡重载、玩家死亡、分屏或回放时,这个全局引用会过期。更好的做法是关卡持有玩家,UI 通过信号更新;确需广播时,用 Autoload 发事件,而不是让它拥有节点:

extends Node

signal player_hit(health: int)
signal scene_change_requested(path: String)

func request_scene(path: String) -> void:
    scene_change_requested.emit(path)

场景切换器监听 scene_change_requested,用 get_tree().change_scene_to_file() 或手动切换。Autoload 负责事件和数据,不负责“现在树上有谁”。

四、组合:把能力做成子节点或组件

Godot 没有 C# 那种强制接口体系,但组合非常自然。受伤、可对话、可推动,都可以是独立节点:

Player (CharacterBody2D)
├── Movement
├── Health
├── Hitbox
└── Sprite2D

Health 只管理生命并发出 died;Movement 只读输入写速度;根节点把它们接线。敌人复用 Health 和 Hitbox,换一套 ActorStats 和 AI 节点即可。

避免的结构是 Player 继承 Actor 继承 Entity 再继承 Moveable。场景树已经是组合容器,再用深继承,两种模型会打架。

信号接线优先在关卡或根场景完成,而不是每个组件去 get_parent()。组件对外只发信号,根场景决定谁听谁。这样 Health 可以原样丢进箱子、敌人、可破坏墙壁。

小结

中型 Godot 项目能走多远,取决于数据和对象有没有分开。数值进 Resource,可复用物体进 PackedScene,跨关卡状态才进 Autoload,能力用子节点组合。下一篇把这套结构送出编辑器:调试、性能和导出时,哪些设置会在发布当天才爆出来。

下一篇:Godot 技术实践(四):从可玩到可发布的调试、性能与导出

评论 (0)

加载评论中...
RELATED NOTES
03 entries
01Godot 技术实践(四):从可玩到可发布的调试、性能与导出阅读文章 →02Godot 技术实践(二):用状态机把 2D 角色做成可维护的循环阅读文章 →03Godot 技术实践(一):用场景树把游戏当成可组合的程序阅读文章 →