【Ogre编程入门与进阶】第七章 帧监听与用户输入

来源:互联网 发布:java。 大数据 编辑:程序博客网 时间:2024/05/17 02:17

在这一章中我们将学习一个非常重要的概念:帧监听,我们将和大家一起学习怎样使用帧监听的功能去对每一帧需要更新的内容更新,另外,我们还会学习怎样使用前面我们提到过的OIS的输入系统。

帧监听

       这一小节我们同样使用前面我们已经定义好的模板代码,不过我们需要做些许改动,我们知道我们定义的那个Example1类,我们让它继承了ExampleApplication类,但我们也知道C++支持多继承,因此现在我们让它再继承一个类FrameListener,因为这里封装了我们需要的所有帧监听的操作,继承这个类之后我们需要重写几个FrameListener中定义的虚函数,以便我们可以对每帧的更新操作进行控制,下面是继承了FrameListener并重写了对应的函数后的完整代码:

第一步:

#include <windows.h>

#include "ExampleApplication.h"

 

class Example1 : public ExampleApplication,public Ogre::FrameListener

{

public:

     void createScene()

     {

     }

 

     bool frameStarted(const FrameEvent& evt)

     {

         return true;

     }

     bool frameRenderingQueued(const FrameEvent& evt)

     {

         return true;

     }

     bool frameEnded(const FrameEvent& evt)

     {

         return true;

     }

protected:

private:

};

 

INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )

{

     Example1 app;

     app.go();

     return 0;

}

第二步:完成上面的代码后,我们给Example1类新添加一个成员变量,如下所示:

private:

     Ogre::SceneNode*m_Node;

第三步:添加Example1的一个构造函数,把我们新添加的成员变量m_Node初始化为NULL:

Example1()

{

     m_Node = NULL;

}

第四部:向frameStarted成员函数中添加如下代码:

bool frameStarted(const FrameEvent& evt)

{

     if (m_Node)

    {

        m_Node->translate(Ogre::Vector3(0.1,0,0));

    }

     return true;

}

第五步:给Example1类新增加一个createFrameListener的函数,这个函数和前面我们提到过的createCamera已经createViewports等函数一样,都是在ExampleApplication中定义的虚函数:

void createFrameListener()

{

     mRoot->addFrameListener(this);

}

 

第六步:我们在createScene函数中添加一个实体,并实例化m_Node,以便我们能看到效果:

void createScene()

{

     Ogre::Entity*ent =mSceneMgr->createEntity("Sinbad","Sinbad.mesh");

     m_Node = mSceneMgr->getRootSceneNode()->createChildSceneNode();

     m_Node->setScale(10.0f,10.0f,10.0f);

     m_Node->attachObject(ent);

}

到此为止帧监听的大部分工作我们已经完成,我们可以编译并运行程序,我们会看到场景中有个人物不断的向右移动。但是请读者朋友注意,一旦我们运行这个程序,我们“很难”将其真正关闭,之所以这样说是因为我们并没有对退出当前的应用程序做任何处理,因此,你将会发现当我们像以前那样按ESC键退出的时候会没有任何响应,而当我们点击右上角的关闭按钮时似乎程序可以退出了,但是其实还是没有真正退出,而只是窗口隐藏掉了,读者朋友可以同时按住CTRL+ALT+DEL键进入任务管理器查看一下,比如我们现在这个应用程序名称是OgreTest2,我们会发现即使点击了关闭按钮,我们仍然能看到当前应用还是在后台运行:

我们现在有两种方式可以真正结束应用程序,第一种就是像我上面这样选择OgreTest2.exe,然后点击结束进程按钮,则可结束当前应用程序,第二种方式就是,如果大家运行程序的时候是在debug调试模式下(或者直接按F5键)运行的程序,可以到Visual Studio中选择 菜单——调试——停止调试(或者直接同时按下SHIFT+F5键),即可终止应用程序,至于这到底是什么原因导致的这种情况以及怎样解决这个问题,而为什么之前我们的应用程序可以正常退出(其实是因为ExampleApplication中已经为我们做好了这些工作,而我们重写了一些函数,因此这些功能需要我们自己来做了),我们会在用户输入部分向大家介绍怎样实现这一功能。

代码分析:

完成了所有这些代码,我们现在就开始从头一步一步分析我们刚才的这些代码到底对我们的程序施加了怎样的控制:

