int21h

Skripty a levely ve hrách

Každá hra musí mít něco, v čem se bude hráč pohybovat, aneb herní engine bez dat je na nic. Dá se tak říci, že ač je engine důležitý, data jsou mnohem důležitější, protože když děláte hru, musíte mít engine tak jako tak. Engine je vlastně program. Pryč jsou doby, kdy se tvůrci snažili programovat i prostředí přímo v programu (i já jsem to chvíli zkoušel), protože každý hned brzy přijde na to, že i primitivní level je lepší uložit jako mapu (buď do nějakého externího souboru nebo přímo do programu např. jako 2D pole ARRAY, kde 1 byte odpovídá objektu/zdi, jako např. u Wolfa 3D). Ovšem s chováním samotných postaviček ve hře to byl většinou problém. Hlavně kvůli rychlosti bylo totiž chování napevno naprogramováno v hlavním programu jako EXE kód (např. Doom), a ostatní měli možnost (když už vůbec něco) maximálně změnit texturu věcí, nebo časování snímků (popř. ještě např. účinnost zbraní). Ale rozhodně ne např. inteligenci, chování, atd. nebo si přidávat vlastní věci.

Dnes se to řeší pomocí skriptů. Autor engine tedy nevymýšlí žádné chování věcí, ani jim nedělá základy (i když i to je možné), ale místo toho nabídne tvůrcům levelům skriptovací jazyk na úrovni assembleru (Pascalu, C, vlastního jazyka) a haldu proměnných s funkcemi např. na vytvoření objektu, atd., kdy si každý tvůrce levelu udělá vše, co potřebuje sám (pravda, hra pak může být náchylnější na chyby, zvláště, pokud prostor sdílí více "skriptařů"). Autor levelu tedy obtěžuje tvůrce engine maximálně tehdy, když potřebuje přidat nějakou novou instrukci. Naopak není závislý na tom, jaké změny provede programátor v kódu. Pro oba to znamená sice více práce (engine musí být schopen daný skript vykonávat a skriptař se musí naučit "programovat"), ale level designéři dostanou do ruky mocné nástroje, díky kterým se jejich hra bude lišit od té bez skriptů minimálně jako Doom a Half Life. To záleží na tom, zda skript bude podporovat instrukce čistě jen jako ZOBRAZTEXT, PRESUNPOSTAVU, nebo zda tam bude i základní jazyk typu MOV, INC, MUL, kde si tvůrce bude moci naprogramovat i vlastní umělou inteligenci (ovšem s tím, že čím méně toho instrukce dělá, tím je pomalejší, protože většina režije připadne na její dekódování).

Dnes se proto zaměříme na tvorbu levelů a skriptů. Nečekejte ovšem mnoho programování, půjde spíše o teorii. K programování Vám mohu říci jen jedno: potřebujete umět zpracovávat text (odělovat slova pomocí mezer), a nahrazovat skupinu slov jednoduše číslem. Zpracování levelu v textové podobě odpovídá skriptům. V binární podobě jej buď načtete na začátku hry a podle definice vytvoříte prostředí, nebo jej čtete během hry tak, jak se v něm hráč a postavy pohybují. Abych nebyl pes, ukážu Vám dva programy, které teoreticky dokáží přeložit jeden řádek skriptu do skupiny bytů (při dekompilaci budete postupovat obráceně, každé skupině bytů přiřadíte nějaké slovo), a také program, který už přeložený skript bude vykonávat (vykonávání skriptu v textové podobně je shodné s jeho kompilací, jen namísto vytváření sekvence bytů už provedete rovnou nějakou akci):

procedure ZpracujRadek(Radek : string) : longint;
var Slova : array[0..15] of string[8];
    Slovo : string;
    Udaj : array[0..4] of byte;
    Vysledek : longint;
    poc : byte;
    slov : byte;
    i : byte;
    ch : integer;
