Ogre中级教程

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow

也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!

               

中级教程一

来自 Ogre wiki

动画, 两点间移动, 和四元数基础

作者: Culver.

内容

目录

[隐藏]

介绍

这个教程里包括怎么样得到一个模型,并添加模型动画,最后让模型可以在两个预先定义的点之间走动。在此将讲述如何用基本的四元数方法保持模型移动的时候正面一直朝着我们指定的方向。你必须一点点的将代码加入到你的项目中,并在每次加入新代码后编译并察看demo运行的结果。

本课的最终代码在这里

前期准备

首先,这个指南假设你已经知道如何设置Ogre的项目环境以及如何正确编译项目。该例子同样使用STL 中的queue数据结构。那么预先了解如何使用queue是必要的,至少你需要知道什么是模版。如果你不熟悉STL,那么我像你推荐STL参考[ISBN 0596005563],它可以帮助你在将来花费更少的时间。

准备开始

首先,你需要为这个Demo创建一个新项目,在项目中添加一个名为"MoveDemo.cpp"的文件并加入如下代码:

#include "ExampleApplication.h"#include <deque>using namespace std;class MoveDemoListener : public ExampleFrameListener{public:    MoveDemoListener(RenderWindow* win, Camera* cam, SceneNode *sn,        Entity *ent, deque<Vector3> &walk)        : ExampleFrameListener(win, cam, false, false), mNode(sn), mEntity(ent), mWalkList( walk )    {    } // MoveDemoListener    /* This function is called to start the object moving to the next position       in mWalkList.    */    bool nextLocation( )    {        return true;    } // nextLocation( )    bool frameStarted(const FrameEvent &evt)    {        return ExampleFrameListener::frameStarted(evt);    }protected:    Real mDistance;                  // The distance the object has left to travel    Vector3 mDirection;              // The direction the object is moving    Vector3 mDestination;            // The destination the object is moving towards    AnimationState *mAnimationState; // The current animation state of the object    Entity *mEntity;                 // The Entity we are animating    SceneNode *mNode;                // The SceneNode that the Entity is attached to    std::deque<Vector3> mWalkList;   // The list of points we are walking to    Real mWalkSpeed;                 // The speed at which the object is moving};class MoveDemoApplication : public ExampleApplication{protected:public:    MoveDemoApplication()    {    }    ~MoveDemoApplication()     {    }protected:    Entity *mEntity;                // The entity of the object we are animating    SceneNode *mNode;               // The SceneNode of the object we are moving    std::deque<Vector3> mWalkList;  // A deque containing the waypoints    void createScene(void)    {    }    void createFrameListener(void)    {        mFrameListener= new MoveDemoListener(mWindow, mCamera, mNode, mEntity, mWalkList);        mFrameListener->showDebugOverlay(true);        mRoot->addFrameListener(mFrameListener);    }};#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32#define WIN32_LEAN_AND_MEAN#include "windows.h"INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )#elseint main(int argc, char **argv)#endif{    // Create application object    MoveDemoApplication app;    try {        app.go();    } catch( Exception& e ) {#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32        MessageBox( NULL, e.getFullDescription().c_str(), "An exception has occured!",            MB_OK | MB_ICONERROR | MB_TASKMODAL);#else        fprintf(stderr, "An exception has occured: %s/n",                e.getFullDescription().c_str());#endif    }    return 0;}

在我们继续讲解之前,你可以编译这部分代码看下效果。

设置场景

在我们开始之前,需要注意的是已经在MoveDemoApplication中预先定义的三个变量。我们创建的entity实例保存在变量mEntity中,我们创建的node实例保存在mNode中,另外mWalkList包含了所有我们希望对象行走到的节点。

定位到MoveDemoApplication::createScene函数并且加入以下代码。首先,我们来设置环境光(ambient light)到最大,这样可以让我们看到我们放在场景中的所有对象。

       // Set the default lighting.       mSceneMgr->setAmbientLight( ColourValue( 1.0f, 1.0f, 1.0f ) );

接下来我们来在屏幕上创建一个可以使用的机器人。要做到这点我们需要在创建SceneNode之前先为机器人创建一个entity使得我们可以对其进行旋转。

       // Create the entity       mEntity = mSceneMgr->createEntity( "Robot", "robot.mesh" );       // Create the scene node       mNode = mSceneMgr->getRootSceneNode( )->           createChildSceneNode( "RobotNode", Vector3( 0.0f, 0.0f, 25.0f ) );       mNode->attachObject( mEntity );

