Irrlicht 3D Engine 笔记系列之 教程4 - Movement

作者: 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中了,我将这段描述拷贝下来,如下所示:

<span style="font-family:Microsoft YaHei;">	//! Enumeration for all event types there are.
	enum EEVENT_TYPE
	{
		//! An event of the graphical user interface.
		/** GUI events are created by the GUI environment or the GUI elements in response
		to mouse or keyboard events. When a GUI element receives an event it will either
		process it and return true, or pass the event to its parent. If an event is not absorbed
		before it reaches the root element then it will then be passed to the user receiver. */
		EET_GUI_EVENT = 0,

		//! A mouse input event.
		/** Mouse events are created by the device and passed to IrrlichtDevice::postEventFromUser
		in response to mouse input received from the operating system.
		Mouse events are first passed to the user receiver, then to the GUI environment and its elements,
		then finally the input receiving scene manager where it is passed to the active camera.
		*/
		EET_MOUSE_INPUT_EVENT,

		//! A key input event.
		/** Like mouse events, keyboard events are created by the device and passed to
		IrrlichtDevice::postEventFromUser. They take the same path as mouse events. */
		EET_KEY_INPUT_EVENT,

		//! A joystick (joypad, gamepad) input event.
		/** Joystick events are created by polling all connected joysticks once per
		device run() and then passing the events to IrrlichtDevice::postEventFromUser.
		They take the same path as mouse events.
		Windows, SDL: Implemented.
		Linux: Implemented, with POV hat issues.
		MacOS / Other: Not yet implemented.
		*/
		EET_JOYSTICK_INPUT_EVENT,

		//! A log event
		/** Log events are only passed to the user receiver if there is one. If they are absorbed by the
		user receiver then no text will be sent to the console. */
		EET_LOG_TEXT_EVENT,

		//! A user event with user data.
		/** This is not used by Irrlicht and can be used to send user
		specific data though the system. The Irrlicht 'window handle'
		can be obtained from IrrlichtDevice::getExposedVideoData()
		The usage and behavior depends on the operating system:
		Windows: send a WM_USER message to the Irrlicht Window; the
			wParam and lParam will be used to populate the
			UserData1 and UserData2 members of the SUserEvent.
		Linux: send a ClientMessage via XSendEvent to the Irrlicht
			Window; the data.l[0] and data.l[1] members will be
			casted to s32 and used as UserData1 and UserData2.
		MacOS: Not yet implemented
		*/
		EET_USER_EVENT,

		//! This enum is never used, it only forces the compiler to
		//! compile these enumeration values to 32 bit.
		EGUIET_FORCE_32_BIT = 0x7fffffff

	};</span>
          从上面的描述,可以看到,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中,事件的定义是由如下的结构体所定义的:

<span style="font-family:Microsoft YaHei;">//! SEvents hold information about an event. See irr::IEventReceiver for details on event handling.
struct SEvent
{
	//! Any kind of GUI event.
	struct SGUIEvent
	{
		//! IGUIElement who called the event
		gui::IGUIElement* Caller;

		//! If the event has something to do with another element, it will be held here.
		gui::IGUIElement* Element;

		//! Type of GUI Event
		gui::EGUI_EVENT_TYPE EventType;

	};

	//! Any kind of mouse event.
	struct SMouseInput
	{
		//! X position of mouse cursor
		s32 X;

		//! Y position of mouse cursor
		s32 Y;

		//! mouse wheel delta, often 1.0 or -1.0, but can have other values < 0.f or > 0.f;
		/** Only valid if event was EMIE_MOUSE_WHEEL */
		f32 Wheel;

		//! True if shift was also pressed
		bool Shift:1;

		//! True if ctrl was also pressed
		bool Control:1;

		//! A bitmap of button states. You can use isButtonPressed() to determine
		//! if a button is pressed or not.
		//! Currently only valid if the event was EMIE_MOUSE_MOVED
		u32 ButtonStates;

