OGRE/Zaznaczanie myszą i maski SceneQuery

Rozpoczęcie edytuj

Zaczniemy pracę z poniższym kodem. Przy pracy z Ms C++ i SDK proszę pamiętać, żeby podczas tworzenia nowego projektu zanaczyć opcję "CEGUI Support".

#define OGRE_CHANGE1 ((1 << 16) | (1 << 8))
#include <CEGUI/CEGUISystem.h>
#include <CEGUI/CEGUISchemeManager.h>
#include <OgreCEGUIRenderer.h>

#include "ExampleApplication.h"

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

    MouseQueryListener(RenderWindow* win, Camera* cam, SceneManager *sceneManager, CEGUI::Renderer *renderer)
        : ExampleFrameListener(win, cam, false, true), mGUIRenderer(renderer)
    {
        // Domyślne ustawienia zmiennych
        mCount = 0;
        mCurrentObject = NULL;
        mLMouseDown = false;
        mRMouseDown = false;
        mSceneMgr = sceneManager;

        // Redukujemy szybkość ruchów kamerą
        mMoveSpeed = 50;
        mRotateSpeed *= 2;

        // Rejestrujemy tę procedurę w celu uzyskania zdarzeń myszki.
        mEventProcessor->addMouseListener( this );
        mEventProcessor->addMouseMotionListener( this );

        // Tworzymy zapytanie RaySceneQuery
        mRaySceneQuery = mSceneMgr->createRayQuery( Ray( ) );
    } // MouseQueryListener

    ~MouseQueryListener( )
    {
        // Stworzyliśmy zapytanie a więc musimy je też usunąć.
        delete mRaySceneQuery;
    }

    bool frameStarted(const FrameEvent &evt)
    {
        // Przetwarzamy kod bazowego frame listenera. Musimy to zrobić
        // zanim zaczniemy manipulować wektorem translacji.
        if ( !ExampleFrameListener::frameStarted( evt ) )
            return false;

        // Ustawienie zapytania
        Vector3 camPos = mCamera->getPosition( );
        Ray cameraRay( Vector3(camPos.x, 5000.0f, camPos.z), Vector3::NEGATIVE_UNIT_Y );
        mRaySceneQuery->setRay( cameraRay );

        // Wykonanie zapytania
        RaySceneQueryResult &result = mRaySceneQuery->execute();
        RaySceneQueryResult::iterator itr = result.begin( );

        // Pobieramy rezultat i ustawiamy wysokość kamery
        if ( itr != result.end() && itr->worldFragment )
        {
            Real terrainHeight = itr->worldFragment->singleIntersection.y;
            if ((terrainHeight + 10.0f) > camPos.y)
                mCamera->setPosition( camPos.x, terrainHeight + 10.0f, camPos.z );
        }

        return true;
    }

   virtual void onLeftPressed( MouseEvent *e )
   {
       // Ustawienie zapytania
       Ray mouseRay = mCamera->getCameraToViewportRay( e->getX(), e->getY() );
       mRaySceneQuery->setRay( mouseRay );

       // Wykonanie zapytania
       RaySceneQueryResult &result = mRaySceneQuery->execute();
       RaySceneQueryResult::iterator itr = result.begin( );

       // Pobranie wyniku, utworzenie obiektu w pozycji kursora myszki
       if ( itr != result.end() && itr->worldFragment )
       {
           char name[16];
           sprintf( name, "Robot%d", mCount++ );

           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
   }
 
   virtual void onRightPressed( MouseEvent *e )
   {
       CEGUI::MouseCursor::getSingleton().hide( );
   }

   virtual void onLeftReleased( MouseEvent *e )
   {
   }

   virtual void onRightReleased( MouseEvent *e )
   {
           CEGUI::MouseCursor::getSingleton().show( );
   }

   virtual void onLeftDragged( MouseEvent *e )
   {
       Ray mouseRay = mCamera->getCameraToViewportRay( e->getX(), e->getY() );
       mRaySceneQuery->setRay( mouseRay );

       RaySceneQueryResult &result = mRaySceneQuery->execute();
       RaySceneQueryResult::iterator itr = result.begin( );

       if ( itr != result.end() && itr->worldFragment )
       {
           mCurrentObject->setPosition( itr->worldFragment->singleIntersection );
       } // if
   }

   virtual void onRightDragged( MouseEvent *e )
   {
       mCamera->yaw( -e->getRelX() * mRotateSpeed * 2 );
       mCamera->pitch( -e->getRelY() * mRotateSpeed * 2 );
   }

   /* Zdarzenia MouseListenera. */
   virtual void mouseClicked(MouseEvent* e) { }
   virtual void mouseEntered(MouseEvent* e) { }
   virtual void mouseExited(MouseEvent* e)  { }

   // Kiedy naciskamy klawisz.
   virtual void mousePressed(MouseEvent* e)
   {
       // Naciskamy lewy klawisz
       if (e->getButtonID() & MouseEvent::BUTTON0_MASK)
       {
           mLMouseDown = true;
           onLeftPressed( e );
       } // if

       // Naciskamy prawy klawisz
       else if (e->getButtonID() & MouseEvent::BUTTON1_MASK)
       {
           mRMouseDown = true;
           onRightPressed( e );
       } // else if
   } // mousePressed

   // Kiedy puszczamy klawisz.
   virtual void mouseReleased(MouseEvent* e)
   {
       // Puszczamy lewy klawisz.
       if (e->getButtonID() & MouseEvent::BUTTON0_MASK)
       {
           mLMouseDown = false;
           onLeftReleased( e );
       } // if

       // Puszczamy prawy klawisz.
       else if (e->getButtonID() & MouseEvent::BUTTON1_MASK)
       {
           mRMouseDown = false;
           onRightReleased( e );
       } // else if
   } // mouseReleased

   /* Zdarzenia MouseMotionListenera */
   virtual void mouseMoved (MouseEvent *e)
   {
       // Uaktualnienie CEGUI według ruchów myszki
       CEGUI::System::getSingleton().injectMouseMove(e->getRelX() * mGUIRenderer->getWidth(), e->getRelY() * mGUIRenderer->getHeight());
   }

   // Wywoływane gdy następuje przeciąganie myszką.
   virtual void mouseDragged (MouseEvent *e)
   {
       // Uaktualnienie CEGUI według ruchów myszki
       mouseMoved(e);

       // Kiedy przęciągamy lewym klawiszem i mamy obiekt na kursorze
       if ( mLMouseDown )
       {
           onLeftDragged( e );
       } // if

       // Kiedy przeciągamy prawym klawiszem.
       else if ( mRMouseDown )
       {
           onRightDragged( e );
       } // else if
   } // mouseDragged

protected:
    RaySceneQuery *mRaySceneQuery;     // Wskaźnik zapytania
    bool mLMouseDown, mRMouseDown;     // Prawda gdy odpowiedni klawisz myszki jest naciśnięty
    int mCount;                        // Ilość utworzonych robotów
    SceneManager *mSceneMgr;           // Wskaźnik do managera sceny
    SceneNode *mCurrentObject;         // Nowo tworzony obiekt
    CEGUI::Renderer *mGUIRenderer;     // Renderer CEGUI
};