以上这些都是非常基础的,所以我认为不需要再对以上的描述做任何解释。在接下来的代码片断,我们将开始告诉机器人那些地方是它需要到达的。这里需要你们了解一些STL的知识,deque对象是一个高效的双端对列。我们只需要使用它的几个简单的方法。push_front和push_back方法分别将对象放入队列的前端和后端,front和back方法分别返回当前队列前端和后端的元素(PS:注意,这里最好有判空的习惯,用if( empty() ) )pop_front和pop_back两个方法分别从队列两端移除对象。最后,empty方法返回该队列是否为空。下面这些代码添加了两个Vector到队列中,在后面我们移动robot的时候会用到它们。

       // Create the walking list       mWalkList.push_back( Vector3( 550.0f,  0.0f,  50.0f  ) );       mWalkList.push_back( Vector3(-100.0f,  0.0f, -200.0f ) );

接下来,我们在场景里放置一些物体,以标记这个机器人应该朝哪走去。这样使我们能看见机器人在场景里相对于其它物体进行移动。注意它们的位置的负Y部分,这些物体被放在机器人移动目的地的正下方,当它到达指定地点时,它就站在这些物体上面。

       // Create objects so we can see movement       Entity *ent;       SceneNode *node;       ent = mSceneMgr->createEntity( "Knot1", "knot.mesh" );       node = mSceneMgr->getRootSceneNode( )->createChildSceneNode( "Knot1Node",           Vector3(  0.0f, -10.0f,  25.0f ) );       node->attachObject( ent );       node->setScale( 0.1f, 0.1f, 0.1f );       ent = mSceneMgr->createEntity( "Knot2", "knot.mesh" );       node = mSceneMgr->getRootSceneNode( )->createChildSceneNode( "Knot2Node",           Vector3( 550.0f, -10.0f,  50.0f ) );       node->attachObject( ent );       node->setScale( 0.1f, 0.1f, 0.1f );       ent = mSceneMgr->createEntity( "Knot3", "knot.mesh" );       node = mSceneMgr->getRootSceneNode( )->createChildSceneNode( "Knot3Node",           Vector3(-100.0f, -10.0f,-200.0f ) );       node->attachObject( ent );       node->setScale( 0.1f, 0.1f, 0.1f );

最后,我们要创建一个摄像机从适合的角度来观察它。我们来把摄像机移动到更多的位置。

       // Set the camera to look at our handywork       mCamera->setPosition( 90.0f, 280.0f, 535.0f );       mCamera->pitch( Degree(-30.0f) );       mCamera->yaw( Degree(-15.0f) );

现在编译并运行代码。你应该能看到这个样子: [[1]]

在进入下一个部分之前,注意一下MoveDemoListener的构造器,它在MoveDemoApplication::createFrameListener方法里的第一行被调用。除了传入BaseFrameListener的标准参数,还有场景节点、实体、双端队列。

动画

现在我们来设置一些基本的动画。在Ogre里动画是非常简单的。要做的话,你需要从实体对象里获取AnimationState,设置它的选项,并激活它。这样就能使动画活动起来,但你还必须在每一帧后给它添加时间,才能让动画动起来。我们设置成每次移动一步。首先,找到MoveDemoListener的构造器,并添加以下代码:

      // Set idle animation      mAnimationState = ent->getAnimationState("Idle");      mAnimationState->setLoop(true);      mAnimationState->setEnabled(true);

第二行从实体中获取到了AnimationState。第三行我们调用setLoop( true ),让动画不停地循环。而在一些动画里(比如死亡动画),我们可能要把这个设置为false。第四行才把这个动画真正激活。但等等...我们从哪里获取的“Idle”?这个魔术般的常量是怎么飞到这里来的?每个mesh都有它们自己定义的动画集。为了能够查看某个mesh的全部动画,你需要下载OgreMeshViewer才能看到。

现在,如果我们编译并运行这个demo,我们看见了...nothing! 这是因为我们还需要在每一帧里根据时间来更新这个动画的状态。找到MoveDemoListener::frameStarted方法,在方法的开头添加这一行:

       mAnimationState->addTime(evt.timeSinceLastFrame);

现在来编译并运行程序。你应该可以看了一个机器人正在原地踏步了。

移动角色

现在我们执行棘手的任务,开始让这个机器人从一点走到另一点。在我们开始之前,我想介绍一下保存在MoveDemoListener类里的成员变量。我们将使用4个变量来完成移动机器人的任务。首先,我们把机器人移动的方向保存到mDirection里面。我们再把当前机器人前往的目的地保存在mDestination里。然后在mDistance保存机器人离目的地的距离。最后,在mWalkSpeed里我们保存机器人的移动速度。

首先清空MoveDemoListener构造器,我们会用稍微不同的代码来替换。我们要做的第一件事是设置这个类的变量。我们将把行走速度设为每秒35个单位。有一个大问题要注意,我们故意把mDirection设成零向量,因为后面我们会用它来判断机器人是否正在行走。

       // Set default values for variables       mWalkSpeed = 35.0f;       mDirection = Vector3::ZERO;