begin
 FillChar(Udaj,SizeOf(Udaj),0);
 (* přečteme všechna slova na řádku, maximum 16 slov po 8 znacích *)
 slov := 0;
 Slovo := '';
 for poc := 1 to Length(Radek) do
  if Radek[poc] <> #32 then
   Slovo := Slovo+Radek[poc]
    else if Radek[poc-1] <> #32 then
     if Slov < 15 then
      begin
       if length(Slovo) > 8 then Delete(Slovo,9,255);
       for i := 1 to length(Slovo) do
        if Slovo[i] in [#65..#90] then
         Slovo[i] := Char(Ord(Slovo[i])+32);
       Slova[slov] := Slovo;
       Inc(slov);
       Slovo := '';
      end
       else Break;
(* nahlásíme případnou chybu *)
 if Slov = 0 then
 begin
  Udaj[0] := 255;
  Move(Udaj,Vysledek,SizeOf(Vysledek));
  ZpracujRadek := Vysledek;
  Exit;
 end;
 (* máme normalizovaná slova na malá písmena, převedeme instrukci na opkód *)
 if Slova[0] = 'mov' then Udaj[0] := 64 else
 if Slova[0] = 'inc' then Udaj[0] := 40 else
 if Slova[0] = 'dec' then Udaj[0] := 41 else
 if Slova[0] = 'add' then Udaj[0] := 65 else
 if Slova[0] = 'sub' then Udaj[0] := 66 else
 if Slova[0] = 'push' then Udaj[0] := 42 else
 if Slova[0] = 'pop' then Udaj[0] := 43 else
 if Slova[0] = 'xor' then Udaj[0] := 67 else
 if Slova[0] = 'or' then Udaj[0] := 68 else
 if Slova[0] = 'and' then Udaj[0] := 69 else
 if Slova[0] = 'clear' then Udaj[0] := 0 else
 if Slova[0] = 'reset' then Udaj[0] := 1 else
 if Slova[0] = 'jmp' then Udaj[0] := 32 else
 if Slova[0] = 'cmp' then Udaj[0] := 70 else
  Udaj[0] := 255; (* první byte > 128 = chyba *)
 end;
(* vložíme maximálně 3 další operandy *)
(* první písmeno = šířka operandu (1,2,4 byty) do 2 MSB bitů *)
(* zbytek je 0-63 offset v paměti, tj. poskytujeme 256 bytů
   pokud, které jsou adresovatelné přes B: 0-63, W: 0-127, D: celé,
   nebo 68 bytů, kdy šířka operandu neurčuje zároveň násobič offsetu,
   takže d63 neleží na adrese 252 bytů, ale na 63 bytů a tedy obsáhne
   byty 63 až 67 *)
 if Slov > 4 then Slov := 4;
{begin	
  Udaj[0] := 254;
  Move(Udaj,Vysledek,SizeOf(Vysledek));
  ZpracujRadek := Vysledek;
  Exit;
 end;}
 for i := 1 to Slov-1 do
 begin
  if Ord(Slova[i,0]) < 2 then
  begin
   Udaj[0] := 250+i;
   Move(Udaj,Vysledek,SizeOf(Vysledek));
   ZpracujRadek := Vysledek;
   Exit;
  end;
 (* šířka údaje bude zároveň i šířka adresy, tj. údaje jsou i zarovnané *)
  case Slova[i][1] of
  'b' : Udaj[i] := $40;
  'w' : Udaj[i] := $80;
  'd' : Udaj[i] := $c0;
   else begin
         Udaj[0] := 245+i;
         Move(Udaj,Vysledek,SizeOf(Vysledek));
         ZpracujRadek := Vysledek;
         Exit;
        end;
  end;
  Delete(Slova[i],1,1);
  Val(Slova[i],Vysledek,ch);
  if ch <> 0 then
  begin
   Udaj[0] := 240+i;
   Move(Udaj,Vysledek,SizeOf(Vysledek));
   ZpracujRadek := Vysledek;
   Exit;
  end;
  if (Vysledek < 0) or
     (Vysledek > 63) then
  begin
   Udaj[0] := 235+i;
   Move(Udaj,Vysledek,SizeOf(Vysledek));
   ZpracujRadek := Vysledek;
   Exit;
  end;
  Udaj[i] := Udaj[i] or byte(Vysledek);
 end;
 Move(Udaj,Vysledek,SizeOf(Vysledek));
 ZpracujRadek := Vysledek;
end;


var Offset : word;	(* na začátku skriptu nastavit na 0 *)
    Promenne : array[0..255] of byte;
procedure ProvedSkript(Kde : pointer; Vel : word) : boolean;
type TInstr = array[0..3] of byte;
var Prom : longint;
 function Adresa(Udaj : byte) : byte;
 begin
  case ((Udaj and $c) shr 6) of
   1 : Adresa := Udaj and $3f;
   2 : Adresa := (Udaj and $3f) shl 1;
   3 : Adresa := (Udaj and $3f) shl 2;
  end;
 end;
 function Sirka(Udaj : byte) : byte);
 begin
  case ((Udaj and $c) shr 6) of
   1 : Sirka := 1;
   2 : Sirka := 2;
   3 : Sirka := 4;
  end;
 end;
 function ZiskejUdaj(Adresa,Vel : byte) : longint;
 var Udaj : longint;
 begin
  Udaj := 0;
  Move(Promenne[Adresa],Udaj,Vel);
  ZiskejUdaj := Udaj;
 end;
 procedure UlozUdaj(Adresa,Vel : byte; Udaj : longint);
 begin
  Move(Udaj,Promenne[Adresa],Vel);
 end;