class MouseQueryApplication : public ExampleApplication
{
protected:
    CEGUI::OgreCEGUIRenderer *mGUIRenderer;
    CEGUI::System *mGUISystem;
public:
    MouseQueryApplication() : mGUIRenderer(0), mGUISystem(0)
    {
    }

    ~MouseQueryApplication() 
    {
        delete mGUISystem;
        delete mGUIRenderer;
    }
protected:
    void chooseSceneManager(void)
    {
        // Tworzymy managera sceny
#if OGRE_VERSION < OGRE_CHANGE1
       //[Ogre < 1.2]
       mSceneMgr = mRoot->getSceneManager( ST_EXTERIOR_CLOSE );
#else
       //[Ogre > 1.1]
       mSceneMgr = mRoot->createSceneManager( ST_EXTERIOR_CLOSE);
#endif
    }

    void createScene(void)
    {
       // Ustawiamy światło otoczenia
       mSceneMgr->setAmbientLight(ColourValue(0.5, 0.5, 0.5));
       mSceneMgr->setSkyDome(true, "Examples/CloudySky", 5, 8);

       // Pobieramy geometrię terenu
       mSceneMgr->setWorldGeometry( "terrain.cfg" );

       // Ustawiamy kamerę
       mCamera->setPosition( 40, 100, 580 );
       mCamera->pitch( Degree(-30) );
       mCamera->yaw( Degree(-45) );

       // Ustawiamy CEGUI
#if OGRE_VERSION < OGRE_CHANGE1
      //[Ogre <= 1.1]
      mGUIRenderer = new CEGUI::OgreCEGUIRenderer(mWindow, Ogre::RENDER_QUEUE_OVERLAY, false, 3000, ST_EXTERIOR_CLOSE);
#else
      //[Ogre > 1.1]
      mGUIRenderer = new CEGUI::OgreCEGUIRenderer(mWindow, Ogre::RENDER_QUEUE_OVERLAY, false, 3000, mSceneMgr);
#endif
       mGUISystem = new CEGUI::System(mGUIRenderer);

       // Myszka
       //Zwróć uwagę na właściwe nazwy. Może wystąpić TaharezLookSkin.
       CEGUI::SchemeManager::getSingleton().loadScheme((CEGUI::utf8*)"TaharezLook.scheme");
       CEGUI::MouseCursor::getSingleton().setImage("TaharezLook", "MouseArrow");
    }

    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 )
#else
int main(int argc, char **argv)
#endif
{
    // Tworzymy obiekt aplikacji
    MouseQueryApplication 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 occured!", MB_OK | MB_ICONERROR | MB_TASKMODAL);
#else
        fprintf(stderr, "An exception has occured: %s\n",
                e.getFullDescription().c_str());
#endif
    }

    return 0;
}

