开发日志(二):把规则接入 Godot 对局

一、从对象到实际游戏流程

第二次提交新增 DarkForest/Game 和 Godot 场景。第一版已经能判定移动和战斗是否合法;这一步要把玩家输入、NPC 与怪物行动、地图显示和信息面板接入同一场对局。为协调这些流程,项目引入 GameController。

早期 GameSession 负责最小回合规则;GameController 则协调完整对局中的角色集合、随机数、地图、AI 控制器、战斗和玩家操作。本次还加入 AsyncGameSession 与 ActorState,分别组织阶段推进以及每名角色的计时、已知房间和已探索房间;StageMode 列出异步、同步、轮询三种游戏推进方式。正式对局因此需要新的调度逻辑。界面通过探索、移动、潜伏和战斗等命令调用控制器,而不直接修改 Character 状态。

GameController 需要按顺序完成多项操作:生成地图后放置角色,决定 AI 动作后推进阶段,战斗后处理死亡和掉落,并更新玩家可见信息。集中编排便于建立原型,但这些流程若持续增长,控制器也会难以修改和测试。

当前先保留一个可运行的控制入口。后续若测试显示对局调度、遭遇处理、物品交互或视图更新可以独立变化,再将对应职责拆出;拆分依据应是实际耦合,而非预先为每个动作增加一层接口。


二、主要新增类与场景的职责


三、从输入到状态再到画面

注 1.
输入到状态到画面的单向路径
玩家输入 / NPC 选择
→ GameController 校验与编排
→ GameSession / GameMap / Combat / Character 更新领域状态
→ MainUi / MapBoard / MiniMap 读取状态并刷新

这里的箭头表示调用与数据读取的职责,并不要求采用某种特定的单向数据流框架。界面发送命令、读取状态;规则层执行状态变更。若按钮直接修改血量,而战斗结算仍使用旧值,就会产生两个互不一致的结果。


四、运行时规则与验证

1. 固定地图规格

AEditionMap 将 A 版规格写成可执行配置:地图为 15×15,中央 5×5 区域不可进入;人类和怪物分别使用指定的候选出生点。仓库、卧室和密室按地图种子分配,房间类别再限制可能出现的卡牌及其收获等级。给定相同配置和地图种子,可以重现房间布局;这尚不等于重现完整对局。

GameMap 管理通行和邻接,AEditionMap 提供版本专属的区域、出生点与房间类型。这样更换地图规格时,通用地图类无须增加对每一种剧情房间的特殊判断。

2. 玩家输入之外的完整轮询

正式对局包含玩家、普通 NPC 和怪物。GameController.Start 创建地图、角色及其控制器;Advance(TimeSpan delta) 按选定模式推进时间和行动。在轮询模式下,轮到玩家时等待界面命令,轮到 AI 时由控制器依据难度和当前状态选择动作。装备、换弹、使用绷带、丢弃物品及伏击也进入同一流程,回合推进因而不能只按移动次数计算。

NPC 与怪物可以采用不同策略,但移动合法性和战斗结算仍应调用相同的规则对象。例如,怪物若绕过 GameMap.CanMove 自行移动,就可能穿过玩家无法通过的墙。控制器负责选择候选动作,地图和战斗对象负责执行与校验;新增 AI 类型时也沿用这条边界。

3. 从代码验证到场景验证

新增 Main.tscn、StartView.tscn 和 GameView.tscn,分别承担启动、配置输入和对局展示。逻辑测试能检查规则,运行场景能发现按钮连接与显示问题,两者不能互相替代。

4. 状态同步的代价

接入画面后,时间顺序与状态同步成为新的风险:NPC 何时行动、遭遇是否立即结算、怪物与普通 NPC 如何共享阶段,都需要明确约定。GameController 与界面若相互依赖过深,未来展示旧对局时可能被迫重新启动游戏流程。下一步因此需要记录可观察事件,并让画面能够读取既有状态。

5. 三种回合模式

StageMode 提供 Asynchronous、Synchronous 和 Polling 三种推进模式。异步模式为每名角色记录已流逝时间和下一次决策时刻;Advance(TimeSpan delta) 按时间顺序处理当前增量内到期的行动。同一时间片中若有多名角色行动,还须约定处理顺序,因为先后次序可能改变遭遇结果。同步模式让各角色在同一阶段规划后再结算,避免后决策者提前得知他人的选择。轮询模式按角色列表逐个处理,行动主体最明确。

本版所设置的 AI 难度影响的是其决策时间。SampleDecisionTime 把阶段时长分为三段:困难、普通、简单 AI 分别在前、中、后段随机选取决策时刻。因此难度变化会改变行动先后,并可能进一步改变房间抵达顺序与遭遇机会。

