Technické záležitosti

Obsah

API pro organismy

Zajímavou kapitolou je návrh API pro tvorbu organismů. Organismus je tvořen knihovnou DLL, která musí povinně obsahovat funkce OrgGetRequieredVersion a OrgRun. The Life! po svém spuštění načte všechny DLL knihovny v adresáři Org, zjistí, zda exportují zmíněné funkce, a zavolá GetRequiredVersion. Ta musí v současné verzi vrátit hodnotu $00050000, což značí, že organismus je určen pro verzi API 5.0. Z toho je vidět, že API jsem v průběhu vývoje upravoval a to tak, že nebylo zpětně kompatibilní. Při každé takovéto úpravě jsem číslo verze zvedl a bude to tak pravděpodobně i v budoucích verzích. Organismy, které vrátí správné číslo verze API, jsou pak zobrazeny v The Life! v seznamu organismů.

Při každém kroku simulace se pro každý organismus volá procedura OrgRun příslušného DLL. Jako parametr se jí předává ukazatel na rozhraní (interface) IVLFOrganism. Voláním jeho metod pak realizuje organismus svůj "život".

Proč jsem zvolil zrovna rozhraní? Je to ve Windows asi nejstandardnější metoda, jak předat odkaz na soubor metod manipulující s objektem (v C++ by se tomu říkalo abstraktní třída). Podstatnou výhodou je také to, že rozhraní podporují prakticky všechna vývojová prostředí ve Windows (od Borlandu, Microsoftu i od jiných výrobců), takže to usnadňuje tvorbu organismů v jiných jazycích.

Nevýhoda rozhraní je, že se u něj automaticky počítají reference a když jejich počet dosáhne nuly, zavolá se destruktor objektu, který rozhraní implementuje. Nevím, koho v Microsoftu napadlo míchat dohromady abstraktní třídu a počítání referencí, ale docela by mě zajímaly důvody. Object Pascal počítání referencí zajišťuje automaticky při přiřazování, předávání jako parametr apod. To je sice hezké, ale za určitých podmínek (např.při přetypování a uložení objektu jako pointer) se mechanismus chová nekonzistentně a není lehké ohlídat, aby byl počet referencí správný. V jiných jazycích si vše musí programátor dělat ručně sám.

Každá DLL organismu je načtena jen jednou, není tedy tvořena separátní instance pro každý organismus (to by drasticky snížilo výkon aplikace). To má ten důsledek, že pokud v souborech tvořících knihovnu deklarujeme globální proměnné, jsou tyto přístupné z každého organismu. Protože by to příliš zjednodušovalo komunikaci mezi organismy, považuji globální proměnné v organismech za zakázané. Samozřejmě je na autorovi organismu, zda to bude dodržovat, ale zdá se mi to férové.

Zajímavou možností, jak obejít API, je přepsat paměť s daty o organismu. Když OrgRun dostane ukazatel na rozhraní organismu, dá se při znalostech zdrojového kódu The Life! a způsobu, jak Delphi zachází s pamětí, zjistit adresa, kde jsou zapsány důležité proměnné organismu, jako je hmotnost nebo energie. Vzhledem k tomu, že DLL a hlavní EXE soubor sdílí stejný paměťový prostor, není problém tyto hodnoty přepsat. Opět platí, že slušné organismy toto nedělají, ale je to zajímavý námět na hraní si s assemblerem.

Při troše snahy lze zneužít i uživatelské registry — stačí je přetypovat na pointery a rázem je můžete používat k uchovávání dynamicky alokovaných struktur, čímž si můžete libovolně zvětšit paměť organismu. Problémem je, že organismus nikdy předem neví, kdy zahyne, a nemá tak kdy tyto struktury odalokovat. Proto i toto chování organismů považuji za nesportovní.

Popis formátu ENV

ENV je formát binární (tzn. nikoliv textový). Dokáže přesně zaznamenat stav celé simulace. Je optimalizován především na rychlé načítání a ukládání, ale má v sobě i vestavěnou volitelnou možnost komprimace, díky niž se může objem dat v souboru značně zmenšit (běžně i pod 10%). Nevýhodou je značné zpomalení při ukládání a načítání. Komprimace se hodí třeba při posílání souborů přes internet apod.

Popsána je pouze varianta formátu používaná verzí M2. Variantu používanou verzí M1 si mohou zájemci odvodit za domácí úkol ze zdrojáků (M2 umí načítat soubory M1, ale neumí je ukládat).

Soubor se skládá z hlavičky a vlastních datových struktur. Obsah celého souboru se dá popsat pomocí následující tabulky:

Název
Typ
Délka [B]
Popis
ID
Char
8
identifikátor formátu, má vždy hodnotu "MSFF0C06"
Version
Integer
4
verze formátu, má vždy hodnotu $00050000
Flags
Integer
4
má-li hodnotu 1, je veškerý další obsah souboru komprimován pomocí knihovny zlib (bližší popis formátu komprese viz RFC 1250-1252)
má-li hodnotu 0, komprese není použita
Width Integer 4 šířka prostředí
Height Integer 4 výška prostředí
Food Byte Width * Height data o potravě v prostředí (uloženo po řádcích shora dolů, řádky samotné zleva doprava)
Chem Byte Width * Height data o chemikáliích v prostředí (uloženo po řádcích shora dolů, řádky samotné zleva doprava)
Time Integer 4 počet kroků simulace od jejího počátku
OrgCount Integer 4 počet orgranismů
(následující položky se opakují OrgCount-krát)
Species
Char
64
název organismu (ukončený znakem 0, zbývající znaky do 64. nesjou definovány)
Mass
Byte
1
hmotnost organismu
FoodIn
Byte
1
množství potravy v trávicí vakuole organismu
Energy
Byte
1
energie organismu
Padding
Byte
1
výplň, její obsah není definován
X
Integer
4
x-ová souřadnice organismu
Y
Integer
4
y-ová souřadnice organismu
Color
Integer
4
barva organismu (R = bity 0-7, G = bity 8-15, B = bity 16-23, zbývající bity 24-31 nejsou definovány)
Age
Integer
4
věk organismu (počet kroků simulace od vytvoření organismu do okamžiku uložení)
MaxAge Integer 4 maximální věk organismu
GenCount Integer 4 generace organismu
URCount Integer 4 počet alokovaných uživatelských registrů
UR Integer URCount * 4 uživatelské registry
(konec opakování OrgCount-krát)
SpeciesCount
Integer
4
počet druhů organismů v grafu
SpeciesNames Char SpeciesCount * 64 názvy druhů organismů v grafu (každý název je ukončený znakem 0, zbývající znaky do 64. nesjou definovány)
OrgAmounts Integer SpeciesCount * 1024 * 4 počty organismů v grafu (1024 položek s počty organismů seřazenými stejně jako SpeciesNames, 1. položka představuje poslední krok simulace, 2. předposlední, atd.)

Poznámka

Všechny typy dat a konstanty jsou v Object Pascalu (Delphi). Pro céčkaře a jim podobné: Integer je 32bitové celé číslo se znaménkem, Byte je 8bitové celé číslo bez znaménka, Char je 8bitové celé číslo bez znaménka představující jeden znak v ASCII. Konstanty začínající dolarem ("$") jsou v šestnáctkové soustavě.

Zpětná kompatibilita s verzí M1

ENV soubory

Formát ENV souborů byl v průběhu vývoje verze M2 podstatně upraven a není kompatibilní s verzí M1. Verze M2 nicméně umí načítat soubory z verze M1, ale neumí je ukládat.

Organismy

DLL organismů nejsou mezi verzemi M1 a M2 vůbec binárně kompatibilní — pokud zkusíte zkopírovat DLL soubor z jedné verze do adresáře Org verze druhé, ta ho nebude vůbec detekovat jako organismus.

Na úrovni zdrojových kódů by neměl být problém s převodem organismů z verze M1 — změny v API by neměly příliš narušit zpětnou kompatibilitu. Je potřeba zajistit, aby soubor s implementací organismu používal novou verzi unity OrgInterface a na začátek jeho kontrolní procedury je nutné připsat příkaz alokující uživatelské registry — např. AllocUR(5) pokud organismus používá registry 0-4. Pak by mělo vše fungovat.

Při převodu je také nutné si uvědomit, že se oproti verzi M1 změnil algoritmus distribuce potravy a také pohyb stojí víc energie (v závislosti na hmotnosti organismu).

Zdrojový kód a kompilace The Life!

Ve zdrojovém balíčku najdete vše, co potřebujete k vytvoření úplné distribuce The Life! — tj. všechny zdrojové a datové soubory. Nejsou zde žádné závislosti na čemkoliv mimo tento balíček. Všechny soubory mimo adresář zlib jsou moje práce (v adresáři zlib se nachází Pascalský port kompresní knihovny, který není mým dílem). Ve zdrojovém kódu jsem komentoval jen důležitá místa — triviality a kód generovaný Delphi jsem vesměs nechal nekomentované.

