作者: i_dovelemon

日期: 2014 / 12 / 16

来源: CSDN

主题: Event Receiver, Animator, Framerate independent movement and framerate dependent movement



           从今天開始,博主将进行对3D Engine的学习。而且,在博客中将自己学习的心得一一分享给大家。希望可以对大家有所帮助。也希望可以找到志同道合的同伴一起学习3D 游戏引擎方面的知识。



为什么选择Irrlicht?

         在非常久曾经。博主就有实际研究一个引擎的想法。仅仅是一直没有付诸行动。可是期间,也大致的了解过市场上流行的引擎。

博主希望的不是使用3D 游戏引擎做出好玩的游戏,而是对3D引擎内部的工作机制进行了解。所以。对于琳琅满目的商业和非商业开源引擎。须要从中选择一款来进行研究是件非常困难的事情。

         博主上各大论坛,问里面的高手。对于刚開始学习的人来说,哪些引擎适合我们去学习研究。大部分的人都推荐Ogre和Irrlicht这两个引擎。所以,我就都下载实验一下。依照博主眼下的知识储备和对引擎的理解能力来说。研究Irrlicht更加的适合。Irrlicht是全然使用C++开发的一款高效实时的3D渲染引擎,相对于Ogre来说。他没有Ogre里面那些复杂的脚本技术,对于刚開始学习的人的我来说,希望可以看到一个纯粹点的引擎。假设使用脚本封装的太多层。对初次研究的我来说,难度有点大,所以博主终于决定研究Irrlicht这款引擎。它的代码风格组织的十分良好。而且使用的是我所常常使用的C++语言编写,全然可以依靠眼下的知识来对Irrlicht做一些基础性的学习和仿真研究。


关于Irrlicht

        关于Irrlicht的具体信息,大家能够到它的官方站点上去了解。也希望有很多其它的人来和博主一起研究这款引擎。以下是这个引擎的官方站点和配套的社区:

         http://irrlicht.sourceforge.net/

         http://www.irrlicht3d.org/



教程4--Movement

        好了,废话不多说了,进入正题吧。

在官网上。有一系列的教程来帮助我们慢慢的熟悉引擎。所以。博主也就依照这里面提供的教程来一步一步的进行了解。

关于前面几个教程的笔记缺失了。假设后面有时间,博主会补上这些内容。

        在教程4里面,教程向我们展示了怎样在Irrlicht捕捉按键消息,而且对节点进行控制,同一时候也演示了怎样使用Irrlicht,对一个节点施加一个动画效果。

具体的关于这方面的代码,大家能够看教程4。博主在这里就不再赘述了。


Event Receiver

         在教程4中,我们了解到,想要进行对输入输出的处理,我们须要继承一个接口IEventReceiver。然后在我们继承的类里面。复写我们OnEvent方法,而且这种方法会在系统发生事件的时候,自己主动的被引擎所调用。这样。我们仅仅要在这个函数里面,对我们希望处理的消息进行处理就可以。这个就是Event Receiver主要的工作方法。为了深入的了解事件机制是怎样工作的,我们先来看下IEventReceiver这个接口的定义怎样:

<span style="font-family:Microsoft YaHei;">//! Interface of an object which can receive events.
/** Many of the engine's classes inherit IEventReceiver so they are able to
process events. Events usually start at a postEventFromUser function and are
passed down through a chain of event receivers until OnEvent returns true. See
irr::EEVENT_TYPE for a description of where each type of event starts, and the
path it takes through the system. */
class IEventReceiver
{
public:

	//! Destructor
	virtual ~IEventReceiver() {}

	//! Called if an event happened.
	/** Please take care that you should only return 'true' when you want to _prevent_ Irrlicht
	* from processing the event any further. So 'true' does mean that an event is completely done.
	* Therefore your return value for all unprocessed events should be 'false'.
	\return True if the event was processed.
	*/
	virtual bool OnEvent(const SEvent& event) = 0;
};</span>

       这是Irrlicht引擎中关于IEventReceiver接口的定义。上面另一段描写叙述。解释例如以下:

       引擎中非常多类都继承这个接口,所以他们都拥有处理事件的能力。

事件一般是由一个postEventFromUser函数来触发的。而且在触发之后,依照一条责任链条依次传递下去。直到OnEvent函数返回true时结束。

