前面几十篇我们把「画面、手感、存档、上架」都铺了一遍,今天补一个每个游戏都必须有、却最容易做错的基础系统:暂停与时间控制。玩家按下 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 到底停什么:先划清边界

官方把暂停拆成三件事,理解了就不会懵:

  1. 物理层:paused = true 会直接停掉所有节点的 2D/3D 物理与碰撞检测。哪怕某个节点设了 Always 仍在处理,它的物理依然不会工作(因为物理服务器被关了)。
  2. 处理层:节点是否继续跑 _process / _physics_process / _input,由它自己的 Process Mode 决定。
  3. 自动暂停的对象:动画暂停当前动画、音频暂停当前流、粒子暂停——取消暂停后自动恢复,这部分 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 的游戏」这个目标来说,它还有两个实际好处:

  1. 上架体验分:暂停菜单是最早被玩家点到的界面之一。暂停后画面”冻结但干净”、Esc 灵敏、音量条还能调,是”这游戏做得挺用心”的第一道信号。
  2. 提示词训练场:暂停系统非常适合拿 DeepSeek 练手——它是一个边界清晰、约束明确的小功能,能让你练习”把需求拆成:角色 + 任务 + 输入输出 + 约束”,这正是后面写更大系统(关卡、Boss、联机)时的通用套路。

今天学到的最大一条:“停”和”慢”是两套机制,别混用。用 paused 做结构化暂停,用 time_scale 做连续变速,各管各的,代码就不会乱。

明日关注

  • 把暂停菜单与现有 HUD 接起来,确认 Esc / 手柄 Start / 焦点导航(键盘上下选按钮)都能用。
  • 给”处决”或”受击”加一次 0.1 秒内的 hit-stop,实测手感差异,并同步 AudioServer.playback_speed_scale。
  • 整理一份”暂停自检清单”(Timer / Tween / 协程 / 粒子 / 音频 / AI),作为版本发布前的固定检查项。

本文由自动化学习流水线每日生成,坚持即学即记。