Zanim przejdziemy dalej, sprawdź czy możesz skompilować i wykonać powyższy kod. Powinien on robić to samo co opisywaliśmy w poprzednim artykule.

Pokazanie, który obiekt jest zaznaczony edytuj

W tym artykule rozbudujemy kod tak by można było zaznaczać już wcześniej umieszczone obiekty w terenie a następnie je przesuwać. Chcemy mieć możliwość pokazania użytkownikowi, który obiekt wybrał. W grze, prawdopodobnie wybralibyście jakiś specjalny sposób zaznaczania, ale w tym przykładzie użyjemy prostej metody showBoundingBox tworzącej prostopadłościan dookoła obiektu.

Nasz pomysł polega na wyłączeniu "pudełka" otaczającego poprzednio wybrany obiekt i właczeniu go dookoła nowo wybranego. W tym celu musimy doać poniższy kod na początku zdarzenia onLeftPressed:

       // Wyłaczamy "pudełko"
       if ( mCurrentObject )
           mCurrentObject->showBoundingBox( false );

a poniższy na samym końcu tego samego zdarzenia:

       // Włączamy "pudełko"
       if ( mCurrentObject )
           mCurrentObject->showBoundingBox( true );

W tym momencie mCurrentObject zawsze będzie zaznaczony na ekranie.

Dodajemy Ninja edytuj

Dodatkowo rozbudujemy nasz kod tak by obsługiwał nie tylko roboty ale również umożliwiał wstawianie i przesuwanie wojowników ninja. Musimy zatem mieć "tryb robota" i "tryb ninja", które będą determinować jaki obiekt mamy wstawić na powierzchnię gruntu. Użyjemy do tego celu klawisza spacji jako przełącznika a dodatkowo będziemy wyświetlać na ekranie informacje w jakim trybie w danej chwili się znajdujemy.

Najpierw ustawimy MouseQueryListener tak by był na początku w trybie robota. Musimy dodać zmienną która będzie trzymać stan obiektu (czyli będzie wskazywać nasz tryb). Znajdź chronioną sekcję MouseQueryListener i dodaj zmienną:

    bool mRobotMode;                   // bieżący tryb

A teraz dodaj ten kod na końcu konstruktora MouseQueryListener:

        // Ustawiamy wstępne wartości trybu i tekstu pomocy
        mRobotMode = true;
        mWindow->setDebugText( "Tryb [Robot] - by przelaczyc nacisnij klawisz spacji" );

I jesteśmy w trybie robota. Teraz musimy wprowadzić możliwość tworzenia obiektu na podstawie definicji robota lub ninji w zależności od zmiennej mRobotMode. Odnajdź taki kawałek kodu w funkcji onLeftPressed:

           char name[16];
           sprintf( name, "Robot%d", mCount++ );
  
           Entity *ent = mSceneMgr->createEntity( name, "robot.mesh" );

Zamień go na poniższy. W zależności od trybu zapisanego w mRobotMode pobieramy inną definicję i wstawiamy inną nazwę:

           Entity *ent;
           char name[16];
  
           if ( mRobotMode )
           {
               sprintf( name, "Robot%d", mCount++ );
               ent = mSceneMgr->createEntity( name, "robot.mesh" );
           } // if
           else
           {
               sprintf( name, "Ninja%d", mCount++ );
               ent = mSceneMgr->createEntity( name, "ninja.mesh" );
           } // else

Prawie skończone. Teraz tylko trzeba przypisać klawisz spacji do przełaczania trybu. Odszukaj następujący kod w frameStarted:

        if ( !ExampleFrameListener::frameStarted( evt ) )
            return false;

I zaraz za nim wstaw poniższy kod:

       // Przełącza tryb
       if (mInputDevice->isKeyDown(KC_SPACE) && mTimeUntilNextToggle <= 0)
       {
           mRobotMode = !mRobotMode;
           mTimeUntilNextToggle = 1;
           mWindow->setDebugText( "Tryb ["+(mRobotMode ? String("Robot") : String("Ninja")) + "] - by przelaczyc nacisnij klawisz spacji" );
       }

Zrobione. Skompiluj kod i uruchom. Powinieneś mieć możliwość wybrania obiektu, który chcesz wstawić.

Wybieranie obiektów edytuj

A teraz zanurzmy się w najważniejszą część tego artykułu: użycie zapytań RaySceneQueries do wybierania obiektów na ekranie. Zanim jednak zaczniemy modyfikować nasz kod, pozwólcie, że wyjaśnię bardziej dokładnie RaySceneQueryResultEntry. (Proszę klikać na linki i przeglądać na bieżąco strukturę.)

RaySceneQueryResult zwraca iterator do struktur RaySceneQueryResultEntry. Struktura ta zawiera trzy zmienne. Zmienna distance mówi jak daleko znajduje się obiekt wskazany promieniem. Jedna z pozostałych zmiennych nie może być pusta. Zmiena movable zawiera MovableObject jeżeli promień dotknął jakiegoś. Zmienna worldFragment zawiera obiekt WorldFragment jeżeli promień dotknął jakiegoś fragmentu świata, np. terenu.

