Programování pro hračičky/Andělé/Lekce 4

Jak používat klasifikační nálepkuTato stránka je součástí kurzu:
středoškolská
Příslušnost: skupinová

Opakování a probrání domácího úkolu editovat

Projděme si místnosti, v nichž jsme vytvořili virtuální detaily nebo klonované objekty (zejména máme-li něco, co bychom rádi ukázali ostatním). Daří se všechno, jak má, nebo jsme narazili na obtíže?

Prohlédněme si herní předměty, které tvoříme. Narazili jsme na obtíže? A jaké znalosti a dovednosti nám v tuto chvíli ponejvíce chybějí, abychom mohli v práci pokračovat?

Globální a lokální proměnné editovat

Při klonování objektu do místnosti jsme potřebovali proměnnou, do níž jsme ukládali klonovaný objekt. Někam před funkci reset(), kde jsme používali proměnnou batoh, jsme napsali do programového souboru místnosti řádku:

object batoh;

Tím jsme překladači LPC sdělili, že od této chvíle budeme používat identifikátoru batoh[1] jako označení pro ten či onen objekt, tedy programátorsky řečeno jako proměnné typu object. Technicky vzato se jedná pouze o ukazatel na objekt, tedy o paměťovou adresu směřující tam, kde je uložen objekt, který právě jako batoh označujeme, nebo o nulovou hodnotu, značící, že identifikátoru batoh zrovna žádný objekt přiřazen není.

Takto deklarovanou proměnnou batoh můžeme následně užívat kdekoli v dotyčném programovém souboru (s výjimkou klauzur, ale o těch budeme hovořit až mnohem později), tedy v jakékoli funkci, kterou v souboru budeme programovat. V našem případě jsme takto použili proměnnou batoh ve funkci reset(), ale právě tak bychom ji mohli použít třeba ve funkci create() nebo jakékoli jiné.

Kdybychom deklaraci proměnné batoh nenapsali takto mimo všechny funkce, ale třeba do funkce reset():

void reset()
{
  object batoh;
  ...
}

znamenala by stále proměnnou jménem batoh a typu object. Deklarace by však byla platná jen v rámci funkce reset(), tedy jen do onoho závěrečného }. Pokud by se pak označení batoh vyskytlo vně funkce reset(), vyvolalo by chybu.

V prvním případě (proměnná deklarovaná v hlavním textu programu, mimo funkci) můžeme hovořit o proměnné globální, tedy platné v celém textu programu, ve druhém případě (proměnná deklarovaná uvnitř funkce) o proměnné lokální. LPC dovoluje deklarovat proměnnou ještě lokálněji — na začátku jakéhokoli složeného příkazu, tedy po uvozujícím {. Deklarace proměnné pak platí do odpovídajícího }:

void reset()
{
  if (!present("batoh"))
    {
      object batoh;

      batoh=clone_object("/obj/batoh");
      ...
    }
  ...
}

Nově deklarovaná proměnná má zprvu hodnotu 0. V případě typu object to znamená, že ještě neukazuje na žádný objekt. Má-li mít proměnná hodnotu, musíme jí hodnotu přiřadit, což ve výše uvedeném příkladu dělá řádka:

      batoh=clone_object("/obj/batoh");

LPC umožňuje přiřadit proměnné úvodní hodnotu už při deklaraci. V našem příkladu by to znamenalo, že můžeme hned napsat:

object batoh=clone_object("/obj/batoh");

Takovéto přiřazení se provede na začátku příslušné funkce nebo příslušného složeného příkazu, respektive v případě globálních proměnných (tedy je-li použito vně funkce) na samém začátku funkce create() (dříve, než se začne provádět cokoliv v této funkci napsaného).

Datové typy editovat

Stejně, jako může globální nebo lokální proměnná odkazovat na nějaký objekt, může obsahovat též číslo, řetězec či složitější datové struktury. Musíme ji potom ovšem deklarovat jako proměnnou příslušného typu:

int kolik;   // tahle proměnná bude obsahovat celé číslo
float koef;  // tahle proměnná bude obsahovat číslo s desetinnou tečkou
string str;  // tahle proměnná bude obsahovat textový řetězec
mapping m;   // tahle proměnná bude obsahovat asociativní pole

Každé takto deklarované proměnné pak můžeme v běhu programu přiřadit obsah příslušného typu, v nejjednodušším případě konstantu, tedy přímo a neměnně zapsanou hodnotu:

kolik=13;
koef=2.567;
str="Šlapati na trávník přísně jest zapovězeno.";
m=(["name":"blbost","gender":ROD_F,"vzor":VZOR_KOST]);

Z těchto hodnot vidíme, že jsme se vlastně s různými typy již setkali v příkladech předminulé a minulé lekce (a ve svých pokusech s místnostmi a dalšími objekty). Když jsme nastavovali v místnosti světlo voláním funkce set_own_light(), předali jsme jí konstantu typu int. Funkci set_smell() jsme předali konstantu typu string, funkci add_v_item() zase konstantu typu mapping:

set_own_light(1);
set_smell("Trochu to tady smrdí.");
add_v_item(([        // každé položce pro přehlednost dáme vlastní řádku
  "name":"okno",     // (na mezery a řádky se v LPC nehledí, struktura je
  "gender":ROD_N,    // dána správnými oddělovači, třeba těmi středníky a
  "vzor":VZOR_BIDLO, // čárkami, co vidíme vlevo)
  ...
  ]));

