« 在 Lua 中定义类型的简单方法 | 返回首页

立即模式下的鼠标交互处理

最近在做游戏时,发现在立即模式下鼠标的交互部分实现的比较混乱。

在做引擎时,我简单留出了鼠标相关事件的 callback 接口。一开始写游戏时,也就是对付一下写了几行代码,大致可以工作。做了大半个月后,随着交互界面越来越复杂,那些应付用的代码明显不堪重负。有越来越多的边界情况无法正确处理。等到最近想在交互上加一种长按鼠标确认的操作,发现不能再这样对付下去了,就花了一晚上重构了所有和鼠标交互相关的代码。

之前的问题出在哪里?

如果从系统的鼠标消息出发,我们可以从引擎获得鼠标移动、按下、抬起等事件。或许还可以利用系统发送来的点击、双击等等复合事件,但我选择自己用按下和抬起来判断这些。但是,消息机制本身和立即模式是相悖的。采用立即模式编写游戏业务以及交互,获得当下的状态是最自然的,而“消息”不是状态。它是一个队列:在每个游戏帧中,可能没有消息,也可能有多条消息。如果只是处理鼠标的位置和按键状态,那么保留最后一个状态也可以;但是,像点击这种行为,明显不是瞬间的状态,而是过去一段时间的状态叠加后的事件。

除了“点击”,必须处理的还有“焦点”,或叫“悬停”。一个交互元素获得焦点、失去焦点都不是瞬间状态,它们取决于鼠标过去的位置、鼠标位置下交互元素在屏幕上的位置。即使鼠标不移动,但交互元素在屏幕上动了,或消失了、出现了,都可能引起“焦点”的改变。

所以在立即模式下,最好我们可以将点击和焦点这样的“事件”变成某种“状态”,然后统一用立即模式处理。否则混用立即模式的状态判断和消息队列轮询就会比较混乱。

首先,我们应该把系统传来的鼠标消息在帧间累积起来,然后在每个游戏帧发送出去,而不应该在消息抵达的时候立即处理。这样做可以让游戏代码严格的按帧执行,帧间不会触发任何额外的 callback 。所以,在立即模式下,底层传来的不是鼠标移动消息,而是每帧鼠标的位置。即使没有更新位置,也同样会刷新一次鼠标位置信息。如果两帧之间有多个鼠标移动消息,位置当然只需要记录最后一次,游戏可以忽略中间的轨迹(除非以后要做鼠标手势,那再来改进)。

但鼠标按键则不可只保留多个按压抬起事件的最后一个。比如之前如果鼠标处于按下状态、而在两帧之间鼠标按键抬起又按下,如果只取最后的按键状态,就没有改变(都是按下状态),但操作者实际点击了一次鼠标。这个点击操作就被忽略掉了,这是不行的。

那应该怎么处理?首先,鼠标按键的状态和点击应该分离。如果游戏需要查询鼠标按键是抬起还是按下,那么和鼠标位置一样,每帧逻辑都会被推送这些状态信息。但鼠标的点击行为,应该是另一种独立状态:在鼠标的按键按下时,底层应该记录这个时刻的帧序号,并不立即通知游戏。而当鼠标抬起时,就改变了“鼠标点击”的状态。这个鼠标点击的状态为空时,表示点击并未发生,不为空时,状态值是点击的时长:即按下到抬起的帧数。

对于前面举例的情况,如果在帧间依次发生了抬起和按下,“鼠标点击”状态也会从空转换为这次点击的时长。同时底层会重置按下时刻,等待下一次抬起后再改变状态。在极端情况下,如果两帧之间连续发生了非常多次按下和抬起,我情况于只记录第一次的“鼠标点击”时长。除非以后要支持“双击”,那也是另一种手势,需要额外实现了。“鼠标点击”这个状态只会存在一帧,无论这一帧游戏代码有没有检查使用这个状态,该状态都会重置为空。

其次,我们需要一个焦点管理器。鼠标焦点永远只能在一个对象上。立即模式的交互层可以和立即模式的渲染层一样,每帧遍历所有的对象,渲染层将可渲染对象按层次和次序提交给底层;交互层则是按层次和次序依次判断每个对象是否获得了焦点。交互层和渲染层都和对象的屏幕空间位置有关,所以两者其实可以做到一起;当然也可以分开,因为未必所有的对象都同时需要渲染和鼠标交互。

对于焦点管理器,它可以每帧简单的把当前焦点对象以一个状态量提供。因为查询是哪个对象获得了焦点需要遍历一次所有对象,这在渲染时就会做一遍,所以一般我们可以将上一帧的焦点传给当前帧,交互在视觉上差一帧问题不是很大。

和查询当前鼠标按键状态一样,游戏逻辑可以查询当前的焦点是谁。但它和“鼠标点击”结合起来使用就不太方便。所谓鼠标点击,通常指鼠标按下的那一刻,鼠标焦点在一个对象上,而抬起时鼠标焦点还在同一个对象上,才能视为点击了这个对象。简单用鼠标抬起那一刻鼠标焦点的对象不太符合一般的使用习惯。所以,我们可以把“焦点”这个状态加上当前焦点对象获得焦点持续的帧数。这样,想知道“鼠标点击”发生时,是否真的点击了当前焦点对象,只需要比较两个时长即可:“鼠标点击”的时长不能大于“焦点”的持续时长。

有了以上这个基础,我们在编写游戏时就可以方便的以立即模式处理每帧的业务:获得当前帧鼠标的位置、按键状态、鼠标每个按键点击的状态(为空或一个时长)、当前鼠标焦点的状态(焦点对象及焦点持续时长)……

在这个基础上,还可以再做一些封装。因为某些模块从效率考虑并不适合每帧都刷新状态。比如交互界面,只有在焦点状态发生改变时,它的属性才会改变:按钮的视觉效果、屏幕提示文字、等等。这些属性改变的成本比较高,不适合每帧都重置。我们可以把每帧的鼠标焦点再记录下来,只有焦点发生改变时,才做额外处理。这个记录焦点状态变化的东西可以放在栈上的临时结构。立即模式比较适合以自然方式书写业务。

Post a comment

非这个主题相关的留言请到:留言本