3 WORLD - Modul pravidel
3.1 Úvod
Modul pravidel WORLD je centrální částí aplikace
starající se o realizaci hry samotné. Zde se shromažďují všechny
informace o stavu herního světa, komunikuje se s jednotlivými hráči a
realizují se jejich požadavky.
Modul WORLD definuje datové struktury popisující
herní objekty (jednotky, mapu atd.), implementuje metody odpovídající
herním akcím (pohyb, boj atd.) a při síťové hře zajišťuje synchronizaci
klientů.
3.2 Architektura
Síťová povaha hry si vynucuje rozdělení modulu na dvě části:
- část serverovou (dále WORLD_SERVER), která má za úkol shromažďovat veškerá data, provádět v nich změny a hru celkově řídit
- část
klientskou (dále WORLD_CLIENT), která má za úkol se serverovou části
data synchronizovat, změny předávat do uživatelského rozhraní a hráči
vyvolané herní akce zaznamenávat
3.2.1 Rozdělení WORLD_CLIENT / WORLD_SERVER
Oba moduly spolu komunikují výhradně pomocí
zpráv. Pro přehlednost celkového řešení jsme se rozhodli použít model
klient/server jak pro hru po síti, tak i při hře lokální, kdy jsou
zprávy mezi oběma moduly předávány pomocí vrstvy
NET,
resp. přímo pouze pomocí předání objektů v paměti. Veškeré operace tak
probíhají stejně, liší se pouze na nejnižší úrovni metodikou použitou k
předání zpráv.
Základnímu rozdělení odpovídají třídy TWorldClient a TWorldServer.
Obě se starají především o komunikaci a tak jejich základním dílem je
přijímání zpráv z ostatním modulů a reakce na ně. Klientský modul
přijímá zprávy hráče, zprostředkované modulem GUI, a podle typu zprávy
buď sám odpoví, nebo zprávu předá na server. Současně přijímá zprávy od
serveru, kterými je informován o výsledcích vlastních akcí a činnosti
ostatních hráčů. Objekt třídy TWorldServer existuje ve hře vždy jeden, objektů TWorldClient
je právě tolik, kolik se hry účastní lidských hráčů. Modul umělé
inteligence nepotřebuje uživatelské rozhraní a proto komunikuje přímo s
třídou TWorldServer.
3.2.2 Použití WORLD_ENGINE
Pro zjednodušení inicializace a destrukce byla
navržena ještě třetí část modulu WORLD - WORLD_ENGINE, která se stará
pouze o několik málo zpráv na začátku a konci hry, zato však nabízí
uživatelskému rozhraní GUI pohodlnější komunikaci a přesouvá
inicializační kód mezi ostatní části modulu WORLD.
3.2.3 Třída TWorld
Jak modul serveru, tak všechny klientské, uvnitř obsahují objekt třídy TWorld,
který, jak název napovídá, představuje herní svět - tedy mapu,
rozmístění jednotek a budov, informace o hráčích atd. Pro zamezení
konfliktů a nekonzistenci dat v jednotlivých objektech TWorld jsme se rozhodli sjednotit veškeré provádění změn pouze uvnitř modulu TWorldServer, který při každé změně rozesílá všem klientským modulům informace o provedených změnách. Všechny objekty TWorld
jsou tak synchronizovány a mohou tak sloužit jako read-only "zrcadla"
ústřední reprezentace herního světa ze serveru. Aby nedocházelo ke
zbytečné duplicitě, jsou objekty TWorld při hře více hráčů na jednom počítači sdíleny všemi hráči, takže instancí tříd TWorld vždy existuje právě tolik, kolik je do hry zapojeno počítačů plus jedna navíc pro TWorldServer.
Celkové propojení a význam zmíněných modulů ilustrují následující diagramy.

Diagram 3.1: Propojení základních částí modulu WORLD při lokální hře