MovableObjects to w zasadzie dowolny obiekt który można dołączyć do SceneNode (taki jak Entitie, Light, itp). Zerknij na drzewo zależności na tej stronie a zobaczysz jakie typy obiektów mogą być zwracane. Większość normalnych aplikacji wykorzystujących RaySceneQueries używa ich do wyboru i manipulacji różnymi MovableObject na których się kliknie, albo SceneNodes z którymi są połączone. By sprawdzić nazwę MovableObject, wywołaj metodę getName. By otrzymać SceneNode (lub Node) zktórym połaczony jest obiekt, wywołaj getParentSceneNode (lub getParentNode). Zmienna movable w RaySceneQueryResultEntry będzie równa NULL jeżeli rezultatem nie będzie MovableObject.

WorldFragment jest inny od wszystkich pozostałych obiektów. Kiedy jest ustawiony worldFragment z RaySceneQueryResult, oznacza że rezultat jest częścią świata utworzonego przez managera sceny. Typ worldFragment który jest zwracany zależy od SceneManager. Zaimplementowano to tak, że struktura WorldFragment zawiera zmienną fragmentType, która specyfikuje typ zawartego fragmentu świata. Bazując na zmiennej fragmentType jeszcze inne zmienne są ustawiane ([http://www.ogre3d.org/docs/api/html/structOgre_1_1SceneQuery_1_1WorldFragment.html singleIntersection, plany, geometria, lub renderOp). Ogólnie mówiąć RaySceneQueries zwraca jedynie WFT_SINGLE_INTERSECTION WorldFragment-ów. Zmienna singleIntersection jest prostym typem Vector3 zawierającym położenie intersekcji. Inne typy worldfragment-ów nie są w obrębie zainteresowania tego artykułu.

A teraz zerknijmy na przykład. Dobrze by było wyświetlić listę wyników RaySceneQuery. Umożliwia to poniższy kod. (Przyjmujemy że obiekt fout jest typu ofstream, i że został już stworzony za pomocą metody open.)

  RaySceneQueryResult &result = mRaySceneQuery->execute();
  RaySceneQueryResult::iterator itr;
  
  // Petla przeglądająca wyniki
  for ( itr = result.begin( ); itr != result.end(); itr++ )
  {
     // czy to jest WorldFragment?
     if ( itr->worldFragment )
     {
        Vector3 location = itr->worldFragment->singleIntersection;
        fout << "WorldFragment: (" << location.x << ", " << location.y << ", " << location.z << ")" << endl;
     } //  if
   
     // czy to jest MovableObject?
     else if ( itr->movable )
     {
        fout << "MovableObject: " << itr->movable->getName() << endl;
     } // else if
  } // for

Umożliwia on wyświetlenie nazw wszystkich MovableObjects które przecina promień i podać ich lokalizację a także podać lokalizację dotkniętego obiektu world geometry (jeżeli zostanie dotknięty). Niestety niekiedy może on działać dość dziwnie. Na przykład, jeżeli używasz TerrainSceneManager, punkt początkowy promienia musi być ponad terenem gdyż w przeciwnym razie nie zostanie zaliczone dotknięcie. Różni managerzy sceny implementują w różny sposób zapytania RaySceneQueries. Poeksperymentuj przy użyciu innych managerów sceny.

Teraz, gdy powrócimy do naszego kodu RaySceneQuery, powinno cię coś uderzyć: my nie przeglądamy wszystkich rezultatów zapytania! Sprawdzamy tylko pierwszy wynik, którym (w przypadku użycia TerrainSceneManager) jest fragment terenu. Jest to złe bo nie jesteśmy pewni, że TerrainSceneManager zawsze zwraca jako pierwszy fragment terenu. Musimy stworzyć pętlę przeglądająca wszystkie wyniki tak byśmy byli pewni co otrzymaliśmy. Po za tym teraz mamy wykrywać także obiekty które będziemy chcieli przesuwać a nie tylko teren. Dotychczas gdy klikneliśmy na obiekt inny od terenu, program ignorował to i umieszczał za nim nowy obiekt. Trzeba to zmienić.

Po pierwsze musimy upewnić się, który z wyników określa po kliknięciu pierwszy obiekt przez który (lub w który) uderzy nasz promień. Musimy zatem posortować wyniki w zalezności od odległości. Odszukaj następujący kod w funkcji onLeftPressed:

       // Ustawienie zapytania
       Ray mouseRay = mCamera->getCameraToViewportRay( e->getX(), e->getY() );
       mRaySceneQuery->setRay( mouseRay );
  
       // Wykonanie zapytania
       RaySceneQueryResult &result = mRaySceneQuery->execute();
       RaySceneQueryResult::iterator itr = result.begin( );

i zmień go na:

       // Ustawienie zapytania
       Ray mouseRay = mCamera->getCameraToViewportRay( e->getX(), e->getY() );
       mRaySceneQuery->setRay( mouseRay );
       mRaySceneQuery->setSortByDistance( true );
  
       // Wykonanie zapytania
       RaySceneQueryResult &result = mRaySceneQuery->execute();
       RaySceneQueryResult::iterator itr;

Teraz gdy już wyniki zwracane są po kolei musimy zmienić kod obrabiający wyniki zapytania. Usuń następujący fragment:

       // Pobieramy wynik, tworzymy obiekt na podanej pozycji
       if ( itr != result.end() && itr->worldFragment )
       {
               Entity *ent;
               char name[16];
  
               if ( mRobotMode )
               {
                   sprintf( name, "Robot%d", mCount++ );
                   ent = mSceneMgr->createEntity( name, "robot.mesh" );
               } // if
               else
               {
                   sprintf( name, "Ninja%d", mCount++ );
                   ent = mSceneMgr->createEntity( name, "ninja.mesh" );
               } // else
  
               mCurrentObject = mSceneMgr->getRootSceneNode( )->createChildSceneNode( String(name) + "Node", itr->worldFragment->singleIntersection );
               mCurrentObject->attachObject( ent );
               mCurrentObject->setScale( 0.1f, 0.1f, 0.1f );
       } // if

A teraz zróbmy tak by móc wybierać obiekty dostawione wcześniej. Jeżeli klikniemy na obiekcie, mCurrentObject zostanie ustawione na nadrzędny do obiektu SceneNode. Jeżeli zaś kliknięcie nastąpi nie na obiekcie (co oznacza, że kliknięto na terenie) tak jak poprzednio wprowadzimy tam nowego robota. Pierwszą zmiana będzie wprowadzenie pętli 'for' zamiast procedury 'if':

       // Pobieramy wynik, tworzymy obiekt na podanej pozycji
       for ( itr = result.begin( ); itr != result.end(); itr++ )
       {

Teraz sprawdzimy czy pierwszy dotknięty przez promień obiekt to MovableObject, jeżeli tak to mCurrentObject ustawimy na jego SceneNode. Jest tu pewien haczyk. TerrainSceneManager tworzy dla terenu także MovableObjects, zatem możemy teraz dostać jeden z nich. By ominąc ten problem, sprawdzam nazwę obiektu tak by być pewien, że nie jest to element terenu. Zwykle kawałek terenu ma nazwę podobną do "tile[0][0,2]". Po ustawieniu mCurrentObject następuje procedura break. Musimy przecież sprawdzić tylko pierwszy właściwy obiekt (a są one posortowane) zatem nie musimy już dalej przeglądać wyników w pętli.

           if ( itr->movable && itr->movable->getName().substr(0, 5) != "tile[" )
           {
               mCurrentObject = itr->movable->getParentSceneNode( );
               break;
           } // if

Dalej sprawdzimy czy nie nastąpiło dotknięcie fragmentu terenu (WorldFragment).

           else if ( itr->worldFragment )
           {
               Entity *ent;
               char name[16];

W takim przypadku będziemy tworzyć obiekt robota lub ninja w zalezności od stanu mRobotState. Po utworzeniu obiektu stworzymy SceneNode i przerwiemy pętlę.

               if ( mRobotMode )
               {
                   sprintf( name, "Robot%d", mCount++ );
                   ent = mSceneMgr->createEntity( name, "robot.mesh" );
               } // if
               else
               {
                   sprintf( name, "Ninja%d", mCount++ );
                   ent = mSceneMgr->createEntity( name, "ninja.mesh" );
               } // else
  
               mCurrentObject = mSceneMgr->getRootSceneNode( )->createChildSceneNode( String(name) + "Node", itr->worldFragment->singleIntersection );
               mCurrentObject->attachObject( ent );
               mCurrentObject->setScale( 0.1f, 0.1f, 0.1f );
               break;
           } // else if
       } // for

Możesz uwierzyć lub nie, ale to już wszystko! Skompiluj i przetestuj program. Teraz gdy klikniemy na powierzchni terenu utworzymy właściwy obiekt a gdy klikniemy na istniejącym obiekcie pokaże się dookoła niego ramka. Na razie jeszcze nie da sie go przeciągnąć - zrobimy to w następnym kroku. Nasuwa się jednak pytanie, jeżeli już wprowadziliśmy sortowanie i interesuje nas pierwszy (w odległości) obiekt to dlaczego nie użylismy dalej procedury 'if'? Podstawową przyczyną jest to, że wpadlibyśmy w kłopoty gdyby pierwszym obiektem była jedna z "dachówek" (tiles) tworzonych przez TerrainSceneManager. Musimy zastosować pętlę by odszukać inny obiekt.

Jak już do tego doszliśmy, powinniśmy uaktualnić tez kod zapytań RaySceneQuery w pozostałych częściach programu. Zarówno w frameStarted jak i w onLeftDragged interesują nas tylko fragmenty terenu. Nie ma tu potrzeby sortowania wyników jako że po posortowaniu element terenu zawsze byłby na końcu - i dlatego nie właczamy sortowania. Jednak ciągle potrzebujemy pętli by przejrzeć wszystkie wyniki, choćby z tego powodu, że TerrainSceneManager może w przyszłości się zmienić i nie będzie jako pierwszego zwracał właśnie terenu. Zatem po pierwsze odnajdźmy taki kod w frameStarted:

        // Wykonaj zapytanie
        RaySceneQueryResult &result = mRaySceneQuery->execute();
        RaySceneQueryResult::iterator itr = result.begin( );
  
        // Pobierz wynik i ustaw wysokośc kamery
        if ( itr != result.end() && itr->worldFragment )
        {
            Real terrainHeight = itr->worldFragment->singleIntersection.y;
            if ((terrainHeight + 10.0f) > camPos.y)
                mCamera->setPosition( camPos.x, terrainHeight + 10.0f, camPos.z );
        }

i zamieńmy go na:

        // Wykonaj zapytanie
        mRaySceneQuery->setSortByDistance( false );
        RaySceneQueryResult &result = mRaySceneQuery->execute();
        RaySceneQueryResult::iterator itr;
  
        // Pobierz wyniki i ustaw wysokość kamery
        for ( itr = result.begin( ); itr != result.end(); itr++ )
        {
            if ( itr->worldFragment )
            {
               Real terrainHeight = itr->worldFragment->singleIntersection.y;
               if ((terrainHeight + 10.0f) > camPos.y)
                   mCamera->setPosition( camPos.x, terrainHeight + 10.0f, camPos.z );
               break;
            } // if
        } // for

W zasadzie nie trzeba tu nic wyjaśniać. Dodaliśmy linię wyłaczającą sortowanie, potem zmieniliśmy 'if' w pętlę 'for', którą przerywamy tak szybko jak to tylko możliwe po odszukaniu pozycji terenu. Musimy teraz zrobić to samo w funkcji onLeftDragged. Znajdź taki kod w onLeftDragged:

       mRaySceneQuery->setRay( mouseRay );
  
       RaySceneQueryResult &result = mRaySceneQuery->execute();
       RaySceneQueryResult::iterator itr = result.begin( );
  
       if ( itr != result.end() && itr->worldFragment )
       {
           mCurrentObject->setPosition( itr->worldFragment->singleIntersection );
       } // if

i zamień go na:

       mRaySceneQuery->setRay( mouseRay );
       mRaySceneQuery->setSortByDistance( false );
  
       RaySceneQueryResult &result = mRaySceneQuery->execute();
       RaySceneQueryResult::iterator itr;
  
       for ( itr = result.begin( ); itr != result.end(); itr++ )
       {
           if ( itr->worldFragment )
           {
               mCurrentObject->setPosition( itr->worldFragment->singleIntersection );
               break;
           } // if 
       } // if

Przekompiluj i przetestuj program. Nie powinno być żadnych różnic w stosunku do poprzedniego ale teraz zabezpieczyliśmy się przed ewentualnymi kłopotami na przyszłość na wypadek zmian w TerrainSceneManager.

Maski zapytań edytuj

Oczywiście zauważyliście, że nie wazne w jakim trybie jesteśmy możemy zawsze wybrać dowolny obiekt na który klikniemy. Niekiedy jednak warto stosować ograniczenie w możliwości wyboru. Wszystkie MovableObject-y mają możliwość zastosowania masek a zapytania mają możliwość stosowania opartych na nich filtrów. Wszystkie filtry używają operacji binarnego AND.

Po pierwsze musimy utworzyć maski które potem będziemy stosować. Przejdźmy zatem na sam początek klasy MoveQueryListener i dodajmy poniższy kod zaraz za poleceniem 'public':

   enum QueryFlags
   {
       NINJA_MASK = 1<<0,
       ROBOT_MASK = 1<<1
   };

Utworzymy w ten sposób zmienną wyliczeniową z dwoma możliwymi wartościami, które binarnie będą wynosiły 0001 i 0010. Teraz za każdym razem gdy będziemy tworzyć obiekt typu "robot" ustawimy jego maskę za pomocą komendy 'setMask' na wartość ROBOT_MASK czyli 0010 a w przypadku utworzenia obiektu "ninja" na wartość NINJA_MASK czyli 0001. Potem gdy będziemy w trybie "ninja" każemy zapytaniu brać pod uwagę tylko obiekty z maską NINJA_MASK a gdy będziemy w trybie "robot" tylko obiekty z maską ROBOT_MASK. odszukaj następujący kod w onLeftPressed:

               if ( mRobotMode )
               {
                   sprintf( name, "Robot%d", mCount++ );
                   ent = mSceneMgr->createEntity( name, "robot.mesh" );
               } // if
               else
               {
                   sprintf( name, "Ninja%d", mCount++ );
                   ent = mSceneMgr->createEntity( name, "ninja.mesh" );
               } // else

i dodaj dwie dodatkowe linie ustawiające maski:

               if ( mRobotMode )
               {
                   sprintf( name, "Robot%d", mCount++ );
                   ent = mSceneMgr->createEntity( name, "robot.mesh" );
                   ent->setQueryFlags( ROBOT_MASK );
               } // if
               else
               {
                   sprintf( name, "Ninja%d", mCount++ );
                   ent = mSceneMgr->createEntity( name, "ninja.mesh" );
                   ent->setQueryFlags( NINJA_MASK );
               } // else

Ciągle jeszcze musimy spowodować by można było po kliknięciu wybierać obiekty tylko jednego rodzaju. Zrobimy to za pomoca ustawienia maski w zapytaniu tak by zwracany był tylko wybrany typ. Zatem RaySceneQuery ustawimy maskę na ROBOT_MASK w trybie "robota" i na NINJA_MASK w trybie "ninja". Znajdź następujący fragment kodu w onLeftPressed:

       mRaySceneQuery->setSortByDistance( true );

i zaraz za nim dodaj następująca linię:

       mRaySceneQuery->setQueryMask( mRobotMode ? ROBOT_MASK : NINJA_MASK );

Przekompiluj i uruchom program. Teraz możemy wybierać obiekty tylko konkretnego typu. Przy próbie wyboru innego obiektu zostaje za nim utworzony nowy obiekt według trybu w którym się znajdujemy. Jeśli zaś nasz wybranty typ obiektu znajduje się za obiektem innego typu to też zostanie wybrany. W zasadzie skończyliśmy pracę z kodem. Następny rozdział nie będzie już nic w nim zmieniał.

Typy masek zapytań edytuj

Warto jeszcze na jedną rzecz zwrócic uwagę podczas używania zapytań z maskami. Być może dodawałeś do sceny billboardset lub system particle, i chciałeś podobnie go obsłużyć. Jednak jak zauważyłeś zapytanie nigdy nie zwracało tych obiektów choć na nich klikałeś. Tak się dzieje ponieważ zapytanie SceneQuery ma też inną maskę QueryTypeMask, która ogranicza wybór do jednego typu. Domyślnie ograniczenie to ustawione jest tylko na typ entity.

W twoim kodzie, jeśli chciałbyś by zapytanie zwracało BillboardSet-y lub ParticleSystem-y, musisz przed wywołanie zapytania dokonać pewnej dodatkowej czynności:

     mRaySceneQuery->setQueryTypeMask(SceneManager::FX_TYPE_MASK);

Teraz zapytanie zwraca jedynie BillboardSet-y lub ParticleSystem-y.

Jest 6 typów QueryTypeMask zdefiniowanych w klasie SceneManager-a jako statyczne wartości:

 WORLD_GEOMETRY_TYPE_MASK //Zwraca geometrię świata
 ENTITY_TYPE_MASK         //Zwraca entitie-y.
 FX_TYPE_MASK             //Zwraca billboardset-y / particle system-y.
 STATICGEOMETRY_TYPE_MASK //Zwraca statyczną geometrię.
 LIGHT_TYPE_MASK          //Zwraca światła.
 USER_TYPE_MASK_LIMIT     //Zwraca tylko typ maski użytkownika.

Domyślnie QueryTypeMask jeżeli nie ustawiono go ręcznie ma wartość ENTITY_TYPE_MASK.

Więcej o maskach edytuj

Nasze maski są bardzo proste. Jednak warto tu rozszerzyć nieco ich kwestię.

Ustawianie masek dla MovableObject edytuj

Kiedy tworzymy nową maskę, jej binarna reprezentacja musi zawierać tylko jedną jedynkę. Poniżej podaliśmy przykłady poprawnych masek.

00000001
00000010
00000100
00001000
00010000
00100000
01000000
10000000

I tak dalej. Możemy je tworzyć bardzo prosto przez przesunięcie bitowe liczby jeden na odpowiednią pozycję. Robi się to tak:

00000001 = 1<<0
00000010 = 1<<1
00000100 = 1<<2
00001000 = 1<<3
00010000 = 1<<4
00100000 = 1<<5
01000000 = 1<<6
10000000 = 1<<7

aż do 1<<31. Daje nam to możliwość jednoczesnego wykreowania 32 masek dla MovableObject-ów.

Zapytania z wieloma maskami edytuj

Możemy tworzyć zapytania o kilka typów obiektów okreslonych przez rózne maski składając je przy pomocy operatora OR. Powiedzmy, że mamy trzy rózne grupy obiektów w programie:

   enum QueryFlags
   {
       FRIENDLY_CHARACTERS = 1<<0,
       ENEMY_CHARACTERS = 1<<1,
       STATIONARY_OBJECTS = 1<<2
   };

Teraz gdy chcemy znaleść tylko 'przyjaciół' musimy ustawić zapytanie:

   mRaySceneQuery->setQueryMask( FRIENDLY_CHARACTERS );

Jeżeli zaś chcemy znaleść jednocześnie 'wrogów' i 'rzeczy nieruchome' ustawimy je tak:

   mRaySceneQuery->setQueryMask( ENEMY_CHARACTERS | STATIONARY_OBJECTS );

Jeżeli często wykonujemy zapytanie o dwa typy jednocześnie, być może ułatwimy sobie tworząc nową definicję w zmiennej wyliczeniowej:

   OBJECTS_ENEMIES = ENEMY_CHARACTERS | STATIONARY_OBJECTS

I będziemy po prostu wprowadzać do zapytania OBJECTS_ENEMIES.

Zapytanie o wszystko inne niż maska edytuj

Możesz też wykonywać zapytania o wszystko inne niż to co było określone przez daną maskę używając operatora negacji:

   mRaySceneQuery->setQueryMask( ~FRIENDLY_CHARACTERS );

To zapytanie zwróci wszystkie obiekty prócz 'przyjaciół'. Można też i tu składać maski:

   mRaySceneQuery->setQueryMask( ~( FRIENDLY_CHARACTERS | STATIONARY_OBJECTS ) );

To z kolei zwróci wszystkie obiekty prócz 'przyjaciół' i 'obiektów stałych'.

Wybieranie wszystkich obiektów lub zapytanie bez obiektów edytuj

Można też za pomocą masek zrobić kilka interesujących rzeczy. Pamiętając że jeżeli ustawimy maske QM w zapytaniu zwróci nam ono wszystkie MovableObject-y które mają maskę OM jeżeli działanie QM AND OM będzie miało wartość różną od zera. Zatem jeżeli ustawimy maskę zapytania na negację 0 (0xFFFFF....) zapytanie zwróci nam wszystkie MovableObject-y które mają maskę inną od zera.

Używanie natomiast jako maski zera może w niektórych przypadkach być bardzo przydatne. Na przykład TerrainSceneManager nie używa masek gdy zwraca obiekty typu worldFragment. A zatem wykonując następujące zapytanie:

   mRaySceneQuery->setQueryMask( 0 );

W wyniku uzyskamy TYLKO worldFragment-y. Może to ułatwić wiele spraw gdy na ekranie mamy bardzo dużo obiektów a nie chcemy tracić czasu na przeglądanie ich wszystkich w pętli a potrzebujemy tylko określenia położenia terenu.

Ćwiczenia edytuj

Proste ćwiczenia edytuj

TerrainSceneManager tworzy elementy terenu (tiles) z domyślną maską równą ~0 (wszystkie zapytania zwracają je). Rozwiązaliśmy ten problem testując nazwę obiektu i sprawdzając czy jest równa "tile[0][0,2]". Jednakże, nawet gdy nie jest to zaimplementowane w tej chwili, TerrainSceneManager ma wspomagać wiele stron, i może się okazać, że także inne obiekty niż tylko "tile[0][0,2]" będą powodowały problemy w naszym kodzie. Zamiast zatem tworzyć test w pętli, spróbuj zawęzić wyniki przez nadanie wsystkim obiektom typu 'tile' unikalnej maski. (Podpowiedź: TerrainSceneManager tworzy SceneNode zwaną "Terrain" która zawiera wszystkie te 'tile'. W pętli przejrzyj je i nadaj odpowiednie maski.)

Średniotrudne ćwiczenia edytuj

Nasz program działa z dwoma obiektami: robotami i ninja. Jeśli byśmy tworzyli edytor sceny, chcielibyśmy móc wprowadzać dowolną ilość różnych typów obiektów. Uogólnij tak kod by był w stanie wprowadzać dowolny typ obiektu z predefiniowanej listy. Stwórz nakładkę z listą obiektów które według ciebie powinny znaleść się w edytorze (takich jak ninja, roboty, węzły, statki itd.) i przebuduj zapytania by mogły zwracać tylko wybrane typy obiektów. W momencie gdy zaczniemy używać dowolnej liczby typów obiektów, użyj 'Factory Pattern' to prawidłowego tworzenia 'SceneNode-ów' i 'Entitie-ów'.

Ćwiczenia zaawansowane edytuj

  • Uogólnij poprzednie rozwiązanie tak by program czytał wszystkie 'meshe-y' o którym wie Ogre (czyli wszystko odszukane w katalogu Media) i dawał możliwość ich wstawienia, Nie powinno być limitu ilości typów obiektów które ogre może wstawić. Ponieważ jednak masz tylko 32 unikalne maski, musisz mieć szybką możliwość zmiany wszystkich masek obiektów wyświetlonych na ekaranie.
  • Można zauważyć, że kiedy klikasz na obiekcie, obiekt 'unosi się' z dna otaczającego 'pudełka'. Wyraźnie widać to po kliknięciu na na górnej części obiektu i przesunięciu go. Zmodyfikuj program by tego uniknąć.

Ćwiczenia dla poźniejszych studiów edytuj

  • Wprowadź możliwość wybierania wielu obiektów jednoczesnie przez klikanie z wciśniętym jednocześnie klawiszem CTRL. Kiedy będziesz przeciągał tak wybrane, przeciągniesz cała grupę.
  • Wiele edytorów sceny umożliwia definiowanie grup elementów, które potem poruszają się razem. Wprowadź taką możliwość do programu.