Kreslení rotujících hexagonů pomocí hardwarových spritů
Rotující překážky v Next Hexagon nejsou kreslené přímo do Layer 2 bitmapy. Layer 2 se používá hlavně pro pozadí, středový hexagon, HUD a další obrazové části, které dávají smysl jako bitmapová vrstva. Samotné rotující ringy, tedy barevné stěny hexagonů, jsou řešené přes hardwarové sprity ZX Spectrum Next. Důvod je jednoduchý - překážky se neustále pohybují směrem ke středu, rotují spolu s arénou, mění se jejich pattern a musí zůstat ostré a čitelné. Kdyby se každá stěna překreslovala do Layer 2 jako bitmapa, bylo by potřeba velmi pečlivě řešit mazání, obnovu pozadí. U hardwarových spritů je tento problém mnohem menší, protože sprity leží jako samostatná vrstva nad Layer 2 a video čip je skládá do obrazu až při zobrazování.
Renderer ringů je bankovaný. Kód pro kreslení sprite bloků je uložený v samostatné 16KB bance a za běhu se mapuje do MMU slotů 4 a 5, tedy do oblasti $8000-$BFFF. To umožňuje držet hlavní herní kód v jiné bance a zároveň mít specializovaný renderer, který může volat společné geometrické rutiny, číst aktuální rotaci, ring radiusy a patterny překážek. Z hlediska organizace paměti je to výhodné, protože sprite renderer obsahuje nejen samotný kód, ale také sprite patterny pro ring bloky a hráče. Ringy používají patterny 0 až 23, zatímco hráč používá patterny 24 až 63. Tím se celý systém vejde do omezení 8bitové sprite pattern RAM, kde je k dispozici 64 patternů po 256 bajtech.
Důležitá věc je, že se u ringů neotáčí samotný sprite pattern v reálném čase. ZX Spectrum Next sice umí hardwarově zobrazovat sprity, ale neumí z jednoho sprite patternu udělat libovolně natočenou čáru. Proto jsou malé stavební bloky ringů připravené dopředu jako sada předrotovaných sprite patternů. V praxi to znamená, že renderer nepracuje s jedním univerzálním obrázkem „kusu stěny“, který by potom matematicky otáčel. Místo toho má v paměti několik variant téhož bloku, každou nakreslenou pro jiný směr hrany. Když se potom kreslí konkrétní stěna hexagonu, renderer podle její orientace vybere nejbližší odpovídající předrotovaný pattern a ten zapíše do spritů, které se mají zobrazit. Tahle předrotovaná sada patternů je kompromis mezi pamětí a rychlostí. Kdyby bylo potřeba mít kompletní sprite pro každou možnou velikost, každou délku stěny, každou barvu a každé natočení, spotřeba pattern paměti by okamžitě narostla mimo rozumné meze. Next Hexagon to řeší jinak. V paměti nejsou celé předrotované hexagony. Jsou tam jen malé 16 x 16 bloky, které představují krátké kousky stěny v několika směrech. Celý ring vzniká až za běhu tak, že se tyto malé bloky opakovaně rozmístí podél spočítané hrany. Předrotovaný tedy není celý hexagon, ale jen jeho malý stavební prvek.
Předloha pro tyto sprite patterny vzniká mimo hlavní runtime hry. Nejde o data, která by se za běhu generovala Z80 procesorem. Předem se připraví grafické bloky jednotlivých směrů, převedou se do formátu 16 x 16 pixelů a uloží se jako sprite pattern data do banky. Každý pattern má 256 bajtů, protože jeden sprite pattern ZX Spectrum Next má rozměr 16 x 16 pixelů a každý pixel je jeden index do sprite palety. Díky tomu je zápis za běhu jednoduchý: renderer už neřeší, jak má daná čára vypadat po natočení, ale jen vybere číslo patternu, nastaví X a Y souřadnici.
Výběr správného předrotovaného bloku závisí na orientaci kreslené hrany. Renderer nejprve spočítá šest vrcholů hexagonu pro aktuální radius a rotaci. Tím dostane šest úseček, které tvoří potenciální stěny. Pro každou aktivní stěnu pak zná její začátek a konec. Z rozdílu souřadnic je jasné, jakým směrem hrana vede. Podle tohoto směru se vybere příslušný sprite pattern. U hexagonu je situace jednodušší než u libovolného vektorového objektu, protože hrany mají jen omezený počet základních směrů. Celá aréna se sice otáčí v jemnějších krocích, ale vizuální blok se vybírá z připravené sady, která je dostatečně hustá na to, aby pohyb působil plynule.
Je dobré zdůraznit rozdíl mezi geometrickou rotací a grafickou rotací. Geometrie ringu se počítá za běhu - vrcholy hexagonu, poloha jednotlivých vzorků na hraně a výsledné X/Y souřadnice sprite bloků se odvozují z aktuální hodnoty rotation. Stejný princip se používá i u hráče, jen s jiným účelem. Hráč není složený z bloků podél hrany, ale používá samostatné předrotované patterny 24 až 63. Jeho herní úhel může být jemnější, ale pro zobrazení se převede na jednu z dostupných grafických orientací. V článku je proto dobré rozlišovat dvě sady předrotovaných spritů: ring bloky, které tvoří stěny hexagonů, a hráčovy patterny, které určují natočení jeho malé lodi nebo šipky. Obě sady řeší stejný základní problém - místo drahé runtime rotace pixelů se používá předem připravená grafika a za běhu se jen vybírá nejvhodnější pattern.
Základní myšlenka kreslení ringů je taková, že každá překážka je popsaná jako hexagon o určitém radiusu a patternu aktivních stěn. Ve hře existují tři ring sloty, například ring_r1, ring_r2 a ring_r3. Každý slot má svůj radius a svůj pattern. Radius říká, jak daleko od středu se daný ring nachází, zatímco pattern říká, které z šesti hran hexagonu jsou aktivní překážky. Pokud je bit pro danou hranu nastavený, renderer tuto hranu vykreslí jako sérii sprite bloků. Pokud nastavený není, hrana se přeskočí a vznikne průchod, kterým může hráč proletět. Celá obtížnost hry tedy vzniká kombinací toho, jaké hrany jsou aktivní, jak rychle se ringy přibližují ke středu a jak se celá aréna otáčí.
Před samotným kreslením renderer spočítá šest vrcholů hexagonu pro aktuální radius a rotaci. Používá se k tomu stejná logika jako u zbytku hry - aktuální hodnota rotation určuje natočení arény a rutina calc_vertex spočítá pozici jednotlivých vrcholů. Radius ringu se před výpočtem ještě lehce upravuje o konstantní korekci SPRITE_CENTER_OFFSET_R, aby sprite bloky seděly vizuálně na správné místo vůči Layer 2 středu. Potom renderer prochází šest hran hexagonu. U každé hrany se podívá do patternu, jestli se má kreslit. Pokud ano, vezme dvojici sousedních vrcholů a mezi nimi rozmístí několik sprite bloků.
Jedna stěna tedy není jeden dlouhý sprite. Je složená z několika menších 16 x 16 hardwarových spritů, které leží na vypočítaných bodech podél hrany. Každý z těchto spritů používá předrotovaný pattern odpovídající směru dané stěny. Tohle řešení je flexibilní, protože stejný malý sprite blok se dá použít pro mnoho různých radiusů, natočení a délek hran. Není potřeba mít předgenerovaný obrovský počet celých hexagonů pro každou velikost a rotaci. Stačí několik předrotovaných bloků a renderer je rozmístí do prostoru podle aktuální geometrie. Nevýhoda je, že při větších ringách může jedna dlouhá stěna spotřebovat hodně sprite slotů. Proto renderer používá LOD, tedy různý počet vzorků podle velikosti ringu.
LOD v tomto případě neznamená změnu detailního modelu jako u 3D her, ale čistě změnu počtu sprite bloků na jednu hranu. Pro největší ringy se používá hustší vzorkování, protože vzdálené ringy jsou největší a jejich stěny jsou dlouhé. Pokud by se tam použilo málo sprite bloků, stěny by vypadaly roztrhaně a špatně čitelně. U menších ringů, které jsou blíže středu, je hrana kratší a zároveň je tam méně prostoru, takže stačí menší počet bloků. V kódu je to řešené podle aktuálního radiusu. Největší ring může používat až 13 vzorků na hranu, velký ring používá 7 vzorků, střední ringy 6 nebo 5 vzorků a malé ringy 4 vzorky. Tím se drží rozumný poměr mezi čitelností a spotřebou hardwarových spritů.
Rozmístění sprite bloků po hraně je řešené přes jednoduchý DDA výpočet v pevné řádové přesnosti. Renderer si vezme počáteční a koncový bod hrany a spočítá krok jako rozdíl souřadnic dělený šestnácti. Vzorkovací body potom leží na zlomcích hrany, například 2/16, 4/16, 6/16 a podobně podle zvolené LOD úrovně. Důležité je, že se nepoužívá nepřesná aproximace typu „nějaký počet bodů podél čáry“, ale stabilní dělení na šestnáctiny. Díky tomu se dlouhé hrany při rotaci méně třesou a jednotlivé sprite bloky neplavou vůči středu. U rychlé hry je to hodně důležité, protože i malá chyba v pozici bloku je při rotujícím hexagonu vizuálně velmi nápadná.
Každý vzorkovací bod se zaokrouhlí na nejbližší pixel a před odesláním do sprite atributů projde ořezáním. Renderer kontroluje hlavně Y souřadnici jako signed offset od středu obrazovky a X souřadnici jako logickou obrazovou pozici. Pokud je bod mimo obrazovku nebo v nebezpečné oblasti, sprite se vůbec nepošle. To je důležité hlavně u velkých ringů, které částečně leží mimo viditelnou oblast. Dřívější přístup by mohl vést k tomu, že mimoobrazové části přetekly zpět do obrazu nebo se špatně započítaly do počtu spritů. Tady se sprite_count zvyšuje až po všech testech. To znamená, že se počítají jen opravdu emitované sprity, ne ty, které byly vyřazené clippingem.
Samotný zápis spritu probíhá přes hardwarové porty spritů. Renderer nastaví aktuální sprite slot přes port $303B a potom zapisuje atributy přes port $0057. Pro každý sprite se zapíše X souřadnice, Y souřadnice, atribut 2 a atribut 3. X souřadnice je centrovaná tak, aby 16 x 16 sprite seděl středem na vypočítaný bod. Proto se k obrazové souřadnici přičítá posun +24, což odpovídá hardwarovému sprite offsetu a centrování o polovinu sprite patternu. Pokud X souřadnice přeteče přes 255, nastaví se X-MSB bit v atributu 2. Bez toho by sprity na pravém okraji obrazovky mizely nebo se chybně vracely na levou stranu.
Barvy jednotlivých ringů se neřeší samostatnou kopií patternů pro každou barvu, ale pomocí sprite palette offsetu. První ring používá jednu barevnou variantu, druhý ring jinou a třetí ring další. V kódu jsou pro ně připravené offsety například $00, $10 a $20. Sprite pattern tedy může být stejný, ale podle atributu se zobrazí v jiné části sprite palety. To šetří pattern paměť a dovoluje použít omezený počet patternů pro více barevných ringů. Vizuálně pak může mít jeden ring červeno-žlutý charakter, jiný cyan-světlý a další zeleno-žlutý, aniž by bylo nutné duplikovat celou sadu grafiky.
Hráč je také hardwarový sprite, ale je řešený jinak než ringy. Zatímco ringy používají postupně přidělované sprite sloty od začátku sprite listu, hráč má pevný slot 127. To je záměrné, protože hráč musí být vždy vidět a neměl by soupeřit s ring bloky o běžné sloty. Jeho orientace je řešená předrotovanými patterny 24 až 63. Herní úhel hráče má jemnější krok, ale pro sprite pattern se kvantuje do 40 směrů po 9 stupních. Pozice hráče tedy může zůstat jemná, zatímco grafická orientace je omezená na dostupný počet patternů. Pokud hra není ve stavu hraní, dema nebo krátkého freeze po nárazu, hráčův slot se explicitně schová, aby nezůstal viset nad menu nebo game over obrazovkou.
Důležitou součástí rendereru je i úklid starých spritů. Počet emitovaných ring bloků se může každý frame měnit. Jeden frame může mít více aktivních hran, jiný méně. Jeden radius může být částečně mimo obrazovku, jiný celý viditelný. Pokud by renderer jen zapisoval nové sprity a neřešil zbytek seznamu, na konci sprite listu by zůstávaly staré atributy z předchozích framů. Proto se na začátku každého sprite framu nastaví sprite_count na nulu a na konci se porovná aktuální počet s předchozím. Pokud bylo v minulém framu spritů více, přebytečné sloty se schovají zápisem nulových atributů. To zabraňuje tomu, aby se na obrazovce objevovaly zbytky starých stěn.
Později byl do rendereru přidán ještě beat pulse efekt, který byl i v Micro Hexagonu na C64. Princip je velmi jednoduchý: když podle beat mapy zazní kopák, ringy se na několik framů opticky odsunou o pár pixelů směrem od středu a pak se okamžitě vrátí. Nedělá se to změnou skutečných herních radiusů. Kolize zůstávají přesně stejné jako předtím. Mění se pouze radius použitý pro vykreslení. Před tím, než se hodnota ring_r1, ring_r2 nebo ring_r3 předá do kreslicí rutiny, zavolá se malá rutina APPLY_BEAT_PULSE_TO_RADIUS. Ta vezme aktuální vizuální offset beat_pulse_offset a přičte ho k radiusu. Výsledkem je krátké „kopnutí“ ringů ven, ale pouze na úrovni obrazu.
Tohle oddělení vizuálu a logiky je důležité. Kdyby beat pulse měnil skutečný radius ringu, měnil by tím i kolize a hráč by mohl umřít nebo přežít podle krátkého hudebního efektu. To by působilo nefér, protože překážka by se na pár framů opravdu posunula. V aktuálním řešení je pulse jen vizuální. Hráč vidí, že ringy reagují na kopák, hra tím získá rytmický nádech, ale collision systém pořád pracuje s původními hodnotami z rings.asm. Beat pulse je tedy bezpečný efekt, který zvyšuje energii obrazu, ale nemění pravidla hry.
Beat mapa je uložená jako seznam 16bitových frame pozic pomocí DW. Protože hudba běží v 50Hz přehrávání, jedna sekunda odpovídá 50 framům. Hodnota 150 znamená kopák po třech sekundách, hodnota 300 po šesti sekundách a tak dál. Seznam je ukončený hodnotou $FFFF. Při inicializaci nebo restartu hudby se beat čítač vynuluje a ukazatel beat mapy se nastaví na začátek. Každý hudební frame se čítač posune společně s přehrávačem skladby, porovná se s aktuální hodnotou v beat mapě a pokud se shoduje, spustí se pulse tabulka. Při opakování skladby se čítač vynuluje znovu podle hodnoty MUSIC_LOOP_FRAMES, aby beat mapa zůstala synchronizovaná s hudební smyčkou. Díky tomu kopance sedí na hudbu i tehdy, když hráč začne hrát později, nebo když skladba běží už v menu a potom pokračuje během hry.
Samotný průběh odskoku je tabulka hodnot. Například výraznější testovací varianta může být 8, 6, 4, 3, 1, 0, zatímco jemnější finální varianta může být třeba 4, 3, 2, 1, 0. Každý frame se z tabulky vezme další hodnota a uloží se do beat_pulse_offset. Renderer ji pak přičte ke každému aktivnímu ringu. Krátká tabulka s prudkým náběhem a rychlým návratem působí jako úder bubnu. Delší tabulka s měkčím průběhem působí spíš jako pružné zhoupnutí. Výhoda tabulky je v tom, že se dá ladit bez změny logiky rendereru. Stačí změnit hodnoty a délku pulsu.
Celý systém je tak kombinací několika optimalizací. Geometrie ringů se počítá z aktuální rotace a radiusu, aktivní hrany jsou určené šestibitovým patternem, každá hrana se kreslí několika 16 x 16 sprity podle LOD, grafika stěn i hráče je připravená jako sada předrotovaných patternů, barvy se vybírají přes palette offset, hráč má vyhrazený pevný sprite slot a staré nepoužité sprity se po každém framu schovávají. Nad tím je ještě beat pulse, který dočasně přičte malý vizuální offset k radiusu a dodá překážkám hudební reakci. Klíčové ale je, že všechny tyto sprite atributy se zapisují ve správném okamžiku. Díky tomu mohou ringy zůstat rychlé, ostré a dynamické, aniž by se musely překreslovat do Layer 2 bitmapy.
Vím, že Vám tento článek asi nikomu nic nedá, ale on je spíš pro mne, abych věděl jak je tam co dělané (proto jsem uvedl i názvy návěstí), on se mi tento článek bude po čase určitě hodit...