首先,我们还是回到第一步,分析一下FrameListener以及它的几个成员函数。正如这个类名所表达的那样,帧监听类使用到了我们常用的观察者模式,当某个类注册了这个帧监听类时,那么这个类对应的每次的帧更新操作都会传到这个帧监听类的相应函数中去。我们知道3D场景渲染中帧的概念,通常在游戏制作中,我们听到最多的应该就是一个叫做帧率(FPS)的概念,我们常用的帧监听函数有三个:frameStarted 这个函数是在每一帧渲染之前调用,frameRenderingQueued这个函数是在每次对渲染目标发出渲染命令之后调用,frameEnded一般是在每帧渲染之后被调用用来做一些出来操作。这三个函数的调用顺序就是frameStarted——frameRenderingQueued——frameEnded。整个场景在这些渲染过程中处于无限循环之中,直到这三个帧监听函数有任何一个返回了false,那么应用程序就会退出,知道了这一点我们可以想象如果我们在这几个函数中添加一个标志,每次按下ESC键的时候让这个标志激活,而当这个标志激活的时候我们让函数返回false,我们的应用就可以真正退出了,是的,稍微我们将会给大家具体说明怎样来完成这个操作。

第二步和第三步,我们定义了一个SceneNode类型的指针,并在构造函数中初始化为NULL,把它设为成员变量是为了我们能在其它的函数中使用,如果我们只把它定义在createScene函数中,那么其它函数就不太方便访问这个对象了。

第四步,我们也不会陌生,首先判断m_Node指针是否为空,以防止空指针异常,然后我们应该很熟悉这个translate函数,意思就是让这个节点在每次调用frameStarted这个函数的时候都向x轴正方向移动0.1个单位长度。如果读者朋友有多态电脑,你或许会发现运行我们现在这样一个小小的应用程序,虽然我们每次都让模型移动0.1个单位,但是由于电脑性能不同,模型移动的速度也就不一致,有的移动的比较快,有的移动的比较慢,原因是因为,一台性能比较好的电脑或许能够在每秒渲染100帧,而一台比较老旧的电脑或许每秒只能渲染30帧,这样区别就出来了,每秒渲染100帧的那台电脑每秒实际上让模型移动了10个单位,而每秒渲染30帧的那台电脑每秒只让模型移动了3个单位,正常情况下,我们想让我们的应用不管在哪台电脑上都能有同样的渲染效果,而这一点在Ogre中实现起来相当容易,回到帧监听的那三个函数,我们可以看到它们都有这样一个参数:constFrameEvent&evt,而通过FrameEvent这个结构体中就封装了两种类型的数值:RealtimeSinceLastEvent;和RealtimeSinceLastFrame;前者代表自从上一个事件结束到现在所过去的时间,而后者代表自从同一类型的上一个事件结束到现在所过去的时间,这里的事件包括帧开始和帧结束。也就是说,我们可以这样理解,timeSinceLastEvent表示从上一次帧渲染结束到下一次帧渲染开始或者从上一次帧渲染开始到下一帧帧渲染结束的时间,而timeSinceLastFrame表示从上一次帧渲染开始到下一次帧渲染开始或者从上一次帧渲染结束到下一次帧渲染结束所用的时间。我们可以把translate这行代码改为如下代码,这样我们的应用就可以跑在所有性能不同的机器上而能达到一样速度的效果了:

m_Node->translate(Ogre::Vector3(10,0,0)*evt.timeSinceLastFrame);

第五步,就是我们前面提到的注册帧监听的操作,由于当前类已经继承了FrameListener类,因此调用addFrameListener这个函数的时候实际上注册的是FrameListener类型的实例对象,这样注册完成之后每次帧渲染的时候就可以调用帧监听的那几个函数了。但是我们或许会发现一个新的变量mRoot,不难想象它也是在ExampleApplication中定义的,其类型是Ogre::Root,但是,这个类到底是做什么的我们还不是很清楚,Root类对象是Ogre系统的入口,该对象在程序一开始时创建,最后结束时销毁,它可以帮助你配置系统,可以帮助你获得系统中其它对象的指针,比如:场景管理器(SceneManager)、绘制系统(RenderSystem),还有其它的资源管理器(ResourceManagers)等等,当然它的功能远远不止这些,我们也会在后续章节给读者朋友一一介绍。

 

笔者提示:

