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:
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.
Propojení základních částí modulu WORLD při lokální hře
Diagram 3.1: Propojení základních částí modulu WORLD při lokální hře
Propojení základních částí modulu WORLD při síťové 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.
Schéma synchronní komunikace
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.
Schéma asynchronní komunikace
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:
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:
  1. uložení proměnných z C do TCL,
  2. spuštění skriptu,
  3. 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:
Struktura Soubor Popis
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í.
Jednotlivé položky datových struktur jsou tudíž popsány a zdokumentovány v kapitole 6.4 Struktura XML dokumentů.
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í:
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:
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.
  1. kontrola oprávnění k akci
  2. zápis dat do TCL
  3. vyhodnocení akce v TCL
  4. načtení výsledku
  5. zaznamenání výsledků do datových struktur TWorld
  6. 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.
Souřadná síť a výpočet viditelnosti
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:
  1. navýšení doby zranění u zraněných mužů, smrt těch, kteří se již dlouho neléčili
  2. určení výnosů z měst a království
  3. 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
  4. přičtení "životů" stavěným či opravovaným budovám
  5. obnovení bodů pohybu všem jednotkám na maximální hodnotu
  6. 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í.