Hlavní navigace

Píšeme 3D střílečku v Open Inventoru

11. 5. 2007
Doba čtení: 6 minut

Sdílet

Po delší době přinášíme na přání čtenářů další dva díly tutoriálu o Open Inventoru, navazující na předchozí jedenáctidílnou sérii. Články ukáží, jak vytvořit jednoduchou tankovou 3D střílečku. V tomto díle vytvoříme bludiště a jeden tank, se kterým se v bludišti budeme moci prohánět.

Obsah dílu:

  1. vytvoření scény z wrml souborů
  2. obsluha událostí z klávesnice
  3. pohyb tanku
  4. simulační smyčka

Celý projekt nazveme Tanky. V češtině to zní pěkně a kdosi ze zahraničí se vyjádřil ve významu, že je to dobrý název pro projekt. Na otázku proč řekl, že to zní jako zkomolenina slov tank a fancy. Ve hře budou jezdit dva tanky v bludišti a střílet po sobě. Jak už se dá tušit, objevují se ve hře nejen techniky, jak ovládat tank v bludišti, ale i detekce kolizí, aby tank neprojížděl zdmi, komunikace po síti pro hru dvou hráčů proti sobě, i zvuk pro doplnění atmosféry hry.

Tanky

Autorem hry je Daniel Výchopeň, který namodeloval modely v 3D studiu a naprogramoval jádro hry. Tímto mu patří velký dík za množství práce, které do hry investoval.

Dnes si ukážeme jen jádro aplikace. Na pokročilejší věci jako zvuky a detekce kolizí se můžeme těšit v příštím pokračování.

1. Vytvoření scény

Scéna byla namodelována v 3D studiu. Modely byly vytvořeny s nižším počtem trojúhelníků s ohledem na výkon aplikace. Modely jsou celkem tři – tank, bludiště a střela a jsou vyobrazeny na následujících obrázcích:

Tank
Bludiste
Strela

Jejich načtení a zobrazení zajišťuje funkce main:

  // vytvoř kořen scény
  root = new SoSeparator;
  root->ref();

  // Vlož kameru do scény
  kamera = new SoPerspectiveCamera;
  kamera->nearDistance = 4;
  kamera->farDistance = 4096;
  root->addChild(kamera);

  // nastaveni modelu osvetleni
  SoLightModel *lmodel = new SoLightModel;
  lmodel->model.setValue(SoLightModel::BASE_COLOR);
  root->addChild(lmodel);

  // textury z WRML souborů bodou načítány okamžitě, nikoliv na pozadí
  SoVRMLImageTexture::setDelayFetchURL(FALSE);

  // bludiste
  SoFile *model = new SoFile;
  model->name.setValue("models/bludiste.wrl");
  root->addChild(model);

  // inicializace tanku
  tank.init(root);
  tank.setPosition(10,0,-30);
  tank.setRotation(3.1f);

  [...]

  // Obsluha klavesnice
  SoEventCallback * cb = new SoEventCallback;
  cb->addEventCallback(SoKeyboardEvent::getClassTypeId(), keyboardEvent_cb, NULL);
  root->insertChild(cb, 0);

  // Sensor pro nekonečné volání simulationStep
  SoOneShotSensor * sensor =new SoOneShotSensor(simulationStep, NULL);
  sensor->schedule(); 

Většina kódu by měla být zřejmá z minulých tutoriálů. My se zaměříme jen na tři věci:

  • použití VRML formátu
  • obsluha klávesnice
  • vytvoření nekonečné smyčky pro aktualizaci scény

