Hlavní navigace

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

Jan Pečiva

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.

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

Našli jste v článku chybu?

22. 5. 2007 14:19

Pavel.Pstros (neregistrovaný)
Take jsem tuto sestavu nekolik let provozoval a dodnes provozuji pro projekt v C. Nedavno jsem v zamestnani prisel do styku s MS Visual C# a je to uplne o necem jinem. Pokud existuje nejake prostredi s napovedou a automatickym pridelovanim metod (vlastnosti ci polozek) jednotlivych trid pro mono, budu rad, kdyz se o tom dozvim, byt by to mel byt komercni projekt.
Vitalia.cz: Jsou čajové sáčky toxické?

Jsou čajové sáčky toxické?

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Podnikatel.cz: EET: Totálně nezvládli metodologii projektu

EET: Totálně nezvládli metodologii projektu

Vitalia.cz: 9 největších mýtů o mase

9 největších mýtů o mase

DigiZone.cz: „Black Friday 2016“: závěrečné zhodnocení

„Black Friday 2016“: závěrečné zhodnocení

120na80.cz: Pánové, pečujte o svoje přirození a prostatu

Pánové, pečujte o svoje přirození a prostatu

Lupa.cz: Propustili je z Avastu, už po nich sahá ESET

Propustili je z Avastu, už po nich sahá ESET

Podnikatel.cz: Chtějte údaje k dani z nemovitostí do mailu

Chtějte údaje k dani z nemovitostí do mailu

Lupa.cz: Co se dá měřit přes Internet věcí

Co se dá měřit přes Internet věcí

Vitalia.cz: Znáte „černý detox“? Ani to nezkoušejte

Znáte „černý detox“? Ani to nezkoušejte

Vitalia.cz: Proč vás každý zubař posílá na dentální hygienu

Proč vás každý zubař posílá na dentální hygienu

120na80.cz: Na ucho teplý, nebo studený obklad?

Na ucho teplý, nebo studený obklad?

Podnikatel.cz: K EET. Štamgast už peníze na stole nenechá

K EET. Štamgast už peníze na stole nenechá

Lupa.cz: Insolvenční řízení kvůli cookies? Vítejte v ČR

Insolvenční řízení kvůli cookies? Vítejte v ČR

Vitalia.cz: Taky věříte na pravidlo 5 sekund?

Taky věříte na pravidlo 5 sekund?

Vitalia.cz: Paštiky plné masa ho zatím neuživí

Paštiky plné masa ho zatím neuživí

Podnikatel.cz: Podnikatelům dorazí varování od BSA

Podnikatelům dorazí varování od BSA

Vitalia.cz: Jmenuje se Janina a žije bez cukru

Jmenuje se Janina a žije bez cukru

Podnikatel.cz: Snížení DPH na 15 % se netýká všech

Snížení DPH na 15 % se netýká všech

Podnikatel.cz: Udávání a účtenková loterie, hloupá komedie

Udávání a účtenková loterie, hloupá komedie