		//! Is the left button pressed down?
		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的所有接口函数:

<span style="font-family:Microsoft YaHei;">                //! Creates a rotation animator, which rotates the attached scene node around itself.
		/** \param rotationSpeed Specifies the speed of the animation in degree per 10 milliseconds.
		\return The animator. Attach it to a scene node with ISceneNode::addAnimator()
		and the animator will animate it.
		If you no longer need the animator, you should call ISceneNodeAnimator::drop().
		See IReferenceCounted::drop() for more information. */
		virtual ISceneNodeAnimator* createRotationAnimator(const core::vector3df& rotationSpeed) = 0;

		//! Creates a fly circle animator, which lets the attached scene node fly around a center.
		/** \param center: Center of the circle.
		\param radius: Radius of the circle.
		\param speed: The orbital speed, in radians per millisecond.
		\param direction: Specifies the upvector used for alignment of the mesh.
		\param startPosition: The position on the circle where the animator will
		begin. Value is in multiples of a circle, i.e. 0.5 is half way around. (phase)
		\param radiusEllipsoid: if radiusEllipsoid != 0 then radius2 froms a ellipsoid
		begin. Value is in multiples of a circle, i.e. 0.5 is half way around. (phase)
		\return The animator. Attach it to a scene node with ISceneNode::addAnimator()
		and the animator will animate it.
		If you no longer need the animator, you should call ISceneNodeAnimator::drop().
		See IReferenceCounted::drop() for more information. */
		virtual ISceneNodeAnimator* createFlyCircleAnimator(
				const core::vector3df& center=core::vector3df(0.f,0.f,0.f),
				f32 radius=100.f, f32 speed=0.001f,
				const core::vector3df& direction=core::vector3df(0.f, 1.f, 0.f),
				f32 startPosition = 0.f,
				f32 radiusEllipsoid = 0.f) = 0;

		//! Creates a fly straight animator, which lets the attached scene node fly or move along a line between two points.
		/** \param startPoint: Start point of the line.
		\param endPoint: End point of the line.
		\param timeForWay: Time in milli seconds how long the node should need to
		move from the start point to the end point.
		\param loop: If set to false, the node stops when the end point is reached.
		If loop is true, the node begins again at the start.
		\param pingpong Flag to set whether the animator should fly
		back from end to start again.
		\return The animator. Attach it to a scene node with ISceneNode::addAnimator()
		and the animator will animate it.
		If you no longer need the animator, you should call ISceneNodeAnimator::drop().
		See IReferenceCounted::drop() for more information. */
		virtual ISceneNodeAnimator* createFlyStraightAnimator(const core::vector3df& startPoint,
			const core::vector3df& endPoint, u32 timeForWay, bool loop=false, bool pingpong = false) = 0;

		//! Creates a texture animator, which switches the textures of the target scene node based on a list of textures.
		/** \param textures: List of textures to use.
		\param timePerFrame: Time in milliseconds, how long any texture in the list
		should be visible.
		\param loop: If set to to false, the last texture remains set, and the animation
		stops. If set to true, the animation restarts with the first texture.
		\return The animator. Attach it to a scene node with ISceneNode::addAnimator()
		and the animator will animate it.
		If you no longer need the animator, you should call ISceneNodeAnimator::drop().
		See IReferenceCounted::drop() for more information. */
		virtual ISceneNodeAnimator* createTextureAnimator(const core::array<video::ITexture*>& textures,
			s32 timePerFrame, bool loop=true) = 0;

		//! Creates a scene node animator, which deletes the scene node after some time automatically.
		/** \param timeMs: Time in milliseconds, after when the node will be deleted.
		\return The animator. Attach it to a scene node with ISceneNode::addAnimator()
		and the animator will animate it.
		If you no longer need the animator, you should call ISceneNodeAnimator::drop().
		See IReferenceCounted::drop() for more information. */
		virtual ISceneNodeAnimator* createDeleteAnimator(u32 timeMs) = 0;

