Lekce 4 - Optimalizace vykreslování ve 2D hrách
V minulé lekci, Mandelbrotova množina, jsme si ukázali algoritmus pro vykreslení nejznámějšího fraktálu Mandelbrotovy množiny včetně popisu, aplikace a zdrojových kódů.
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.
V další lekci, Plechovka turbo aneb Flood fill po řádcích, si ukážeme algoritmus plechovky turbo aneb Flood fill po řádcích.