好了,搞定了。我们要让机器人动起来。为了让机器人移动,我们只须告诉它改变动画。然而,我们只想要若存在另一个要移动到的地点,就让机器人开始移动。为了这个目的,我们调用nextLocation 函数。把代码加到MoveDemoListener::frameStarted方法的顶部,在调用AnimationState::addTime之前:

      if (mDirection == Vector3::ZERO)       {          if (nextLocation())           {              // Set walking animation              mAnimationState = mEntity->getAnimationState("Walk");              mAnimationState->setLoop(true);              mAnimationState->setEnabled(true);          }      }

如果你现在编译并运行,这个机器人将原地行走。这是由于机器人是以ZERO方向出发的,而我们的MoveDemoListener::nextLocation函数总是返回true。在后面的步骤中,我们将给MoveDemoListener::nextLocation函数添加更多的一点智能。

现在,我们准备要真正在场景里移动机器人了。为了这样做,我们需要在每一帧里让我移动一点点。找到MoveDemoListener::frameStarted方法,我们将在调用AnimationState::addTime之前,我们先前的if语句之后,添加以下代码。这段代码将处理当机器人实际移动的情况;mDirection != Vector3::ZERO。

       else       {           Real move = mWalkSpeed * evt.timeSinceLastFrame;           mDistance -= move;

现在,我们要检测一下我们是否“走过”了目标地点。即,如果现在mDistance小于0,我们需要“跳”到这点上,并设置移动到下一个地点。注意,我们把mDirection设置成零向量。如果nextLocation方法不改变mDirection(即没有其它地方可去),我们就不再四处移动了。

           if (mDistance <= 0.0f)           {               mNode->setPosition(mDestination);               mDirection = Vector3::ZERO;

现在我们移动到了这个点,我们需要设置运动到下一个点。只要我们知道有否需要移动到下一个地点,我们就能设置正确的动画;如果有其它地点要去,就行走。如果没有其它目的地,则停滞。

              // Set animation based on if the robot has another point to walk to.               if (! nextLocation())              {                  // Set Idle animation                                       mAnimationState = mEntity->getAnimationState("Idle");                  mAnimationState->setLoop(true);                  mAnimationState->setEnabled(true);              }               else              {                  // Rotation Code will go here later              }          }

注意,如果queue里已经没有更多的地点要走的话,我们没有必要再次设置行走动画。既然机器人已经在行走了,没有必要再告诉他这么做。然而,如果机器人还要走向另一个地点,我们就要把它旋转以面对那个地点。现在,我们在else括号旁边留下注释占位符;记住这个地点,因为我们后面还要回来。

这里考虑的是当我们离目标地点很近的时候。现在我们需要处理一般情况,当我们正在到达而没有到达的时候。为此,我们在机器人的行走方向上对它进行平移,用move变量指定的值。通过添加以下代码来实现:

           else           {               mNode->translate(mDirection * move);           } // else       } // if

我们差不多做完了,除了还要设置运动需要的变量。如果我们正确地设置了运动变量,我们的机器人就会朝它该去的方向行走。看看MoveDemoListener::nextLocation方法,如果我们用完了所有的地点,它返回false。这是函数的第一行。(注意你要保留函数底部的return true语句)

       if (mWalkList.empty())           return false;

现在我们来设置变量。首先我们从双端队列里取出一个向量。通过目标向量减去场景节点的当前向量,我们得取方向向量。然而我们仍有一个问题,还记得我们要在frameStarted方法里用mDirection乘以移动量吗?如果我们这么做,我们必须把方向向量转换成单位向量(即,它的长度等于一)。normalise函数为我们做了这些事,并返回向量的原始长度。唾手可得,我们需要设置到目的地的距离。

      mDestination = mWalkList.front();  // this gets the front of the deque      mWalkList.pop_front();             // this removes the front of the deque
      mDirection = mDestination - mNode->getPosition();      mDistance = mDirection.normalise();

编译并运行代码。搞定! 现在机器人行走到每一个地点,但它总是面朝着Vector3::UNIT_X方向(它的默认)。我们需要当它向地点移动时,改变它的朝向。

我们需要做的是获得机器人脸的方向,然后用旋转函数将它旋转到正确的位置。在我们上一次留下注释占位符的地方,插入如下代码。第一行获得了机器人脸的朝向。第二行建立了一个四元组,它表示从当前方向到目标方向的旋转。第三行才是真正旋转了这个机器人。

       Vector3 src = mNode->getOrientation() * Vector3::UNIT_X;       Ogre::Quaternion quat = src.getRotationTo(mDirection);       mNode->rotate(quat);

我们在基础教程4里已经对四元组进行过简单的介绍,但在这里才是我们对它的第一次使用。基本上说,四元组就是在三维空间里对旋转的表示。它们被用来跟踪物体是如何在空间里放置的,也可能被用来在Ogre里对物体进行旋转。我们在第一行里调用getOrientation方法,返回了一个表示机器人在空间里面向方向的四元组。因为Ogre不知道机器人的哪一面才是它的正面,所以我们必须用UNIT_X方向乘以这个朝向,以取得机器人当前的朝向。我们把这个方向保存在src变量里。在第二行,getRotationTo方法返回给我们一个四元组,它表示机器人从目前的朝向到我们想让它朝向方向的旋转。第三行,我们旋转节点,以让它面向一个新的方向。

我们创建的代码只剩下一个问题了。这里有一种特殊情况将会使SceneNode::rotate失败。如果我们正试图让机器人旋转180度,旋转代码会因为除以0的错误而崩掉。为了解决这个问题,我们需要测试我们是否执行180度的旋转。如果是,我们只要用yaw来将机器人旋转180度,而不是用rotate。为此,删除我们刚才放入的代码,并用这些代替:

      Vector3 src = mNode->getOrientation() * Vector3::UNIT_X;      if ((1.0f + src.dotProduct(mDirection)) < 0.0001f)       {          mNode->yaw(Degree(180));      }      else      {          Ogre::Quaternion quat = src.getRotationTo(mDirection);          mNode->rotate(quat);      } // else

这些代码的意思应该是比较清楚的,除了包在if语句里的内容。如果两个向量是互相反向的(即,它们之间的角度是180度),它们的点乘就将是-1。所以,如果我们把两个向量点乘而且结果等于-1.0f,则我们用yaw旋转180度,否则我们用rotate代替。为什么我加上1.0f,并检查它是否小于0.0001f? 不要忘了浮点舍入错误。你应该从来不直接比较两个浮点数的大小。最后,需要注意的是,这两个向量的点乘永远是落在[-1,1]这个区域之间的。如果还不太清楚的话,你应该先去学一学最基本的线性代数再来做图像编程! 至少你应该复习一下四元组与旋转基础,查阅关于一本关于基础的向量及矩阵运算的书籍。

好了,我们的代码完成了! 编译并运行这个Demo,你会看见一个机器人朝着指定的地点走动。

巩固练习

简单问题

1. 添加更多的点到路径中。同时在他点的位置放上Knonts来观察他想去哪里。

2. 机器人走完他的有效路程后就应该不存在了!当机器人完成行走,他就应该用执行死亡动画来代替待机动画。死亡的动画叫“Die”。

中级问题

1. 看完教程后,你注意到了mWalkSpeed有点问题吗?我们只是一次性设置了一个值,然后就再也没变过。就好像是一个类的不变的静态变量。试着改变一下这个变量。(提示:可以定义键盘的+和-分别表示加速和减速)

2. 代码中有些地方非常取巧,例如跟踪机器人是否正在走,用了mDirection向量跟Vector3::ZERO比较。如果我们换用一个bool型变量mWalking来跟踪机器人是否在移动也许会更好。实现这个改变。

困难问题

1. 这个类的一个局限是你不能在创建对象后再给机器人行走的路线增加新的目的地点。修补这个问题,实现一个带有一个Vector3参数的新方法,并且将它插入mWalkList队列。(提示:如果机器人还未完成行走过程,你就只需要将目的地点插入队列尾即可。如果机器人已经走完全程,你将需要让它再次开始行走,然后调用nextLocation开始再次行走。)

专家问题

1. 这个类的另一个主要局限是它只跟踪一个物体。重新实现这个类,使之可以彼此独立地移动任意数量的物体。(提示:你可以再创建一个类,这个类包含移动一个物体所需要知道的全部东西。把它存储在一个STL对象中,以便以后可以通过key获取数据。)如果可以不注册附加的framelistener,你会得到加分。

2. 做完上面的改变,你也许注意到了机器人可能会彼此发生碰撞。修复它,或者创建一个聪明的寻路函数,或者当机器人碰撞时检测,阻止它们彼此穿透而过。

 

中级教程2:射线场景查询及基础鼠标用法

有关这篇教程,无论遇到任何问题,都可以到论坛发帖寻求帮助。

目录

[隐藏]

介绍

本教程中,我们会初步创建一个基础场景编辑器。在过程之中,我们会涉及到:

  1. 如何使用RaySceneQueries阻止镜头穿透地面
  2. 如何使用MouseListener和MouseMotionListener接口
  3. 使用鼠标选取地面上的x和y坐标

你可以在这里找到完整代码。跟随着教程,你会慢慢地向你自己的工程项目中增加代码,并且随着编译看到结果。

前期准备

本教程假设你已经知道了如何创建Ogre工程,并且可以成功编译。假设你已经了解了基本的Ogre对象(场景节点,实体,等等)。你也应该熟悉STL迭代器基本的使用方法,因为本教程会用到。(Ogre也大量用到STL,如果你还不熟悉STL,那么你需要花些时间学习一下。)

开始

首先,你需要为此演示程序创建一个新工程。在创建工程时,选空工程、自己的框架,以及初始化进度条和CEGUI支持,不选编译后拷贝。向工程中,增加一个名叫“MouseQuery.cpp”的文件,并向其中添加如下代码:

#include <CEGUI/CEGUISystem.h>#include <CEGUI/CEGUISchemeManager.h>#include <OgreCEGUIRenderer.h>#include "ExampleApplication.h"class MouseQueryListener : public ExampleFrameListener, public OIS::MouseListener{public: MouseQueryListener(RenderWindow* win, Camera* cam, SceneManager *sceneManager, CEGUI::Renderer *renderer)  : ExampleFrameListener(win, cam, false, true), mGUIRenderer(renderer) { } // MouseQueryListener ~MouseQueryListener() { } bool frameStarted(const FrameEvent &evt) {  return ExampleFrameListener::frameStarted(evt); } /* MouseListener callbacks. */ bool mouseMoved(const OIS::MouseEvent &arg) {  return true; } bool mousePressed(const OIS::MouseEvent &arg, OIS::MouseButtonID id) {  return true; } bool mouseReleased(const OIS::MouseEvent &arg, OIS::MouseButtonID id) {  return true; }protected: RaySceneQuery *mRaySceneQuery;     // The ray scene query pointer bool mLMouseDown, mRMouseDown;     // True if the mouse buttons are down int mCount;                        // The number of robots on the screen SceneManager *mSceneMgr;           // A pointer to the scene manager SceneNode *mCurrentObject;         // The newly created object CEGUI::Renderer *mGUIRenderer;     // CEGUI renderer};class MouseQueryApplication : public ExampleApplication{protected: CEGUI::OgreCEGUIRenderer *mGUIRenderer; CEGUI::System *mGUISystem;         // cegui systempublic: MouseQueryApplication() { } ~MouseQueryApplication() { }protected: void chooseSceneManager(void) {  // Use the terrain scene manager.  mSceneMgr = mRoot->createSceneManager(ST_EXTERIOR_CLOSE); } void createScene(void) { } void createFrameListener(void) {  mFrameListener = new MouseQueryListener(mWindow, mCamera, mSceneMgr, mGUIRenderer);  mFrameListener->showDebugOverlay(true);  mRoot->addFrameListener(mFrameListener); }};#if OGRE_PLATFORM == PLATFORM_WIN32 || OGRE_PLATFORM == OGRE_PLATFORM_WIN32#define WIN32_LEAN_AND_MEAN#include "windows.h"INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT)#elseint main(int argc, char **argv)#endif{ // Create application object MouseQueryApplication app; try {  app.go(); } catch(Exception& e) {#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32  MessageBox(NULL, e.getFullDescription().c_str(), "An exception has occurred!", MB_OK | MB_ICONERROR | MB_TASKMODAL);#else  fprintf(stderr, "An exception has occurred: %s/n",   e.getFullDescription().c_str());#endif } return 0;}

在继续下面教程以前,先确保上面代码可以正常编译。

创建场景

找到MouseQueryApplication::createScene方法。下面的代码应该都很熟悉了。如果你不知道其中某些是做什么用的,请在继续本教程前,参考Ogre API。向createScene中,增加如下代码:

        // Set ambient light       mSceneMgr->setAmbientLight(ColourValue(0.5, 0.5, 0.5));       mSceneMgr->setSkyDome(true, "Examples/CloudySky", 5, 8);       // World geometry       mSceneMgr->setWorldGeometry("terrain.cfg");       // Set camera look point       mCamera->setPosition(40, 100, 580);       mCamera->pitch(Degree(-30));       mCamera->yaw(Degree(-45));

既然我们建立了基本的世界空间,那么就要打开光标。打开光标,要使用CEGUI函数调用。不过在此之前,我们需要启用CEGUI。我们首先创建一个OgreCEGUIRenderer,然后创建系统对象并将刚创建的Renderer传给它。创建CEGUI我们会专门留待后续教程介绍,现在只要知道创建mGUIRenderer时必须以最后一个参数告诉CEGUI你要用那个场景管理器。

       // CEGUI setup       mGUIRenderer = new CEGUI::OgreCEGUIRenderer(mWindow, Ogre::RENDER_QUEUE_OVERLAY, false, 3000, mSceneMgr);       mGUISystem = new CEGUI::System(mGUIRenderer);

现在我们需要实际显示光标了。同样地,我不打算过多解释这些代码。我们会在后面的教程中详细介绍。(其实也没什么,就是设置了一下CEGUI的窗口和鼠标的样式。——Aaron注释)

       // Mouse       CEGUI::SchemeManager::getSingleton().loadScheme((CEGUI::utf8*)"TaharezLookSkin.scheme");       CEGUI::MouseCursor::getSingleton().setImage("TaharezLook", "MouseArrow");

如果你编译并运行这个程序,你会发现一个光标出现在屏幕中央,但它还动不了。

帧监听器介绍

这是程序要做的全部事情。FrameListener是代码中复杂的部分,所以我会花一些时间强调我们要完成的东西,以便在我们开始实现它之前,使你有一个大体的印象。

  • 首先,我们想要将鼠标右键绑定到“鼠标观察”模式。不能使用鼠标四下看看是相当郁闷的,所以我们首先对程序增加鼠标控制(尽管只是在我们保持鼠标右键按下时)。
  • 第二,我们想要让镜头不会穿过地表。这会使它更接近我们期望的样子。
  • 第三,我们想要在地表上用鼠标左键点击一下,就在那里增加一个实体。
  • 最后,我们想要能“拖拽”实体。即选中我们想要看到的实体,按住鼠标左键不放,将它移动到我们想要放置的地方。松开鼠标左键,就又会将它锁定在原地。

要做到这几点,我们要使用几个受保护的变量(这些已经加到类中了):

    RaySceneQuery *mRaySceneQuery;      // 射线场景查询指针    bool mLMouseDown, mRMouseDown;     // 如果按下鼠标按钮,返回True    int mCount;                        // 屏幕上机器人的数量    SceneManager *mSceneMgr;           // 指向场景管理器的指针    SceneNode *mCurrentObject;         // 新创建的物休    CEGUI::Renderer *mGUIRenderer;     // CEGUI渲染器

变量mRaySceneQuery握有RaySceneQuery的一个拷贝,我们会它来寻找地面上的坐标。变量mLMouseDown和mRMouseDon会追踪我们是否按下鼠标键(例如:如果按下鼠标左键,则mLMouseDown为true;否则,为false)。mCount计数屏幕上有的实体数。mCurrentObject握有指向最近创建的场景节点的指针(我们将用这个“拖拽”实体)。最后,mGUIRenderer握有指向CEGUI Renderer的指针,我们将用它更新CEGUI。

还要注意的是,有许多和鼠标监听器相关的函数。在本演示程序中,我们不会全部用到,但是它们必须全部在那儿,否则编译会报错说你没定义它们。

创建帧监听器

找到MouseQueryListener构造函数,增加如下初始化代码。注意,由于地形相当小,所以我们也要减少移动和旋转速度。

        // Setup default variables        mCount = 0;        mCurrentObject = NULL;        mLMouseDown = false;        mRMouseDown = false;        mSceneMgr = sceneManager;        // Reduce move speed        mMoveSpeed = 50;        mRotateSpeed /= 500;

为了MouseQueryListener能收到鼠标事件,我们必须把它注册为一个鼠标监听器。如果对此不太熟悉,请参考基础教程5。

        // Register this so that we get mouse events.        mMouse->setEventCallback(this);

最后,在构造函数中我们需要创建一个RaySceneQuery对象。用场景管理器的一个调用创建:

        // Create RaySceneQuery        mRaySceneQuery = mSceneMgr->createRayQuery(Ray());

这是我们需要的全部构造函数了,但是如果我们创建一个RaySceneQuery,以后我们就必须销毁它。找到MouseQueryListener析构函数(~MouseQueryListener),增加如下代码:

        // We created the query, and we are also responsible for deleting it.        mSceneMgr->destroyQuery(mRaySceneQuery);

在进入下一阶段前,请确保你的代码可以正常编译。

增加鼠标查看

我们要将鼠标查看模式绑定到鼠标右键上,需要:

  • 当鼠标被移动时,更新CEGUI(以便光标也移动)
  • 当鼠标右键被按下时,设置mRMouseButton为true
  • 当鼠标右键被松开时,设置mRMouseButton为false
  • 当鼠标被“拖拽”时,改变视图
  • 当鼠标被“拖拽”时,隐藏鼠标光标

找到MouseQueryListener::mouseMoved方法。我们将要增加代码使每次鼠标移动时移动鼠标光标。向函数中增加代码:

       // Update CEGUI with the mouse motion       CEGUI::System::getSingleton().injectMouseMove(arg.state.X.rel, arg.state.Y.rel);

现在找到MouseQueryListener::mousePressed方法。这段代码当鼠标右键按下时,隐藏光标,并设置变量mRMouseDown为true。

       // Left mouse button down       if (id == OIS::MB_Left)       {           mLMouseDown = true;       } // if       // Right mouse button down       else if (id == OIS::MB_Right)       {           CEGUI::MouseCursor::getSingleton().hide();           mRMouseDown = true;       } // else if

接下来,当鼠标右键抬起时,我们需要再次显示光标,并将mRMouseDown设置为false。找到mouseReleased函数,增加如下代码:

       // Left mouse button up       if (id == OIS::MB_Left)       {           mLMouseDown = false;       } // if       // Right mouse button up       else if (id == OIS::MB_Right)       {           CEGUI::MouseCursor::getSingleton().show();           mRMouseDown = false;       } // else if

现在,我们有了全部准备好的代码,我们想要在按住鼠标右键移动鼠标时改变视图。我们要做的就是,读取他自上次调用方法后移动的距离。这可以用与基础教程5中旋转摄像机镜头一样的方法实现。找到TutorialFrameListener::mouseMoved函数,就在返回状态前,增加如下代码:

       // If we are dragging the left mouse button.       if (mLMouseDown)       {       } // if       // If we are dragging the right mouse button.       else if (mRMouseDown)       {           mCamera->yaw(Degree(-arg.state.X.rel * mRotateSpeed));           mCamera->pitch(Degree(-arg.state.Y.rel * mRotateSpeed));       } // else if

现在如果你编译并运行这些代码,你将能够通过按住鼠标右键控制摄像机往哪里看。

地形碰撞检测

我们现在要实现它,以便当我们向着地面移动时,能够不穿过地面。因为BaseFrameListener已经处理了摄像机移动,所以我们就不用碰那些代码了。替代地,在BaseFrameListener移动了摄像机后,我们要确保摄像机在地面以上10个单位处。如果它不在,我们要把它移到那儿。请跟紧这段代码。我们将在本教程结束前使用RaySceneQuery做几件别的事,而且在这段结束后,我不会再做如此详细的介绍。

找到MouseQueryListener::frameStarted方法,移除该方法的全部代码。我们首先要做的事是调用ExampleFrameListener::frameStarted方法。如果它返回false,则我们也会返回false。

        // Process the base frame listener code.  Since we are going to be        // manipulating the translate vector, we need this to happen first.        if (!ExampleFrameListener::frameStarted(evt))            return false;

我们在frameStarted函数的最开始处做这些,是因为ExampleFrameListener的frameStarted成员函数移动摄像机,并且在此发生后我们需要在函数中安排我们的剩余行动。我们的目标及时找到摄像机的当前位置,并沿着它向地面发射一条射线。这被称为射线场景查询,它会告诉我们我们下面的地面的高度。得到了摄像机的当前位置后,我们需要创建一条射线。这条射线有一个起点(射线开始的地方),和一个方向。在本教程的情况下,我们的方向是Y轴负向,因为我们指定射线一直向下。一旦我们创建了射线,我们就告诉RaySceneQuery对象使用它。

       // Setup the scene query       Vector3 camPos = mCamera->getPosition();       Ray cameraRay(Vector3(camPos.x, 5000.0f, camPos.z), Vector3::NEGATIVE_UNIT_Y);       mRaySceneQuery->setRay(cameraRay);

注意,我们已经使用了5000.0f高度代替了摄像机的实际位置。如果我们使用摄像机的Y坐标代替这个高度,如果摄像机在地面以下,我们会错过整个地面。现在我们需要执行查询,得到结果。查询结果是std::iterator类型的。

        // Perform the scene query        RaySceneQueryResult &result = mRaySceneQuery->execute();        RaySceneQueryResult::iterator itr = result.begin();

在本教程中的这个地形条件下,查询结果基本上是一个worldFragment的列表和一个可移动物体(稍后的教程会介绍到)的列表。如果你对STL迭代器不太熟悉,只要知道调用begin方法获得迭代器的第一个元素。如果result.begin() == result.end(),那么无返回结果。在下一个演示程序里,我们将处理SceneQuery的多个返回值。目前,我们只要挥挥手,在其间移动。下面的这行代码保证了至少返回一个查询结果(itr != result.end()),那个结果是地面(itr->worldFragment)。

        // Get the results, set the camera height        if (itr != result.end() && itr->worldFragment)        {
  

worldFragment结构包含有在变量singleIntersection(一个Vector3)中射线击中地面的位置。我们要得到地面的高度,依靠将这个向量的Y值赋值给一个本地变量。一旦我们有了高度,我们就要检查摄像机是否低于这一高度,如果低于这一高度,那么我们要将摄像机向上移动至地面高度。注意,我们实际将摄像机多移动了10个单位。这样保证我们不能由于太靠近地面而看穿地面。

            Real terrainHeight = itr->worldFragment->singleIntersection.y;            if ((terrainHeight + 10.0f) > camPos.y)                mCamera->setPosition( camPos.x, terrainHeight + 10.0f, camPos.z );        }        return true;

最后,我们返回true,继续渲染。此时,你应该编译测试你的程序了。

地形选择

在这部分中,每次点击鼠标左键,我们将向屏幕上创建和添加对象。每次你点击、按住鼠标左键,就会创建一个对象并跟随你的光标。你可以移动对象,直到你松开鼠标左键,同时对象也锁定在那一点上。要做到这些,我们需要改变mousePressed函数。在MouseQueryLlistener::mousePressed函数中,找到如下代码。我们将要在这个if语句中增加一些代码。

       // Left mouse button down       if (id == OIS::MB_Left)       {           mLMouseDown = true;       } // if

第一段代码看起来会很熟悉。我们会创建一条射线以供mRaySceneQuery对象使用,设置射线。Ogre给我们提供了Camera::getCameraToViewpointRay;一个将屏幕上的点击(X和Y坐标)转换成一条可供RaySceneQuery对象使用的射线的好用函数。

           // Left mouse button down           if (id == OIS::MB_Left)           {               // Setup the ray scene query, use CEGUI's mouse position               CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();               Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width), mousePos.d_y/float(arg.state.height));               mRaySceneQuery->setRay(mouseRay);

接下来,我们将执行查询,并确保它返回一个结果。

               // Execute query               RaySceneQueryResult &result = mRaySceneQuery->execute();               RaySceneQueryResult::iterator itr = result.begin( );               // Get results, create a node/entity on the position               if (itr != result.end() && itr->worldFragment)               {
  

既然我们有了worldFragment(也就是点击的位置),我们就要创建对象并把它放到位。我们的第一个难题是,Ogre中每个实体和场景节点都需要一个唯一的名字。要完成这一点,我们要给每个实体命名为“Robot1”,“Robot2”,“Robot3”……同样将每个场景节点命名为“Robot1Node”,“Robot2Node”,“Robot3Node”……等等。首先,我们创建名字(更多关于sprintf的信息,请参考C语言)。

               char name[16];               sprintf( name, "Robot%d", mCount++ );

接下来,我们创建实体和场景节点。注意,我们使用itr->worldFragment->singleIntersection作为我们的机器人的默认位置。由于地形太小所以我们也把他缩小为原来的十分之一。注意我们要将这个新建的对象赋值给成员变量mCurrentObject。我们将在下一段要用到它。

                   Entity *ent = mSceneMgr->createEntity(name, "robot.mesh");                   mCurrentObject = mSceneMgr->getRootSceneNode()->createChildSceneNode(String(name) + "Node", itr->worldFragment->singleIntersection);                   mCurrentObject->attachObject(ent);                   mCurrentObject->setScale(0.1f, 0.1f, 0.1f);               } // if               mLMouseDown = true;           } // if

现在编译运行程序。你可以在场景中点击地形上任意地点放置机器人。我们几乎完全控制了我们的程序,但是在结束前,我们需要实现对象拖拽。我们要在这个if语句段中添加代码:

       // If we are dragging the left mouse button. if (mLMouseDown) { } // if

接下来的代码段现在应该是不言而喻的。我们创建了一条基于鼠标当前位置的射线,然后我们执行了射线场景查询且将对象移动到新位置。注意我们不必检查mCurrentObject看看它是不是有效的,因为如果mCurrentObject未被mousePressed设置,那么mLMouseDown不会是true。

       if (mLMouseDown)       {           CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();           Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width),mousePos.d_y/float(arg.state.height));           mRaySceneQuery->setRay(mouseRay);           RaySceneQueryResult &result = mRaySceneQuery->execute();           RaySceneQueryResult::iterator itr = result.begin();           if (itr != result.end() && itr->worldFragment)               mCurrentObject->setPosition(itr->worldFragment->singleIntersection);       } // if