除了像上面这种采用多继承的方式外,我们完全可以不采用这样的办法,我们可以专门再定义一个新类,让它继承FrameListener,里面重载FrameListener中对应的虚函数,唯一不同的是我们需要在Example1中增加一个成员变量,作为对新定义的这个帧监听类的引用即可,这样同样也是一种很好的代码编写方式,如果读者感兴趣可以查看ExampleApplication.h中的源代码,其中就是使用了这种方式,这里我们之所以用一种新的方式是希望能给读者更多选择的余地。

 

添加用户输入支持

       如前面所示,虽然我们的模型能移动起来了,但是我们的应用程序却不能安装正常方式退出,因此,这一节我们为它增加按键支持,当我们按下键盘上的ESC键是应用可以正常退出。这一章我们会用到我们在前面较早时候就提到过的一个库,OIS(面向对象的输入系统),OIS不是Ogre3D的一部分,它是一个独立的项目,在它背后有着和Ogre3D一样的开发团队来维护它,Ogre应用程序向导提供的模板代码和ExampleApplication.h中都使用它来做为输入功能,Ogre3D对它提供了支持,同样,对于OIS的各个文件目录配置方式如果不正确,我们的程序也不会跑起来,具体的配置方式我们在相应模板已有介绍。

       虽然我们现在是自己重写代码构建,但是我们还是在模拟ExampleApplication在背后提供给我们的功能,我们之所以这样做是想让大家对Ogre的这些基本操作逐渐熟悉起来,因此这里我们同样还是会用到OIS。

添加键盘响应

       我们现在不需要重新使用一个新的模板代码,我们继续在前面增加过FrameListener功能代码的基础上继续添加代码:

第一步:同前面继承FrameListener一样,这次我们让Example1再继承一个基类OIS::KeyListener,如下所示:

class Example1 :publicExampleApplication,publicOgre::FrameListener,publicOIS::KeyListener

第二步,向前面添加帧监听的函数一样重写OIS::KeyListener类中的几个虚函数,如下:

// OIS::KeyListener

     bool keyPressed( const OIS::KeyEvent &arg )

     {

         return true;

     }

     bool keyReleased( const OIS::KeyEvent &arg )

     {

         return true;

     }

第三步:在Example1类中增加三个新的私有成员变量:

OIS::InputManager*mInputManager;

OIS::Keyboard*mKeyboard;

Ogre::RealmX;

Ogre::RealmZ;

bool mShutDown;

第四步:在Example1的构造函数中增加如下代码:

mInputManager = NULL;

mKeyboard = NULL;

mX = 0;

mZ = 0;

mShutDown = false;

第五步:接着前面创建的createFrameListener函数里的内容继续添加如下代码(注意这个函数体中原来的那行注册帧监听的代码不要覆盖掉):

OIS::ParamListpl;

size_t windowHnd = 0;

std::ostringstreamwindowHndStr;

mWindow->getCustomAttribute("WINDOW", &windowHnd);

windowHndStr << windowHnd;

pl.insert(std::make_pair(std::string("WINDOW"),windowHndStr.str()));

 

mInputManager = OIS::InputManager::createInputSystem(pl );

mKeyboard = static_cast<OIS::Keyboard*>(mInputManager->createInputObject(OIS::OISKeyboard,true ));

mKeyboard->setEventCallback(this);

第六步:修改frameStated函数中的代码,使其如下所示:

bool frameStarted(const FrameEvent& evt)

{

     if(mShutDown)

     {

         return false;

     }

     mKeyboard->capture();

     if (m_Node)

     {   

         m_Node->translate(Ogre::Vector3(50,0,0)*evt.timeSinceLastFrame);

     }

return true;

}

第七步:增加Example1的析构函数,并在其中增加如下代码:

if( mInputManager )

{

     mInputManager->destroyInputObject(mKeyboard );

     OIS::InputManager::destroyInputSystem(mInputManager);

     mInputManager = NULL;

}

 

编译并运行程序,并尝试按下ESC键,你将发现现在我们的程序可以正常退出了。

 

代码分析:

首先,在第一步中,我们继承了KeyListener,这是一个键盘监听类,我们对键盘按键的回调函数就在这个类中定义;在第二步中,我们实现了在KeyListener类中定义的两个纯虚函数,它们分别响应键盘按下和键盘抬起操作;在第三步中,我们定义了几个成员变量,mInputManager代表OIS的输入管理类,mKeyboard代表键盘操作类,mX和mZ两个变量用来控制模型的移动(后面我们会用到),mShutDown用来标识什么时候程序需要退出;在第四步中我们将这几个变量初始化;第五步,首先我们定义了一个OIS参数列表,其实这个变量中是以键值对的形式存放数据的,转到OIS中对ParmList的定义我们就可以发现它的实际定义样式std::multimap<std::string,std::string>,这是标准模板库中的知识,如果读者对这里并不是太了解,并不太影响读者阅读代码,但笔者建议读者多了解一些标准模板库中的东西,因为或许读者已经发现,Ogre中大量用到了标准模板库中的知识,况且STL容器已经是现代C++程序所必有的特性之一,它能使你的程序更简单易懂。换言之,如果你能理解STL的话可以让你更容易的使用Ogre。我们回到代码,可以看到紧接着的几行代码的意思就是获取渲染窗口的句柄并将其转换为字符串形式最终以键值对的形式封装到前面定义的OIS的参数列表中;然后就是把这个参数列表变量传递给InputManager,通过它的createInputSystem真正创建了输入管理类对象,倒数第二行代码是通过获得的这个输入管理类对象创建了一个键盘输入对象,这样我们就可以开始捕获键盘事件了,最后一行代码是注册键盘响应事件的回调函数,只有这样当键盘事件发生时,我们重写的keyPressed和keyReleased两个函数才会被调用。

下面我们再来回顾一遍OIS输入系统到底是怎样和我们的应用窗口建立关联的:

我们首先简要了解一下窗口句柄的概念,窗口句柄窗口的资源标识,它有一连串的数字组成,这串数字是通过操纵系统创建的,每一个窗口都有唯一的句柄标识,而我们前面定义的输入系统恰恰需要的就是这个句柄,因为如果没有这个句柄,我们定义的输入系统就不能获取输入事件,Ogre为我们创建了一个渲染窗口,因此我们可以通过这个渲染窗口对象的getCustomAttribute函数获得窗口的句柄,我们把获取到的这个句柄通过stringsstream可以转换成字符串的形式,最终传递给我们的输入系统,这样我们的输入系统就知道捕获哪个窗口的事件了。

第四步中,我们通过创建的键盘输入对象的capture函数在每帧都去捕获键盘事件,这样当我们键盘事件输入的情况下就会被它捕获到,然后通过判断是哪个键按下来完成响应的操作,这里我们定义了捕获到ESC键被按下的情况下frameStarted函数就返回false,因为帧渲染保持循环的条件是是必须返回true,所以当我们返回false的时候程序就终止了,这样我们就完成了通过键盘控制程序退出的操作。当然,程序退出之前我们需要释放输入系统以便安全退出,因此我们把释放的操作放在了析构函数中。

为了让大家更熟悉用键盘输入对模型的控制,我们再添加几个用键盘控制模型的操作:

找到frameStarted函数中的这行代码:m_Node->translate(Ogre::Vector3(50,0,0)*evt.timeSinceLastFrame);

替换为如下代码:

m_Node->translate(mX*evt.timeSinceLastFrame,0,mZ*evt.timeSinceLastFrame);

然后,向keyPressed函数添加如下代码:

bool keyPressed( const OIS::KeyEvent &arg )

{

     if(arg.key ==OIS::KC_ESCAPE)

     {

         mShutDown = true;

         return false;

     }

     if(arg.key ==OIS::KC_W)

     {

         mZ = -50;

     }

     if(arg.key ==OIS::KC_S)

     {

         mZ = 50;

     }

     if(arg.key ==OIS::KC_A)

     {

         mX = -50;

     }

     if(arg.key ==OIS::KC_D)

     {

         mX = 50;

     }

     return true;

}

接着,向keyReleased函数中增加如下代码:

bool keyReleased( const OIS::KeyEvent &arg )

{

     if(arg.key ==OIS::KC_W)

     {

         mZ = 0;

     }

     if(arg.key ==OIS::KC_S)

     {

         mZ = 0;

     }

     if(arg.key ==OIS::KC_A)

     {

         mX = 0;

     }

     if(arg.key ==OIS::KC_D)

     {

         mX = 0;

     }

     return true;

}

编译并运行代码,用WSAD键就可以控制模型的前后左右移动了。

       添加鼠标响应:

第一步:继续给Example1类增加一个新的成员私有变量:

OIS::Mouse*   mMouse;

第二步:在构造函数中初始化为NULL:

mMouse = NULL;

第三步:在createFrameListener函数体的最后加入如下代码:

mMouse = static_cast<OIS::Mouse*>(mInputManager->createInputObject(OIS::OISMouse,true ));

第四步:在frameStarted函数体的最开始处增加如下代码:

mMouse->capture();

第五步:在frameStarted函数体的m_Node->translate(mX*evt.timeSinceLastFrame,0,mZ*evt.timeSinceLastFrame);紧接着一行增加如下代码:

float rotX = mMouse->getMouseState().X.rel *evt.timeSinceLastFrame* -1;

float rotY = mMouse->getMouseState().Y.rel *evt.timeSinceLastFrame * -1;

mCamera->yaw(Ogre::Radian(rotX));

mCamera->pitch(Ogre::Radian(rotY));

第六步:在析构函数体中mInputManager->destroyInputObject(mKeyboard );行代码下紧接着一行加入下面一行代码:

mInputManager->destroyInputObject(mMouse );

 

编译并运行程序,你将会发现现在程序有和以前一样不仅能用键盘控制也能根据鼠标的移动模型左右晃动了。

代码分析:

     我们看到添加鼠标响应的步骤和前面添加键盘响应很类似,因此这里我们只讨论两处不同的地方,我们看一下第五步中完成的操作,我们这里查询鼠标状态用getMouseState函数(其实另一种查询键盘的方式我们也可以用isKeyDown函数),它返回一个MouseState类型的实例,它包含了鼠标按钮的状态信息,因此我们可以通过获取到的鼠标状态信息获取鼠标移动的相对值,从而完成对相机的移动操作。

笔者提示:这里鼠标的操作我们换了一种和键盘响应不太一样的方式,但请读者朋友注意,我们同样可以利用和键盘响应一样的方法,让Example1类继承鼠标事件监听对应的类OIS::MouseListener,我们只需重写其对应的mouseMoved、mousePressed、mouseReleased函数即可完成和上面一样的鼠标控制操作,而且这种方式更为简单明了,如果读者朋友感兴趣可以尝试一下。另外,请读者朋友保存这份模板代码,下面的章节可能我们还会使用到。

显示模式

     记得我们在前面章节曾经简单提过设置相机的显示模式的概念,通常我们看到的模型都是以实体显示,但有时我们想以其他形式显示模型,这时候就需要对显示模式进行调整,Ogre里通常我们可以设置三种显示模式:实体模式、线框模式、点模式。

     完成这种操作很简单,如果读者留心的话,前面我们已经多次提到,在前面我们继承ExampleApplication的程序实际上在运行后,按下键盘上的R键即可在三种模式之间切换,由于现在我们的程序重写了Ogre给我们提供的示例程序的功能,因此,这里我们自己手动再加上这部分功能:

第一步:给类Example1添加一私有成员变量:

Ogre::PolygonMode  mPolyMode;

第二步:在构造函数中初始化这个变量:

mPolyMode = Ogre::PolygonMode::PM_SOLID;

第三步:在keyPressed函数的末尾(return true;之前)加上下面的代码:

if(arg.key ==OIS::KC_R)

{

     if(mPolyMode ==PM_SOLID)

{

         mPolyMode = Ogre::PolygonMode::PM_WIREFRAME;

}

else if(mPolyMode ==PM_WIREFRAME)

     {

         mPolyMode = Ogre::PolygonMode::PM_POINTS;

     }

     else if(mPolyMode == PM_POINTS)

     {

         mPolyMode = Ogre::PolygonMode::PM_SOLID;

     }

     mCamera->setPolygonMode(mPolyMode);

}

编译并运行程序,按下键盘上的R键我们就可以在三种模式之间任意切换了。

 

PS:很久以前就打算把学习Ogre中遇到的知识分享给大家(虽然只是一些皮毛),但是说来惭愧,一直很懒,直到最近才抽出点时间写一些自己的理解(Ogre入门级的东西),所以难免会有很多不足之处,分享是一种快乐,同时也希望和大家多多交流!

(由于在Word中写好的东西发布到CSDN页面需要重新排版(特别是有很多图片时),所以以后更新进度可能会比较慢,同时每章节发布的时间可能不一样(比如说我首选发布的是第二章,其实第一章就是介绍下Ogre的前世今生神马的,相信读者早就了解过~~~),但是我会尽量做到不影响大家阅读,还望大家谅解。)


上述内容很多引用了网上现有的翻译或者内容,在此一并谢过(个人感觉自己有些地方写得或者翻译的不好),还望见谅,转载请注明此项!