Obrázek 3.2: Propojení základních částí modulu WORLD při síťové hře
3.2.4 Třídy TWorldEngine a TWorldServerEngine
Ve smyslu klient / server se nese i dvojice tříd TWorldEngine a TWorldServerEngine, které slouží k realizaci složitějších dotazů nad reprezentací TWorld
v modulu klientském, resp. serverovém. Některé herní operace nebo
dotazy GUI není možno zodpovědět přímo - ptá-li se hráč například na
dosah pohybu jednotky, nebo chce-li zobrazit seznam možných cílů útoku,
je k sestavení odpovědi nutné použít připravenou funkci. A právě k tomu
slouží modul TWorldEngine, který dokáže vyhodnocovat složitější (ale stále read-only) operace nad třídou TWorld.
Potomkem této třídy je TWorldServerEngine,
který navíc dokáže vyhodnocovat i operace, které ve svém důsledku stav
světa změní. Provedené změny pak pomocí mechanismu synchronizace posílá
jednotlivým hráčům.
3.3 Životní cyklus
Žádná z částí modulu WORLD nemá vlastní "aktivní
tělo", veškerá zde prováděná činnost se děje pouze v reakci na příchozí
zprávy od ostatních modulů, především z uživatelského rozhraní.
Aktivnější je modul pouze při začátku a konci hry. Při inicializaci se
modul WORLD dostává ke slovu jako poslední (nepočítáme-li modul umělé
inteligence, který nemusí být vůbec použit) a to až po nastavení
parametrů hry a připojení hráčů ve zprávě MSG_GAME_START. WORLD_ENGINE vyvolá inicializaci TWorldServeru a příslušného počtu TWorldClientů na jednotlivých počítačích hry se účastnících. Po obdržení potvrzení připojení klientů k hernímu serveru (MSG_JOIN_CONFIRMATION)
následuje načtení herních dat z RM a jejich rozeslání klientům. Po
načtení herních dat a inicializaci herní mapy v uživatelském rozhraní
posílají klienti druhé potvrzení, MSG_GUIMAP_READY. V momentě, kdy jej přijme server od posledního klienta, je hra spuštěna zprávou MSG_LETS_GO.
Od této chvíle dává WORLD od řízení hry ruce pryč a reaguje pouze na
dotazy a požadavky hráčů, ať už živých (zprostředkovaných modulem GUI),
nebo umělých (AI). Po každé akci však testuje, zdali nedošlo k odpojení
či poražení některého hráče, nebo celkovému vítězství. V takovém
případě zahrne klienty závěrečnou synchronizací a hru ukončí zprávou MSG_ENDGAME.
3.4 Komunikace
Valná část modulu WORLD tedy spočívá v
komunikaci - buďto ve smyslu klient/server synchronizace, nebo ve
vyměňování zpráv s ostatními moduly. Následující odstavce jednotlivé
části rozeberou podrobněji.
3.4.1 Synchronizace
Synchronizace klientských dat vychází z jednoduchého modelu spoléhajícího se na vrstvu sítě
NET
zaručující doručení zpráv přes síť i v případě poruch či chyb spojení.
Základní stavební jednotkou synchronizace je kontejner historie
THistory,
jakýsi zásobník herních událostí. Každá akce hráče, který je momentálně
na tahu, přidá na vrchol zásobníku jednu či více událostí (pohyb
jednotky vyvolá přesun jednotky a třeba i obsazení města). Každý z
klientů si herní události zaznamenává k sobě a po jejich přijetí zasílá
serveru potvrzení.
TWorldServer má tak
přehled, které události který klient již přijal a na základě toho
každému zasílá pouze ty, které v jeho reprezentaci světa ještě nejsou
zaznamenány. Důležité je zdůraznit, že synchronizace vždy probíhá z
iniciativy WORLD_SERVERu a o ihned poté, co se stav herního světa
nějakým způsobem změnil. Doba, která uplyne od vyvolání akce na
klientovi po její zobrazení je tak nejkratší možná.
3.4.2 Komunikace s GUI
Pomineme-li inicializaci hry, komunikuje
uživatelské rozhraní pouze na lokální úrovni s příslušným modulem
WORLD_CLIENT. Z read-only podstaty modulu TWorld umístěného uvnitř TWorldClient
vychází i rozdělení komunikace s ním na synchronní a asynchronní.
Zajímá-li se hráč v GUI o konkrétní jednotku a chce vědět, kolik má
tato životů, je dotaz směrován přímo do struktur třídy TWorld a odpověď je navrácena synchronně. Chce-li však hráč jednotkou posunout, jinými slovy, žádá-li si změnu ve strukturách TWorld, je z důvodů konzistence dotaz nejprve předán modulu TWorldServeru,
který jej vyhodnotí, zaznamená a změny mechanismem synchronizace oznámí
výsledek akce všem hráčů včetně hráče, který akci provedl. Jelikož musí
tato komunikace probíhat stejně v případě hry lokální i síťové, je
řešena asynchronně.
3.4.2.1 Synchronní komunikace
Schéma synchronní komunikace znázorňuje následující diagram.

Diagram 5.3: schéma synchronní komunikace
Jako příklad uvádíme zprávu MSG_GET_ATTACK_RANGE,
která slouží k získání možných cílů útoku jednotky. GUI následně
vybrané hexy zvýrazní červeným podkladem a umožní tak hráči na vybraný
cíl kliknout a zaútočit.
enum {
...
/// Zadost o vypsani hexu, na nichz stoji jednotky ci budovy, na ktere muze
/// jednotka v danem kole zautocit.
/// @see PACKET_GET_ATTACK_RANGE
/// @see TPacket_RET_ATTACK_RANGE
/// GUI -> WORLD_CLIENT
,MSG_GET_ATTACK_RANGE
...
};
Komentář zprávy uvádí, že parametrem má být struktura PACKET_GET_ATTACK_RANGE a návratovou hodnotou je třída TPacket_RET_ATTACK_RANGE. Definice obou struktur (stejně tak všech, které jsou odkazovány z této části souboru Interface.h) najdeme v souboru world/world_messages.h.
Aby byl příklad kompletní, uvedeme si i obě zmíněné definice a na následném příkladu ukážeme, jak celkové volání probíhá.
/// Zaklad zpravy pro komunikace WORLD_CLIENT <-> GUI
struct PACKET_WORLD_GUI
{
/// typ zpravy (MSG_neco)
int type;
/// hrac, kteremu je zprava urcena, nebo ktery zpravu posila
int player_id;
};
struct PACKET_GET_ATTACK_RANGE : public PACKET_WORLD_GUI
{
/// ID jednotky, o kterou se zajimame
UNIT_ID unit_id;
};
/// Univerzalni trida pro vraceni slozitejsich datovych struktur v MSG systemu.
class TPacket_SyncResult
{
public:
/// Implicitni konstruktor
TPacket_SyncResult();
/// Konstruktor pro vraceni chyboveho kodu
TPacket_SyncResult(K8_ERROR code);
/// Status, se kterym volani zpravy skoncilo. Ma hodnotu 0, je-li vsechno
/// v poradku, jinak kod chyby.
int status;
};
class TPacket_RET_ATTACK_RANGE : public TPacket_SyncResult
{
public:
std::vector<HEX_ID> attack_range;
};
Z uvedených definic je zřejmá základní
hierarchie zpráv vyměňovaných mezi moduly WORLD a GUI. Zprávy putující
od GUI k WORLD posílají jako svůj parametr struktury odvozené od PACKET_WORLD_GUI. Ve společné (zděděné) části definují svůj typ (v tomto případe MSG_GET_ATTACK_RANGE)
a ve zbytku příslušné parametry příslušné k danému typu zprávy (v tomto
případě stačí jediný parametr - ID jednotky, která chce útočit).
Podobná situace se týká návratových hodnot z WORLD do GUI vracených.
Společným předkem je třída TPacket_SyncResult, která obsahuje v těle jedinou datovou položku - int status, nabývající hodnoty 0 v případě, že byla zpráva zpracována v pořádku, resp. nenulové hodnoty určující kód chyby (z výčtu enum K8_ERROR). Konkrétní odpověď na zprávu, potomek TPacket_RET_ATTACK_RANGE pak ve svém těle obsahuje potřebná data k popsání výsledku (v tomto případě ID hexů, na které může jednotka z PACKET_GET_ATTACK_RANGE útočit).
Jelikož je typ zprávy specifikován v položce type struktury PACKET_WORLD_GUI, posílají se všechny zprávy pod jednotnou hlavičkou MSG_WORLD_GUI,
což umožňuje celý systém zpráv rozdělit na alespoň dvouúrovňovou
hierarchii. Pro úplnost zbývá uvést příklad volání takovéto zprávy.
Veškerá další synchronní komunikace mezi moduly WORLD a GUI probíhá na
stejném principu.
PACKET_GET_ATTACK_RANGE msg;
msg.type = MSG_GET_ATTACK_RANGE;
msg.player_id = player_id;
msg.unit_id = unit_id;
TPacket_RET_ATTACK_RANGE * result = (TPacket_RET_ATTACK_RANGE *)
KSendGlobalMessage(MSG_WORLD_GUI, MOD_GUI, MOD_WORLD_CLIENT, &msg);
if (result->status == 0) {
// zpracování seznamu hexu result->locations
...
} else {
// reakce na chybu result->status
...
}
// dealokace promenne result
delete result;
3.4.2.2 Asynchronní komunikace
Dotazy měnící stav herního světa jsou, jak již
bylo zmíněno, posílány do modulu WORLD_SERVER a jejich výsledky
asynchronně vraceny. Aby nebylo GUI zatěžováno nutností rozlišovat,
komu má kterou zprávu posílat, adresuje GUI všemi svými zprávami
klientský modul WORLD_CLIENT a teprve ten se stará o rozlišení typu
zpráv a v případě potřeby zprávy na server přeposílá. Stejná logika se
týká i opačného směru zpráv, totiž aktualizačních informací. Ty putují
z WORLD_SERVER modulu do WORLD_CLIENT a odtamtud jsou předány GUI. V
principu se při dotazu vyhodnocovaném asynchronně jedná z pohledu GUI o
dvě zprávy - jednu při odeslání (GUI → WORLD_CLIENT) a druhou, po
zpracování na serveru, příchozí a popisující změny světa (WORLD_CLIENT
→ GUI). Od odeslání dotazu po příjem odpovědi je uživatelské rozhraní
zamčené a neumožňuje provádět jiné akce, aby se zamezilo neplatným
operacím.
Schéma asynchronní komunikace popisuje na příkladu pohybu jednotky následující diagram.