Kdybychom se snažili vnutit funkci set_own_light() místo čísla třeba řetězec "světlo", dopadlo by to stejně, jako kdybychom se takový řetězec snažili přiřadit výše deklarované celočíselné proměnné kolik — v programu by došlo k chybě.[2]

Stejně jako mají hodnotu určitého typu konstanty 1 nebo "světlo", má v LPC hodnotu určitého typu každý výraz, ale také každá volaná funkce. Se součtem nebo násobkem celých čísel můžeme zacházet opět jako s celým číslem, se spojením dvou řetězců opět jako s řetězcem atd.:

int kolik;
string str;

kolik=3*kolik+15;
str="Dobrý "+"den!";

Právě tak může na místě konstanty nebo proměnné určitého typu stát volání funkce, která má podle deklarace týž datový typ. Funkci set_smell() jsme předali řetězec, který popisuje zápach objektu ve hře. Opačnou funkcí je query_smell(), která nám nastavený řetězec sdělí, tedy má hodnotu typu řetězec:

string str1,str2;

str1="Čicháš, čicháš... ";
str2=str1+query_smell();

Datový typ parametrů, které funkci předáváme, i datový typ samotné funkce se deklaruje obdobně jako datový typ proměnné. Deklaraci funkce je možno zapsat samostatně (a v některých případech je to i nutné), častěji se však setkáme s tím, že hned po deklaraci následuje tělo funkce, tedy onen kousek programu, který má daná funkce provádět. Tak například zmíněná funkce query_smell() vypadá ve zdrojovém souboru (/i/item/smell.c) takto:

string query_smell() 
{ 
  return smell; 
}

Proměnná smell je typu string a je deklarována na začátku souboru, aby s ní mohly mohly pracovat obě funkce sloužící k nastavování a zjišťování řetězce, který se má hráči projevovat jako zápach objektu. Druhá z těchto funkcí, set_smell(), vypadá ve zdrojovém souboru takto:

void set_smell(string str) 
{
  smell = strlen(str) && str[<1] != '\n' ? wrap(str) : str;
}

Voláme-li funkci set_smell(), musíme jí předat jeden parametr typu string, což je vyjádřeno onou deklarací string str mezi závorkami. Předaný parametr pak bude v těle funkce vystupovat jako lokální proměnná str (samozřejmě by bylo možno zvolit i libovolné jiné jméno).

Slůvko void na místě datového typu funkce znamená, že funkce nemá hodnotu žádnou (což můžeme považovat za jakýsi nulový datový typ).

Každá funkce musí mít ve své deklaraci takto udán právě jeden datový typ. Parametry oproti tomu nemusí mít žádné, může mít jeden a může jich mít více. Ty se pak — jak v deklaraci, tak ve volání funkce — oddělují čárkami. Tak jsme například už při vytváření prvních místností používali (byť zatím jen okopírováno podle vzoru) volání funkce add_type(), když jsme nastavovali, že se jedná o místnost s umělým osvětlením:

void create()
{
  ...
  add_type("osvětlení",1);
  ...
}

Když se podíváme do souboru /i/room.c na její deklaraci, uvidíme v ní tytéž dva parametry oddělené čárkou:[3]

void add_type(mixed key, mixed data)
{
  ...
}

Přehled datových typů editovat

Přehled všech možných datových typů, s nimiž se můžeme v LPC setkat, najdeme ve článku o LPC na Wikipedii. Zde si probereme typy používané v dialektu LPC, který se používá v mudu Prahy, tedy LPMud. Podrobně se podíváme na ty, které budeme potřebovat nejdříve, a letmo si zmíníme ty ostatní.

Objekt (object)
Na objekty odkazuje typ object. Jak jsme si vysvětlili výše, není v proměnné tohoto typu uložen objekt samotný, nýbrž jeho paměťová adresa. Tuto paměťovou adresu nemůžeme zapsat přímou konstantou, můžeme ji však vždy znovu zjistit funkcí touch(), které jako parametr předáme interní jméno objektu. To je v případě jedinečných objektů rovno směrníku souboru, v případě klonovaných objektů směrníku souboru rozšířenému o znak # a jednoznačný číselný identifikátor:
   object ob1,ob2;

   ob1=touch("/w/salatdo/touch/bublator");
   ob2=touch("/obj/batoh#35646");
Funkce clone_object(), kterou jsme již také poznali, oproti tomu umístí do paměti novou kopii prototypu objektu a jako svou hodnotu vrátí adresu této nové kopie. Na to je dobré myslet, abychom případně neztratili programátorský přístup k objektům, s nimiž chceme ve hře pracovat.
Celé číslo (int)
Celočíselnou hodnotu vyjadřuje typ int (z anglického „integer“). Jedná se o 32-bitové celé číslo se znaménkem, tedy celé číslo od -2147483648 do 2147483647. Konstanta tohoto typu se zapisuje jako posloupnost číslic bez mezer, před niž je případně předsazeno znaménko minus:
   int cislo1,cislo2;

   cislo1 = 789;
   cislo2 = -45;