		//! Creates a special scene node animator for doing automatic collision detection and response.
		/** See ISceneNodeAnimatorCollisionResponse for details.
		\param world: Triangle selector holding all triangles of the world with which
		the scene node may collide. You can create a triangle selector with
		ISceneManager::createTriangleSelector();
		\param sceneNode: SceneNode which should be manipulated. After you added this animator
		to the scene node, the scene node will not be able to move through walls and is
		affected by gravity. If you need to teleport the scene node to a new position without
		it being effected by the collision geometry, then call sceneNode->setPosition(); then
		animator->setTargetNode(sceneNode);
		\param ellipsoidRadius: Radius of the ellipsoid with which collision detection and
		response is done. If you have got a scene node, and you are unsure about
		how big the radius should be, you could use the following code to determine
		it:
		\code
		const core::aabbox3d<f32>& box = yourSceneNode->getBoundingBox();
		core::vector3df radius = box.MaxEdge - box.getCenter();
		\endcode
		\param gravityPerSecond: Sets the gravity of the environment, as an acceleration in
		units per second per second. If your units are equivalent to metres, then
		core::vector3df(0,-10.0f,0) would give an approximately realistic gravity.
		You can disable gravity by setting it to core::vector3df(0,0,0).
		\param ellipsoidTranslation: By default, the ellipsoid for collision detection is created around
		the center of the scene node, which means that the ellipsoid surrounds
		it completely. If this is not what you want, you may specify a translation
		for the ellipsoid.
		\param slidingValue: DOCUMENTATION NEEDED.
		\return The animator. Attach it to a scene node with ISceneNode::addAnimator()
		and the animator will cause it to do collision detection and response.
		If you no longer need the animator, you should call ISceneNodeAnimator::drop().
		See IReferenceCounted::drop() for more information. */
		virtual ISceneNodeAnimatorCollisionResponse* createCollisionResponseAnimator(
			ITriangleSelector* world, ISceneNode* sceneNode,
			const core::vector3df& ellipsoidRadius = core::vector3df(30,60,30),
			const core::vector3df& gravityPerSecond = core::vector3df(0,-10.0f,0),
			const core::vector3df& ellipsoidTranslation = core::vector3df(0,0,0),
			f32 slidingValue = 0.0005f) = 0;

		//! Creates a follow spline animator.
		/** The animator modifies the position of
		the attached scene node to make it follow a hermite spline.
		It uses a subset of hermite splines: either cardinal splines
		(tightness != 0.5) or catmull-rom-splines (tightness == 0.5).
		The animator moves from one control point to the next in
		1/speed seconds. This code was sent in by Matthias Gall.
		If you no longer need the animator, you should call ISceneNodeAnimator::drop().
		See IReferenceCounted::drop() for more information. */
		virtual ISceneNodeAnimator* createFollowSplineAnimator(s32 startTime,
			const core::array< core::vector3df >& points,
			f32 speed = 1.0f, f32 tightness = 0.5f, bool loop=true, bool pingpong=false) = 0;</span>

            从上面的函数接口中,可以看到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接口的完整定义:

<span style="font-family:Microsoft YaHei;">	//! Animates a scene node. Can animate position, rotation, material, and so on.
	/** A scene node animator is able to animate a scene node in a very simple way. It may
	change its position, rotation, scale and/or material. There are lots of animators
	to choose from. You can create scene node animators with the ISceneManager interface.
	*/
	class ISceneNodeAnimator : public io::IAttributeExchangingObject, public IEventReceiver
	{
	public:
		//! Animates a scene node.
		/** \param node Node to animate.
		\param timeMs Current time in milli seconds. */
		virtual void animateNode(ISceneNode* node, u32 timeMs) =0;

		//! Creates a clone of this animator.
		/** Please note that you will have to drop
		(IReferenceCounted::drop()) the returned pointer after calling this. */
		virtual ISceneNodeAnimator* createClone(ISceneNode* node,
				ISceneManager* newManager=0) =0;

		//! Returns true if this animator receives events.
		/** When attached to an active camera, this animator will be
		able to respond to events such as mouse and keyboard events. */
		virtual bool isEventReceiverEnabled() const
		{
			return false;
		}

		//! Event receiver, override this function for camera controlling animators
		virtual bool OnEvent(const SEvent& event)
		{
			return false;
		}

		//! Returns type of the scene node animator
		virtual ESCENE_NODE_ANIMATOR_TYPE getType() const
		{
			return ESNAT_UNKNOWN;
		}

		//! Returns if the animator has finished.
		/** This is only valid for non-looping animators with a discrete end state.
		\return true if the animator has finished, false if it is still running. */
		virtual bool hasFinished(void) const
		{
			return false;
		}
	};</span>