ActorState 分别保存每名角色的阶段、计时、预定动作、战斗选择,以及已知和已探索房间。控制器由此区分全局地图状态与角色的局部认知。例如 NPC 可能知道玩家尚未发现的房间,但玩家小地图只能读取自己的 PlayerSeen。StageVersion 则供界面判断可见状态是否需要刷新:即使地图结构不变,角色得到的新信息仍可能改变显示。

6. A 版地图

A 版配置固定为 15×15 的方格,中央坐标从 5 到 9 的 5×5 区域封闭。地图外缘与中央之间形成可走的环带,人类出生点设在五个指定坐标,怪物有两个指定坐标。版本配置还安排七个仓库、九个卧室、四个密室,并随机把两个候选人类出生点改成卧室。房间类别:仓库可出现长刀、左轮、子弹或消音器,卧室可出现左轮和子弹,密室可出现左轮、子弹或消音器;某些卡牌还要求更高的收获等级。

AEditionMap.Create(seed) 用地图种子决定房间类别;GameController.Start 另为行动和战斗创建运行时随机数。相同地图种子只能保证布局相同,不能单独保证 NPC 决策或整局结局相同。后续模拟器因此分别记录地图种子和过程随机种子,避免“复现地图”的同时还“复现对局”。

中央封闭区使坐标距离不再等于实际路径长度。怪物若只按曼哈顿距离接近声源,可能被挡在墙的一侧。MonsterController 从 GameMap.Neighbors 取得可走邻居,再按通行路径到暴露痕迹的距离选择下一步;没有痕迹时才在中央区域附近巡游。它还避开其他怪物已占据的下一格。这些选择都建立在地图的实际通行关系上。

7. 玩家看见的是线索

移动后,控制器除了更新坐标,还记录房间访问、活动痕迹与可能的灯光暴露。玩家走入近期有其他主体活动的房间,界面可以显示“有活动痕迹”;遇到死亡角色则提示尸体。它们都不同于直接揭示敌人的当前位置:线索可能滞后,也可能只有方向或强度。小地图与消息面板应投影玩家确实获得的信息,这样探索、潜伏和照明才有意义。

玩家长时间不下命令时也需要明确结果。TimeoutCurrentStage 在遭遇中把超时选择设为攻击,在行动阶段则令玩家原地潜伏。这个决定避免 UI 停滞让全局时间无法推进,但也影响生存策略,因此属于游戏规则而非单纯界面便利功能。未来若要改超时惩罚,应同时检查控制器、提示文案和自动化对局行为。

8. 普通 NPC 与怪物的目标并不相同

普通 NPC 的长期方向仍是探索未知房间;怪物更重视近期暴露痕迹。控制器用 ExposureTrace 记录位置、强度、原因和回合,怪物优先追踪沿可走通道距离较近的痕迹,同距离时优先较强的痕迹。没有可追踪目标时,它在中央封闭区附近移动,而不是像 NPC 一样系统地搜遍所有未探索房间。怪物战斗决策也更激进,原则上选择攻击;只有已经受伤且允许回退时,才保留少量回避机会。两类 AI 共享地图和战斗解析器,却拥有不同的目标函数,玩家才需要区分“有人在寻找物资”和“有东西正在追随声音”。

这里的追踪还有一个重要前提:痕迹随回合过期。GameController 在推进中清理旧活动与旧暴露记录,使怪物不会永远追着很久以前的一次枪声走。若以后把痕迹保留时间调长,怪物的压力会增加,但也更可能被过时信息牵引。

9. 一次玩家移动涉及的投影

以玩家从 A 房间进入 B 房间为例,控制器先用 GameMap.CanMove 判定是否合法,再清除潜伏状态并保存原位置,随后更新角色坐标、房间访问计数和玩家已知房间。若携带发光道具,还会在 B 房间产生暴露痕迹;若那里已有其他生物的活动记录或尸体,信息面板给出相应提示;最后检查是否与活着的角色发生遭遇。MapBoard 应画出新坐标,MiniMap 应扩展玩家可见区域,房间视图应更换地板和线索。一个“移动”按钮因此牵动规则、知识、感知与多处画面,不只是把图标平移一格。

这种链条也说明为什么仅测试 CanMove 不够。合法移动可能正确改变坐标,却忘记更新小地图;画面可以正确显示新房间,却忘记触发同房间敌人的遭遇。后续记录结构化事件时,可以让移动、访问、暴露和遭遇各有独立记录,帮助定位是哪一个环节缺失。