具体了解每个事件从何处产生,而且责任链是怎样的请看EEVENT_TYPE这个类型的描写叙述。

       在这个接口里面,仅仅有一个纯虚拟函数OnEvent。也就是在对象收到消息的时候,进行消息处理的唯一的函数了。对这个函数的解释例如以下所看到的:

        当你想要将消息继续传递下去的时候,那么就在这个函数的末尾返回false。当你想要终止消息的传递的时候,请返回true。

       这个描写叙述,告诉了我们应该怎么样终止责任链和怎样继续沿着责任链传递消息下去。

       当博主看到这里的时候,有个疑问。我们继承这个接口实现的一个事件接受器,如教程4中所看到的。它处在责任链的哪一个部分了?是最開始进行处理的?还是最后进行处理的了?

       针对这个疑问。博主,查看了EEVENT_TYPE中责任链的描写叙述。原来,在Irrlicht中,用户定义的事件接收器在不同的情况下,所处的位置实际是不同的。可是大致能够分为例如以下几种情况,这些情况都具体的描写叙述在EEVENT_TYPE中了。我将这段描写叙述拷贝下来。例如以下所看到的:

          从上面的描写叙述,能够看到,Irrlicht引擎,将事件的类型分为6大不同的基础类型,而每一种基础类型都拥有自己的责任链传递方式。我们来一一了解下。

          第一种是EET_GUI_EVENT。也就是GUI事件消息。这样的事件消息是由GUI环境和GUI元素在响应按键或者鼠标时所产生的。

当GUI元素接受到这个事件的时候,要么处理它然后返回true,要么就是将该事件传递给GUI元素的父节点,直到传递到根节点为止。假设在传递到根节点之后,依旧没有被抛弃。那么就会调用用户定义的事件接收器来对消息进行处理。

          另外一种是EET_MOUSE_INPUT_EVENT,也就是鼠标输入事件。

鼠标事件是由设备产生。而且传递给IrrlichtDevice::postEventFromUser函数来对操作系统的消息进行响应。鼠标事件首先传递到用户定义的事件接受器中,然后传递到GUI环境和它的GUI元素节点中,最后在传递到场景管理器中,传递给当前活跃的相机,来进行处理。

          第三种是EET_KEY_INPUT_EVENT,也就是键盘按键事件。相同的。这个事件也是有机器设备产生的,然后传递给了IrrlichtDevice::postEventFromUser来进行响应。和上面鼠标事件共享相同的责任链。

         第四种是EET_JOYSTICK_INPUT_EVENT。也就是手柄事件。同上面鼠标和键盘一样。通过IrrlichtDevice::postEventFromUser来传递,然后经过相同的责任链进行事件的处理。

          第五种是EET_LOG_TEXT_EVENT,也就是日志事件。日志事件只传递给用户定义的事件接受器。假设在用户定义的事件接收器中,将这些消息抛弃了,那么将不会在控制台下输出日志信息。

         第六种是EET_USER_EVENT。也就是用户自己定义的事件。

这样的类型的消息。Irrlicht并不使用,而只将Irrlicht当成是事件中转站来传递事件。

         以上六种基本情况,就是Irrlicht引擎所支持的事件处理机制的全部情况了。


         事件处理机制。除了产生和传递这种基本条件之外。另外一个十分重要的内容就是事件本身的定义。

在Irrlicht中。事件的定义是由例如以下的结构体所定义的:

bool isLeftPressed() const { return 0 != ( ButtonStates & EMBSM_LEFT ); } //! Is the right button pressed down? bool isRightPressed() const { return 0 != ( ButtonStates & EMBSM_RIGHT ); } //! Is the middle button pressed down?

