先决条件
本教程假定你已经拥有了c++程序设计的知识,并且已经安装和编译了一个Ogre的应用程序(如果你在设置你的应用程序中有困难,请参考this guide获得更详细的编译步骤)。这个教程同时也是建立在上一章基础上的,因此默认你已经了解了上个教程的内容。
[编辑]介绍
在这一课里,你将学会OIS的带缓冲的输入, 这不同于上节课中无缓冲的输入。而且在这节课中,当鼠标键盘的事件发生时,我们会立即处理它,不会像上节课那样每一帧只处理一次。请注意这里只是对缓冲输入的一个简单介绍,而不是完整的如何使用OIS的教程。若想了解更多内容,请查阅相关的OIS使用教程。
你可以找到这节课的源代码。在你学习本课的时候,你应该逐个地将这些代码添加到你自己的工程里去,然后构建并观察结果。
[编辑]从这开始
本课是基于上一课的,但我们将改变输入的方式。既然功能基本上是相同的,我们就使用上次的TutorialApplication类,但我们会从TutorialFrameListener开始。用你喜欢的编译器创建一个工程,再添加一个包含如下代码的源文件:
#include "ExampleApplication.h" class TutorialFrameListener : public ExampleFrameListener, public OIS::MouseListener, public OIS::KeyListener { public: TutorialFrameListener(RenderWindow* win, Camera* cam, SceneManager *sceneMgr) : ExampleFrameListener(win, cam, true, true) { } bool frameStarted(const FrameEvent &evt) { if(mMouse) mMouse->capture(); if(mKeyboard) mKeyboard->capture(); return mContinue; } // MouseListener bool mouseMoved(const OIS::MouseEvent &e) { return true; } bool mousePressed(const OIS::MouseEvent &e, OIS::MouseButtonID id) { return true; } bool mouseReleased(const OIS::MouseEvent &e, OIS::MouseButtonID id) { return true; } // KeyListener bool keyPressed(const OIS::KeyEvent &e) { return true; } bool keyReleased(const OIS::KeyEvent &e) { return true; } protected: Real mRotate; // The rotate constant Real mMove; // The movement constant SceneManager *mSceneMgr; // The current SceneManager SceneNode *mCamNode; // The SceneNode the camera is currently attached to bool mContinue; // Whether to continue rendering or not Vector3 mDirection; // Value to move in the correct direction }; class TutorialApplication : public ExampleApplication { public: void createCamera(void) { // create camera, but leave at default position mCamera = mSceneMgr->createCamera("PlayerCam"); mCamera->setNearClipDistance(5); } void createScene(void) { mSceneMgr->setAmbientLight(ColourValue(0.25, 0.25, 0.25)); // add the ninja Entity *ent = mSceneMgr->createEntity("Ninja", "ninja.mesh"); SceneNode *node = mSceneMgr->getRootSceneNode()->createChildSceneNode("NinjaNode"); node->attachObject(ent); // create the light Light *light = mSceneMgr->createLight("Light1"); light->setType(Light::LT_POINT); light->setPosition(Vector3(250, 150, 250)); light->setDiffuseColour(ColourValue::White); light->setSpecularColour(ColourValue::White); // Create the scene node node = mSceneMgr->getRootSceneNode()->createChildSceneNode("CamNode1", Vector3(-400, 200, 400)); // Make it look towards the ninja node->yaw(Degree(-45)); // Create the pitch node node = node->createChildSceneNode("PitchNode1"); node->attachObject(mCamera); // create the second camera node/pitch node node = mSceneMgr->getRootSceneNode()->createChildSceneNode("CamNode2", Vector3(0, 200, 400)); node = node->createChildSceneNode("PitchNode2"); } void createFrameListener(void) { // Create the FrameListener mFrameListener = new TutorialFrameListener(mWindow, mCamera, mSceneMgr); mRoot->addFrameListener(mFrameListener); // Show the frame stats overlay mFrameListener->showDebugOverlay(true); } }; #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) #else int main(int argc, char **argv) #endif { // Create application object TutorialApplication app; try { app.go(); } catch(Exception& e) { #if OGRE_PLATFORM == PLATFORM_WIN32 || 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; }
如果你是在Windows下使用OgreSDK的,请确定添加“"[OgreSDK_DIRECTORY]/samples/include”目录到这个工程(ExampleApplication.h文件所在的位置)除了标准包含以外。如果使用的是Ogre的源代码包,这个文件应该在“"[OgreSource_DIRECTORY]/Samples/Common/include”目录中。不要试着去运行程序,因为我们还没有定义键盘的行为。如果你有问题,请查看这个Wiki页获得设置你编译器的信息,如果仍然有问题请试着去看看帮助栏。
注意:如果你没有把你的工程设置成使用Ansi C++ ,并收到关于MessageBox的错误信息,你可能需要把对MessageBox的引用改成MessageBoxA。
程序的控制与上一课的相同。
[编辑]简单的缓冲输入
[编辑]介绍
在上一次课里,我们使用的是无缓冲的输入,也就是说,在每一帧里我们查询OIS::Keyboard和OIS::Mouse实例的状态,以判断它们是否被按下。而缓冲输入使用了一个listener接口,以便在事件发生时通知你的程序。比如,当一个键被按下时,会触发一个 KeyListener::keyPressed 事件,而当这个键被释放(不再按下)时,KeyListener::keyReleased 事件被触发给所有已注册的KeyListener类。这些能用在追踪按键的时间,或判断按键在上一帧中是否没有被按下。
通过OIS::JoystickListener 接口,OIS也支持无缓冲的操纵杆事件,但在本课我们不会涉及到。
关于OIS的监听系统有一点要注意的是,对于每一个Keyboard,Mouse,Joystick对象只能有一个监听器。这样是为了简单(也为了速度)。多次调用setEventCallback函数(后面会讲到)的结果是只有最后一次注册的监听器才得到事件消息。如果你有多个对象需要获得Key,Mouse,或Joystick事件,你只有自己写一个消息分发。还有,千万记得在frameStarted方法里调用Keyboard::capture和Mouse::capture。OIS不会使用线程(或其它玩意儿)来确定键盘鼠标的状态,所以你必须指明什么时候去获取输入。
[编辑]键盘监听界面
OIS的KeyListener接口提供了两个纯虚函数。第一个是keyPressed函数(每次按下某个键时调用它),还一个是keyReleased(每次离开某个键时调用它)。传入这些函数的参数是一个KeyEvent,它包含被按下/释放的按键的键码。
[编辑]鼠标监听界面
MouseListener接口比KeyListener接口要稍微复杂一些。它包含查看何时鼠标键被按下/释放的函数: MouseListener::mousePressed 和 MouseListener::mouseReleased. 它还包含一个mouseMoved函数,当鼠标移动时调用它。这些函数都接收一个MouseEvent对象,在"state"变量里保存着当前鼠标的状态。
需要注意的是,MouseState对象即包含了鼠标移动的相对XY坐标(即,从上一次调用MouseListener::mouseMoved开始,它所移动的距离),还包含了绝对XY坐标(即,屏幕上的准确位置)。
[编辑]代码
[编辑]TutorialFrameListener的构造函数
在我们开始修改TutorialFrameListener之前,请注意我们对TutorialFrameListener类做了两处大的改变:
class TutorialFrameListener : public ExampleFrameListener, public OIS::MouseListener, public OIS::KeyListener 我们继承了OIS的MouseListener和KeyListener类,这样我们才能从它们那里接收事件。注意,OIS的MouseListener处理鼠标按钮事件,也处理鼠标移动事件。
同样,ExampleFrameListener的构造函数也有变化:
: ExampleFrameListener(win, cam, true, true)
这个"true, true"参数指明了我们将要使用带缓冲的键盘鼠标输入。我们接下面会深入介绍如何手动地设置OIS,以及Ogre的其它部分。
[编辑]变量
上一课的变量在这里有些改变。我移除了mToggle和mMouseDown变量(再也用不到了)。我还添加了一些其它的:
Real mRotate; // 旋转常量 Real mMove; // 运动常量 SceneManager *mSceneMgr; // 当前的场景管理器 SceneNode *mCamNode; // 当前摄像机附着的场景节点 bool mContinue; // 是否要继续渲染 Vector3 mDirection; // 指向正确的移动方向
mRotate, mMove, mSceneMgr, 以及mCamNode与上一课的相同(虽然我们会改变mRotate的值,因为它有不同的用处)。 mContinue变量是frameStarted方法的返回值。当mContinue为false的时候,程序退出。mDirection变量指定了在每一个帧里我们如何移动摄像机节点。
[编辑]TutorialFrameListener构造函数
在构造函数里,我们像在上一课那样初始化一些变量,并把mContinue设成true。添加如下代码到TutorialFrameListener的构造函数里:
// Populate the camera and scene manager containers mCamNode = cam->getParentSceneNode(); mSceneMgr = sceneMgr; // 设置旋转和移动速度 mRotate = 0.13; mMove = 250; // 继续渲染 mContinue = true;
在ExampleFrameListener的构造函数里已经取得了OIS的mMouse和mKeyboard对象。我们调用这些输入对象的setEventCallback方法,把TutorialFrameListener注册成一个监听器。
mMouse->setEventCallback(this); mKeyboard->setEventCallback(this);
最后,我们还要把mDirection初始化成零向量(因为我们最开始不需要它动):
mDirection = Vector3::ZERO;
[编辑]键盘绑定
在我们深入之前,我们应该设置Escape键用来退出程序。找到TutorialFrameListener::keyPressed方法,每当键盘上一个键被按下时,都会调用这个方法并传入一个KeyEvent对象。我们能够通过这个对象的"key"变量来获取按键的键码(KC_*)。基于这个值,我们构造一个switch,为绑定所有程序里用到的按钮。
bool keyPressed(const OIS::KeyEvent &e) { switch (e.key) { case OIS::KC_ESCAPE: mContinue = false; break; } return true; }
我们继续之前请保证以上都能编译和运行。
我们需要在switch语句里为其它按钮做绑定。首先我们要让用户按1、2键进行视口的切换。
代码基本上与上节课相同(需要包括进switch语句),除了不需要处理mToggle变量:
case OIS::KC_1: mCamera->getParentSceneNode()->detachObject(mCamera); mCamNode = mSceneMgr->getSceneNode("CamNode1"); mCamNode->attachObject(mCamera); break; case OIS::KC_2: mCamera->getParentSceneNode()->detachObject(mCamera); mCamNode = mSceneMgr->getSceneNode("CamNode2"); mCamNode->attachObject(mCamera); break;
瞧!这比用临时变量来保存按键时间要干净多了。
接下来我们要添加键盘移动。每次用户按下移动按键,我们都要朝正确的方向加上或者减去mMove:
case OIS::KC_UP: case OIS::KC_W: mDirection.z -= mMove; break; case OIS::KC_DOWN: case OIS::KC_S: mDirection.z += mMove; break; case OIS::KC_LEFT: case OIS::KC_A: mDirection.x -= mMove; break; case OIS::KC_RIGHT: case OIS::KC_D: mDirection.x += mMove; break; case OIS::KC_PGDOWN: case OIS::KC_E: mDirection.y -= mMove; break; case OIS::KC_PGUP: case OIS::KC_Q: mDirection.y += mMove; break;
当按键被释放时,我们要立即取消mDirection向量上的移动。找到keyReleased方法,添加如下代码:
switch (e.key) { case OIS::KC_UP: case OIS::KC_W: mDirection.z += mMove; break; case OIS::KC_DOWN: case OIS::KC_S: mDirection.z -= mMove; break; case OIS::KC_LEFT: case OIS::KC_A: mDirection.x += mMove; break; case OIS::KC_RIGHT: case OIS::KC_D: mDirection.x -= mMove; break; case OIS::KC_PGDOWN: case OIS::KC_E: mDirection.y += mMove; break; case OIS::KC_PGUP: case OIS::KC_Q: mDirection.y -= mMove; break; } // switch return true;
好了,我们能根据按键输入对mDirection进行更新了,我们还需要让它真正移动起来。下面的代码与上节课是一样的,添加到frameStarted函数里:
mCamNode->translate(mDirection * evt.timeSinceLastFrame, Node::TS_LOCAL);
编译并运行程序。我们现在已经有了带缓冲的输入来控制运动!
[编辑]鼠标绑定
现在我们已经完成了键盘绑定,接下来轮到鼠标了。我们从点击鼠标左键来控制灯的开关开始。找到mousePressed函数并看看它的参数。用OIS,我们可以访问MouseEvent和MouseButtonID。我们用MouseButtonID作为switch条件,来确定按下的是哪个按钮。用下面的代码替换掉mousePressed函数里的:
Light *light = mSceneMgr->getLight("Light1"); switch (id) { case OIS::MB_Left: light->setVisible(! light->isVisible()); break; } return true;
编译并运行。 噢耶! 成功了,剩下来的事情就是绑定鼠标右键来进入鼠标观察模式。每当鼠标移动时我们都检查右键是否按下。如果是,我们基于相对运动来转动摄像机。通过传入函数的MouseEvent对象,我们能获取相对运动。它包含一个"state"变量,里面有鼠标的状态(是关于鼠标的详细信息)。MouseState::buttonDown告诉我们是否一个特定的按钮被按下,而“X”和“Y”变量告诉我们鼠标的相对运动。找到mouseMoved方法,用以下代码替换掉原来的:
if (e.state.buttonDown(OIS::MB_Right)) { mCamNode->yaw(Degree(-mRotate * e.state.X.rel), Node::TS_WORLD); mCamNode->pitch(Degree(-mRotate * e.state.Y.rel), Node::TS_LOCAL); } return true;
编译并运行程序。当鼠标右键被按下时,摄像机就以自由视角的模式工作。
[编辑]其他输入系统
OIS是非常棒的,应该满足你的应用程序的大部分需求。话说回来,你也有其它的选择。有的窗口系统像wxWidgets也许正是你想要的,人们已经成功地把它整合进了Ogre。如果你不介意你的应用程序是特定平台的,你也可以使用标准的Windows消息系统,或者从Linux GUI工具集中选择一个。
你也可以试一试SDL,它是一个跨平台的带窗口输入系统,并支持摇杆/手柄输入。我不能指导你用Ogre如何设置wx/gtk/qt这些(因为我自己也没搞过-_-),但我有许多将SDL的摇杆/手柄输入整合进Ogre的成功经验。为了使SDL的操纵杆系统生效,通过调用SDL_Init和SDL_Quit,把你的应用程序包裹起来(放在你的程序main函数里,或放在你的应用对象里):
SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_NOPARACHUTE); SDL_JoystickEventState(SDL_ENABLE); app.go(); SDL_Quit();
为了设置Joystick,调用SDL_JoystickOpen并传入操纵杆编号(你能通过0、1、2...来指定多个操纵杆):
SDL_Joystick* mJoystick; mJoystick = SDL_JoystickOpen(0); if ( mJoystick == NULL ) ; // error handling
如果JoystickOpen返回NULL,表示打开joystick有问题。这基本上意味着你请求的摇杆不存在。使用SDL_NumJoysticks来确定连接在系统上的摇杆数量。当你结束之后,需要关闭摇杆:
SDL_JoystickClose(mJoystick);
为了使用摇杆,调用JoystickGetButton和JoystickGetAxis。我个人使用的是PS2的手柄,所以我有四个轴向和12个按钮来玩。这是我的操纵代码:
SDL_JoystickUpdate(); mTrans.z += evt.timeSinceLastFrame * mMoveAmount * SDL_JoystickGetAxis(mJoystick, 1) / 32767; mTrans.x += evt.timeSinceLastFrame * mMoveAmount * SDL_JoystickGetAxis(mJoystick, 0) / 32767; xRot -= evt.timeSinceLastFrame * mRotAmount * SDL_JoystickGetAxis(mJoystick, 3) / 32767; yRot -= evt.timeSinceLastFrame * mRotAmount * SDL_JoystickGetAxis(mJoystick, 2) / 32767;
mTrans在后面要传入到摄像机的SceneNode::translate方法,xRot传入到SceneNode::yaw,而yRot传入到SceneNode::pitch。注意,SDL_JoystickGetAxis返回一个从-32767到32767的值,所以我得把它缩小到-1到1的范围。这应该可以让你开始使用SDL摇杆输入了。若你想查看更多关于这个领域的信息,SDL joystick的头文件是最好的文档。
如果你想要认真地在你的程序时使用SDL,你应该查阅标准SDL文档。