begin
 (* vykonáme 1 instrukci skriptu *)
 case TInstr(Mem[Seg(Kde^):Ofs(Kde^)+Offset])[0] of
  0 : {voláme reset úrovně}
  1 : ClearVRAM;
  64 : Move(
	Promenne[Adresa(TInstr(Mem[Seg(Kde^):Ofs(Kde^)+Offset])[1])],
	Promenne[Adresa(TInstr(Mem[Seg(Kde^):Ofs(Kde^)+Offset])[2])],
        Sirka(TInstr(Mem[Seg(Kde^):Ofs(Kde^)+Offset])[1]));
  40 : begin
        Prom := 0;
        Move(Promenne[Adresa(TInstr(Mem[Seg(Kde^):Ofs(Kde^)+Offset])[1]),
	     Prom,Sirka(TInstr(Mem[Seg(Kde^):Ofs(Kde^)+Offset])[1]));
        Inc(Prom);
        Move(Prom,Promenne[Adresa(TInstr(Mem[Seg(Kde^):Ofs(Kde^)+Offset])[1]),
	     Sirka(TInstr(Mem[Seg(Kde^):Ofs(Kde^)+Offset])[1]));
       end;
  {atd.}
 end; 
 (* podle počtu operandů 0-3 zvýšíme offset ve skriptu *)
 case TInstr(Mem[Seg(Kde^):Ofs(Kde^)+Offset])[0] of
  0..31 : Inc(Offset);
  32..63 : Inc(Offset,2);
  64..95 : Inc(Offset,3);
  96..127 : Inc(Offset,4);
 end;
 if Offset >= Vel then ProvedSkript := False
  else ProvedSkript := True;
 (* vrátí-li False, byl dosažen konec skriptu *)
end;

A teď se už pustíme do nějakého toho teoretického popisu. Pokud jste minulé dvě procedury hned nepochopili, nic si z toho nedělejte. Pojďte si nyní přečíst teorii a pak to zkuste znovu.

Skriptovací jazyk

Tento typ dat slouží k větší variabilnosti hry. Skripty mohou být jak textové, tak binární. Engine umí většinou pracovat s oběma, záleží, zda to bude interpret (TXT, něco jako Basic), nebo virtuální počítač (binární, něco jako EXE pod DOSem či spíše DOSbox/DOSemu). Editor většinou umí vytvářet oba, ale člověk asi nebude s binární podobou pracovat moc dobře.

Interpret jazyka

Skript v textové podobě se používá většinou jen v editoru. Tam může být vykonáván např. při debugování takového skriptu. Pro program to znamená číst řádek po řádku a podle zadaných řetězců vykonat určité činnosti. V samotném editoru to nevadí, ale ve hře, kde se většinou snažíme o maximální rychlost, nám to moc nepomůže. Naopak se takový skript dobře uchovává a můžete ho předat hře bez nutnosti ho nějak kompilovat nebo mít vůbec nějaký editor (jehož vývoj trvá většinou déle než vývoj samotného engine). Může vypadat např. nějak takto:

	sila	1
	mov	b50,w55
	nop
	jmp	4
	add	b0,b1
	clear
	zobraz	9
	jmp	0  

Kompilátor/Dekompilátor

Pokud se rozhodnete pro binární podobu, bude muset mít editor kompilátor, který ji pro engine vytvoří. Spočívá to v hlavně v procházení celého souboru řádek po řádku a vytváření postupně sekvence binárních údajů. Pokud budete chtít mít možnost daná data ještě editovat, musíte si vytvořit i dekompilátor. Ten naopak vždy přečte (v našem případě) 1 byte a podle toho, jaká to je instrukce (opkód), napíše na řádek její symbolické jméno (text) a také její parametry. Poté se o stanovený počet bytů posune dále (u nás podle opkódu) a pokračuje až do konce daného pole. Samotný engine pak potřebuje alespoň čtečku:

Čtečka binárního skriptu

Engine má práci velmi zjednodušenou. Naopak musí zase přesně vědět, který opkód znamená jakou instrukci a pokud se to změní v editoru, musí se to změnit i ve hře. Pokud by si engine skript kompiloval sám, byl by závislý jen na jménech instrukcí. Pokud má již skript v binární podobě, stačí mu ho pouze nahrát do paměti a ihned s ním pracovat. Pokud má hra měnit skripty dynamicky, je to lepší. V opačném případě, kdy je možné skripty nahrát při načítání úrovně, můžeme mít skripty textové (a tedy nepotřebujeme ani editor), protože si je engine zkompiluje sám (dneska to trvá pár zlomků vteřiny). Skripty v binání podobně jsou ovšem hůře editovatelné bez editoru, což může být ale také účel toho, proč se používají (navíc jsou mnohem menší). Binární skripty totiž vypadají skoro jako BMP nebo EXE soubory.

Levely a lokace

Pokud máte skripty, jistě budete potřebovat definovat prostředí, ve kterém se hráč, postavy, předměty, zdi, atd. budou vyskytovat, pohybovat a reagovat na sebe. Opět budete potřebovat pro nějaké složitější prostředí editor (pokud neděláte levely wolfa3D nebo plošinovky Prince, kde si vystačíte s Poznámkovým blokem).

Textový popis levelu

Tento způsob má tu výhodu, že pro vytváření levelu nepotřebujete editor (i když tu samozřejmě může být), protože vlastně vytváříte TXT soubory (to může být nevýhoda pro editor, který bude muset z dat ve své paměti vytvářet textové řetězce). Takový level může být dost velký, protože se vše popisuje slovy, nikoliv byty (takže může zabírat třeba až 16x tolik). Engine po načtení navíc stejně bude muset převést textové řetezce do nějaké binární podoby, aby mohl s mapou rychleji pracovat. To může být výhoda v tom, že můžete změnit formát těchto dat, aniž byste museli měnit editor. Nevýhoda spočívá v tom, že se zde těžko tvoří složitější levely např. pro Dooma, nebo nějaké, kde je potřeba např. ještě určit typ podlah, jejich výšky, osvětlení, atd. Prakticky je to skoro až nemožné (=velmi pracné, budete potřeba jednu a tutéž mapu nakreslit několikrát, přičemž pokaždé tam bude jiný údaj). Vzhled takového levelu pak může být třeba takovýto (tento popis může samozřejmě vytvořit i sám editor podle toho, jak jste si level nakreslili a jaké jste mu v různých oknech dali parametry - třeba podle náhledů naimportovaných textur):

pozadi=obloha.bmp
sirka=50
vyska=90
objektu=5
textury=(x)zed.bmp (D)dvere.bmp (1)sloup.bmp (o)obraz.bmp
mapa={
xxxxxxxxxxxxx
xxxo xxxx xxx
xxxx xxx   xx
xx  V      1x
xxxoxxDxxoxxx
xxxxxx xxxxxx
xxxxxxSxxxxxx
xxxxxxxxxxxxx
}

Binární popis levelu

Zde produkujete level na způsob souboru EXE (resp. bude tak vypadat, např. WAD soubory u Dooma). Toto má pár výhod: level nebude tak snáze editovatelný (pokud tomu chcete zabránit), a zároveň může být už ve formátu, v jakém ho očekává engine. Tedy ten ho může jen načíst z disku a hned s ním pracovat. Také zabere méně místa než textový a grafický popis. Nevýhoda je, že musíte mít editor, který navíc bude obsahovat de/kompilátor buď do textové podoby, nebo do něčeho, kde budete moci level vytvářet v grafickém prostředí (např. editor DEU pro Dooma). Zde je každý příkaz reprezentován bytem, stejně tak všechny mapy, atd., což počítači zjednoduše práci na příkaz CASE, a nemusí porovnávat celé řetezce. V programu je ale možné tvořit snadno např. dlaždicové levely, kde na každou dlaždici připadá celý RECORD s hodně údaji. To by se ručně tvořilo dost těžko.

Pokud se rozhodnete pustit se do tvorby vlastní hry a nemáte okolo sebe dostatečný počet schopných (a hlavně nadšených lidí - to je základ: k čemu je člověk, který něco umí, když je líný a neustále se na něco vymlouvá?; to už je raději lepší člověk, který je sice neschopný, ale snaživý, protože u něj je velká šance nejen, že něco opravdu udělá, ale že se v tom i zlepší) spolupracovníků, abyste dali dohromady tým (alespoň po jednom programátorovi (popř. v kombinaci s tvůrcem kódu pro grafiku, zvuky, myš&klávesnici a síť), grafikovi, zvukaři (možno smíšeným s hudebníkem), tvůrcem levelů (popř. i skriptů) a scénáristovi (může být i kdokoliv z předchozích)), porozhlédněte se raději na Internetu, a přidejte se buď do nějakého stávajícího týmu, nebo k člověkovi, který má buď už něco rozdělaného, nebo alespoň vizi (nabízím sám sebe cca. za 6-12 měsíců, kdy budu po 3 letech konečně připraven s editorem a konceptem hry). Konec reklamy :-). Příště se Vám už pokusím ve 2 až 3 dílech vysvětlit zvukové karty.

2006-11-30 | Martin Lux
Reklamy:
Naše kulturistika Svaz kulturistiky a fitness ČR