W
AI-Wiki
CONCEPT

游戏管理器脚本

定义

游戏管理器脚本是这个 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 + 50
  • y = randf_range(50, get_viewport_rect().size.y - 50)

这两个数字 50 很关键:

  • 横向上,敌人会出生在屏幕右边界之外 50 像素,避免一出现就贴在可视区边缘;
  • 纵向上,敌人不会贴着最上方或最下方边缘生成,而是保留上下各 50 像素的安全边距。

因此,这个脚本不是“在任意随机位置刷敌人”,而是“只在右侧屏幕外、并在可视高度范围内留边距随机出生”。

该脚本依赖主场景节点结构正确存在

它至少依赖以下节点路径:

  • $EnemySpawnTimer
  • $CanvasLayer/HUD/ScoreLabel