Desetinné číslo (float)
Desetinné číslo se vyjadřuje typem float (podle plovoucí, tedy proměnlivý počet desetinných míst oddělující řádové čárky). Je to 32-bitové číslo, které v 10 bitech zaznamenává polohu řádové čárky a ve zbylých příslušně zaokrouhlenou mantisu. Konstanta se zapisuje jako celočíselná a desetinná část oddělené tečkou s případným předsazeným znaménkem minus, případně ještě doplněná o zápis exponentu, tedy malého či velkého písmena E, za nímž následuje případné znaménko plus nebo minus a nepřerušenou posloupností číslic zapsaný exponent:
   float x,y;

   x = 343.9901;
   y = -1.467e76;
Řetězec (string)
Textové hodnoty neboli řetězce se ukládají do typu string. Řetězec může mít prakticky libovolnou délku. Řetězcová konstanta se zapisuje jako text uzavřený do programátorských uvozovek (případné programátorské uvozovky se do textu zapisují jako dvojznak \").[4] Chceme-li pro přehlednost zapsat delší text na více řádek, pak prostě text rozdělíme dvěma uvozovkami a mezi nimi odřádkujeme:
   string text, textik, textec;

   textik = "Nějaký pěkný textík.";
   text = "Text ukončený odřádkováním.\n";
   textec = "Text pojednávající o se\120u, v němž se neobjeví "
            "dohledatelné slovo \"se\120\".";
Asociativní pole (mapping)
Typ mapping slouží k ukládání asociativních polí, tedy datových struktur, v nichž je vždy určitému klíči přiřazena určitá hodnota, ke které je pak možno přes tento klíč přistupovat. Klíčem může být číslo, řetězec nebo objekt, hodnotou libovolný typ. Tento typ již známe z vytváření virtuálních detailů pomocí funkce add_v_item(), takže již víme, že konstanta tohoto typu se uzavírá do dvojznakových závorek ([ a ]), klíče jsou od přiřazených hodnot odděleny dvojtečkou a jednotlivé páry klíčů a hodnot jsou odděleny čárkami (přičemž za posledním párem může rovněž následovat čárka):
   mapping okno = ([
     "name" : "okno",
     "gender" : ROD_N,
     "vzor" : VZOR_BIDLO,
     "long" : "Úplně obyčejné okno.",
   ]);
   ...
   add_v_item(okno);
Možné je též tzv. vícenásobné asociativní pole, kde za každým klíčem následuje více hodnot, oddělených vzájemně středníky (ovšem počet hodnot musí být u všech klíčů stejný).
Na hodnotu přiřazenou určitému klíči se zeptáme konstrukcí pole[klíč], tedy např.:
    string co,popis;

    co=okno["name"];
    popis=okno["long"];
Pole (*)
Operátorem * se značí typ pole, tedy seznam hodnot nějakého jiného typu, který při deklaraci musíme uvést před tento operátor. Tento operátor vlastně označuje ukazatel na paměťové umístění, tedy podobně jako u typu object v samotné proměnné není uložena její hodnota, ale její paměťová adresa. S konstantou tohoto typu jsme se již setkali, když jsme nastavovali vedlejší identifikátory objektů, takže si můžeme vybavit, že se uzavírá do dvojznakových závorek ({ a }) a že jednotlivé prvky pole jsou odděleny čárkami (přičemž za posledním může rovněž následovat čárka):
   int * mesdny = ({ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 });
   string * mesice = ({ "leden", "únor", "březen", "duben", "květen", "červen",
                        "červenec", "srpen", "září", "říjen", "listopad", "prosinec" });
Na jednotlivý prvek pole můžeme odkazovat konstrukcí pole[index], kde index je pořadí prvku v poli, přičemž první prvek má pořadové číslo 0:
   string info;
   int i;

   i=random(12);  // náhodné číslo od 0 do 11
   info=mesic[i]+" má "+to_string(mesdny[i])+" dní";
Klauzury, symboly a chráněná pole
LPC umožňuje zacházet s uzavřenými úseky programu (klauzurami) jako s datovým typem, tedy ukládat je do proměnných, předávat je funkcím jako parametry atd. V praxi to znamená, že můžeme například definovat vzhled objektu místo prostého řetězce funkcí, která pak podle aktuální situace vzhledový řetězec teprve vytvoří. Podrobněji se tomuto mechanismu budeme věnovat později, zatím nám stačí vědět, že proměnné a funkce deklarované jako closure, které tu a tam potkáme ve zdrojových textech, jsou tohoto typu. Symboly a chráněná pole souvisí s klauzurami a ve zdrojových textech se s nimi setkáme tak zřídka, že o nich zatím nemusíme vědět nic, než že také existují.

Neurčitý typ „mixed“ a modifikátor „varargs“ editovat

Výše jsme se setkali s příkladem funkce add_type(), jejíž dva parametry byly deklarovány jako mixed. Touto funkcí je totiž možno nastavit celou řadu různých vlastností místnosti, které není možno pokrýt jedním datovým typem, a tak vyhrazeným slovem mixed deklarujeme, že na tomto místě se nemá hledět na typ předávané hodnoty, nýbrž že hodnota se má bez typové kontroly předat dál. Uvnitř funkce se samozřejmě musí typ předané hodnoty nějak rozlišit (protože jinak by samozřejmě mohlo dojít k chybě tím, že bychom například s celým číslem zacházeli jako s řetězcem nebo jako s polem), ale na to se podíváme až ve chvíli, kdy to budeme prakticky potřebovat.

Jako mixed můžeme deklarovat nejen parametr funkce, nýbrž i funkci samotnou (pak tím dáváme najevo, že funkce může vracet hodnoty různých typů), a ovšem proměnnou:

mixed x;

x=1345;
...
x=6.783;
...
x="Hola hej!";
...
x=({8,4,2,13});

Velmi důležitá je možnost použití neurčitého typu mixed v deklaraci pole. Tu využijeme, potřebujeme-li deklarovat buď pole, jehož prvky jsou různých typů, nebo pole polí, tedy pole obsahující další pole (byť by byla všechna typově shodná):

mixed * pole;

pole = ({ 1, "měsíc", 2, "oči", 3, "oheň" });
...
pole = ({ ({ "one", "jedna" }), ({ "two", "dva" }), ({ "three", "tři" }) });

Čím je mixed pro různé typy, tím je varargs (z anglického „variable arguments“) pro různý počet parametrů. Připojíme-li tento modifikátor před slovo deklarující typ funkce, stanovíme tím, že funkcí nemusí být předán plný počet parametrů; pokud jich funkci předáme méně, zbylé se automaticky doplní nulovými hodnotami. Typické použití vidíme třeba na funci move(), jejíž volání s neúplným počtem parametrů značně zjednodušuje zápis:[5]

varargs int move(mixed kam, int jak, string odchod, string prichod)
{
  ...
}

...

batoh->move(this_object());
...
hrac->move(para_room,MOVE_MAGIC);
...
zaba->move("sever",MOVE_NORMAL,"Žába odhopkává na sever.","Z jihu sem hopká žába.");

Soukromí a veřejnost editovat

První kroky při vytváření vlastního objektu nám připomněly princip dědičnosti: Jakmile je naprogramována nějaká třída objektů s určitými metodami, můžeme těchto již hotových metod využívat při programování dalších objektů, pokud do nich onu již hotovou třídu vdědíme. V programových knihovnách Prahů najdeme například třídu /i/weapon/strelna, definující všechny metody potřebné pro to, aby se objekt dal ve hře používat jako střelná zbraň. Chceme-li nyní naprogramovat nějakou specifickou střelnou zbraň, která bude potřebovat nějaké metody navíc (například kuš, kterou je před výstřelem nutno natáhnout, ale která potom dostřelí i do vedlejší místnosti), zařadíme někam na začátek souboru řádku inherit "/i/weapon/strelna"; — a objekt, který programujeme, bude mít rázem k dispozici všechny možnosti střelné zbraně, takže se budeme moci soustředit jen na své lahůdky, zamýšlené nad tento běžný rámec.

Podívejme se nyní na tento děj přesněji.

Když se podíváme do nitra souboru /i/weapon/strelna.c, zjistíme hned na začátku, že třída střelných zbraní dědí obecnou třídu zbraní, zvanou /i/weapon/weapon_logic, a ta zase, jak můžeme zjistit následně, dědí nám již známé základní třídy /i/item, /i/move a /i/value, a navíc /i/item/life, která se stará o životnost objektu, tedy jeho postupným opotřebováním až ke zničení.[6] Naše nová střelná zbraň tedy dědí velmi rozsáhlý soubor vlastností a metod, v němž si musíme udělat pořádek.

Soukromé a veřejné proměnné editovat

Pod řádkem definujícím dědičnost následují nejprve definice konstant, které už předběžně známe a jimiž se budeme podrobně zabývat později. Pod nimi pak najdeme deklarace proměnných, s nimiž střelná zbraň zachází. Za příklad si vezměme třeba proměnnou reload_message:

private string reload_message = "Počkej, musíš přece nabít.\n";

V této proměnné je uložena hláška, kterou má dostat hráč, jenž se snaží střílet, aniž by nabil. Proměnná je deklarována jako řetězec a hned je jí nastavena implicitní hodnota. Na začátku deklarace však vidíme ještě vyhrazené slovo private. Tím sdělujeme, že deklarace proměnné reload_message platí jen v rámci tohoto jednoho programového souboru, tedy že například v naší zbrani, která tento soubor dědí, už nemůžeme tuto proměnnou přímo používat. Vně tohoto souboru je s ní možno zacházet jen prostřednictvím následně definovaných metod:

void set_reload_message(string str) { reload_message = str; }
string query_reload_message() { return  reload_message; }

Chceme-li tedy například v programu své nové střelné zbraně přenastavit tuto hlášku, nemůžeme napsat přímo třeba reload_message="A co takhle třeba nabít?\n", nýbrž musíme zavolat příslušnou metodu:

set_reload_message("A co takhle třeba nabít?\n");

Tato zdánlivě nesmyslná komplikace — definice dvou nových funkcí a jejich volání místo obyčejného přiřazovacího výrazu — je smysluplná jak při dědění souboru, tak při zacházení zvenčí. Ať už programátor, který dědí do svého objektu třídu střelných zbraní, má nápady jakékoliv, nepotřebuje přímo s proměnnou reload_message dělat nic jiného, než se moci zeptat na její hodnotu a případně tuto hodnotu přenastavit; jasně definované metody pro tyto dvě akce jsou též ochrannou pojistkou, aby se s touto proměnnou skutečně nic jiného nedělo. Na druhou stranu může chtít tuto proměnnou zjistit nebo přenastavit programátor jiného objektu, například místnosti, do níž se má naklonovat nějaká ne zcela obvyklá střelná zbraň; i ten pak může na proměnnou reload_message „sáhnout“:

zbran->set_reload_message("A co takhle třeba nabít?\n");

Jinak je tomu s proměnnou no_arrow_message, která je deklarována o něco níže. Do této proměnné se ukládá hláška, již dostane hráč, který nemá po ruce žádné další střely. Neukládá se však jako řetězec, nýbrž jako klauzura, tedy kus programového kódu, který dynamicky hlášku vytvoří podle toho, za jaké situace je zavolán:

closure no_arrow_message;

Před určením typu není v deklaraci napsáno private, tedy proměnná není soukromá. Je to (až na některé speciální případy, které nás zatím nezajímají) stejné, jako bychom před deklaraci napsali public, a znamená to, že proměnná je veřejná, tedy přímo přístupná i z objektů, které tuto třídu dědí.

Díky typové volbě closure nemusí být hláška o nedostatku střel statická, tj. vždy stejná, ale je možno například zjistit, zda hráč má toulec, a ohlásit nejen, že mu došly šípy, nýbrž též, že už nemá žádné šípy v toulci, nebo třeba i jen přizpůsobit podobu hlášky gramatickému rodu hráče, který zbraň právě drží v ruce. Klauzura je však pro člověka nečitelná (kdybychom si nechali ukázat hodnotu proměnné no_arrow_message, spatřili bychom něco jako <free lambda 0x0x113901a4>, tedy paměťovou adresu onoho kusu programového kódu a informaci, zda a jak jsou jí přiřazeny nějaké parametry). Nemá přílišný smysl dávat nečitelnou hodnotu proměnné no_arrow_message k dispozici nějakou zjištovací funkcí query_no_arrow_message(), která by se dala volat i z jiných objektů.[7] Protože však programátor této třídy chtěl umožnit svým programátorským kolegům, kteří budou tuto třídu využívat při vytváření vlastních objektů, aby s touto klauzurou v případě potřeby pracovali, ponechal ji deklarovánu jako veřejnou.

Soukromé a veřejné funkce editovat

Stejně, jako může být modifikátorem private prohlášena za soukromou proměnná, může jím být prohlášena za soukromou i funkce. Kdybychom hledali v téže třídě /i/weapon/strelna, nalezli bychom takto deklarovánu funkci zjišťující, zda je v toulci (či obecněji jakémkoliv zásobníku střel) ještě nějaká střela potřebného druhu, a v případě, že je, pak vrátí její objekt:[8]

private object inhalt(object koecher)
{
  return present(pfeil_id, koecher);
}

Takováto deklarace znamená, že funkci inhalt() můžeme volat jen přímým voláním v rámci tohoto programového souboru. Kdybychom ji použili v objektu, který tuto třídu dědí, došlo by k chybě stejně, jako bychom se pokusili volat neexistující funkci, a kdybychom se ji pokusili zavolat z jiného objektu, stejně jako v případě neexistující funkce by se nestalo nic.

Deklarovat funkci jako soukromou má smysl tehdy, když slouží jako pomocná funkce jiným funkcím v rámci téhož programu, ale buď nepředpokládáme, že by mělo smysl ji volat odjinud, nebo z nějakého důvodu chceme takovému volání zamezit.

Implicitní soukromí editovat

Řekli jsme si, že proměnná či funkce, která je deklarována bez modifikátoru (tedy například bez onoho private), se považuje za veřejnou. To je implicitní chování LPC, které ovšem můžeme změnit. Napíšeme-li do programu:

default private;

pak od tohoto místa všechny proměnné a funkce, které nemají výslovně uveden jiný modifikátor (tedy třeba public), jsou považovány za soukromé.

Můžeme rovněž určit, že chceme za soukromé implicitně považovat jen proměnné, zatímco funkce mají být implicitně veřejné:

default private variables public functions;

Právě tak ovšem můžeme příslušné chování zapnout jen pro část programového kódu, a potom zase vypnout:

// deklarace soukromých proměnných:
default private;
int zapnuto;
string napis;
object majitel;
default public;

int query_zapnuto() // tato funkce je už zase veřejná
{
  return zapnuto;
}

Další modifikátory editovat

Při prohlížení zdrojových souborů můžeme krom modifikátorů private a public narazit ještě na několik dalších: u proměnných na nosave a static, u funkcí na protected, static a nomask. Zatím stačí, když budeme vědět, že se jedná o něco jako podrobnější odstupňování mezi úplným soukromím a úplnou veřejností. Podrobně se k nim vrátíme později.

Tu a tam narazíme rovněž na modifikátory u příkazu inherit. Rovněž v tomto případě se spokojíme s vědomím, že se jedná o jakousi úpravu veřejnosti nebo soukromosti zděděných proměnných a funkcí, kterou si rozebereme až v některé z dalších lekcí.

Druhy funkcí a jejich volání editovat

Funkce jsme dosud používali spíše intuitivně. Okopírovali jsme si konstrukci ukázanou v příkladu, změnili jsme parametry, a ono to nějak fungovalo. Nyní již víme, že funkce mohou být soukromé nebo veřejné, a pravděpodobně nám již došlo, že při programování vlastních místností a objektů jsme se zatím setkali jen s těmi veřejnými (zatímco ty soukromé jsou ukryty někde v útrobách zděděných programových modulů). Ovšem i mezi nimi musíme rozlišovat několik základních druhů.

Funkce externí a lokální editovat

Mezi funkcemi, které jsme použili v dosavadních příkladech, se již objevila funkce present(), kterou jsme zjišťovali, zda je v jednom objektu (například místnosti) přítomen jiný (například naklonovaný batoh), nebo funkce clone_object(), kterou jsme vytvářeli klon objektu:

void reset()
{
  if (!batoh || !present(batoh))
    {
      batoh=clone_object("/obj/batoh");
      batoh->move(this_object());
    }
}

Funkci present() nebo clone_object() bychom mohli použít kdekoliv v textu programu bez ohledu na to, zda jsme zdědili nějaké další programové moduly, protože patří k externím funkcím (nebo, jsme-li ochotni používat hantýrku mudových programátorů, k efunům).

Některé z těchto externích funkcí jsou definovány přímo v ovladači, tedy patří nedílně do jazyka LPC, resp. jeho varianty, v níž pracujeme. Funkce present() a clone_object() jsou dvě z nich. Takovýmto funkcím můžeme pro odlišení říkat vlastní externí funkce. Jiné externí funkce jsou definovány v objektu /secure/simul_efun/simul_efun. Těm říkáme simulované externí funkce, protože nejsou implicitní součástí programovacího jazyka, ale ovladač s nimi zachází, jako by jeho součástí byly. K nim patří například funkce normstr(), se kterou jsme se už také setkali. Pro naši programátorskou praxi zatím nebude toto rozlišení na vlastní a simulované externí funkce důležité, stačí nám vědět, že mohou pocházet z těchto dvou zdrojů, ale obojí jsou přístupné odkudkoliv.[9]

Všechny ostatní funkce, s nimiž se můžeme v LPC setkat, jsou lokální funkce (neboli lfuny), tedy funkce definované v jednom konkrétním zdrojovém souboru. Objekt, v němž taková funkce má být používána, pak musí tento zdroj buď přímo obsahovat, nebo dědit. A naopak, má-li někde být použita takováto funkce, potřebujeme vždy nějaký objekt, v němž je takto definována. Stejně pojmenovaná funkce přitom může být definována ve více různých zdrojových souborech a mít v nich různý význam.[10] Teprve dvojice tvořená objektem a funkcí určuje, jaká funkce se má vlastně zavolat.

Funkce místní a vzdálené editovat

Nejprostší volání lokální funkce už známe například z nastavování jednotlivých vlastností místnosti. Soubor vytvářené místnosti obsahuje někde na začátku informaci o dědění třídy /i/room, která zase dědí například třídu /i/item/smell, v níž je definována funkce set_smell(). Díky tomu pak máme v celém dalším textu programu k dispozici funkci set_smell(), a můžeme ji volat stejně jako funkce externí:

set_smell("Ve vzduchu cítíš lehký závan chlévské mrvy.");

Jiná situace je s funkcemi, které jsou definovány v jiném objektu. Když například v místnosti klonujeme jiný objekt, kterému chceme přiřadit nějaký zápach, nemůžeme prostě vložit do programu místnosti samotné set_smell(...), protože to by nestavilo zápach místnosti, nýbrž musíme zavolat funkci definovanou v dotyčném objektu. Takové volání nejčastěji zapisujeme pomocí operátoru ->:

ob=clone_object("/i/potrava");
...
ob->set_smell("Vařená brokolice smrdí jako... vařená brokolice.");

Operátor -> je vlastně jen jiným způsobem zápisu volání externí funkce call_other(), tedy poslední řádku minulého příkladu bychom mohli právě tak zapsat jako:

call_other(ob,"set_smell","Vařená brokolice smrdí jako... vařená brokolice.");

Při volání lokální funkce v jiném objektu tedy vlastně voláme ovladač, který vyhledá dotyčný jiný objekt a pošle mu zprávu, že se po něm chce, aby vykonal danou funkci s danými parametry. Pokud taková funkce v onom objektu neexistuje, nestane se nic. Pokud je špatně zadán objekt (tedy například je na místě objektu 0, protože objekt, na který ukazovala objektová proměnná, mezitím zanikl), ohlásí ovladač chybu. Pokud jsou špatně zadány parametry funkce, pak k chybě dojde nebo nedojde podle toho, jak je naprogramována daná funkce v onom objektu.

Mezi externími funkcemi najdeme též funkci this_object(), která vrací ukazatel na objekt, jehož program se právě vykonává. Tak můžeme například při nastavování místnosti místo přímého volání funkce set_smell() zavolat vzdálenou funkci v objektu this_object(), a dosáhneme stejného výsledku:

this_object()->set_smell("Ve vzduchu cítíš lehký závan chlévské mrvy.");

Přímé volání a volání tímto způsobem však nejsou zcela záměnné. Kupříkladu soukromou funkci můžeme volat přímo, zatímco volání pomocí call_other() funkci nenajde, protože objekt obdrží zprávu od ovladače, že má zavolat dotyčnou funkci, ale protože se bude jednat o zprávu zvenčí, odpoví, že takovou funkci nezná.

Souběh a překrývání funkcí editovat

Protože v různých třídách může být pod týmž jménem definována různá funkce, potřebujeme mechanismus, jak z objektu volat funkci definovanou v konkrétní zděděné třídě. Slouží k tomu operátor ::.

Příklad použití můžeme vidět například ve třídě /i/money/banker definující bankéře. Třída dědí jednak třídu prodavače (bankéř umí totéž, co prodavač), jednak zvláštní třídu specificky bankéřských dovedností, a při inicializaci měny, s níž má bankéř pracovat, je pak potřeba stejnou hodnotou inicializovat i příslušné funkce v obou zděděných modulech:

inherit "/i/money/prodavac";
inherit "/i/money/banker_i";

...

void set_valuta(mixed val)
{
  prodavac::set_valuta(val);
  banker_i::set_valuta(val);
}

Konstrukce prodavac::set_valuta(val) zavolá funkci set_valuta() s parametrem val ve funkci zděděné ze třídy /i/money/prodavac, banker_i::set_valuta(val) provede totéž pro třídu /i/money/banker_i. Pokud jméno děděné třídy obsahuje znak, který se nesmí vyskytnout v identifikátoru (mohl by to být třeba spojovník, který se může vyskytnout ve jménu souboru, nebo lomítko, kdyby bylo nutno pro odlišení označovat třídu celým směrníkem), je potřeba je uzavřít do uvozovek (v současných knihovnách mudu Prahy se však takový případ nevyskytuje). Pokud je mezi zděděnými třídami jen jedna, která definuje funkci daného jména, pak můžeme pojmenování třídy úplně vynechat, tedy psát jen :: a jméno zděděné funkce. S tím jsme se už setkali, když jsme vytvářeli zárodek vlastního objektu:

void create()
{
  ::create();
  ...
}

U funkcí, které jsou typu void a nemají žádné parametry, můžeme též použít zkratkového zápisu k volání téže funkce ve všech zděděných modulech. I s tím jsme se už setkali na začátku vytváření vlastního objektu (hvězdička je nám i z operačního systému známa jako „žolík“ označující libovolný soubor, uvozovky potřebujeme proto, že hvězdička není znak povolený v identifikátoru):

void create()
{
  "*"::create();
  ...
}

Všimněme si, že lokální funkce, z nichž v uvedených příkladech voláme funkci zděděnou, se jmenuje stejně jako ona zděděná funkce. Nemusí tomu tak být vždy, ale je to případ zdaleka nejčastější.

Funkce, z níž v příkladu s bankéřem voláme funkce set_valuta() zděděné ze třídy /i/money/prodavac i ze třídy /i/money/banker_i, se jmenuje zase set_valuta(). Tato nově definovaná funkce nyní překrývá všechny stejnojmenné zděděné funkce. Kdybychom lokálně zavolali funkci set_valuta() před touto deklarací, zavolala by se funkce ze třídy /i/money/prodavac, protože ta je uvedena jako první a v ní by tedy byla funkce daného jména nalezena; jakmile však byla deklarována nová funkce, volá set_valuta() tuto novou funkci (tedy i kdybychom takové volání napsali přímo do těla této funkce, zavolala by sebe samu). Tato nově definovaná funkce je rovněž jediná, která by se zavolala, kdybychom volali funkci set_valuta() pomocí call_other().

Překrývání funkcí se používá všude tam, kde potřebujeme, aby se objekt v něčem choval jinak než třída, kterou dědí. Ve většině případů se přitom též popsaným způsobem volá překrytá funkce, protože je potřeba, aby se vedle nově dodaných programových příkazů provedly též všechny příkazy, které tato překrytá funkce obsahuje. V některých případech však takového volání není zapotřebí:

string query_smell()
{
  send_message_to(this_player(),MT_FEEL,MA_SMELL,
    wrap("Au! Jak je vidno, i při očichávání se může člověk o bodlák popíchat."));
  this_player()->add_hp(-1,0,0,"Trny bodláku se ti staly osudnými.");
  return wrap("Bodlák překvapivě docela hezky voní.");
}

V tomto příkladu překrýváme funkci query_smell(), která by jinak vracela řetězec nastavený funkcí set_smell() (a naformátovaný do řádků). Naše nová funkce nejprve pošle právě jednajícímu, tedy bodlák očichávajícímu hráči zprávu, že se o tento bodlák popíchal, pak mu odečte jeden životní bod, a pak vrátí popis zápachu, který by jinak vrátila standardní funkce query_smell().[11]

Úkoly do příští lekce editovat

Prohlédněme si dokumentaci ke třídám, které jsme vdědili do objektu, jejž vytváříme, případně si projděme samotné zdrojové texty těchto tříd, tedy ony programové soubory, na něž odkazujeme v příkazu inherit. Případně použijme andělského povelu inherit s parametrem -r (tedy např. inherit -r bublátor, máme-li bublátor právě po ruce) a podívejme se, jaké všechny třídy náš objekt vlastně dědí, a podle chuti nahlédněme do toho či onoho zdrojového textu. Přečtěme si (pomocí povelu ?, definovaného Podprahovou encyklopedií) dokumentaci k jednotlivým funkcím, na které přitom narazíme.

Pokud náš objekt má být například druh zbraně, pak si přečtěme v Podprahové encyklopedii (v sekci Jak to funguje) kapitolu o zbraních, a případně si projděme zdrojové soubory ve složce /i/weapon, tedy /i/weapon/weapon_logic.c, /i/weapon/zblizka.c atd. Narazíme při tom například na funkci set_weapon_class, a můžeme si hned v encyklopedii pomocí ? set_weapon_class nechat vypsat nápovědu k této funkci. Pokud se v souboru weapon_logic.c podíváme do jejího nitra, zjistíme, že obsahuje jen přiřazení nastavené hodnoty, ale že se při tom používá jakási funkce normstr(), na niž se zase můžeme zeptat v encyklopedii.

Poučení nám nabízejí samozřejmě i zcela základní třídy /i/item.c, /i/move.c a /i/value.c, které jsme do svého objektu vdědili v případě, že jsme nenašli žádnou specifickou třídu, která by byla našim záměrům bližší. Můžeme se též podívat do encyklopedie do sekce Lfuny, v ní na Lfuny utříděné podle skupin a tam na skupinu Basic, a pak se podívat na popisy některých tam vypsaných funkcí.

Při tomto zkoumání narazíme na různé funkce, o nichž jsme možná dosud ani neměli tušení a jimiž je možno zajímavě nastavovat vlastnosti našeho objektu. S těmi si následně můžeme trochu pohrát a svůj objekt s jejich pomocí co nejvíce přiblížit svým představám. V případě zmíněné zbraně třeba zjistíme, jak se nastavují její bojové vlastnosti a případně též dovednosti, které se jejím používáním cvičí (a zjistíme též, že platí určité programátorské směrnice, které udržují hru hratelnou — například aby výrobou superúčinných zbraní najednou nepřestaly být všechny zlé potvory hráčům nebezpečné — a které najdeme v encyklopedii v sekci Směrnice). Ovšem i v případě zcela obyčejných objektů, dědících jen zmíněné základní moduly, si můžeme velmi pohrát například s funkcí set_value(), umožňující podrobné rozlišení ceny předmětu platné v různých oblastech hry.

Pokud po tom všem ještě budeme mít chuť do práce, pak můžeme zkusit ve svém objektu překrýt i některou jinou funkci než create() (v případě zbraně by to mohla být třeba funkce do_break(), která se volá v okamžiku, kdy se zbraň rozbije — zbraň rozhodně nemusí jen vydat hlášku, může též hráče zranit, vypadnout mu z ruky, rozbít nějaký z okolních předmětů a pod., nač si jen programátorsky troufneme).

Poznámky editovat

  1. LPC rozlišuje v identifikátorech malá a velká písmena. Pokud jednou deklarujeme proměnnou jako batoh, pak ji musíme vždy označovat jako batoh, a ne třeba jako Batoh (to by ovšem mohla být nějaká jiná proměnná). Zkušenost ovšem ukazuje, že není radno odlišovat proměnné jen velkým písmenem (prostě se to člověku snadno splete). Běžná konvence je psát výhradně velkými písmeny konstanty (například nám již známé CISLO_J nebo VZOR_KOST) a u proměnných — až na výjimky — používat jen písmena malá.
  2. Co to přesně znamená, že v programu dojde k chybě, si objasníme později. Zatím se spokojme s obecnou představou, že pak program nefunguje, dokud se chyba neopraví.
  3. Proč je v deklaraci funkce add_type() použito datového typu mixed, a ne spíše očekávatelného string a int, si vysvětlíme o kousek níže.
  4. Dvojznak \" je příkladem zápisu zvláštního znaku do řetězce. Jinými příklady jsou \n, znamenající novou řádku, \t, znamenající odskok na nejbližší tabulátorovou pozici, nebo \\, znamenající obrácené lomítko, protože to pro tento jeho význam není možno zapsat jednoduše. Rovněž libovolný znak aktuální znakové sady (v případě Prahů ISO 8859-2) je možno zapsat jako obrácené lomítko a desítkové číslo odpovídající kódu ve znakové sadě, tedy třeba \120 místo znaku x.
  5. V uvedeném příkladu volání move() se samotným udáním cíle pohybu znamená tichý přesun objektu, používaný například při umisťování nových objektů do hry. MOVE_NORMAL znamená normální herní pohyb do sousední místnosti, MOVE_MAGIC pohyb mimořádnými prostředky např. pomocí magických schopností nebo předmětů (existuje ještě MOVE_SECRET, pohyb obzvláště utajený, který na rozdíl od nulového parametru ani neoznámí provedení pohybu některým obslužným funkcím). Třetím a čtvrtým parametrem funkce move() jsou hlášky, které se mají vyslat do okolí v místnosti, kterou pohybující se objekt právě opouští, a v místnosti, do níž se přesunuje; nejsou-li zadány, použijí se standardní hlášky.
  6. Pro zjištění dědičných vztahů z prostředí hry slouží andělský příkaz inherit (viz pomoc inherit).
  7. Najdou se i případy, kdy takové dání klauzury k dispozici smysluplné je, ale nyní se spokojme s tím, pro co se rozhodl dávný programátor této třídy.
  8. Německé názvy jako inhalt (obsah), koecher (toulec) a pfeil (šíp) najdeme na mnoha místech programových knihoven, a to tam, kde se jedná o části převzaté z knihoven německého mudu UNItopia, na jejichž základě byly vybudovány programové knihovny Prahů.
  9. S pomocí Podprahové encyklopedie můžeme tyto dva druhy externích funkcí rozlišit velmi snadno: vlastní externí funkce mají originální popis v angličtině, simulované externí funkce v češtině a s udáním zdrojového souboru ve složce /secure/simul_efun.
  10. Můžeme se v Podprahové encyklopedii podívat například na dokumentaci funkce set_value(). Příkaz ? set_value nám zobrazí popis tří podobných, ale ve své funkci lehce odlišných funkcí, definovaných ve třech různých zdrojových souborech.
  11. Přesné popisy funkcí set_message_to(), this_player(), wrap() a add_hp(), jakož i významy jejich parametrů si laskavý čtenář dohledejž v Podprahové encyklopedii.

Pomocné stránky editovat