The Life! je napsán v Borland Delphi 6. Ke kompilaci by mělo stačit otevřít v tomto nástroji projekt Life.dpr a zvolit Project|Compile Life. V nižších ani vyšších verzích jsem kompilaci nezkoušel, a tak nevím, zda se vám povede. Ve vyšších verzích by ale vše nejspíš mělo fungovat.

Organismy by měly jít zkompilovat obdobným způsobem. Zkoušel jsem je kompilovat i s Delphi 3 a vše fungovalo.

Problémem je kompilace DLLInstall.exe. Tento soubor je určen k nahrání potřebných DLL do systému Windows 95, pokud je tam uživatel nemá (viz Známé problémy). Pokud ho zkompilujete v Delphi 6, bude ale sám tyto knihovny ke svému spuštění vyžadovat! Ze začarovaného kruhu se dá vymotat tím, že DLLInstall zkompilujete v nějaké nižší verzi Delphi (já používám Delphi 3 a funguje to).

Rychlost × paměť

Při implementaci jsem několikrát musel řešit klasické programátorské dilema, zda-li optimalizovat program na rychlost nebo paměťovou náročnost. Ve většině případů jsem jednoznačně volil rychlost.

Místo, kde je to asi nejvíc vidět, jsou metody ForeignAllocedURCount, ForeignUR, SetForeignUR, OrgAt a Attack třídy TOrg. V nich je potřeba najít organismus, který leží na daných souřadnicích. Při několika desítkách tisíc organismů je velmi neefektivní procházet celý seznam organismů a vybírat toho, který má správné souřadnice. Proto jsem přidal pomocné seznamy organismů na každém řádku prostředí. Tyto seznamy se stále udržují aktuální, podle toho, jak se organismy pohybují, rodí a hynou. Při hledání organismu na nějakém políčku pak stačí prohledat jen jeden z těchto pomocných seznamů. Výsledkem optimalizace sice není kvalitativní snížení složitosti vyhledávání (stále je lineární), ale prohledávaný seznam je podstatně kratší (v průměru tolikrát, kolik je řádků v prostředí — a těch jsou typicky stovky). Nevýhodou je několik desítek až stovek kB paměti zabraných navíc.

Ve verzi M2 je tato optimalizace dotažena ještě dál — seznamy organismů se udržují dokonce pro každé políčko.

Seznam organismů

Zajímavým problémem bylo, jak navrhnout samotný hlavní seznam organismů. Z analýzy vyplynulo, že potřebuju především tyto tři operace:

Delphi má na tyto účely vyhrazený vlastní třídu TList, což je generický seznam pointerů. TList je interně implementovaný jako dynamicky alokované pole s tím, že se přealokovává vždy "po větších kusech", tedy ne při přidání/odebrání každé jednotlivé položky. Přístup k položkám je O(konst), přidávání na konec seznamu je sice obecně O(n), ale vzhledem k tomu, že se přealokovává jen čas od času, většinou je konstantní. Největší nevýhodou je odebírání zevnitř seznamu, protože všechny položky za tou odebíranou se musí posunout — operace má tedy lineární složitost, ale praxe ukazuje, že je velmi pomalá.

Jako řešení jsem zkusil navrhnout vlastní třídu TDoubleLinkedList, která navenek odpovídá TListu (tj. implementované metody jsou shodné), ale je to dvojitě zřetězený lineární spojový seznam. Přidávání i je O(konst), ale je pomalejší než u TListu. Odebírání má stejně jako u TListu lineární složitost, ale je mnohonásobně rychlejší. Problémem je přístup k položkám, který je O(n). Zkusil jsem implementovat speciální kešovací mechanismus optimalizující vícenásobný přístup k jedné položce a sekvenční přístup, ale i tak je simulace s tímto seznamem v průměru dvakrát pomalejší, než s TListem. Zatím je tedy TDoubleLinkedList defaultně vypnut (jde zapnout kompilační volbou), ale možná se někdy ještě k tomu dostanu a třeba TList trumfnu :-)

Zajímavým problémem je také procházení seznamem, když se pro každý organismus spouštějí kontrolní procedury. To znamená, že libovolné organismy mohou přibýt nebo naopak zemřít a změnit tak právě procházený seznam. Abych se vyhnul problémům s pohyblivými mezemi a podobnými jevy, zavedl jsem několik pravidel:

Vliv těchto pravidel na rychlost simulace není skoro žádný (pouze testování na nil navíc), protože je úplně jedno, zda se položky odebírají hned, nebo až později. Vliv na zjednodušení programu je značný.