编译运行程序。现在全部都完成了。点击几次后,你得到的结果应该看起来如下图所示。 [image:Intermediate_Tutorial_2.jpg]

进阶练习

简单练习

  1. 要阻止摄像机镜头看穿地形,我们选择地形上10个单位。这一选择是任意的。我们可以改进这一数值使之更接近地表而不穿过吗?如果可以,设定此变量为静态类成员并给它赋值。
  2. 有时我们确实想要穿越地形,特别是在场景编辑器中。创建一个标记控制碰撞检测开关,并绑定到一个键盘上的按键上。确保碰撞检测被关闭时,你不会在frameStarted中进行SceneQuery场景查询。

中级练习

  1. 当前我们每帧都要做场景查询,无论摄像机是否实际移动过。修补这个问题,如果摄像机移动了,只做一次场景查询。(提示:找到ExampleFrameListener中的移动向量,调用函数后,测试它是否为Vector3::ZERO。)

高级练习

  1. 注意到,每次我们执行一个场景查询调用时,有许多代码副本。将所有场景查询相关功能打包到一个受保护的函数中。确保处理地形一点不交叉的情况。

进阶练习

  1. 在这个教程中,我们使用了RaySceneQueries来放置地形上的对象。我们也许可以用它做些别的事情。拿来中级教程1的代码,完成困难问题1和专家问题1。然后将那个代码融合到这个代码中,使机器人行走在地面上,而不是虚空中。
  2. 增加代码,使每次你点击场景中的一点时,机器人移动到那个位置。

 

中级教程三

鼠标选取以及场景查询遮罩

目录

[隐藏]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值