               一开始看这个接口,我们就可以发现,这个接口继承至一个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的申明,如下所示:

<span style="font-family:Microsoft YaHei;">namespace scene
{
	class CSceneNodeAnimatorFlyCircle : public ISceneNodeAnimator
	{
	public:

		//! constructor
		CSceneNodeAnimatorFlyCircle(u32 time,
				const core::vector3df& center, f32 radius,
				f32 speed, const core::vector3df& direction, 
				f32 radiusEllipsoid);

		//! animates a scene node
		virtual void animateNode(ISceneNode* node, u32 timeMs);

		//! Writes attributes of the scene node animator.
		virtual void serializeAttributes(io::IAttributes* out, io::SAttributeReadWriteOptions* options=0) const;

		//! Reads attributes of the scene node animator.
		virtual void deserializeAttributes(io::IAttributes* in, io::SAttributeReadWriteOptions* options=0);

		//! Returns type of the scene node animator
		virtual ESCENE_NODE_ANIMATOR_TYPE getType() const { return ESNAT_FLY_CIRCLE; }
		
		//! Creates a clone of this animator.
		/** Please note that you will have to drop
		(IReferenceCounted::drop()) the returned pointer after calling
		this. */
		virtual ISceneNodeAnimator* createClone(ISceneNode* node, ISceneManager* newManager=0);

	private:
		// do some initial calculations
		void init();

		// circle center
		core::vector3df Center;
		// up-vector, normal to the circle's plane
		core::vector3df Direction;
		// Two helper vectors
		core::vector3df VecU;
		core::vector3df VecV;
		f32 Radius;
		f32 RadiusEllipsoid;
		f32 Speed;
		u32 StartTime;
	};


} // end namespace scene</span>

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

<span style="font-family:Microsoft YaHei;">//! animates a scene node
void CSceneNodeAnimatorFlyCircle::animateNode(ISceneNode* node, u32 timeMs)
{
	if ( 0 == node )
		return;

	f32 time;

	// Check for the condition where the StartTime is in the future.
	if(StartTime > timeMs)
		time = ((s32)timeMs - (s32)StartTime) * Speed;
	else
		time = (timeMs-StartTime) * Speed;

//	node->setPosition(Center + Radius * ((VecU*cosf(time)) + (VecV*sinf(time))));
	f32 r2 = RadiusEllipsoid == 0.f ? Radius : RadiusEllipsoid;
	node->setPosition(Center + (Radius*cosf(time)*VecU) + (r2*sinf(time)*VecV ) );
}</span>

<span style="font-family:Microsoft YaHei;">ISceneNodeAnimator* CSceneNodeAnimatorFlyCircle::createClone(ISceneNode* node, ISceneManager* newManager)
{
	CSceneNodeAnimatorFlyCircle * newAnimator = 
		new CSceneNodeAnimatorFlyCircle(StartTime, Center, Radius, Speed, Direction, RadiusEllipsoid);

	return newAnimator;
}</span>

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

          好了,至此,我们大概了解了要实现一个Animator需要的内容。明天,博主将动手实际的实现一个自己的Animator,毕竟只有动手之后才知道理解的是否是正确的。

         

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

          1.将Animator与Node设计进行分离,比较类似策略模式,能够让Node选择使用哪一种Animator,并且Animator也与需要进行动画的Node进行解耦

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

          由于Irrlicht的Animator设计的是在太过简单,以后研究下cocos2d-x的Action机制,试试看能够将该特性移植到Irrlicht中来,这样对于以后进行游戏开发将非常的容易。



Framerate Independent and Framerate dependent

          在教程4的最后一段内容,了解到在游戏开发中,控制移动是有两种不同的方式的。一种被称为Framerate Independent的控制方式,也就是与帧率无关的移动控制方法,换句话说,就是使用时间来控制移动。比如,当我们的游戏,由于CPU资源过于紧张而导致帧率下降,如果使用帧率无关的控制方式,那么就会发现不管帧率下降与否,人物在1s的时间内,移动的距离都是一样的。

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

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

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

        


  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值