《Defold游戏开发入门》2.3 游戏机制与逻辑构建

以状态、事件、计时器、UI、分数和关卡为主线,搭建可验证、可扩展的 Defold 游戏机制。

2.3 游戏机制与逻辑构建

美术和移动都已经存在时,游戏仍可能不像一个游戏:它没有清楚的开始与结束,没有可以学习的风险,没有稳定的计分,也没有让玩家理解当前处境的界面。机制层负责把孤立的对象编排成一次完整体验。本章用“短局制收集游戏”说明如何组织主循环、状态、事件、UI、分数和关卡,而不把所有规则塞进一个脚本。

先定义可玩的循环

写代码前用一句话描述循环:玩家在限定时间内移动并收集目标,躲避危险,达到目标分数后进入下一关,生命耗尽或时间结束则结算。然后把它拆成可观察状态:bootmenuplayingpausedvictorygame_over。状态不是画面名称,而是系统允许什么行为的合同。例如 playing 才推进敌人和计时,paused 不处理世界输入但可处理菜单,game_over 只允许重试或返回菜单。

游戏控制器应拥有这份全局状态,并通过消息通知其他对象。进入 playing 时通知生成器开始、HUD 显示、玩家取得输入焦点;离开时取消计时、停止生成、清理临时对象。不要让每个敌人自己猜测“游戏是否暂停”,也不要只靠把画面盖上一层半透明面板来实现暂停。可见结果和规则状态必须一起改变。

帧循环、计时器与确定性

update(self, dt) 是持续行为的地方,但它不是万能的日程表。倒计时、冷却和插值适合由 dt 累积:self.remaining = math.max(0, self.remaining - dt)。一次性延迟动作,如两秒后生成第一波敌人,适合定时器或明确的调度器。无论使用何种方式,进入暂停或场景卸载时都要知道它是否仍会触发;遗忘的回调是“离开关卡后还在刷怪”的常见原因。

不同对象的更新顺序不应被当作规则依据。如果子弹和敌人都在 update 中移动,不能假设谁先执行。需要严格顺序的情况使用消息、统一控制器,或让物理系统产生碰撞结果后再处理。固定时间步的物理计算也应与纯视觉动画区分:前者追求稳定模拟,后者追求平滑呈现。把随机数、计时和状态变化记录下来,会使偶现 bug 更容易复现。

事件模型:让事实流向系统

把关键交互表达为事件:coin_collectedplayer_hittimer_expiredgoal_reached。事件的发送者描述已经发生的事实,控制器根据当前状态决定后果。比如金币在 playing 状态下被收集,控制器增加分数、通知 HUD、检查关卡目标;若游戏已结束,同一事件应被忽略。这样可以避免对象在错误状态下继续修改分数。

事件数据应足够用于决策,但不过度携带内部实现。{ points = 5, combo_eligible = true } 是稳定契约;把整张敌人内部状态表传给 HUD 则会造成耦合。对重要事件建立命名表或常量模块,避免脚本间因拼写不同而悄悄失效。事件日志也很有用:在开发构建中记录状态转移、分数变更和关卡完成原因,能迅速发现重复派发。

UI 不只是贴在画面上

GUI 应展示玩家需要决策的信息:当前分数、剩余时间、生命、目标、暂停选择和交互反馈。信息层级比装饰更重要。最高优先级的生存信息放在稳定位置,文字与背景保持对比,按钮有正常、按下、禁用等明确反馈。不同长宽比下,GUI 节点应通过布局、锚点和调整策略保持可读,不要只在自己的电脑窗口中摆正。

UI 脚本接收来自控制器的显示消息,例如 score_changedshow_pause,再把数据转化为文本或动画。按钮则发送语义事件,如 resume_requestedrestart_requested,由控制器确认是否允许。这个双向边界能防止 UI 直接篡改规则。所有可点击元素还要考虑输入焦点、触摸区域和重复点击;在按钮按下后暂时禁用或等待状态确认,可避免连续创建多个场景。

分数、生命与数值设计

