前面几十篇我们把「画面、手感、存档、上架」都铺了一遍,今天补一个每个游戏都必须有、却最容易做错的基础系统:暂停与时间控制。玩家按下 Esc,你以为只是弹个菜单——结果发现:计时器还在偷偷走、角色飘在半空、粒子还在喷、菜单按钮点不动、慢动作一开物理直接穿模。这些「暂停后依然在跑的东西」,就是今天要一次性关掉的洞。
今日速览
get_tree().paused = true不是「停止一切」:它只强制停掉 2D/3D 物理与碰撞检测,其余节点是否还处理,取决于各自的 Process Mode。- Process Mode 有五档:
Inherit(默认,看父/祖父)/Pausable/WhenPaused/Always/Disabled。暂停菜单的根节点要设 WhenPaused。 - 被暂停的节点,
_process / _physics_process / _input / _input_event都不再被调用,但信号(signal)照常触发——这是最容易踩的坑。 - 动画 / 音频 / 粒子会自动暂停并恢复,不用你手动处理;但物理服务器默认关闭,暂停中想让某个物体继续动,得用
PhysicsServer2D.set_active()。 Engine.time_scale是「变速齿轮」:影响 Timer、_process、_physics_process的 delta,但不影响音频速度,也不自动调整physics_ticks_per_second——慢动作要单独接音频与物理。
核心学习内容
1. paused 到底停什么:先划清边界
官方把暂停拆成三件事,理解了就不会懵:
- 物理层:
paused = true会直接停掉所有节点的 2D/3D 物理与碰撞检测。哪怕某个节点设了Always仍在处理,它的物理依然不会工作(因为物理服务器被关了)。 - 处理层:节点是否继续跑
_process/_physics_process/_input,由它自己的 Process Mode 决定。 - 自动暂停的对象:动画暂停当前动画、音频暂停当前流、粒子暂停——取消暂停后自动恢复,这部分 Godot 帮你做了。
一个反直觉但关键的点:信号仍然会触发。也就是说,暂停时 Timer.timeout 或你自定义的 signal 连着的函数照跑。如果你把「扣血」「倒计时」挂在 Timer 上,暂停时它可能还在走——这就是”暂停不干净”的元凶之一。
2. Process Mode 五档:一张表选对
| 模式 | 含义 | 典型用途 |
|---|---|---|
| Inherit(默认) | 跟随父节点;一路往上找不到非 Inherit 的,就跟随 SceneTree.paused | 普通游戏内容 |
| Pausable | 只在未暂停时处理 | 明确要停的游戏逻辑 |
| WhenPaused | 只在暂停时处理 | 暂停菜单 / 设置面板的根节点 |
| Always | 无论暂停与否都处理 | 全局输入、音乐、转场、Debug 工具 |
| Disabled | 完全停止处理 | 临时冻结的对象 |
暂停菜单的标准做法:把菜单根节点的 Process Mode 设为 WhenPaused,这样它和它的所有子节点在暂停时才工作(按钮、动画都正常),未暂停时”隐身”。然后用 _input 或 _unhandled_input 监听 Esc 来切换。
3. 让 Esc 在暂停时也能被听到
新手最常卡的点:给菜单脚本写了 _input,结果暂停后按 Esc 没反应——因为托管游戏内容的节点已被暂停。解法是把「监听菜单键」的逻辑放进一个 Always 的节点,最省事的就是在 Autoload 单例里处理,或给菜单根节点自己设 Always 并在内部判断状态。
# Autoload: PauseManager.gd —— process_mode = Always
extends Node
var _menu: CanvasLayer
var _paused := false
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS # 关键:暂停后仍处理输入
_menu = preload("res://ui/pause_menu.tscn").instantiate()
add_child(_menu)
_menu.hide()
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"): # Esc / 手柄 Start
toggle()
func toggle() -> void:
_paused = not _paused
get_tree().paused = _paused
_menu.visible = _paused
get_tree().root.propagate_notification(NotificationWMWindowFocusIn) # 可选:刷新 UI
4. Engine.time_scale:慢动作 / 子弹时间 / 命中停顿
Engine.time_scale 是「游戏时钟相对真实时间的倍速」:2.0 两倍速,0.5 半速。它会影响 Timer、SceneTreeTimer,以及所有用到 delta 的模拟(_process / _physics_process)。
做「击杀慢动作」的典型写法:
func hit_stop(duration := 0.12, scale := 0.05) -> void:
Engine.time_scale = scale
# 用 ignore_time_scale = true,让"真实时间"到点后恢复,否则计时会被自己拖慢
await get_tree().create_timer(duration, true, false, true).timeout
Engine.time_scale = 1.0
三个必须记住的坑:
- 坑一 · 计时器会被自己拖慢:
create_timer默认受time_scale影响。要按”真实时间”恢复,必须把第 4 个参数ignore_time_scale设为true,否则慢动作 0.1 秒会被拉成 2 秒。 - 坑二 · 音频不跟着慢:
time_scale不影响音频播放速度。要一起变调,得单独设AudioServer.playback_speed_scale(例如设为和 time_scale 同值),BGM 才会跟着”下坠”。 - 坑三 · 物理精度会掉:
time_scale > 1.0时每个物理 tick 会覆盖更长的时间,模拟变糙;大幅加速时应同步提高Engine.physics_ticks_per_second。另外官方建议尽量别把 time_scale 设成 0.0(行为可能变得不可预期),真想冻结逻辑,用paused更干净。
一句话记忆:“停”用 paused(结构化、干净),“慢”用 time_scale(连续、要自己管音频和物理)。两者可以叠加——暂停菜单弹出时 paused=true,处决动画慢放时 time_scale=0.2。
🎯 DeepSeek 提示词
提示词 1 · 生成一个「暂停菜单 + PauseManager」完整骨架
适用场景:游戏里还没有暂停功能,想让 DeepSeek 一次给出可用的场景结构与脚本。
你是一位资深 Godot 4 游戏开发者。请帮我实现一个「暂停系统」。
【目标】
一个 Autoload 单例 PauseManager,负责:切换暂停、显示/隐藏暂停菜单、处理 Esc/手柄 Start 输入。
【环境约束】
- Godot 4.x,GDScript,2D 游戏
- 暂停时:游戏物理与逻辑停止,但暂停菜单与背景音乐继续运行
- 输入用 InputMap 动作名 "ui_cancel",不要写死键码
- 暂停菜单放在 CanvasLayer 里,根节点 Process Mode 设为 WhenPaused
【输出格式】
1. 场景树结构(文字描述层级,标注每个关键节点的 Process Mode)
2. PauseManager.gd 完整脚本,含详细中文注释
3. 暂停菜单 root 脚本(继续 / 设置 / 退出三个按钮)
4. 一段"如何接入现有游戏"的 3 步说明
【硬性要求】
- 暂停/恢复都用 get_tree().paused 切换,并同步菜单显隐
- 不要用 Timer 做倒计时(避免暂停后仍在走)
- 脚本可直接复制运行,不要省略任何函数
使用说明:把生成结果里的场景树照着搭一遍,给”游戏内容根节点”和”暂停菜单根节点”分别设好 Process Mode(前者 Pausable/Inherit,后者 WhenPaused),Esc 就能用了。
提示词 2 · 实现「命中慢动作(Hit-Stop / 子弹时间)」
适用场景:想要击打”有重量”、处决/闪避时短暂慢放,但不确定 time_scale 的坑。
你是 Godot 4 专家。请实现一个可复用的「时间缩放管理器 TimeControl」。
【功能需求】
- hit_stop(duration, scale):瞬时把时间拉到 scale,duration 秒后恢复正常
- slow_motion(duration, scale):平滑地进入慢动作再平滑恢复(用 Tween)
- 支持"慢动作与暂停叠加":暂停时不生效、恢复后继续
- 处理真实时间:恢复计时必须不受 Engine.time_scale 影响
【必须覆盖的细节,请在注释里说明】
- 为什么 create_timer 要传 ignore_time_scale = true
- 音频如何同步减速(AudioServer.playback_speed_scale)
- time_scale > 1.0 时物理精度问题与 physics_ticks_per_second 的关系
- 为什么不建议把 time_scale 设为 0
【输出格式】
1. TimeControl.gd(Autoload)完整脚本
2. 一个调用示例:玩家击杀敌人时触发 0.12 秒的 0.05 倍慢放,同时音频减速
3. "常见错误写法 vs 正确写法"对照表,至少 3 组
使用说明:把 TimeControl 注册为 Autoload,在造成伤害/击杀的位置调 TimeControl.hit_stop(0.12, 0.05) 即可,注意别在每帧里反复调用。
提示词 3 · 用 DeepSeek 排查「暂停之后还在动」类 bug
适用场景:暂停后仍有东西在跑(计时器、Tween、粒子、协程、敌人 AI),让 DeepSeek 当 debug 助手定位。
你是 Godot 4 调试专家。我遇到"暂停不干净"的问题,请帮我定位根因并给出修复。
【现象】
(在这里粘贴你的现象,例如:暂停菜单弹出后,敌人还在原地匀速移动 / 倒计时还在减少 / 血条还在缓慢回血)
【我的相关代码】
(粘贴相关脚本与节点 Process Mode 设置)
【请按以下结构回答】
1. 最可能的 3 个根因,按概率排序,每个说明"为什么会绕过 paused"
2. 逐个给出最小验证方法(打印什么、看哪个属性)
3. 修复方案:优先用"结构化修复"(Process Mode / 换掉 Timer / 用 signal 解耦),而不是打补丁
4. 输出一份"暂停自检清单":一个 checklist,逐项确认哪些东西该停、哪些该继续
【约束】
- 只诊断我贴的代码,不要凭空假设项目结构
- 若信息不足,先列出需要我补充的信息,再给初步判断
- 代码用 Godot 4.x GDScript
使用说明:把真实报错现象和代码贴进去,重点看它给的”暂停自检清单”——把它固化成版本发布前的一个检查项,比每次现场找 bug 高效得多。
学习心得
暂停与时间缩放,看着是小功能,其实是游戏状态管理的第一课。它逼着你想清楚:“哪些是游戏世界(该停),哪些是系统层(该一直跑)“——这个边界一旦划清,输入、UI、音频、存档、联机都会跟着变干净。
对「用 DeepSeek 做一款能上架 Steam 的游戏」这个目标来说,它还有两个实际好处:
- 上架体验分:暂停菜单是最早被玩家点到的界面之一。暂停后画面”冻结但干净”、Esc 灵敏、音量条还能调,是”这游戏做得挺用心”的第一道信号。
- 提示词训练场:暂停系统非常适合拿 DeepSeek 练手——它是一个边界清晰、约束明确的小功能,能让你练习”把需求拆成:角色 + 任务 + 输入输出 + 约束”,这正是后面写更大系统(关卡、Boss、联机)时的通用套路。
今天学到的最大一条:“停”和”慢”是两套机制,别混用。用 paused 做结构化暂停,用 time_scale 做连续变速,各管各的,代码就不会乱。
明日关注
- 把暂停菜单与现有 HUD 接起来,确认 Esc / 手柄 Start / 焦点导航(键盘上下选按钮)都能用。
- 给”处决”或”受击”加一次 0.1 秒内的 hit-stop,实测手感差异,并同步
AudioServer.playback_speed_scale。 - 整理一份”暂停自检清单”(Timer / Tween / 协程 / 粒子 / 音频 / AI),作为版本发布前的固定检查项。
本文由自动化学习流水线每日生成,坚持即学即记。
评论