游戏管理器脚本
定义
游戏管理器脚本是这个 Godot 小游戏骨架中的中心控制脚本,用来统一处理两类核心循环:一是定时生成敌人,二是按存活时间累积分数并把结果显示到 HUD 上。
文中给出的实现直接 extends Node,说明它不是玩家那样的运动体,也不是敌人那样的碰撞区域,而是挂在主场景上的逻辑协调者。
在本文档中的语境
这个脚本出现在“阶段三:写核心脚本”中,配套的是一个非常小但完整的可运行原型:
- 玩家使用
CharacterBody2D实现移动,并被限制在屏幕范围内。 - 敌人使用
Area2D从屏幕右侧向左移动,碰到玩家后切到游戏结束场景。 - 游戏管理器则负责持续制造敌人,并把“活了多久”转成分数显示在界面上。
因此,游戏管理器脚本对应的是 Godot AI 原型闭环 中“最小可玩主循环”的管理层,而不是复杂的关卡系统、波次系统或状态机框架。
脚本结构
文中脚本如下:
extends Node
@export var enemy_scene: PackedScene
var score := 0.0
var is_game_over := false
func _ready() -> void:
$EnemySpawnTimer.timeout.connect(_spawn_enemy)
func _process(delta: float) -> void:
if is_game_over:
return
score += delta
$CanvasLayer/HUD/ScoreLabel.text = "分数: %d" % int(score)
func _spawn_enemy() -> void:
var enemy := enemy_scene.instantiate()
enemy.position = Vector2(
get_viewport_rect().size.x + 50,
randf_range(50, get_viewport_rect().size.y - 50)
)
add_child(enemy)
从这段代码可以明确抽出 3 个关键状态字段和 3 个关键方法。
关键字段
enemy_scene: PackedScene:导出的敌人场景引用。它不是敌人实例本身,而是一个可被instantiate()的场景资源。score := 0.0:分数,使用浮点数累计,表示玩家存活时间的连续值。is_game_over := false:游戏是否结束的布尔开关,用来阻止分数继续增长。
关键方法
_ready():初始化时连接定时器信号。_process(delta):每帧处理计分和 UI 刷新。_spawn_enemy():实例化敌人并放到屏幕右侧的随机高度位置。
关键机制或组成
1. 通过 PackedScene 生成敌人
@export var enemy_scene: PackedScene 的作用,是把敌人预制场景暴露给编辑器,以便在主场景里手动赋值。
脚本生成敌人时并不是写死某个节点,而是:
- 调用
enemy_scene.instantiate()创建一个新敌人; - 给这个新实例设置出生坐标;
- 再通过
add_child(enemy)把它加到当前场景树中。
这是一种典型的 Godot 场景实例化方式。它把“敌人长什么样、带什么碰撞体、挂什么脚本”封装在敌人场景里,而让 游戏管理器脚本 只负责何时生成、生成到哪里。
2. 用定时器信号驱动敌人刷新
在 _ready() 中,脚本执行:
$EnemySpawnTimer.timeout.connect(_spawn_enemy)
这行代码说明主场景中存在一个名为 EnemySpawnTimer 的定时器节点,而且其 timeout 信号会在定时到达时触发 _spawn_enemy()。
这意味着敌人生成不是在 _process() 里按帧判断,也不是在 while 循环里硬刷,而是交给 Timer 以固定节奏触发。这样更符合 Godot 的事件驱动结构,也更容易通过编辑器直接调整生成间隔。
3. 分数以存活时间累加
_process(delta) 中的核心逻辑是:
score += delta
这里的 delta 是每帧经过的时间,因此分数本质上不是“击杀数”,而是“生存秒数”的连续累加值。
但界面显示时又做了:
"分数: %d" % int(score)
也就是说:
- 内部计分保留浮点精度;
- HUD 上显示为整数;
- 玩家看到的是逐秒增长的整数分数,而不是带小数的时间。
4. HUD 分数显示路径是固定节点路径
脚本直接更新:
$CanvasLayer/HUD/ScoreLabel.text = "分数: %d" % int(score)
这表明主场景中至少存在这样一条节点层级:
CanvasLayer- 其下有
HUD - 其下有
ScoreLabel
并且 ScoreLabel 是一个具有 text 属性的文本节点。
这个路径属于当前原型里最直接的写法:简单、可读、上手快,但也依赖场景节点名称与层级完全匹配。
细节与边界
is_game_over 在给出的代码中主要用于阻止继续计分
_process(delta) 的开头先判断:
if is_game_over:
return
这表示一旦游戏结束,后续 score += delta 和 HUD 刷新都会停止。
但要注意,文中给出的敌人脚本在撞到玩家后执行的是:
body.queue_free()get_tree().change_scene_to_file("res://scenes/GameOver.tscn")
也就是说,当前示例里“游戏结束”的主要表现是直接切换到游戏结束场景,而不是在同一主场景内把 is_game_over 设为 true 后继续停留。因此,这个布尔值更像是为状态控制预留的接口,而不是当前演示流程中唯一的结束机制。
敌人出生位置有明确的数值边界
_spawn_enemy() 中敌人的位置被设置为:
x = get_viewport_rect().size.x + 50y = randf_range(50, get_viewport_rect().size.y - 50)
这两个数字 50 很关键:
- 横向上,敌人会出生在屏幕右边界之外 50 像素,避免一出现就贴在可视区边缘;
- 纵向上,敌人不会贴着最上方或最下方边缘生成,而是保留上下各 50 像素的安全边距。
因此,这个脚本不是“在任意随机位置刷敌人”,而是“只在右侧屏幕外、并在可视高度范围内留边距随机出生”。
该脚本依赖主场景节点结构正确存在
它至少依赖以下节点路径:
$EnemySpawnTimer$CanvasLayer/HUD/ScoreLabel