Optimalizace vykreslování ve 2D hrách

Algoritmy Grafické Optimalizace vykreslování ve 2D hrách

Dneska se pokusím popsat několik možných způsobů, jak optimalizovat vykreslování ve 2D hrách.

Nastínění příkladové situace

Představte si jednoduchou strategickou hru, která má vykreslovat nějakou mapu. Pro jednoduchost se zatím bude kreslit pouze terén. Velikost políčka mapy bude třeba 64x64 pixelů, velikost mapy třeba 256x256 políček.

Ve hře je pole textur s názvem Textury a v něm načteno několik textur pro různé terény, zatím postačují třeba textury pro trávu a vodu. Dále je ve hře třída Policko, která má vlastnost Teren, která určuje, jestli se na tom místě vykreslí tráva nebo voda. Pole políček (Policko) s názvem Mapa, do kterého jsou náhodně vygenerovány jedničky a nuly (tráva nebo voda). Dvě číselné proměnné, které nám říkají, o kolik je kamera (pohled) posunutá doprava/dolů. Pokud začnete bez rozmýšlení psát vykreslování mapy, nejspíš skončíte s kódem podobným tomuto:

for (int y=0; y<256; y++)
for (int x=0; x<256; x++)
{
        drawTexture(x*64-PosunMapyX, y*64-PosunMapyY, Textury[Mapa[x,y].Teren]); // vykreslí políčko mapy, bere v potaz posun mapy (kamery)
}

Pokud se nepoužívá vykreslovací engine, který by sám prováděl test, jestli textura bude vidět na obrazovce, tak s největší pravděpodobností se v tuto chvíli fps nedostane ani přes 100 (většina kreslících enginů ho nepoužívá).

To je na primitivní 2D hru docela málo. Problém je samozřejmě v tom, že se kreslí 65536 (256x256) políček, což už je docela vysoké číslo. A hlavní problém je v tom, že spousta z nich ani není vidět, takže se kreslí zbytečně.

Optimalizace první - Nekreslit mimo obrazovku

Optimalizovat tento kód je zatím docela triviální, řešením je vykreslovat jen políčka, která mají šanci být vykreslena, takže po chvíli přemýšlení skončíte třeba s něčím takovým: (v proměnných Screen.Width a Screen.Height je rozlišení obrazovky):

int minx=(posunMapyX/64); // X index nejlevějšího políčka, záleží jen na tom,
                          // jak je posunutá mapa
int maxx=minx+(Screen.Width/64)+1; // X index nejpravějšího políčka je index
                                   // nejlevějšího políčka + maximální počet
                                   // políček vedle sebe (což záleží pouze na rozlišení)
int miny=(posunMapyY/64);
int maxy=miny+(Screen.Height/64)+1;

// osetreni indexu, aby se necetlo mimo mapu
if (minx<0) minx=0;
if (miny<0) miny=0;
if (maxx>=256) minx=255;
if (maxy>=256) miny=255;

for (int y=miny; y<maxy; y++)
for (int x=minx; x<maxx; x++)
{
        drawTexture(x*64-PosunMapyX, y*64-PosunMapyY, Textury[Mapa[x,y].Teren]);
        // vykreslí políčko mapy, bere v potaz posun mapy (kamery)
}

Teď už se vykreslují jen políčka, co jsou vidět na obrazovce, fps už použitelné, ale stále to může být lepší.

Optimalizace druhá - Textury

Optimalizací, která vás (pokud netušíte, jak uvnitř funguje vykreslování přes grafickou kartu) nejspíš vůbec nenapadne je snížení počtu přepínání textur při kreslení - pokud kreslíte stále stejnou texturu, fps jsou vysoko, pokud se střídají i třeba jen dvě textury, fps klesají hodně nízko, což je způsobeno režií přepnutí textury na grafické kartě. Pokud se textury střídají třeba po každém druhém vykreslení, tak je to značně neefektivní.

Varianta 1 - tudy radši ne