Diagram 3.4: schéma asynchronní komunikace
3.4.3 Spolupráce s AI
Modul umělé inteligence do značné míry pracuje
podobně jako živý hráč prezentovaný uživatelským rozhraním. Vzhledem k
tomu, že nepotřebuje žádné vizuální informace a pracuje přímo z daty,
je modul AI vždy provozován na stejném počítači, jako modul
WORLD_SERVER a jejich komunikace probíhá vždy přímo. Základním kamenem
jsou tytéž zprávy popsané v předchozí kapitole o GUI.
Tah umělého hráče začíná zprávou MSG_AI_ON_TURN.
V jejím těle jsou obsaženy ukazatele na mapu, seznamy jednotek, budov a
další prvky struktury TWorld. Na základě těch a vlastních dat uložených
z dřívějška vygeneruje modul AI strategický plán, který se v tomto kole
pokusí zrealizovat. Jeho zpracování pak probíhá v jednoduchém cyklu
výměny zpráv, kdy AI nejprve zašle zprávu a poté zjišťuje její důsledky.
Jelikož jsou pro komunikaci s AI používány
stejné zprávy, jako pro komunikaci s GUI, je použita obdobná
hierarchie, v tomto případě vycházející ze zprávy MSG_WORLD_AI a k ní příslušné struktury PACKET_WORLD_AI.
Podrobnější informace o generování strategického plánu a jeho aktualizaci naleznete v kapitole
Strategický plánovač.
3.4.4 Spolupráce s RM
RM, tedy manažer zdrojů je v kontaktu s modulem WORLD pouze při následujících příležitostech:
- Inicializace pravidel a načtení mapy - největší objem
operací, kdy se nahrávají struktury popisující vlastnosti jednotek a
budov, TCL skripty i celá herní mapa.
- Uložení hry
- Závěrečné uvolnění zdrojů, kdy jsou data nahraná při začátku hry navrácena zpět do RM k dealokaci.
Postup je vždy podobný a opět stavěný na
zprávách. WORLD si pomocí zprávy vyžádá ukazatel na objekt
představující rozhraní k daným datům a s ním dále pracuje pomocí volání
jeho metod.
3.4.5 Spolupráce s NET
Modul NET je používán při klient - server
komunikaci. Při skutečné hře po síti zprávy zajišťuje posílání nad
protokolem TCP/IP, při lokální hře předává objekty se zprávou přímo. Z
pohledu modulu WORLD je však tato činnost transparentní a v obou
případech stejná.
Nosnou strukturou dat je XML, do které se zpráva
zapíše, předá a následně načte. Proto ke každé zprávě posílané mezi
moduly WORLD_SERVER a WORLD_CLIENT existují metody writeToXML(TPackage * package) a readFromXML(TPackage * package), které pomocí metod třídy TPackage zapíší, resp. načtou obsah zprávy z, resp. do XML.
Modul NET se účastní i příprav hry, kdy se inicializuje herní server a připojují jednotliví klienti. Za zmínku stojí i zprávy MSG_CLIENT_HAS_DISCONNECT a MSG_NET_LOST_CONNECTION.
První zmíněnou obdrží server v případě, kdy se některý z připojených
klientů odpojí ze hry, aniž by předtím hru korektně vypnul. Může se tak
stát při výpadku síťového spojení nebo chybě klientova počítače. V
takovém případě je daný hráč nahrazen umělou inteligencí a hra
pokračuje dál. Druhou zmíněnou zprávu obdrží v obdobném případě sám
klient. Ten již sám ve hře pokračovat nemůže a tak je pouze upozorněn
chybovým hlášením a hra na jeho počítači je ukončena.
3.5 Skriptovací jazyk TCL
Aby byla pravidla hry snadno rozšiřitelná a
modifikovatelná, snažili jsme se pro realizaci herních akcí použít v
maximální možné míře skriptovací jazyk a v C++ kódu zpracovat jen nutné
minimum operací. Skriptovací jazyk umožňuje snadnou úpravu postupů,
podmínek i číselných koeficientů bez nutnosti znovu překládat celý
projekt a usnadňuje tak jednak vývoj hry jako takové, tak její následné
úpravy. Při výběru skriptovacího jazyka jsme se rozhodli pro TCL, kvůli
jeho snadné syntaxi a bezproblémovému napojení na kód jazyka C. Většina
metod tříd TWorldEngine a TWorldServerEngine
tedy ke svému vyhodnocení používá právě skriptů napsaných v jazyce TCL.
Příslušné TCL skripty jsou volány pro vyhodnocení souboje jednotek, pro
počítání ceny za pohyb, pro modelování počasí i pro předávání tahů.
Veškeré skripty a operace s nimi se zakládají na oficiální knihovně
TCL,
kterou jsme pro naše použití "obalili" jednoduchým objektovým modelem
umožňujícím snazší převod proměnných mezi prostředími jazyka C a TCL.
Knihovnu a její zdrojové kódy lze nalézt v adresáři
external/TCL/, naše rozšířené pak v
common/TCL.
Základní metodou je spuštění TCL skriptu Tcl_Eval(interpreter, code),
která jako druhý parametr dostane kód v jazyce TCL, přeloží jej a
provede. Prvním parametrem je ukazatel na objekt TCL interpreteru,
který zmíněnou činnost provede. Naše "nadstavba" zavádí dvě třídy - TTCL_Interpreter a TTCL_Script.
První uvedená řeší na nižší úrovni samotné navázání na nativní funkce a
struktury knihovny TCL, zatímco druhá slouží jako pohodlnější rozhraní
pro uživatele. Realizuje celý proces inicializace, spuštění i
vyhodnocení skriptu, přičemž sama kontroluje a řeší poněkud
nesystematickou netypovost jazyka TCL a reaguje na vzniklé chyby pomocí
výjimek. TTCL_Interpreter je jednotlivými objekty TTCL_Script
sdílen. Platnost proměnných v prostředí TCL interpretu však přesahuje
volání jednotlivých skriptů a tak je vhodné nemít interpreter jeden,
ale více - objektů třídy TTCL_Interpreter
se tedy vytvoří po jednom na každém místě, kde se používají TCL skripty
(WORLD_SERVER, WORLD_CLIENT a AI) a existují v paměti po celou dobu
hry. Jmenné prostory se tak nemíchají, což zabraňuje některým těžko
předvídatelným chybám.
Protože samotný skriptovací jazyk nemá
prostředky, jak přímo ovlivnit stav proměnných platných v prostředí
jazyka C, natož nějakou vlastnost herního světa, je veškeré skriptování
prováděno ve třech krocích:
- uložení proměnných z C do TCL,
- spuštění skriptu,
- načtení proměnných z TCL do C.
3.5.1 Uložení a načtení proměnných
Většina skriptů má nějaký vstup a všechny
skripty dávají nějaký výstup. Základní sadu datových typů poskytovanou
TCL knihovnou jsme rozšířili o datový typ TTCL_List (spojový seznam) a TTCL_Array (asociativní pole). Kombinací všech typů pak vznikla abstraktní třída TTCL_Struct, definující metody writeToTCL a readFromTCL.
Její potomci (například struktury popisující vlastnosti jednotek nebo
herní mapu) pak v těchto metodách implementují uložení všech svých
členských struktur, skládající se z mnoha zpracování základních datových
typů.
Popsaná změť typů a tříd má jediný účel -
převést hodnotu proměnné (libovolného typu) do prostředí skriptu TCL,
kde může být podle ní vyhodnocena zvolená operace. Příkladem budiž
souboj jednotek, ke kterému potřebujeme mj. znát sílu a počet životů
obou nepřátel. Výstupem skriptu počítajícího takový souboj pak bude
výčet ztrát na obou stranách. Takový se opačným postupem načte do
prostředí C, kde se dále zpracuje.
3.5.2 Spuštění skriptu
Samotné vykonání TCL kódu vychází z funkce Tcl_Eval, které předává kód TCL (char *) získaný od modulu RM, který schraňuje všechny TCL kódy z adresáře res/xml/scripts. Po spuštění analyzuje návratovou hodnotu a v případě chyby vyvolává výjimku E_8K_TCL_Error
s patřičným popisem. Bohužel, do ladících možností jazyka TCL se nám
nepodařilo dostatečné proniknout, takže hledání chyb ve skriptech se
často muselo obejít jen s dosti mlhavým popisem chyby a bez specifikace
čísla řádku.
3.5.3 Struktura TCL_SCRIPT
Pro přesnější upevnění funkce a použití TCL skriptů jsme zavedli strukturu
TCL_SCRIPT,
sestávající ze tří položek: seznamu vstupů (jejich jmen a typů),
seznamu výstupů a samotného TCL kódu. Pro každý skript v projektu
použitý byla jedna takováto kostra definována a uložena jako XML soubor
v adresáři
res/xml/scripts. Jejich načítání je řešeno
modulem RM a umožňuje při použití skriptů pouze předat ukazatele na vstupní a výstupní proměnné a nestarat se již o jejich typy.
Schématický kód v následující ukázce popisuje,
jak vypadá volání TCL skriptu, včetně uložení a načtení vstupních,
resp. výstupních dat a odchycení chyby.
int input1 = 1, input2 = 2, output;
TCL_SCRIPT * code = ...; // TCL kod vepsany primo, nebo ziskany z RM
TTCL_Script script(&interpreter); // interpreter = inicializovany TCL interpreter
script.loadStruct(code);
script.setVar("delenec", &input1);
script.setVar("delitel", &input2);
try {
script.run();
script.getVar("podil", &output);
// vyuziti vysledku output
...
}
catch (E_8K_TCL_Error &e) {
// zpracovani vyjimky e
...
}
3.5.4 Volání zpráv
Složitější skripty nemají jasně definovaný
vstup, nebo si mohou během svého výpočtu chtít vyžádat další informace.
Za tímto účelem jsme do TCL skriptů zabudovali napojení na náš interní
MSG systém, takže TCL skripty mohou během svého provádění pomocí
speciálního příkazu KSendMessage
poslat zprávu, jejíž parametry se převedou do datových typů jazyka C a
vyhodnotí standardní cestou. Situace ohledně návratových hodnot však
byla o něco složitější, proto jsme se rozhodli problém obejít a místo
toho jako parametry zpráv předávat jména proměnných, do nichž má
adresát zprávy své výsledky uložit. Jako příklad nám poslouží skript
vyhodnocující, zda nedošlo k obsazení města. Ten prochází jednotlivá
políčka daného města hledá na nich nepřátelské jednotky. Jeho vstupem
je však pouze seznam políček města, a tak se na přesný obsah políček,
jakožto na vlastnosti objektů na nich stojících musí dotazovat
dodatečně, právě pomocí zpráv.
1: set player_in_the_city 0;
2: for {set i 0} {$i < $town(citysize)} {incr i} {
3: KSendMessage $MSG_GET_HEX_BY_ID "city_hex" $town(position, $i);
4: if {($city_hex(unit) > 0)} {
5: KSendMessage $MSG_GET_LIVING_UNIT "city_unit" $city_hex(unit);
6: if {$player_in_the_city == 0} {
7: # prvni objevena jednotka ve meste
8: set player_in_the_city $city_unit(player);
9: } elseif {$city_unit(player) != $player_in_the_city} {
10: # ve meste jsou jednotky alespon dvou ruznych hracu
11: set player_in_the_city 0;
12: break;
13: }
14: }
15: }
Na řádku 2 skript žádá o data $i-tého hexu města. Atributy tohoto pole chce uložit jako asociativní pole názvu $city_hex. Na řádku 5 pak obdobným způsobem požaduje vytvoření asociativního pole $city_unit a uložení dat jednotky z tohoto hexu do něj.
3.6 Struktury a třídy
Hlavním posláním modulu WORLD je správa a
zpřístupnění veškerých herních dat. V první řadě bylo nutné navrhnout
datové struktury pro jednotlivé objekty vyskytující se ve hře (hexy,
jednotky atd.). Navržené struktury musely být dostatečně univerzální,
aby umožnily uživatelům přidávat do hry nové jednotky či měnit jejich
vlastnosti. Návrh struktur probíhal ruku v ruce s tvorbou XML souborů,
do nichž jsou data ukládána. Význam a uložení definic nejdůležitějších
struktur shrnuje následující tabulka:
TERRAIN |
plan/plan.h |
Terén herního políčka |
HEX |
plan/plan.h |
Herní políčko |
MAP |
plan/plan.h |
Dvourozměrné pole HEXů, popisující celý herní svět. |
TOWN |
plan/plan.h |
Seznam polí a udání vlastníka města na mapě MAP. |
KINGDOM |
plan/plan.h |
Udání vlastníka a umístění centra království na mapě MAP. |
UNIT |
units/unit.h |
Definice vlastností společných pro všechny jednotky stejného typu. |
LIVING_UNIT |
units/unit.h |
Popis konkrétní jednotky ve hře. |
BUILDING |
buildings/building.h |
Definice vlastností společných pro všechny budovy stejného typu. |
LIVING_BUILDING |
buildings/building.h |
Popis konkrétní budovy ve hře. |
PLAYER |
players/player.h |
Vlastnosti hráče a popis jeho připojení. |
Druhým úkolem bylo tato data udržet pohromadě v datovém úložišti, od kterého bylo požadováno několik vlastností:
- Snadný a oboustranný převod celé struktury do formátu XML pro načítání herních zdrojů a ukládání a načítání celých herních map.
- Možnost
serializace a deserializace dílčích objektů do formátu XML, v němž jsou
při synchronizaci v podobě zpráv převáděny po síti.
- Možnost přepisu do TCL a zpět pro využití ve skriptech.
Byla proto navržena jednoduchá hierarchie
abstraktních tříd a šablon, z jejichž kombinací pak vychází třídy
reprezentující skutečná herní data.
3.6.1 Základ hierarchie tříd a šablon
Základním hlediskem, dle kterého lze návrh tříd
rozdělit je charakter dat, která obsahují. Jedná se buďto o třídy
reprezentující jednotlivé herní objekty anebo o třídy tvořící jejich
datové kontejnery umožňující sekvenční i přímý přístup k položkám.
3.6.1.1 Šablona TStruct_Holder
Nejjednodušší z nyní probíraných struktur je šablona TStruct_Holder. Jejím posláním je vytvořit "objektovou obálku" nad datovou strukturou (struct) a starat se o její korektní inicializaci i dealokaci. Jako řešení byla zvolena šablona template<class T> class TStructHolder, kde třídou T mohou být třeba právě struktury jako BUILDING, LIVING_UNIT atd.
3.6.1.2 Abstraktní třída TXML_Struct
Schopnost dat zapisovat svůj obsah do formátu XML a načítat jej zpět představuje abstraktní třída
TXML_Struct, jejíž dvě metody
readFromXML() a
writeToXML() pak konkrétním způsobem implementuje každý potomek v závislosti na svých datových položkách. Využívá přitom přímo metod třídy
TPackage (viz
5.5 Přenášená data).
3.6.1.3 Abstraktní třída TTCL_Struct
Obdobnou funkci plní třída
TTCL_Struct
jejímž posláním je reprezentovat schopnost složitějších objektů zapsat
své položky do TCL a posléze načíst zpět. Zápis a čtení představují
metody
readFromTCL() a
writeToTCL().
3.6.1.4 Šablony TDA_Holder a TSA_Holder
Jako definice společného předka pro všechny datové kontejnery byly navrženy dvě šablony
TDA_Holder a
TSA_Holder.
První pro kontejnery, jejichž délka a obsah se v průběhu hry mění,
druhá pro datové seznamy, jejichž délka i rozsah klíčů je pevně daná.
Obě jsou rozšířením šablony
std::vector, ke které přidávají implementaci metod
TXML_Struct i
TTCL_Struct. Šablona
TDA_Holder navíc dokáže svá data převést z a do šablony
DA, která je nositelem dat při načítání a ukládání hry pomocí modulu
RM.
3.6.2 Statická data
Z hlediska obsahu lze herní data dělit na statická a
dynamická.
Statická data se během hry nemění, jejími představiteli jsou například
tabulky vlastností typů jednotek a budov, kódy a masky TCL skriptů
apod. Data jsou načítána při začátku hry jak na serveru, tak na všech
klientech. Jejich shodnost není kontrolována (hráč se tedy může pokusit
"podvádět" změnou vlastností jednotek ve svých souborech), nicméně díky
tomu, že se veškeré akce provádějí centralizovaně na serveru, nemohou
mít případné rozdíly na hru vliv.
Statická data jsou po svém načtení zapsána do
interpreteru TCL, kde zůstávají v platnosti a beze změn až do konce
hry. Zvolenou datovou strukturou v TCL jsou nehomogenní asociativní
pole (zanoření typu array) poskytující
dostatečně obecné možnosti pro zápis celých i desetinných čísel,
pravdivostních hodnot i textových řetězců současně.
V následujících několika odstavcích si popíšeme jednotlivé skupiny reprezentovaných dat.
3.6.2.1 Typy jednotek
Hra 8K zavádí několik typů jednotek. Jejich
počet není omezen, uživatelé mohou přidávat nové - vždy však musejí
definovat všechny vlastnosti jednotky a vytvořit její 3D model. Každá
živoucí jednotka vyskytující se ve hře je pak představitelem jednoho z
typů. Typ jednotek (ve zdrojových kódech mu odpovídá
struct UNIT a její "objektová obálka"
class TUnit)
definuje některé vlastnosti společné pro všechny jednotky stejného
typu. Jsou jimi koeficienty pro pohyb, koeficienty pro útok, schopnosti
stavby, koupě bonusů a výchozí počty životů, bodů pohybu, útočná a
obranná čísla apod. Některé z těchto vlastností mohou jednotky ve hře
zlepšovat koupí bonusů. Jelikož počty typů jednotek nejsou nikde pevně
ukotveny, rozhodli jsme se zavést ještě "kategorii jednotky", která
každému typu přiřazuje jednu z následujících hodnot: pěchota (
UT_INFANTRY), jízda (
UT_CAVALRY), stroj (
UT_VEHICLE), jednotlivec (
UT_INDIVIDUAL).
Díky tomuto rozdělení je možno každé jednotce definovat i bonusové
koeficienty pro boj s jednotkami dané kategorie, například zvýhodnění
jízdy proti pěchotě.
Jednotlivé vlastnosti jsou popsány v kapitole
6.4.3 Jednotky.
3.6.2.2 Typy budov
Obdobnou roli hrají typy budov, kterých bylo do
hry vestavěno 6. Šíře jejich vlastností je však výrazně nižší, neboť
samotné budovy nedisponují tolika možnostmi k činnosti, jako jednotky.
Jednotlivé vlastnosti jsou popsány v kapitole
6.4.8 Budovy.
3.6.2.3 Typy terénu
Datová struktura představující typ terénu v sobě
nese informaci o jeho vlivu na viditelnost a přesnost střelby. Ostatní
vlastnosti jsou vyjádřeny jednotlivými koeficienty v popisu jednotek.
3.6.2.4 Typy bonusů
Bonusy jsou balíčky zlepšující vlastnost
konkrétní jednotky, kterými je možné jednotky ve hře vybavovat. Bonus
jakožto datový typ sestává ze tří položek:
- určení vlastnosti, které se týká (útok, obrana, dosah vidění, dosah útoku, dosah pohybu, počet životů)
- číselné navýšení této vlastnosti (+1, +3, ...)
- cena, kterou koupě takového bonusu stojí
Každý bonus je označen jednoznačným
identifikátorem, například ID 1 = síla +1, ID 8 = dostřel + 2 atd. Ve
strukturách budov je potom uvedeno, které bonusy lze v té které budově
kupovat. Trochu odlišný seznam je součástí typů jednotek. Tam jsou
vymezeny pouze vlastnosti, které může jednotka zlepšovat. Je-li mezi
nimi například "síla", znamená to, že jednotka může kupovat všechny
bonusy zlepšující sílu.
Zakoupené bonusy se pak jako jakési nálepky přidávají k jednotkám (ukládání zajišťuje položka bonuses struktury LIVING_UNIT), přičemž pro každou zlepšovanou vlastnost se ukládá vždy dosavadní nejlepší zakoupený bonus. Jejich účinky se tedy nesčítají.
3.6.2.5 TCL skripty
Položky struktury
TCL_SCRIPT, totiž seznam a popis vstupů, výstupů a samotný TCL kód, byly již popsány v kapitole
3.5.3 Struktura TCL_SCRIPT. Zde je namístě dodat, že pro její snazší využití byla vytvořena nadřazená třída
TTCL_Script,
která implementuje na nejnižší úrovni import a export základních typů
proměnných z TCL do C a naopak. Její stěžejní metodou je pak
run(), která zajistí vykonání TCL kódu a odchycení případných chyb.
3.6.2.6 Třída TRules
Třída TRules
sdružuje všechny výše zmíněné objekty do jednoho "adresáře", který je
jako celek přístupný modulu AI, modulu WORLD a je na začátku celý
vepsán do prostředí interpretu TCL. Všechny prováděné skripty tak mají
přístup ke všem statickým datům v pravidlech. Jelikož se tato během hry
nemění, není pro přístup k nim třeba využívat zprávy. Namísto toho jsme
použili jedno veliké asociativní pole (v TCL typ array), kde například hodnota udávající koeficient prostupnosti lesa pro jednotku lučištnic (id 1) vypadá takto:
set coeficient $unit_types(1, movement_terrain, $TT_FOREST);
Za vysvětlení stojí poslední parametr adresy, $TT_FOREST. Ten je převedenou formou konstanty TT_FOREST, definovanou v jednom z .h
souborů. Podobně existují pro konstanty z C jejich kopie v TCL i v
mnoha dalších případech. Převod je prováděn jednou, při spuštění
programu a platnost proměnných trvá až do jeho ukončení. Problémem se
ukázala být neexistence globálních proměnných v TCL, tedy takových
proměnných, které by byly platné jak v globálním kontextu, tak uvnitř
volání funkcí. Tento nedostatek musel být řešen tak, že uvnitř funkcí,
které některou z globálních konstant používají, byla jejich platnost
jmenovitě zajištěna příkazem global.
3.6.3 Dynamická data
Druhou skupinou jsou data dynamická, která v
průbehu hry mění svůj obsah i počet. Jednotky ztrácejí životy, umírají,
na druhé straně jsou kupovány nové. Od objektů je požadována schopnost
působení v prostředí TCL, tedy implementace metod TTCL_Struct a současně možnost posílat data po síti, tedy implementace metod TXML_Struct. Navíc bylo třeba zajistit bezpečný přístup k datům za použití více vláken. Toho bylo dosaženo pomocí funkcí SDL_LockMutex() a SDL_UnlockMutex() volaných vždy před zahájením přístupu k datům a po jeho ukončení.
3.6.3.1 Jednotky
Jak jednotky živé, tak ty již ztracené, jsou reprezentovány strukturou LIVING_UNIT, resp. její objektovou nadstavbou TLivingUnit. Základní položkou je LIVING_UNIT::type, což je celočíselná hodnota určující, kterého typu (UNIT)
je jednotka "exemplářem". Další položky specifikují momentální stav
jednotky, její počet životů, počet bodů pohybu, zkušenosti, zranění,
seznam koupených bonusů atd.
3.6.3.2 Budovy
Zcela analogicky funguje dvojice LIVING_BUILDING a TLivingBuilding, která kromě určení typu (LIVING_BUILDING::type) udržuje informace o vlastníkovi, počtu "životů", fázi výstavby a orientaci.
3.6.3.3 Hráči
Postavu hráče ve hře představuje struktura PLAYER, resp. třída TPlayer, sdružující informace o připojení hráče do hry, jeho penězích i statistiky jeho dosavadní hry.
3.6.3.4 Počasí
Počasí je prezentováno strukturou WEATHER (odpovídající třída TWeather) definující stav počasí (jedna ze tří hodnot WS_SUNNY, WS_RAIN, WS_SNOW)
a dobu jeho dosavadního trvání. Z těchto dvou hodnot je při začátku
každého kola za použití náhody jednoduchým způsobem modelován vývoj
počasí.
3.6.3.5 Třída THistory
Význam třídy
THistory byl již zmíněn v kapitole
3.4.1 Synchronizace. Ve zkratce jde o zásobník, zaznamenávající vše, co se od nahrání hry v herním světě událo. Nejníže položenou třídou je
TAction,
představující záznam jedné konkrétní akce, například výsledek boje či
pohybu. Nositelem dat o takové události je vždy tělo patřičné zprávy,
která se k dané události váže. V případě boje by to bylo
TPacket_RET_UNIT_ATTACK. Jedná se tytéž typy, použité při komunikace s GUI a AI.
Výše v hierarchii stojí třída TActionContainer
rozšiřující záznam události o seznam hráčů, kteří již její provedení
zaregistrovali a zaznamenali do svých datových struktur. Zmíněná THistory je pak právě seznamem takovýchto záznamů. Odlehčenou variantou je třída TClientHistory,
která shromažďuje záznam historie hry na straně klienta. Jelikož nemusí
držet seznamy synchronizovaných hráčů, jedná se jen o zásobník objektů
třídy TAction.
3.7 Herní akce
Dosud bylo popsáno, jak jsou data
reprezentována, jak k nim hráči mohou přistupovat a jak probíhá
synchronizace. Zbývá se podrobněji podívat na průběh samotného
vyhodnocování herních akcí.
Základem, platným pro většinu akcí je následující postup.
- kontrola oprávnění k akci
- zápis dat do TCL
- vyhodnocení akce v TCL
- načtení výsledku
- zaznamenání výsledků do datových struktur TWorld
- odeslání dat do synchronizace
V první fázi se kontroluje, je-li hráč akci
zasílající na tahu, je-li vůbec spuštěna nějaká hra apod. Týká-li se
akce nějaké jednotky, je kontrolováno, že hráči skutečně patří. Podrobné
kontroly oprávnění závislé na konkrétním typu akce se provádí až ve
fázi tři, uvnitř TCL skriptu. Tam dojde na test stavu jednotky,
kontrolu jejích schopností apod. Pokud se během provádění skriptu
zjistí, že k akci není dostatek oprávnění (nebo třeba peněz), jsou
další kroky přeskočeny a hráč, který akci vyvolal, je na její neplatnost
upozorněn zprávou
MSG_RCT_ERROR. V opačném případě následuje provedení patřičných změn ve strukturách
TWorld uvnitř
TWorldServer, poté synchronizace a tytéž změny v
TWorld uvnitř jednotlivých modulů WORLD_CLIENT.
Některé herní akce jsou jednoduché - doplnění
pouze přidá chybějící životy, koupě jednotky vytvoří novou instanci
TLivingUnit apod. Podrobnějšímu pohledu na některé herní akce se věnují
následující odstavce. Stojí za to zdůraznit, že se v tuto chvíli jedná
o popis především konkrétních TCL skriptů, které jsou snadno
modifikovatelné a to i zcela zásadním způsobem.
3.7.1 Pohyb jednotek
První částí pohybu je zobrazení dostupných cílů.
To je realizováno obdobou záplavového algoritmu, která vychází z
aktuální pozice jednotky a přes sousední hexy postupuje do všech stran.
Přitom si v každém uzlu pamatuje, kolik minimálně bodů pohybu na cestu
do něj utratila. Vybere-li si hráč cíl pohybu, vyrazí do něj
jednotka po optimální cestě nalezené algoritmem
A*.
Zdánlivě tedy výpočet cesty do cílového hexu probíhá zbytečně dvakrát.
Ve skutečnosti je to nutnost při síťové hře, neboť zatímco zobrazení
cílů je akce volaná na klientovi, který teoreticky může mít jiné
zdrojové soubory definující vlastnosti jednotek, výpočet
A*
algoritmem je volán na serveru a je tak pro všechny účastníky hry
stejný. Není tedy možné "podsouvat" serveru příkazy k pohybu na
nedostupný hex.
Jádrem celého výpočtu je metoda TWorldEngine::getDistance(),
která na základě údajů o jednotce a dvou sousedních hexech určí cenu (v
bodech pohybu) za přechod od jednoho hexu k druhému. Počítá přitom s
trojicí koeficientů daných strukturou UNIT
- koeficientem pro pohyb v převýšení (do kopce a s kopce), pro pohyb v
terénu a pro pohyb v nadmořské výšce. Výchozí hodnotou je 1, která se
uvedenými koeficienty dále násobí. Na závěr vstupují do hry ještě
speciální pravidla pro vstup do budov - jednotka se po vstupu do budovy
nemůže již ve stejném kole dále hýbat. Vstup do budovy tedy jednotku
stojí celý dosud nepoužitý zbytek bodů pohybu.
3.7.2 Vidění
Model vidění sestává ze tří složek - vidění
jednotek, měst a budov. Viditelnost z měst a budov je tvořena pouze
jejich kruhovým okolím, jehož poloměr je dán typem budovy. Pro města je
dosah vidění vždy jeden hex. U jednotek je situace o něco složitější.
Tam se v úvahu kromě dosahu vidění té které jednotky bere i
"průhlednost" terénů na hexech, přes které jednotka daným směrem hledí,
stejně jako jejich nadmořská výška. Výpočet do šířky prohledává okolí
jednotky a pro každý z hexů sousedící s již úspěšně prověřeným hexem
volá TCL skript určující, je-li na daný hex z aktuální pozice jednotky
vidět. Jelikož souřadný systém, jímž jsou hexy číslovány není nikterak
ideální pro geometrické výpočty, je pro účely zjištění viditelnosti
situace promítnuta na "čtverečkovanou" síť. Na takové síti je nakreslena pomyslná spojnice jednotky a
hexu, na který hledí. Po této spojnici se pak výpočet posouvá a na
hexech, přes které přechází kontroluje jejich výškový rozdíl a terén. Z
definice některých terénů vyplývá, že hex za nimi "schovaný" již nebude
vidět (platí to například pro les či hvozd). Podobné pravidlo platí v
momentě, kdy v cestě stojí výškový rozdíl vyšší než dva stupně.
Převod hexové sítě na výpočetně vhodnější
čtvercovou síť a náznak postupu při kontrole viditelnosti popisuje
následující obrázek.

