先決條件
本教程假定你已經擁有了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)#elseint 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文檔。