V tomto příkladu používáme VRML 2 file formát, neboť 3D studio MAX nepodporuje nativní formát Open Inventoru. Naopak VRML 2 je dobře podporován. Dokonce díky VRML 2 a jeho podpoře zvuku se 3D zvuk objevil dodatečně i v Inventoru. Nechme však zvuk i detaily na další pokračování a můžeme VRML uzavřít nejčastějším problémem, kterého si programátor u VRML2 všimne: textury se ve VRML 2 načítají na pozadí. Uživatel prohlížející si VRML světy na internetu tuto volbu uvítá, neboť místo černé obrazovky a čekání, než se mu pomalou dial-up linkou natáhnou objemné textury, může ihned prohlížet alespoň neotexturovaný svět. V naší aplikaci ale takovéto chování působí poněkud neprofesionálně – dívat se na neotexturovaný model. Navíc zde padá důvod pomalého dial-up, neboť model se z disku načte velmi rychle. Celou věc zakážeme příkazem:

SoVRMLImageTexture::setDelayFetchURL(FALSE);

Obsluha klávesnice pro ovládání pohybu tanku je zajištěna uzlem SoEventCallback. Ten obecně slouží pro události jakýchkoliv vstupních zařízení jako je klávesnice a myš. Kdykoliv se objeví nějaká z událostí od těchto zařízení, může být obsloužena callbackem (čti: kólbekem), který si můžeme zaregistrovat v nódu SoEventCallback, ten však musí být vložen v grafu scény. Obecně je lépe jej vložit někde na začátek grafu, aby se ušetřil čas procházení scény, přestože je tento čas ztěží měřitelný.

Nekonečnou smyčku pro aktualizaci scény vytvoříme sensorem, který je pojmenován poněkud nenápaditě: „sensor“. Ten se odkazuje na funkci simulationStep, která provádí aktualizaci scény a na jejím konci znova scheduluje sensor, takže je zaručeno její neustálé volání.

2. Odchytávání událostí z klávesnice

Skoro v každé normální hře potřebujeme odchytávat události stisku nebo puštění klávesy. Celá obsluha se v našem případě děje ve funkci keyboardEvent_cb. Během vytváření scény jsme již nainstalovali SoEventCallback, aby zavolal tuto funkci při jakékoliv události klávesnice. V praxi se pak funkce vyplní výrazem switch, složitějším „ifem“, který provede akci podle stisknuté klávesy. My použijeme ještě jiné řešení – cyklus přes pole.

Základem je struktura „Klávesa“, ze které je sestaveno pole „Klávesy“. Každé klávese odpovídá jeden boolean, který hodnotou TRUE indikuje, že je klávesa stisknuta:

  struct Klavesa
  {
    enum { nahoru = 0, dolu, vlevo, vpravo, strela, zvedniHlaven, sklonHlaven };
    SoKeyboardEvent::Key klavesa;
    bool stisknuta;
  } klavesy[] =
  {
    {SoKeyboardEvent::UP_ARROW, false},
    {SoKeyboardEvent::DOWN_ARROW, false},
    {SoKeyboardEvent::LEFT_ARROW, false},
    {SoKeyboardEvent::RIGHT_ARROW, false},
    {SoKeyboardEvent::SPACE, false},
    {SoKeyboardEvent::M, false},
    {SoKeyboardEvent::N, false}
  }; 

Toto pole je pak obslouženo v již dříve zmíněném callbacku:

  void keyboardEvent_cb(void * userdata, SoEventCallback * node)
  {
    const SoEvent * event = node->getEvent(); // obsluha pole "klavesy"
    for(int i=0; i<sizeof(klavesy); ++i)
    {
      if(SoKeyboardEvent::isKeyPressEvent(event, klavesy[i].klavesa))
      klavesy[i].stisknuta=true;
      if(SoKeyboardEvent::isKeyReleaseEvent(event, klavesy[i].klavesa))
      klavesy[i].stisknuta=false;
    }

    // obsluha ESC
    if(SoKeyboardEvent::isKeyPressEvent(event, SoKeyboardEvent::ESCAPE))
    SoWin::exitMainLoop();
    node->setHandled();
  } 

3. Pohyb tanku