分数不是简单的整数变量,而是反馈系统。它告诉玩家当前行为是否正确,也可驱动连击、奖励和关卡门槛。把分数变化集中在控制器中:提供一个受约束的 add_score 路径,记录原因,更新 HUD,并在必要时触发里程碑。不要让任意对象都可直接写 self.score = self.score + x,否则重复碰撞或消息重发很快会产生难查的数值错误。

生命、无敌帧和伤害也需要明确规则。玩家受到伤害后,是立即死亡、扣生命并击退,还是短暂无敌?无敌期间同一碰撞是否应播放反馈但不扣血?把这些决定写成状态和持续时间,而不是散落在多个 if 中。调数值时不要只改一个常量;同时记录目标体验,例如“新手应能在首关存活约四十五秒”,再根据测试数据调整敌人密度、伤害和补给。

关卡、保存与重开

关卡应有独立的输入、初始布局、完成条件和清理边界。小项目可以把每关做成集合,控制器加载当前索引;可重用的规则留在共享系统中,关卡特有配置放在数据或集合属性中。进入关卡时建立一次性状态,离开时删除动态对象和定时器。重开不是“把变量归零”这么简单,它应重新走一条可预测的初始化路径,避免上局残留的事件、声音或引用影响新局。

保存数据应只包含真正需要跨会话保留的内容,如解锁关卡、设置、最高分和教程进度。不要把整套运行时对象状态直接序列化为存档;它会与版本变化紧密耦合。为保存数据提供版本号和默认值,未来新增字段时才能兼容旧玩家。涉及排行或付费等可信数据时,客户端数据不能被当作最终事实,必须设计服务端校验。

可测试的机制清单

对每个状态写出进入条件、允许输入、允许事件、退出条件和画面效果。对每个事件写出发送者、数据、接收者、在不同状态下的行为。然后做“异常路径”测试:暂停时收集物碰撞;倒计时归零与最后一枚金币同帧发生;重开后旧定时器触发;按钮被快速连点;场景切换时玩家仍持有方向键。优秀的机制代码并不是只在正常流程中工作,而是在这些边界下仍然可解释。

本节练习是完成一个两关收集游戏:开始菜单进入第一关,收集目标后结算并进入下一关,倒计时或生命归零后显示重试,暂停时世界逻辑冻结,HUD 始终显示正确值。先用占位资源完成全部规则,再邀请他人试玩并记录三个最困惑的时刻。那些困惑往往比“功能有没有做完”更能指导下一轮设计。

一个可追踪的状态转移表

把状态机写成表格或文档,而不只写在脑中。对于案例游戏,menu 接受开始和设置请求;loading 接受加载成功或失败;playing 接受暂停、受伤、收集、完成和超时;paused 接受继续或退出;result 接受重试、下一关或返回菜单。每一行都应指定进入动作、离开动作和禁止的事件。这样“玩家死亡与时间结束同一帧发生”不再是模糊的偶然,而是一个可以事先定义优先级的转移。

状态转移函数最好成为唯一入口。它先检查当前状态是否允许目标状态,随后停止旧状态的计时器和输入,写入新状态,最后发出显示和系统通知。不要允许任意脚本直接改 self.state;这样会绕过清理与日志。开发构建中记录 old -> new、触发事件和时间戳,出现“卡在暂停界面”时便能从日志还原路径。对非法转移使用断言或显眼警告,宁可在开发时早暴露,也不要静默忽略后留下半初始化场景。

时间系统的边界

游戏里通常同时存在多种时间:真实时间、可暂停的游戏时间、动画时间、冷却时间和 UI 过渡时间。把它们全部用一个 dt 推进,会在暂停、慢动作和后台恢复时产生混乱。至少应明确哪些计时器随游戏暂停,哪些界面动画仍继续,哪些必须在场景销毁时取消。将计时任务登记到拥有它的对象,避免匿名回调在对象不存在后修改旧状态。

倒计时的显示还要考虑舍入。内部可以保存精确秒数,UI 决定展示 ceilfloor 还是格式化后的分秒;不要用显示文本再反解析回规则数值。超时判断应使用内部数值并确保只发生一次。若最后一秒内同时达成目标,按体验决定“胜利优先”或“超时优先”,并将此决定写入测试用例。

UI 交互的失败模式