Řešení je několik, na první pohled nejjednodušší je asi projet celý cyklus vícekrát - při prvním průchodu kreslit jen vodu, při dalším trávu apod., ale pokud by počet textur rostl, tak by to bylo pomalejší a pomalejší, až by to bylo pomalejší, než bez této optimalizace.

Varianta 2 - lepší

Druhým řešením by bylo při průchodu mapu pole hned nekreslit, ale házet je do nějakého seřazeného pole/seznamu a vykreslit pak ten - už seřazený podle textur, takže by se režie snížila. Např. XNA něco podobného nabízí - nastavením SpriteSortMode­.Texture v metodě Begin u SpriteBatch se toto provádí interně, ale výsledek je pořád pomalejší než vykreslování pouze jedné textury.

Varianta 3 - ideální varianta

Nejoptimálnějším řešením je místo několika textur použít jednu velkou, ve které jsou všechny možné terény a typ terénu jen ovlivňuje, z které části té textury se má číst. Tento způsob vykreslení je stejně rychlý, jako když se kreslí stále jen jeden typ terénu.

Všechno?

Možná to vypadá, že tím možnosti optimalizace vykreslování končí, ale to je omyl, existuje ještě minimálně jedna velká optimalizace vykreslování, třeba v mé strategii dokáže zvýšit fps až na trojnásobek (!).

Tato optimalizace staví na tom, že terén se neanimuje a pohledem hráč posouvá jen občas a vykreslení jedné velké textury je mnohem rychlejší, než vykreslení spousty malých textur.

Tuto metodu jsem si pro sebe nazval "cachování terénu" a funguje tak, že si vytvořím pomocnou texturu velkou jako obrazovka a terén vykresluji jen do ní a to jen pokud hráč pohnul od minulého snímku výřezem mapy. Tato pomocná textura se mezi jednotlivými snímky nemaže, takže její obsah zůstává. Při kreslení celé scény pak kreslím jen tu pomocnou texturu a pak samostatně ještě objekty, ty už přímo do scény.

Kreslení celé scény - terén i objekty se pak řeší tedy nějak takto:

if (pohnuloSeKamerouOdPOslednihoSnimku)
{
        VykreslitTerenDoPomocneTextury();
}

VykresliPomocnouTexturu();
VykresliZbyleObjekty();

Tato metoda se dá samozřejmě ještě dále upravit - třeba do pomocné textury vykreslit větší část terénu, než je velikost obrazovky a pak stačí pomocnou texturu překreslit až když se pohled mapy posune o více, než o jeden pixel, ale zase pokaždé překreslíte větší část mapy, než je nutné.

Shrnutí

  • Vykreslujte jen textury, které mají šanci se vyskytnout na obrazovce.
  • Snažte se minimalizovat střídání vykreslovaných textur, ideálně všechny obrázky nacpěte do jedné textury a kreslete to z ní.
  • Vykreslení jedné velké plochy je rychlejší, než vykreslení spousty malých. Pokud se nějaká vrstva/část obrazovky nemění příliš často, tak ji cachujte - vykreslete si ji do pomocné textury a místo spousty volání kreslení malých ploch vykreslujte jednu velkou.

 

  Aktivity (1)

Článek pro vás napsal Luboš Běhounek (Satik)
Avatar
Autor se nyní živí programováním v C++, ale jeho nejoblíbenějším jazykem je C#. Občas si otevře OllyDbg a pohrabe v assembleru...

Jak se ti líbí článek?
Celkem (4 hlasů) :
55555


 



 

 

Komentáře

Avatar
Jiří Gracík
Redaktor
Avatar
Jiří Gracík:

Dobrá práce, to by mě v životě nenapadlo řešit :) O prvním způsobu jsem tak nějak podvědomě věděl, ale nikdy jsem to neaplikoval, schválně to někdy zkusím :)

Odpovědět 26.3.2013 20:57
Creating websites is awesome till you see the result in another browser ...
Avatar
Petr Nymsa
Redaktor
Avatar
Petr Nymsa:

Chtěl bych se zeptat. Úplně nevím co si mám představit pod
"vykreslete si ji do pomocné textury a místo spousty volání kreslení malých ploch vykreslujte jednu velkou" ?

Jinak zatím optimaluzuju pouze vykreslování viditelných ale mám každý objekt s vlastní texturou a ty iteruju a kreslím. Možná bude lepší když tedy udělám jednu velkou texturu a objekty budou v sobě držet pouze Source rectangle pro vykreslení správné části textury. Je to tak ? :)

Odpovědět 12.6.2013 22:01
Pokrok nezastavíš, neusni a jdi s ním vpřed
Avatar
Odpovídá na Petr Nymsa
Luboš Běhounek (Satik):

"vykreslete si ji do pomocné textury a místo spousty volání kreslení malých ploch vykreslujte jednu velkou"

Pod tím si představ to, že když máš třeba terén složený ze spousty malých čtverečků, tak nevykresluješ v každém obrázku všechny čtverečky, ale "předvykreslíš" si je všechny jednou do velké pomocné textury (velké jako obrazovka).

Při dalších obrázcích nevykresluješ tu spoustu malých čtverečků, ale vykreslíš jako terén jen tu pomocnou texturu (a na ni pak další pohyblivé/animované objekty jeden po druhém).

Vykreslení jedné velké textury je několikanásobně rychlejší než vykreslení spousty malých, i když je výsledná plocha stejně velká, protože se eliminuje téměř veškerá režie.

Pokud se hýbeš pohledem po mapě, pak musíš tu pomocnou texturu samozřejmě aktualizovat a znova si ji vykreslit, případně tuhle optimalizaci přímo při pohybu po mapě nepoužívat
(případně by se to ještě dalo obejít tím, že by sis tu pomocnou texturu udržoval o kus větší než obrazovka - měl na každé straně rezervu a vykreslil až když pohledem dojedeš na kraj té pomocné textury (viz obrázek).

"Jinak zatím optimaluzuju pouze vykreslování viditelných ale mám každý objekt s vlastní texturou a ty iteruju a kreslím. Možná bude lepší když tedy udělám jednu velkou texturu a objekty budou v sobě držet pouze Source rectangle pro vykreslení správné části textury. Je to tak ?"
Ano, to je mnohem mnohem rychlejší a šetrnější metoda :) .

Odpovědět 12.6.2013 22:27
:)
Avatar
Luboš Běhounek (Satik):

Jinak jen pro porovnání, když jsem zkoušel psát svůj 2D engine, tak v full HD rozlišení jsem před těmito optimalizacemi měl asi 10 fps.
Když jsem kreslil jen textury, co jsou na obrazovce, tak jsem se dostal na 100 fps, když jsem začal místo spousty textur používat jednu velkou, ze které se tahaly všechny obrázky, tak se fps dostalo asi na 1500 fps a s cachováním terénu do pomocné textury jsem měl asi 3000 fps (když se s kamerou nepohybovalo).

Odpovědět 12.6.2013 22:33
:)
Avatar
Petr Nymsa
Redaktor
Avatar
Odpovídá na Luboš Běhounek (Satik)
Petr Nymsa:

Díky :) Zatím to vidím tak že jádro "enginu" přepíšu aby fungovalo na velké textury a nebralo malé vlastní textury které vykresluje. Navíc si tím zajistím větší flexibilitu...

Odpovědět 12.6.2013 22:35
Pokrok nezastavíš, neusni a jdi s ním vpřed
Avatar
Odpovídá na Petr Nymsa
Luboš Běhounek (Satik):

Když se připomeneš zítra po 9. večer nebo v pátek, tak ti můžu poslat zdrojáky od tohohle http://www.itnetwork.cz/…ik-strategie pro "inspiraci" :)

Až dodělám bakalářku, tak snad zas budu pokračovat :D

Odpovědět 12.6.2013 22:40
:)
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 6 zpráv z 6.