Diagram 3.5: Souřadná síť a výpočet viditelnosti hexu [0,3] z hexu [6,2]
Během pohybu jednotky se oblast jejího vidění posouvá s ní. Hexy, které dosud hráč prostřednictvím žádné ze svých jednotek, měst či budov dosud neviděl, zůstávají zakryty černotou. U hexů, které již v minulosti viděl, ale pohybem, nebo ztrátou jednotky o dohled na ně přišel se uplatňuje tzv. "fog of war", což znamená, že hráč sice vidí terén hexu, ale případné dění na něm mu zůstává skryto. O reprezentaci těchto možností se starají struktura HISTORY_HEX a výčet enum VISIBILITY.
3.7.3 Boj
Vstupem pro výpočet boje je určení útočící
jednotky a jejího cíle, kterým je jiná jednotka, opuštěná budova, nebo
jednotka v budově. V následující fázi se dají dohromady všechny
koeficienty, které výsledek boje ovlivní. Základem je útočné a obranné
číslo jednotky (UNIT::attack, UNIT::defense), případně obranný bonus
budovy (BUILDING::defense_bonus). Podle konkrétní situace se pak počítá
s bonusem jednotky proti typu nepřátelské jednotky, bonusem při boji s
budovami, bonusem pro boj v daném terénu atd. Nevyužity nezůstanou ani
zkušenosti jednotky, počet jejích mužů a její zakoupené bonusy. Po
"semletí" všech koeficientů dohromady získáme dvě čísla - celkovou sílu
útoku a celkovou sílu obrany. Obě protistrany si ještě "hodí" pomyslnou
kostkou, jejíž výsledek je upraven tak, že nabývá hodnot od 1 do 2 a
celkovou sílu násobí. Poměrem výsledných sil se určí ztráty obránce,
které jsou pomocí dalšího hodu kostky rozděleny mezi ztracené životy a
způsobená zranění. Byla-li napadena jednotka kryjící se v budově, je
podle typu útoku (střelba, zteč, katapult, ...) určen poměr, ve kterém
se ztráty rozdělí mezi jednotku a budovu.
V případě boje ztečí se obdobným způsobem vypočítá síla protiútoku a ztráty na straně útočníka.
Docela na závěr se z utržených ztrát vypočítají
zkušenosti, které si protivníci z duelu odnesou, případně je provedeno
jejich povýšení na vyšší úroveň.
3.7.4 Střídání tahů
Mnoho událostí ve hře se generuje při začátku
tahu každého hráče. Přitom je nutné zachovat jejich pevné pořadí, aby
celá souslednost dávala smysl. Od první do poslední probíhají akce na
začátku každého tahu takto:
- navýšení doby zranění u zraněných mužů, smrt těch, kteří se již dlouho neléčili
- určení výnosů z měst a království
- spočítání výdajů na žold armády (v potaz se neberou muži, kteří v kroku 1 zahynuli) případné propuštění těch, na které nezbylo
- přičtení "životů" stavěným či opravovaným budovám
- obnovení bodů pohybu všem jednotkám na maximální hodnotu
- vstup schválených diplomatických smluv v platnost
Ve chvíli, kdy se na tahu vystřídají všichni
hráči, dochází ke změně kola, kdy se navíc na základě dosavadní doby
trvání počasí a náhodně generované hodnoty vyhodnocuje případná změna
počasí.