Výpočet pohybu není obyčejně triviální operací a silně doporučuji využívat odladěných metod tříd SbMatrix, SbRotation a SbVec3f, což jsou asi nejčastěji používané třídy. Často se například změna orientace tělesa počítá několikerým násobením rotací podle jednotlivých os, a výslednou rotaci převedeme na matici a tou vynásobit aktuální transformaci tělesa. Celou operaci je možné vyjádřit optimalizovanou rovnicí. Proč ale strávit tři pracovní dny vyjádřením a odladěním těchto rovnic, když můžeme využít metod inventorovských tříd, které sice budou o chloupek pomalejší, nicméně nám zůstane čitelný kód, který nebude těžké poopravit podle případných nových požadavků. Nejlepší čas na optimalizace je těsně před dokončením aplikace, kdy už většinou jsou algoritmy odladěné a kdy už jsme schopni porovnávat, které části kódu brzdí aplikaci nejvíce a náš programátorský čas bude odměněn největším ziskem.

V naší aplikaci se proti této zásadě proviníme, ale berme to spíše jako varování, neboť kinematika tanku není nějak složitá a kód je tak-tak čitelný.

Základem je kód který čte stav jednotlivých kláves a podle toho modifikuje proměnné tanku. Například rychlost tanku je šipkou dopředu ovládána tímto kódem:

  float rychlost=tank.getRychlost(); // nova rychlost tanku

  // zrychlovani
  if(klavesy[Klavesa::nahoru].stisknuta)
  {
    if(rychlost<maxv)
    {
      rychlost += (float)uplynulyCas*zrychleni;
      if(rychlost>maxv)
      rychlost = maxv;
    }
  }else
  {
    if(rychlost>0)
    {
      rychlost-= (float)uplynulyCas*zpomaleni;
      if(rychlost<0)
        rychlost = 0;
    }
  }

  [...]

  // ulozeni nove rychlosti
  tank.setRychlost(rychlost); 

Podobný kód se opakuje i pro šipku dozadu, doprava i doleva, a pro další klávesy. Po obsloužení všech těchto kláves se vypočte nová pozice tanku v závislosti na jeho rychlosti a času od minulého snímku:

  // nova pozice tanku
  float s=(rychlost+tank.getRychlost())*(float)uplynulyCas; // draha ujeta tankem
  tank.getPosition(x, y, z);

  x-=s*sinf(natoceni);
  z-=s*cosf(natoceni);

  tank.setPosition(x, y, z); 

Podobný kód se týká i natočení tanku. Na závěr je ještě upravena pozice kamery, která simuluje „pohled třetí osoby“ (third person view). Odpovídající kód najdeme ve zdrojácích.

4. Simulační smyčka

Simulační smyčka nám už asi nebude úplně cizí. Zde je její kód:

  void simulationStep(void *data, SoSensor *sensor)
  {
    // spočítej čas od minulého volání simulationStep
    static double casNovy=SbTime::getTimeOfDay().getValue(), casStary;
    casStary=casNovy;
    casNovy=SbTime::getTimeOfDay().getValue();
    double uplynulyCas=casNovy-casStary; // aktualizuj scénu
    prepocitejScenu(casNovy, uplynulyCas); // rescheduluj sensor (způsobí nové zavolání této funkce)
    sensor->schedule();
  } 

O prvotní zavolání se postará funkce main, kde jsme zaschedulovali sensor odkazující se na funkci simulationStep. Tato funkce je tedy brzy zavolána a o nekonečný cyklus se stará poslední řádek této funkce, který sensor opět zascheduluje. Funkce prepocitej scenu provádí pouze animaci tanku a předcházející výpočet spočítá čas od minulého volání této funkce.

CS24_early

Závěr

V tomto díle jsme si ukázali jednoduchý pohybující se tank. Pro reálnou hru však ještě hodně chybí. V příštím díle by tedy měla následovat detekce kolizí, podpora sítě a ještě množství dalších drobností.

Ke stažení

zdrojáky: 5–1-Tanky-1.zip
tutoriál: tutorial12.zip

Seriál: Open Inventor

Byl pro vás článek přínosný?

Autor článku