界面需要处理玩家并不按预期操作的情况:快速连点、在过渡动画中按返回、窗口失焦、触摸从按钮内滑出、设置修改后立即退出。每个按钮点击应有防抖或状态校验,屏幕转换期间不应留下两个同时可点的层。文本更新应来自单一显示模型,避免分数在 HUD 与结算页各自计算导致不一致。对关键操作给出声音、视觉或加载反馈,让玩家知道输入是否被接收。

可访问性同样属于机制。颜色不能是生命或敌友的唯一信号;倒计时可提供音效或形状变化;触摸按钮需要足够大的热区;文字要在最小屏上读得清。把这些要求纳入 GUI 的验收,而非等到设计结束后补救。好的 UI 不是把所有数值显示出来,而是在正确时机让玩家得到足以做决定的信息。

从试玩数据回到规则

试玩记录应区分事实与解释。事实包括第几秒死亡、在哪个位置停留、按了几次暂停、是否完成教程;解释可能是“玩家不理解目标”。先收集多个事实,再提出假设,例如目标文字被背景淹没、敌人生成缺少预警或重试按钮太小。每次只改变一个假设相关变量,并比较完成率、平均时长和主观反馈。否则即使结果变好,也无法知道是哪项改动起作用。

当机制需要扩大时,优先增加可组合的规则,而不是堆叠特殊分支。例如连击可以作为分数系统对连续事件的一个修饰器,关卡目标可以作为控制器的配置,挑战模式可以重用同一状态机但换一份数据。能用已有事件和状态表达的新内容,往往比新建一条绕开架构的捷径更可靠。

机制完成的定义

当一个机制“看起来能用”时,再问四个问题:能否从任意允许状态进入并离开;相同事件重复到达是否仍正确;存档、重开和场景切换后是否仍正确;玩家是否能从画面和声音理解结果。以拾取为例,必须验证触发器与多个对象重叠、拾取时暂停、拾取后立刻过关、拾取动画尚未结束就重开等路径。通过这些问题的机制,才值得被其他系统依赖。

把验收步骤写在项目中并定期执行。随着新道具、新关卡和新 UI 加入,旧假设会被打破;一份短小的机制回归清单能在问题还局部时发现它。工程结构的价值,正是在持续变化中保持这份可验证性。

机制设计还应预留观测点:每局开始、状态切换、关键事件、结算和异常路径都能被记录或显示。观察点不是为了收集无尽数据,而是为了在玩家反馈“有时不对”时,能还原当时的状态与事件顺序。可观察的系统才是可迭代的系统。

用规则防止重复与竞态

当多个事件在同一帧到达时,使用“事件只消费一次”的标记或状态转换来防止重复结算。例如进入 result 后,所有后续的 player_diedtimer_expiredgoal_reached 都只被记录,不再改变结果;重开时创建新的局编号,旧编号的延迟事件直接丢弃。这样的局编号也有助于排查场景切换后仍收到旧消息的问题。

对会累积的值,如分数、连击、经验和库存,明确每次变化的来源、上限、下限和展示格式。把数值裁剪放在集中函数中,避免负分、超过最大生命或 UI 与内部值不一致。机制复杂以后,最危险的不是一个明显崩溃,而是数值在少见路径中悄悄偏离;集中规则和事件日志是最有效的防线。

若未来要加入教程、成就或每日挑战,也不要让它们直接篡改流程。教程订阅状态与事件并给出提示,成就观察已验证事实,挑战替换关卡数据和目标条件。保持核心循环不被横向功能侵入,项目才能在内容增长时仍然稳定。

当不同系统需要同一份只读信息时,例如当前关卡号、目标分数或难度档,不要让它们各自推算;由控制器发布一次明确状态或提供受约束的查询接口。单一事实来源能避免 UI、存档和规则在边界条件下产生不同答案。

将关卡切换、暂停、结算等全局操作集中处理,还能使音频、输入焦点和动态对象的清理顺序保持一致。玩家感受到的“流程顺畅”,本质上来自这些看不见的状态边界没有遗漏。

每次新增一种全局状态,都更新转移表、冒烟测试和 UI 文案,三者必须共同演进。

这能把设计意图留在代码之外仍可查证的地方。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「defold」更多文章