资讯动态

GameMaker复合状态判断:用with遍历实现全部隐藏联动

发布时间:2026/10/14 23:32:01 来源:尧图企业网站定制
在GameMaker开发里给对象做隐藏是最基础的操作但如果每一个物体都隐藏就把obj_fly_male_first也隐藏这种需求第一个版本大概率会写崩。这不是因为隐藏本身有多难而是每一个物体都隐藏本质上是一个复合状态拿单个事件的视角去逐个判断永远会漏掉状态变化的瞬间。我项目里这个obj_fly_male_first是一个飞行编队的组合容器需要伪装成编队成员全部隐身时容器本身也消失一开始用逐点埋线的方式写出现间歇性失效最后用GameMaker的with遍历做一个集中状态聚合一行判断就把逻辑理顺了。这篇把整个排查思路、最终方案和过程中踩掉的坑都记录一下如果你也在做组合角色、分组UI或者任何跟着一组对象状态走的联动逻辑可以直接套用。1. 问题还原当全部隐藏变成一个复合状态1.1 原始需求到底在说什么标题这句话其实包含了两个层级的信息。第一层是存在一组物体这组物体每个都有自己的隐藏状态第二层是当且仅当这组里每一个物体都被隐藏时另一个叫obj_fly_male_first的对象也要隐藏。注意每一个……都……这个表述它在逻辑上是一个全称判断翻译成逻辑表达式就是A1隐藏 AND A2隐藏 AND A3隐藏 ... AND An隐藏。这个AND运算的严格性导致它不能简单绑定在某个成员的事件里。举一个很常见的反例假设A1、A2、A3三个成员你在A1的隐藏代码后面直接写如果A2和A3也隐藏就隐藏obj_fly_male_first。这个判断在A1隐藏的同时执行但此时A2、A3可能还没隐藏所以判断结果是false。等过几个帧A2、A3陆续也隐藏了此时没有任何代码再去检查A1是否已经隐藏因为这段检查代码写在了A1的事件里A1不会再触发第二次。于是obj_fly_male_first就永远卡在可见状态。逻辑一点没错缺的只是在所有成员状态变化完毕后重新做一次整体判定。这也是GameMaker新手特别容易踩的一个点把状态判断塞进触发当前事件的实例里认为只要处理了触发者就能代表全局。真实项目里控制A1隐藏的可能是一个动画状态机控制A2隐藏的又是另一个战斗逻辑两者之间根本没有任何先后保证。你去A1里检查A2得到的结果只能是某一瞬间的切片不是稳定的最终态。1.2 为什么逐点埋线会时灵时不灵如果你也试过在每个成员的地方写同步判断大概率见过这样的现象手动测试时怎么都能触发放到真实流程里就时灵时不灵。原因很简单成员隐藏的顺序不固定。我把A组隐藏顺序从A1先藏、A2后藏改成反序之后原本正常的联动立刻失效。更麻烦的情况是某个成员隐藏前会先做一个淡出动画visible从true变为false发生在动画的最后几帧而另一个成员可能一条代码就瞬间隐藏。两个动作不同步你在淡出成员还在visibletrue的时候去做是否全部隐藏的检查自然得到false等它真正visiblefalse时检查时机已经错过了。这种事件驱动局部判断的组合在状态变化来自多路信号时本质上就是脆弱的。之前还见过一种写法在obj_fly_male_first自己的Step事件里每帧判断三个兄弟对象是否都隐藏。这个做法比埋点靠谱多了因为每帧都会重新读状态不会再错过。但随之而来的问题是如果兄弟对象本身是动态生成、会被销毁的直接访问变量会报错如果帧率高了、对象数量上百每帧遍历又显得浪费。所以这套方案的优化方向就是后面要讲的集中聚合。2. visible变量的真实作用域先消除三个认知误差2.1 visible只是绘制开关不是存活开关在GameMaker里把对象的visible设为false最直观的结果是该对象的绘制事件不再执行图形不会出现。但它仍然是一个活着的实例仍在参与Step事件、碰撞事件、各种其他事件仍然占用内存仍然可以被with找到。很多人把隐藏理解成消失这是第一个误差。这个误差会直接影响你的判断逻辑。比如你在检查是不是所有物体都隐藏了的时候如果某个对象其实已经不在场上被销毁但你又用了它的id去读取visible就会报错或者读到无意义的值反过来如果你把对象不存在脑补成对象隐藏了那判断结果又会偏。所以做这个需求前一定要先明确你的隐藏到底指visiblefalse还是指instance不存在。这两个状态在GameMaker里是完全不同的两件事。2.2 instance_exists检查的是存不存在不是显不显示那些用到数组引用或者动态对象id的项目随手就会写出if (obj_a ! noone)或者if (instance_exists(obj_a))来推断业务状态。但instance_exists只能回答这个对象的实例在不在房间里它回答不了这个对象是否可见。一个实例完全可以在instance_exists返回true的同时visiblefalse。反过来visibletrue的实例如果被销毁了instance_exists就是false但visible这个变量根本不存在你读取它就会出问题。判断目标用到的函数/变量含义常见误区对象是否存活instance_exists / id房间里有没有这个实例存不存在不等于显不显示对象是否绘制visible绘制事件是否执行visiblefalse不等于实例消失对象是否参与逻辑instance_deactivate事件是否全部暂停隐藏后仍然可能参与碰撞和逻辑所以在聚合判断里我把隐藏统一成两层定义目标对象存活且目标对象的visiblefalse或者目标对象已经不存活那从结果上看它也谈不上可见应该视为不会阻止容器隐藏。这就是后面代码里为什么除了visible判断还要加instance_exists兜底的原因。2.3 隐藏后碰撞还在别忽略逻辑残留最后一个是老生常谈但容易忘记的visiblefalse不会关闭碰撞检测。两个飞行编队成员都隐藏了如果它们还维持着物理碰撞或者命中判定玩家可能被看不见的东西挡路。这时候你的隐藏需求可能不只是visible的问题还涉及到instance_deactivate。不过本文的标题场景是只需要visible联动所以代码里我不会引入deactivate机制但你在真实项目里要主动想一下obj_fly_male_first隐藏后你是否希望它的碰撞、按键、AI逻辑也都停掉如果是建议在设置visiblefalse的地方同时调用instance_deactivate_self()如果只是图面上隐身逻辑还得保留那就只改visible。这一步想清楚后面聚合判定才不会做无用功。3. 集中判定方案一个with循环就把状态算出来3.1 基础写法全部隐藏的判断模板把前面的思路落地成代码核心就是一段with遍历。假设我需要检查的是三个兄弟对象obj_brother_a、obj_brother_b、obj_brother_c以及它们各自的多个实例最终控制obj_fly_male_first的visible。代码放在一个管理对象或obj_fly_male_first自己的Step事件中/// Step事件集中判定所有目标成员是否全部隐藏 var all_hidden true; with (obj_brother_a) { if (visible) { all_hidden false; } } with (obj_brother_b) { if (visible) { all_hidden false; } } with (obj_brother_c) { if (visible) { all_hidden false; } } if (all_hidden) { obj_fly_male_first.visible false; } else { obj_fly_male_first.visible true; }很多人第一次看到这段代码会有疑问all_hidden在最开始用var声明在with块里直接给它赋值改的到底是哪个变量答案是不会错。因为var声明的变量是局部变量它在整个脚本或事件作用域里都生效即使进入with块也不会被实例作用域覆盖修改的就是同一个局部变量。这里的关键是var三个字母不能省如果写成不带var的all_hidden true;那游戏会在当前实例上创建一个实例变量进入with后再赋值改的就是被遍历的子实例上的同名变量控制器里的all_hidden永远保持初始值整个判断直接失效。这个坑我后面的章节会再细说。3.2 按类型聚合动态实例组也能用用with的好处是它自动遍历该类obj的所有现存实例不管有多少。如果有更细粒度的分组比如只想把带有team_marker 1的实例纳入检查可以在with里加条件var all_hidden true; with (obj_brother_a) { if (team_marker 1 visible) { all_hidden false; } }注意这里如果team_marker只在部分实例上定义了没有定义的地方读取会报错可以先variable_instance_exists(id, team_marker)做一个兜底。实际项目里更常见的做法是把这些成员都挂在同一个父对象下用父对象名直接遍历。例如所有需要参与判定的兄弟对象都继承自obj_front_group那么代码可以简化成var all_hidden true; with (obj_front_group) { if (visible) { all_hidden false; } } obj_fly_male_first.visible !all_hidden;这个写法有一个明显好处当项目后面新增了同组物体只要挂在同一个父对象下这段代码一个字都不用改新成员自动纳入判断范围。对长期维护非常友好。3.3 判空与提前退出的取舍需要注意的是with (obj_front_group)在没有实例时不会进入块内all_hidden会保持初始的true结果就是把obj_fly_male_first直接隐藏。这个行为很可能不是你要的。如果场景里就是没有任何兄弟对象在场你真的希望容器也隐藏吗如果不是那初始值就要根据业务来定或者先判断一下这个组到底有没有活的实例var all_hidden true; var any_instance false; with (obj_front_group) { any_instance true; if (visible) { all_hidden false; } } if (any_instance all_hidden) { obj_fly_male_first.visible false; } else { obj_fly_male_first.visible true; }这段代码里我把没有任何实例单独当成一种例外情况处理。只有当组里至少有一个对象、且所有对象都隐藏时才联动隐藏容器。对于大多数组合角色场景这个语义更贴合直觉。至于性能with遍历是原生操作判断几十个实例每帧一次毫无压力不要担心。如果你实在想优化可以在确定某个实例visibletrue后提前退出但GML的with块里break会把整个事件退出容易误伤后续代码我的建议是少于100个实例就别整这些花活。提示在with块里面不要使用break来提前退出它会直接退出当前事件/脚本如果后面还有业务代码会被一起跳过。4. 触发时机与事件顺序这步选错了方案会前功尽弃4.1 推荐放Step事件别放Draw事件聚合代码写好后放哪里执行同样重要。我的建议是放在一个稳定的Step事件中最好是独立的管理对象或者obj_fly_male_first自己的Step事件。Step事件会每帧执行一次执行时机在实例更新逻辑阶段修改visible后到绘制阶段就能立刻生效。很多人图方便会把判定写进Draw事件或者End Draw事件这么做在大项目里会出问题。Draw事件的执行顺序在Step之后而且它本身是被visible控制的。你在Draw事件里改visibletrue或false本帧的绘制很可能已经做了预排序或者已经画完一部分结果就是画面闪烁一帧或者干脆这个对象自己把自己画没了。联动逻辑应该放在逻辑阶段而不是绘制阶段——这是GameMaker事件模型的一个基本常识但也是最容易被绕进去的地方。4.2 事件执行顺序带来的中间态问题即使你选对了Step事件还面临一个次级问题多个实例的Step事件在GameMaker里默认按实例创建顺序执行顺序并不是完全可控的。如果你的判定代码放在某一个成员自己的Step事件里而这个成员在Step顺序中排在最前面那它执行时有可能会在其他成员真正改变visible之前先读取状态读到一个中间态。举例A2和A3都在自己的Step事件里因为某种条件把visible设成false但它们的Step排在A1后面。A1在自己的Step里做聚合检查看到A2、A3还是visibletrue于是判定还没全隐藏但同一帧的稍后A2、A3就把自己隐藏了。帧末obj_fly_male_first保持可见。下一帧A1又会检查此时A2、A3已经是false于是才隐藏。结果就是容器比成员慢了一帧。单看一帧没什么大不了但如果项目里有频繁的显示/隐藏切换慢一帧会造成可见性闪烁或者碰撞判定时序错乱。解决这个中间态的思路是不要让参与状态变化的成员自己当裁判。新建一个管理对象或者在obj_fly_male_first自己的Step事件中做聚合检查而obj_fly_male_first本体的Step事件顺序可以通过深度设置、实例层等方式进行调整让它的Step在所有成员Step之后执行。如果不方便调顺序还有另一个取巧方案把聚合检查放到End Step事件中。End Step在所有普通Step之后执行这相当于天然地避开了成员间的执行顺序问题。代码逻辑不变只是换个事件挂载点中间态就消失了。这个技巧我用过好几次特别管用。5. 回头补的坑我在调试中遇到的三件麻烦事5.1 被误解的变量作用域with块里的赋值不是写到控制器上刚才提到过var的作用域这个坑我实际踩过一次。最早我写聚合代码时没有用var直接写all_hidden true; with (obj_brother_a) { if (visible) { all_hidden false; } }结果不管成员怎么隐藏all_hidden永远是true。排查了很久才发现with块里的all_hidden false写到了被遍历的子实例上控制器自己那个all_hidden根本没被改。这个行为是GameMaker作用域设计的特点with切换的是实例上下文未加前缀的变量赋值默认落到当前上下文。解决办法就是早期加上var声明或者在with里用other.all_hidden false显式写回控制器。用other的方式在嵌套循环里容易混乱不如var干净。我把这个原因写出来是希望你别在这上面浪费一晚上。5.2 对象被销毁时instance_exists要分开处理项目做到中期有些成员对象不再是常驻的而是刷怪或任务系统动态生成、动态销毁。比如某个成员在战斗结束时直接instance_destroy()而不是设置visiblefalse。这时候聚合代码直接遍历该对象的with不会报错因为GameMaker的with对已不存在的对象不会做任何事但不存在是被当成什么都查不到还是隐藏了取决于你代码里怎么定义初始值。我之前就遇到过所有成员都被销毁了obj_fly_male_first应该隐藏但我的聚合代码要求至少有一个实例存在才联动于是容器就一直亮着。后来在代码里加了剩余存活实例数量的判断var alive_count 0; var all_hidden true; with (obj_front_group) { alive_count; if (visible) { all_hidden false; } } if (alive_count 0 all_hidden) { obj_fly_male_first.visible false; } else if (alive_count 0) { // 按业务需求决定要不要隐藏我这里选择隐藏 obj_fly_male_first.visible false; }这么做以后无论是全部隐藏还是全部销毁容器都能正确联动。你在自己的项目里要根据业务决定清场后容器是否消失不要想当然。5.3 性能焦虑是多余的但可以加个频控有些朋友一看到每帧遍历就担心帧率。说实话GameMaker的with遍历效率很高几十个实例的visible判断一帧一次连零点几毫秒都到不了。我测试过一个场景里同时有5个组合容器、每个容器管30个成员总共150个实例聚合逻辑放在Step事件里跑Profiler显示这部分开销在0.1ms左右完全不用焦虑。如果你仍然想优化或者你的项目是把这套逻辑挂在几千个独立个体上那可以给检查加个频控比如每隔几帧才做一次完整判定。但要注意频控会让容器隐藏的响应最多延迟N帧得自己权衡。我一般只会在出现明显卡顿后才加这种优化不会被每帧遍历这件事吓到。如果需求是响应必须非常快但又不希望每帧全遍历还有一个事件驱动的计数器方案让每个成员在隐藏时给控制器发一个消息控制器维护一个当前隐藏数的变量当隐藏数等于组成员数时立刻让容器隐藏。这个方案省掉了遍历但要求所有成员隐藏/显示时都严格通知一次少通知一次状态就错乱。对调试不太友好我建议只在对象数量特别大的时候才考虑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