bool isMiddlePressed() const { return 0 != ( ButtonStates & EMBSM_MIDDLE ); } //! Type of mouse event EMOUSE_INPUT_EVENT Event; }; //! Any kind of keyboard event. struct SKeyInput { //! Character corresponding to the key (0, if not a character) wchar_t Char; //! Key which has been pressed or released EKEY_CODE Key; //! If not true, then the key was left up bool PressedDown:1; //! True if shift was also pressed bool Shift:1; //! True if ctrl was also pressed bool Control:1; }; //! A joystick event. /** Unlike other events, joystick events represent the result of polling * each connected joystick once per run() of the device. Joystick events will * not be generated by default. If joystick support is available for the * active device, _IRR_COMPILE_WITH_JOYSTICK_EVENTS_ is defined, and * @ref irr::IrrlichtDevice::activateJoysticks() has been called, an event of * this type will be generated once per joystick per @ref IrrlichtDevice::run() * regardless of whether the state of the joystick has actually changed. */ struct SJoystickEvent { enum { NUMBER_OF_BUTTONS = 32, AXIS_X = 0, // e.g. analog stick 1 left to right AXIS_Y, // e.g. analog stick 1 top to bottom AXIS_Z, // e.g. throttle, or analog 2 stick 2 left to right AXIS_R, // e.g. rudder, or analog 2 stick 2 top to bottom AXIS_U, AXIS_V, NUMBER_OF_AXES }; /** A bitmap of button states. You can use IsButtonPressed() to ( check the state of each button from 0 to (NUMBER_OF_BUTTONS - 1) */ u32 ButtonStates; /** For AXIS_X, AXIS_Y, AXIS_Z, AXIS_R, AXIS_U and AXIS_V * Values are in the range -32768 to 32767, with 0 representing * the center position. You will receive the raw value from the * joystick, and so will usually want to implement a dead zone around * the center of the range. Axes not supported by this joystick will * always have a value of 0. On Linux, POV hats are represented as axes, * usually the last two active axis. */ s16 Axis[NUMBER_OF_AXES]; /** The POV represents the angle of the POV hat in degrees * 100, * from 0 to 35,900. A value of 65535 indicates that the POV hat * is centered (or not present). * This value is only supported on Windows. On Linux, the POV hat * will be sent as 2 axes instead. */ u16 POV; //! The ID of the joystick which generated this event. /** This is an internal Irrlicht index; it does not map directly * to any particular hardware joystick. */ u8 Joystick; //! A helper function to check if a button is pressed. bool IsButtonPressed(u32 button) const { if(button >= (u32)NUMBER_OF_BUTTONS) return false; return (ButtonStates & (1 << button)) ? true : false; } }; //! Any kind of log event. struct SLogEvent { //! Pointer to text which has been logged const c8* Text; //! Log level in which the text has been logged ELOG_LEVEL Level; }; //! Any kind of user event. struct SUserEvent { //! Some user specified data as int s32 UserData1; //! Another user specified data as int s32 UserData2; }; EEVENT_TYPE EventType; union { struct SGUIEvent GUIEvent; struct SMouseInput MouseInput; struct SKeyInput KeyInput; struct SJoystickEvent JoystickEvent; struct SLogEvent LogEvent; struct SUserEvent UserEvent; }; };</span>


          Irrlicht引擎,为了使事件定义可以通用与上面所定义的6中基本事件类型,它将这6种事件都定义在SEvent这个结构中了。除此之外,它还加上一个成员,用于标示该事件确切的属于哪一种情况,也就是上面的EventType属性了。

         关于每一种类型确切的结构,将在以后使用遇到的时候具体的研究,这里仅仅从整体架构上面来分析下。


         从上面关于事件机制的分析中,博主学到了几样东西,分享一下给大家:

         1.事件机制须要有事件产生方式,和事件传递路径。以及事件本身定义来进行构造

         2.事件的产生,往往依赖于特定功能的实现,也是对特定功能的响应操作。

         3.事件的传递路径,能够使用设计模式中的责任链的方法来进行设计,便于事件进行层级处理

         4.在设计一个系统时,可能各个子系统的传递路径并不同样,我们不能如果他们传递路径是同样的,最好可以让子系统自定义自己的传递路径

         5.一个事件处理系统,除了内置的事件处理功能之外,最好可以让用户定义自己的事件处理器,而且在传递路径上要可以让用户决定传递是否结束。

         以上就是博主研究分析Irrlicht引擎。所获取的关于事件处理机制的知识。


Animator

         在教程4中,演示了使用Irrlicht的Animator特性,来制造一些内置的动画效果。这些动画效果都是通过ISceneManager这个结构来创建一个继承ISceneNodeAnimator接口的对象来实现的。所以。有必要对引擎的这个特性进行一下分析。我们首先来看下。Irrlicht可以创建出哪些Animator,以下是在ISceneManager接口中定义的关于创建Animator的全部接口函数:


            从上面的函数接口中,能够看到Irrlicht支持例如以下的几种Animator:

            RotationAnimator -- 创建一个环绕自身进行旋转的Animator

            FlyCircleAnimator -- 创建一个环绕指定中心进行旋转的Animator(教程4中就是使用这个Animator)

            FlyStraightAnimator -- 创建一个沿着两点进行移动的Animator

            TextureAnimator -- 创建一个纹理Animator

            DeleteAnimator -- 创建一个删除Animator。用于随着时间来渐进的删除节点的Animator

            CollisionResponseAnimator -- 创建一个进行碰撞检測和反应的Animator

            FollowSplineAnimator -- 创建一个尾随的Animator

            从上面可以看到,Irrlicht对于Animator的特性支持的不是非常多。博主所熟悉的一款2D游戏引擎cocos2d-x对于这种Animator特性的支持就非常的好。

所不同的是在cocos2d-x中,这个特性叫做Action。

