OGRE/Buforowane wejście

Z Wikibooks, biblioteki wolnych podręczników.

Rozpoczęcie[edytuj]

Podobnie jak w poprzednim rozdziale oprzemy się na zalążku programu OGRE, który należy wkleić w pierwszy utworzony plik .cpp. Uzupełnimy go w trakcie pracy o klasę TutorialFrameListener. Na razie nadpiszmy metody dziedziczone z klasy ExampleApplication. Odszukaj linię:

    /** createScene jest funkcją czysto wirtualną w ExampleApplication,

i przed nią wstaw poniższy kod:

     void createCamera(void)
     {
         // create camera, but leave at default position
         mCamera = mSceneMgr->createCamera("PlayerCam"); 
         mCamera->setNearClipDistance(5);
     }
 
     void createFrameListener(void)
     {
         // Create the FrameListener
         mFrameListener = new TutorialFrameListener(mWindow, mCamera, mSceneMgr);
         mRoot->addFrameListener(mFrameListener);
 
         // Show the frame stats overlay
         mFrameListener->showDebugOverlay(true);
     }

Oraz wewnątrz metody createScene wstaw:

         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" );

Buforowane wejście[edytuj]

Wprowadzenie[edytuj]

W poprzednim rozdziale używaliśmy niebuforowanego wejścia, dlatego też co każdą klatkę sprawdzaliśmy stan obiektu InputReader, który przechowuje informację jakie przyciski klawiatury i myszy są wciśnięte. Buforowane wejście używa interfejsu mouse i key listenera, aby uaktualniać dane np. kiedy przycisk został wciśnięty zdarzenie KeyListener::keyPressed jest wywoływane, natomiast kiedy zostanie on opuszczony (przecisk nie jest już przyciskany) KeyListener::keyReleased zostaje wywołany. Dzięki temu nie musimy przechowywać dodatkowych zmiennych, aby określać, czy dany przycisk został wciśniety, a także blokować akcji na pewien czas.

Wady[edytuj]

Ogrowy system buforowanego wejścia nie jest całkowicie spójny, o czym będziemy mówić później. Istnieją takie części systemu, które są całkowicie błędne i nie mogą być używane. Ogre nie obsługuje też joysticków i gamepadów, co może stanowić duży problem w pewnych typach gier.

Dlaczego Ogrowy system wejścia jest częściowo wadliwy i nie zgodny wewnętrznie? Odpowiedź jest prosta - Ogre jest projektowany jako silnik graficzny. Prawdopodobnie system wejścia w Ogre nie istniałby, gdyby nie potrzeba tworzenia dem dla różnych platformy.

W skrócie system wejścia w wielu przypadkach jest dobry i wiele problemów może zostać pokonanych. Może zaistnieć systuacja, że będziemy potrzebować innych systemów wejść. Zostanie to omówione na końcu rozdziału.

Interfejs buforowanego wejścia[edytuj]

Zarejestrujemy jeden FrameListener, aby odbierał zdarzenia zarówno od myszy jak i klawiatury. Nie zawsze jest to najlepszym wyjściem. W większych projektach często może być wygodniej i praktyczniej podzielić zdarzenia myszy i klawiatury na dwa odrębne FrameListener-y.

Użyjemy trzech interfejsów. Interfejs KeyListener będzie używany do odbierania wszystkich zdarzeń związanych z klawiaturą, MouseListener używac będziemy do zdarzeń związanych z przyciskaniem przycisków myszy, a interfejs MouseMotionListener będzie związany z ruchem myszy.

KeyListener[edytuj]

Interfejst KeyListener określa kilka funkcji służących do pobierania wejścia klawiatury. Metoda keyPressed jest wywoływana, kiedy przycisk zostanie wciśnięty. Metoda keyReleased wywoływana jest, kiedy przycisk zostanie opuszczony. Funkcja keyClicked jest wywoływana, kiedy przycisk został wciśnięty i zostaje opuszczony (prawie zawsze jest odpowiednikiem keyReleased). Kiedy przycisk zostanie wciśnięty będą wywołane trzy zdarzenia: pierwsze - kiedy przycisk został wciśnięty (keyPressed), drugi - kiedy przycisk zostanie zwolniony (keyReleased), a ostatnie po tych obu (keyClicked).

MouseListener[edytuj]

Interfejs MouseListener określa funkcje odpowiedzialne za zdarzenia związane z przyciskaniem przycisku myszy. Funkcja mousePressed zostanie wywołana, kiedy użytkownik wciśnie przycisk myszy. Funkcja mouseReleased zostanie uruchomiona, kiedy przycisk myszy zostanie opuszczony. Funkcja mouseClicked nie działa, dlatego nie próbuj jej używać (ale ponieważ jest to funkcja wirtualna, możemy ją utworzyć). mouseEntered i mouseExited są funkcjami wirtualnymi, których nie będziemy używać, więc nadpiszemy je, aby były puste.

MouseMotionListener[edytuj]

Interfejs MouseMotionListener definiuje funkcje służące odpowiedzialne za zdarzenia związane z ruchem myszy. Funkcja mouseMoved jest wywoływana, kiedy mysz jest przesuwana, mouseDragged jest wywoływana, kiedy mysz jest przesuwana z przyciśniętym przyciskiem. Istnieje jeszcze trzecia funcja mouseDragMoved, która wywoływana jest gdy puszcza się coś co się przeciągało (w założeniu miała obsługiwać przeniesienie przesuwanego obiektu do celu) ale jak się wydaje jest ona napisana z błędem.

TutorialFrameListener[edytuj]

Zdefiniujmy naszą nową klasę TutorialFrameListener pamiętając o zmianach jakie chcemy wprowadzić w stosunku do poprzedniego artykułu. Po pierwsze dodajemy więcej interfejsów. Poniższy kod zacznij wprowadzać przed linią:

 // Dziedziczymy ExampleApplication

 class TutorialFrameListener : public ExampleFrameListener, public MouseMotionListener, public MouseListener
 {
 public:

Dzięki takiej definicji odziedziczyliśmy klasy MouseMotionListener i MouseListener, które umożliwiają nam odbieranie odpowiednich zdarzeń. Normalnie powinieneś dodać też KeyListener, ale ponieważ ExampleFrameListener dziedziczy ten interfejs, my nie musimy tego robić.

Zmieniamy także konstruktor ExampleFrameListener:

    TutorialFrameListener( RenderWindow* win, Camera* cam, SceneManager *sceneMgr )
        : ExampleFrameListener(win, cam, true, true)
    {
    }

Dwa argumenty true mówią, że będziemy używali buforowanego wejścia dla klawiatury i myszy.

Zmienne[edytuj]

W stosunku do poprzedniego rozdziału wprowadziliśmy kilka zmian w zmiennych klasy TutorailFrameListener. Usuneliśmy mToggle i mMouseDown, ponieważ przy buforowanym wejściu ich nie potrzebujemy. Dodaliśmy za to inne.

 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
 
     enum DirectionCodes
     {
         X = 0,
         Y = 1,
         Z = 2
     };
 };

Zmienne mRotate, mMove, mSceneMgr i mCamNode' służą do tego samego, co w poprzednim tutorialu (chociaż mMove nie będzie tu stała - wykorzystamy ją inaczej). Zmienna mContinue jest zwracana przez metodę frameStarted. Jeśli ustawimy mContinue na false, to zakończymy program. Zmienna mDirection będzie przechowywać informację, jak mamy przesunąć węzeł kamery co każdą klatkę.

Dodatkowo zdefiniowaliśmy potrzebną później stałą wyliczeniową DirectionCodes.

Konstruktor[edytuj]

Wewnątrz konstruktora zainicjujmy zmienne.

         // Wstawienie kamery i ustawienie managera sceny
         mCamNode = cam->getParentSceneNode( )->getParentSceneNode( );
         mSceneMgr = sceneMgr;
 
         // ustawienie szybkości obrotu i przesunięcia
         mRotate = 72;
         mMove = 250;
 
         // kontunujemy rendering
         mContinue = true;

Powinniśmy teraz zarejestrować nasz TutorialFrameListener jako Key, Mouse i MouseMotion listener. Klasa ExampleFrameListener zarejestrowała już samego siebie jako KeyListener, więc tę część zostawimy zakomentowaną. W normalnym przypadku jednak trzeba by było rejestrację wykonać samodzielnie.

         //mEventProcessor->addKeyListener( this );
         mEventProcessor->addMouseListener( this );
         mEventProcessor->addMouseMotionListener( this );

Na koniec musimy zainitializować mDirection jako wektor zerowy (ponieważ na początku nie chcemy się poruszać):

         mDirection = Vector3::ZERO;

Reagowanie na klawiaturę KeyListener[edytuj]

keyPressed[edytuj]

Zdefiniujmy teraz metodę TutorialFrameListener::keyPressed. Metoda ta będzie wykonywana, gdy jakiś klawisz zostanie wciśnięty. Jako argument wprowadzimy wskaźnik do obiektu KeyEvent. Możemy sprawdzić, jaki klawisz został wciśnięty (KC_*) wywołując z tego obiektu funkcję getKey. Użyjemy instrukcji switch, aby do określonego klawisza przypisać pewną akcję. Wszystkie opisane niżej metody wpisujemy w dowolnym miejscu klasy TutorialFrameListener przed dyrektywą protected.

   // KeyListener
   virtual void keyPressed(KeyEvent* e) 
   {
      switch ( e->getKey( ) )
      {

Sprawdzimy najpierw, czy nie został przyciśnięty klawisz Escape. Jeśli tak się stanie, to kończymy program. Zrobimy to ustawiając zmienną mContinue na false.

        case KC_ESCAPE:
            mContinue = false;
            break;

Wstawmy teraz obsługę pozostałych klawiszy. Umożliwimy użytkownikowi, aby mógł przestawiać widok z kamery używając klawiszy 1 i 2. Kod jest taki sam jak w poprzednim rozdziale, z wykątkiem tego, że znajduje się w instrukcji switch i nie używamy już zmiennej mToggle:

        case KC_1:
             mCamera->getParentSceneNode()->detachObject( mCamera );
             mCamNode = mSceneMgr->getSceneNode( "CamNode1" );
             mSceneMgr->getSceneNode( "PitchNode1" )->attachObject( mCamera );
             break;
 
        case KC_2:
             mCamera->getParentSceneNode()->detachObject( mCamera );
             mCamNode = mSceneMgr->getSceneNode( "CamNode2" );
             mSceneMgr->getSceneNode( "PitchNode2" )->attachObject( mCamera );
             break;

Jak zapewne zauważyłeś, kod ten wygląda czyściej i ładniej, niż ten z dodatkową zmienną wstrzymującą odbieranie zdarzeń przez pewien czas.

Dodajmy teraz możliwość przesuwania się za pomocą klawiatury. Za każdym razem, kiedy użytkownik wciśnie klawisz odpowiedzialny za ruch, będziemy dodawać lub odejmować mMove (w zależności od kierunku) odpowiednią wspołrzędną wektora przesunięcia:

        case KC_UP:
        case KC_W:
            mDirection.z -= mMove;
            break;
 
        case KC_DOWN:
        case KC_S:
            mDirection.z += mMove;
            break;
 
        case KC_LEFT:
        case KC_A:
            mDirection.x -= mMove;
            break;
 
        case KC_RIGHT:
        case KC_D:
            mDirection.x += mMove;
            break;
 
        case KC_PGDOWN:
        case KC_E:
            mDirection.y -= mMove;
            break;
 
        case KC_PGUP:
        case KC_Q:
            mDirection.y += mMove;
            break;
      }
   }

keyRelased[edytuj]

Kiedy klawisz zostanie zwolniony musimy musimy wycofać zmiany poczynione przy jego przyciśnięciu. W tym celu zdefiniujemy podobną do wyżej opisanej metodę TutorialFrameListener::keyRelased.

   virtual void keyReleased(KeyEvent* e)
   {
      switch ( e->getKey( ) )
      {
        case KC_UP:
        case KC_W:
            mDirection.z += mMove;
            break;
 
        case KC_DOWN:
        case KC_S:
            mDirection.z -= mMove;
            break;
 
        case KC_LEFT:
        case KC_A:
            mDirection.x += mMove;
            break;
 
        case KC_RIGHT:
        case KC_D:
            mDirection.x -= mMove;
            break;
 
        case KC_PGDOWN:
        case KC_E:
            mDirection.y += mMove;
            break;
 
        case KC_PGUP:
        case KC_Q:
            mDirection.y -= mMove;
            break;
        } // switch
      }

keyClicked[edytuj]

Dodatkowo zdefiniujemy pustą metodę TutorialFrameListener::keyClicked bo nie chcemy by cokolwiek było wykonywane w takim przypadku.

   virtual void keyClicked(KeyEvent* e) { }

powiązanie z ruchem kamery we frameStarted[edytuj]

I wreszcie powiążemy nasz uaktualniony za pomocą klawiatury wektor przesunięcia z ruchem kamery wewnątrz metody frameStarted. Należy dodać, że gdybyśmy wstawili ten kod do funkcji keyPressed, ruch został by wykonany tylko w chwili przyciśnięcia przycisku. My jednak chcemy, aby poruszał się zawsze, co klatkę, kiedy odpowiedni klawisz jest wciśnięty. Zdefiniujmy zatem metodę TutorialFrameListener::frameStarted:

    bool frameStarted(const FrameEvent &evt)
    {
        mCamNode->translate( mCamNode->getOrientation() * mDirection * evt.timeSinceLastFrame );
        return mContinue;
    }

Jeżeli chcesz możesz teraz sprawdzić swój program. Jednak by klasa TutorialFrameListener mogła być zainicjowana musimy tymczasowo wprowadzić definicje pozostałych wymaganych metod. Wprowadzić je możemy za lub przed metodą frameStarted.

   // MouseDragged
   void mouseMoved(MouseEvent* e) { }
   void mouseDragged(MouseEvent* e) { }
   // MouseListener
   void mouseClicked(MouseEvent* e) { }
   void mouseEntered(MouseEvent* e) { }
   void mouseExited(MouseEvent* e) { }
   void mousePressed(MouseEvent* e) { }
   void mouseReleased(MouseEvent* e) { }

Przekompiluj teraz i uruchom program. Można poruszać kamerą za pomocą klawiatury.

Reagowanie na mysz[edytuj]

mousePressed[edytuj]

Teraz oprogramujemy obsługę myszy. Zaczniemy od włączania lub wyłączania swiatła za pomocą lewego przycisku myszy. Znajdź metodę mousePressed. Jest ona wywoływana z obiektem MouseEvent. Wcześniej używaliśmy wartości 0, 1, 2 itd., aby określić który przycisk został wciśnięty. Tym razem do tego celu użyjemy maski. Poniższy kod wprowadź wewnątrz metody.

        if ( e->getButtonID() & MouseEvent::BUTTON0_MASK )
        {

Jest to bardziej estetyczne, ale zawsze musisz pamiętać, że używając wywołań MouseListenera stosujesz maskę przycisków, natomiast używając wszystkich innych funkcji związanych myszką musisz używać numeru przycisku (0, 1, 2...), aby skontrolować, czy jest wciśnięty. Ostatecznie dodajemy jeszcze w mousePressed instrukcje identyczne, jak w poprzednim rozdziale:

             Light *light = mSceneMgr->getLight( "Light1" );
             light->setVisible( ! light->isVisible() );
        }

Możesz teraz przekompilować i uruchomić aplikację. Jak widać nasze założenia powoli się spełniają.

mouseMoved[edytuj]

Musimy jeszcze tylko wstawić możliwość obracania kamery za pomocą poruszania myszą z wciśniętym prawym przyciskiem myszy. W tym celu wewnątrz metody mouseMoved wstawiamy kod który po rozpoznaniu za pomocą maski czy wciśnięty jest prawy przycisk zmienia ustawienia kamery zgodnie z ruchem myszki:

        if ( e->getButtonID() & MouseEvent::BUTTON1_MASK )
        {
             mCamNode->yaw( Degree(-e->getRelX( ) * mRotate) );
             mCamNode->getChild( 0 )->pitch( Degree(-e->getRelY() * mRotate) );
        }

Przekompiluj i uruchom program.

Ups! Nie działa! Otóż znów ocieramy się o błędy w funkcjach wejścia systemu Ogre. Jak dotychczas (do Ogre 1.2 włącznie) w wyłowaniach MouseMotion otrzymujemy obiekt MouseEvent, w którym metoda getButtonID zawsze zwraca 0. Pomińmy więc ten problem i zróbmy tak, aby nasza metoda działała dla ruchu myszką bez wciśniętego żadnego przycisku myszy. Usuwamy funkcję if pozostawiając jej wnętrze:

        //if ( e->getButtonID() & MouseEvent::BUTTON1_MASK )
        //{
             mCamNode->yaw( Degree(-e->getRelX( ) * mRotate) );
             mCamNode->getChild( 0 )->pitch( Degree(-e->getRelY() * mRotate) );
        //}

Przekompiluj i uruchom program. Teraz przesuwając mysz bez wciśniętego żadnego klawisza poruszymy kamerę. Można usunąć ten problem poprzez umieszczaniu w mousePressed i mouseReleased wartość logiczną, która będzie wynosiła true lub false, w zależności czy przycisk jest wciśnięty czy nie.

Można też powyższy kod przenieść do metody mouseDragged co spowoduje, że ruchy kamerą będą odbywać się przy wciśniętym dowolnym przycisku myszki.

Inne systemy wejścia[edytuj]

Jak wspomnieliśmy wcześniej, możesz użyć w Ogre także innych systemów wejścia. Można nawet skorzystać z innych systemów zarządzania oknami, takich jak wxWidgets, który został dobrze zintegorwany z Ogre. Warte uwagi jest także SDL, który dostarcza wieloplatformowy system wejścia i tworzenia okien. Jedną z jego cech jest to, że obsługuje wejście joystick/gamepad, które nie istnieje w Ogre. Aby uruchomić SDLowy system joysticków, wstaw do swojej aplikacji wywołania SDL_Init i SDL_Quit (może to być w funkcji main albo w twoim obiekcie aplikacji):

        SDL_Init( SDL_INIT_JOYSTICK | SDL_INIT_NOPARACHUTE );
        SDL_JoystickEventState( SDL_ENABLE );
 
        app.go();
  
        SDL_Quit( );

Aby skonfigurować Joystick wywołaj SDL_JoystickOpen z numerem Joysticka (możesz używać wiele joysticków używając 0, 1, 2...):

    SDL_Joystick* mJoystick;
    mJoystick = SDL_JoystickOpen( 0 );
 
    if ( mJoystick == NULL )
        ; // wychwytywanie błędów

Jeśłi SDL_JoystickOpen zwróci NULL, wtedy napotkaliśmy problem z otworzeniem joysticka. Prawie zawsze to oznacza, że joystick o który prosiliśmy nie istnieje. Możesz użyć SDL_NumJoysticks, aby się dowiedzieć ile joysticków jest w systemie. Potrzebujesz także zamknąć the joystick przed zakończeniem programu:

    SDL_JoystickClose( mJoystick );

Aby skorzystać z joysticka wywołujemy funkcje SDL_JoystickGetButton i SDL_JoystickGetAxis. Tak więc możemy napisać kod okreśłający wektor przemieszczenia np. w ten sposób:

        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 będzie później wstawiane do kamery metody SceneNode::translate, xRot będzie wykorzystywane w SceneNode::yaw, a yRot zostanie użyte do SceneNode::pitch. Należy dodać, że SDL_JoystickGetAxis zwraca wartość pomiędzy -32767 i 32767. Tak więc w kodzie wartość ta zostanie przeskalowana na mieszczącą się między -1 i 1. Więcej informacji o joystickach w SDL możemy się znaleźć nagłówku.

Jeśli zamierzasz na poważnie wykorzystać SDL, powinieneś się także zapoznać z dokumentacją.

Kod źródłowy[edytuj]

Dostępny tu jest kompletny kod źródłowy wykorzystany w powyższym artykule.