事件与使用

DinS          Written on 2017/12/15

一、事件的概念

事件(Event)在程序中也是一个很普遍的概念了,跟用户打交道的程序基本上都属于“事件驱动型”。Urho3D的事件也是一样的概念:当某事件触发后程序去执行某个功能(通常称为回调函数)。

Urho3D有许多内置的事件名,本专题将简略介绍主要的几个,更详细的说明见官网。用户也可以自定义事件,不过略复杂,这个就不介绍了。一般而言内置事件足够用。

为了让事件能够触发成功,用户需要显式注册事件,有固定的写法,稍后介绍。

二、属于程序主循环的事件

先要说一下程序主循环是什么。
当引擎初始化后程序就进入了主循环,所谓主循环就是程序一直在转动(也许是一个while),即使没有得到用户输入也不直接退出。这样一来程序才可能一直显示出来,并随时响应用户输入。

每一次循环称为一帧(frame),在一帧内引擎按照次序触发了以下事件:
E_BEGINFRAME。这个事件触发后Input和Network子系统会有行动
E_UPDATE。在这里程序逻辑执行,通常会引发Scene更新,进而触发Scene相关事件
E_POSTUPDATE。顾名思义,是E_UPDATE事件触发后执行的内容,UI子系统逻辑在这里更新
E_RENDERUPDATE。这里Renderer子系统更新,画面重新渲染,包括Scene和UI
E_POSTRENDERUPDATE。这里一般只进行一些逻辑更新,即使更新了画面也不会显示了,因为渲染事件已经结束
E_ENDFRAME。一帧结束

那么一帧有多长时间?这个可以通过timestep获取。当游戏运行流畅时每一个timestep应该是一样的,如果有卡顿发生那个这个timestep就会不一样。这一帧里面触发的事件就属于主循环事件,这些事件之所以重要是因为每一帧都会调用。而且从逻辑上来讲,游戏里的所有改变最终都是因为这几个事件触发而发生的,引起了一大串连锁调用,因此有必要研究研究。

注册主循环事件很简单,使用如下形式即可:
SubscribeToEvent(EVENT_NAME, URHO3D_HANDLER(MyClass, MyEventHandler));
第一个EVENT_NAME就是上述6个事件之一,后面那个是一个宏,用于简化代码书写,后面紧跟的是当前的类,以及自定义的事件处理函数。
看代码来可能更容易理解:

大图点这里

固定写法。
当然只有在真正需要注册事件时才应该注册,这里仅仅是为了展示。
从语义上来理解:比如第二行表达的意思是当E_UPDATE事件被触发后,调用HandleUpdate函数。

接下来再看看各个回调函数:

大图点这里

首先要注意,所有函数的签名都是一样的,必须是这样否则就不是合法回调函数。HandleUpdate之外的那三个函数实际上没有什么作用,仅仅是展示而已,这里就是输出字符串到log文件,整好学习一下写日志的方法。

HandleUpdate函数有实质性内容:旋转Box节点。从语义上看,依靠scene_获得的Box节点,然后将其转动某个方向。理解到这里即可,本专题重点在事件。
看第一行是如何获得timestep的。我们通过传入的eventData来获取传入参数,Urho3D定义的VariantMap可以视为万能型数据结构,因此只依靠这个就能够获得任意类型、任意规模的参数。
基于效率的考虑,每种Event共用一个VariantMap,所以这里是引用。至于哪种Event有哪种参数,需要慢慢探索了。稍后为介绍查看方法。

在运行之前,先来猜想一下程序会如何运转。
我们知道这几个事件是每一帧都会触发,于是只要程序不退出,这几个事件的回调函数会一遍遍调用,所以日志会不停地刷,至于Box节点,必然会不停地转。

让我们运行看看效果是否跟我们猜想的一样:

虽然还不太知道Box转的规律,但是确实是一直在转,跟我们的猜测一致。
再看看日志:

确实在不断刷,与预期相符。从日志里还可以看出一些问题,timestep大体上稳定在0.005左右。而且我们也可以算出1秒走了多少帧。
1秒的开始:

1秒结束:

行数减一下可以约740行,一帧输出4行日志,所以是185帧。
每一秒走过的帧数有波动,不过大致上是这个数。

在概念上和使用上掌握了主循环事件后,做一个小结。
主循环事件一共有6个,可以分成3类:
一帧的开始和结束,即E_BEGINFRAME和E_ENDFRAME。一般而言我们用不到这两个事件。
逻辑处理,即E_UPDATE和E_POSTUPDATE。这是使用的最多的,所有跟程序逻辑有关的更新都在这里执行,比如移动物体、碰撞检测等。E_POSTUPDATE用于执行第二轮逻辑更新,用到的机会不多。
画面渲染,即E_RENDERUPDATE和E_POSTRENDERUPDATE。一般而言我们不需要显式调整渲染,只要逻辑更新后引擎会自动处理。当然如果你想手动调整一些渲染细节,就可以在这里进行。

三、属于对象的事件

之前介绍的主循环事件虽然重要,但仅仅是事件的一个形式,另一个广泛使用的是对象事件。顾名思义,就是对象在某种条件下触发事件,然后执行某个回调函数。
对象事件的使用跟主循环事件类似,毕竟都属于事件这个范畴,区别在于注册事件时需要指明对象,比如下面紧接着sprite代码部分,增加一行代码:

大图点这里

唯一区别就是第一个参数指明了对象。
这句代码表达的语义就是一旦sprite被点击,就调用HandleClosePressed函数。

现在有了这样一个问题:我怎么知道哪个对象能够执行哪些事件?更进一步,事件的参数有哪些?这时候就应该去阅读源码。在E_CLICKEND上按F12:

会跳转到UIEvent.h,在这里可以看到具体事件说明,以及可用参数。
该.h内的其他事件就是UI系统支持的事件,需要时来查即可。

我们继续实现这个回调函数:

函数签名与主循环事件一致。实际上所有的事件的回调函数都是这样的。
在这里再次展示了如何获取事件参数,这些也就是通过阅读.h知道的。
回调函数的功能就是把鼠标点击的坐标输出到日志,然后退出。

问题来了:鼠标呢?我们之前的所有例子都只是展示一个界面,看不到鼠标,而且退出都退出不了,只能alt+F4。
下面一个专题让我们来研究处理用户输入,见《响应用户输入》。