cocos2d-x中对Action的支持非常的好。可以通过动作之间的组合,延迟等等做出非常复杂的Action操作出来。希望以后的Irrlicht版本号可以添加这种特性,这样就更加方便游戏开发人员来进行游戏开发工作了。

           那么,没有cocos2d-x中的Action特性,我们也想实现那些特性怎么办了?博主眼下所知道的方法就是用户们自己继承ISceneNodeAnimator,来创建独立于Irrlicht引擎的Animator,这样就行从一定程度上扩展Irrlicht引擎关于Animator特性的支持了。那么。要可以创建出Animator,我们须要对ISceneNodeAnimator这个接口十分的了解才行,所以我们先来了解下ISceneNodeAnimator这个接口。以下是ISceneNodeAnimator接口的完整定义:


               一開始看这个接口,我们就行发现,这个接口继承至一个IEventReceiver,也就是说Animator可以接受事件,而且进行事件处理。我们在来看下它内部有哪些个函数。

           函数: virtual void animateNode(ISceneNode* node, u32 timeMs) = 0 ;

           这个函数就是用来对node进行Animator操作的函数。它具有须要进行Animator的节点的对象node,以及进行Animator的时间timeMs等。

也就是说,实际的对节点的操作就是在这里进行的。


            函数:virtual ISceneNodeAnimator* createClone(ISceneNode* node, ISceneManager* newManager=0) = 0 ;

           这个函数用于创建当前Animator的一个克隆物。


            函数:virtual bool isEventReceiverEnabled() const ;

            用于推断当前的Animator是否可以接受事件。


             函数: virtual bool hasFinished(void) const ;

            用于推断非循环的Animator是否结束的函数。


            在大致的了解了这个接口之后,我们知道,想要实现自己的Animator,只要继承该接口。而且复写里面的函数animateNode函数就能够了。

可是这不过大体上理解,具体情况究竟怎样了?我们来实际的看下Irrlicht引擎内部的Animator的animateNode函数是怎样编写的。就拿本教程的FlyCircleAnimator来说吧。

            在源码中,找寻FlyCircleAnimator的申明,例如以下所看到的:


              从这个类的申明中能够发现,该类复写了接口的animateNode方法以及createClone方法,而且在内部定义了用于完毕自己Animator任务的成员属性。好了,我么来看下animateNode和createClone方法的实现怎样:


          分析下。这两个函数。不是非常复杂。而可以让节点运动的函数就是animateNode方法了,在这个函数里面,它首先检查了节点是否为空,然后计算经过的时间,最后改变节点在圆上的位置。而clone函数。就是简单的将对象又一次创建一遍而已。

          好了,至此。我们大概了解了要实现一个Animator须要的内容。

明天,博主将动手实际的实现一个自己的Animator,毕竟仅仅有动手之后才知道理解的是否是正确的。

         

          从对Irrlicht引擎的Animator特性的研究中,能够学到例如以下的几个内容:

          1.将Animator与Node设计进行分离,比較类似策略模式,可以让Node选择使用哪一种Animator。而且Animator也与须要进行动画的Node进行解耦

          2.开发共用的动画接口。能够让用户自由的实现自己的Animator。

          因为Irrlicht的Animator设计的是在太过简单,以后研究下cocos2d-x的Action机制,试试看可以将该特性移植到Irrlicht中来。这样对于以后进行游戏开发将很的easy。



Framerate Independent and Framerate dependent

          在教程4的最后一段内容,了解到在游戏开发中,控制移动是有两种不同的方式的。一种被称为Framerate Independent的控制方式,也就是与帧率无关的移动控制方法,换句话说,就是使用时间来控制移动。

比方,当我们的游戏。因为CPU资源过于紧张而导致帧率下降。假设使用帧率无关的控制方式,那么就会发现无论帧率下降与否,人物在1s的时间内,移动的距离都是一样的。

          而除了Framerate Independent的方式之外,就是Framerate dependent的控制方式了。这样的控制方式,表示的意思是不是依据时间来控制移动,而是在每一帧的间隔里面都移动同样的距离。这样的移动情况会受到帧率的影响,假设帧率下降的非常多,那么人物的移动就会变的奇慢无比。

          那么是不是说。第一种方式就是比另外一种方式好了?也不是。想想看,假设因为你在某一帧的时候,游戏读取某个大数据文件。导致在这帧的时候,出现卡帧的现象,也就是在大于1s的时间。没有进行换帧操作。那么当接下来进行换帧之后,实际上仅仅是经过了1帧。可是时间上却超过了1s。假设此时游戏是Framerate Independent的,就会出现人物突然跳跃的情况。这样的情况在网络游戏中常常遇到。博主希望打lol,常常在网络延迟的时候,因为帧之间接受数据的时间大于1s。导致接下来的一帧突然跳跃到对方阵营中。从而无情的被敌方蹂躏。

           所以,以上两种方法。各有优缺点。读者们最好依据情况来分析,你须要使用哪一种来进行。假设可以糅合使用这两种。而且可以依据情况进行切换。是否可以避免出现博主被敌方蹂躏的现象了???期待大家可以想出结合这两种方式长处的解决方式出来!!


        


相关文章: