Ez itt a harmadik nekirugaszkodás. Kevésbé járatosaknak ajánlom az előző kettőt. A másodikban addig jutottunk, hogy mindenre tettünk textúrát. Most az irányítás, az ajtók és egy eddigi bug kijavítása lesz a téma.

 
Az irányítás

Hogy mennyire látszik szaggatottnak egy ilyesféle program működése, nagyon is függ attól, hogy mozgatjuk a képzeletbeli emberkénket. Megfelelő mozgatással sokkal simább kinézet érhető el.
Be kell vezetni a gyorsulást, lassulást, a falak melletti elcsúszást, és kell valami normális billentyűzet-kezelési technika (nem readkey és társai.)
 

Az alapok

Legalább kétféle mozgást kellene tudnunk. Egyenes vonalban (előre illetve hátra) , és fordulni (jobbra, balra). A mozgást az illető mozgási sebessége (a programban ForwInc) és a fordulási sebessége (AngInc) határozza meg.


 

Ezek szerint:

Az Y összetevő azért mínusz, mert a mi koordinátarendszerünkben (ld. első nekifutás a témából) az Y tengely fordítva mutat.
Ezekkel az értékekkel kell lépni a GridMap-en, miután fordultunk, azaz a nézet szögéhez hozzáadtuk a fordulást. A sebességeken kívül bevezethetjük a gyorsulást is. A sebességet növeljük , vagy csökkentjük amíg el nem értük a maximális illetve minimális értéket. Gyorsulunk, ha be van nyomva a gomb, fékezünk, ha fel van engedve. Ahogy az alábbi programrészletben:
 

Procedure Control;begin    {Ha lenyomtuk a felfele nyilat}    if KeyMap[MOVE_UP] then   begin       {Ha már eddig is előre mentünk, és még        nem értük el a maximális sebességet, akkor gyorsítunk}       if (ForwInc>0) and (ForwInc<MaxForwInc) then ForwInc:=ForwInc*FIncInc;      {Ha hátramenet közben nyomtunk felfele nyilat, akkor        belefékezünk, vagy ha már lassan megyünk hátra}       {akkor átváltunk előremenetre}       if (ForwInc<0) then begin ForwInc:=ForwInc/(2*FIncInc);          if ForwInc>-1 then ForwInc:=1;end;      {Ha eddig álltunk, akkor elindulunk előre}       if ForwInc=0 then ForwInc:=4;   end    else {Ha lefele nyilat nyomtunk}    if KeyMap[MOVE_DOWN] then   begin      {Ha már eddig is hátra mentünk, és még nem értük el a maximális       tolatási sebességet, akkor gyorsítunk}       if (ForwInc<0) and (ForwInc>-MaxForwInc) then          ForwInc:=ForwInc*FIncInc;      {Ha előremenet közben nyomtunk hátra nyilat, akkor        belefékezünk,}       {vagy ha már lassan megyünk előre, akkor átváltunk hátramenetre}       if ForwInc>0 then begin ForwInc:=ForwInc/(2*FIncInc);           if ForwInc<1 then ForwInc:=-1; end;     {Ha eddig álltunk, akkor elindulunk hátra}       if ForwInc=0 then ForwInc:=-4;   end    else {Ha se a felfele, se a lefele nyilat nem nyomjuk, akkor           lassítunk}    begin       ForwInc:=ForwInc/FIncInc;      {Ha már nagyon lassú, akkor nullázzuk}       if (ForwInc>-1) and (ForwInc<1) then ForwInc:=0;   end; . . . end;

Ez a részlet az előre illetve hátra mozgatást végzi. A fordulásnál ugyanígy működik a vezérlés, azzal a különbséggel, hogy ott csak akkor kell gyorsítani, ha egy helyben állunk, egyébként járás közben konstans szöggel fordulunk el.

A programrészletből bizonyára kitűnt, hogy egy gomb állapotát (be van nyomva, vagy nincs) egy tömbből vesszük (KeyMap). Ez a tömb 128 elemű, a billentyűzeten található összes gombot tartalmazza. A tömb magától nem fogja igazra állítani a lenyomott gombokhoz rendelt elemeket, ezt nekünk, egy saját billentyűzet-megszakításból kell megtennünk.

A programban működő megszakítás, és megszakítás átirányítás DPMI-re való. Az átirányításhoz különösebb magyarázatot nem tudnék adni, így kell csinálni. A billentyűzet-kezelő megszakítás száma 9H, ezt kell átirányítani a saját rutinunkra, ami természetesen assembly. Free pascalban nem az Intel, hanem az AT&T asm szintakszis az alapértelmezett, ez elsőre kicsit furcsán néz ki, de meg lehet szokni. A megszakítás minden gomb lenyomásakor illetve felengedésekor meghívódik. Lenyomáskor a 60H-s portról beolvasott érték a billentyű SCAN kódja, felengedéskor az érték SCAN kód plusz 128. Tehát az átírandó elem tömbindexét az alsó 7 bit határozza meg, a beírt értéket pedig a 8. Bit. A felengedett billentyűkhöz nullát írunk, a lenyomottakhoz nem nullát. Ha először and-eljük a portról beolvasott értéket 128-al, majd ezt xor-oljuk 128-al, akkor felengedéskor (ekkor volt 8. Bit) 0-t  kapunk, lenyomáskor 128-at, ami nagyon megfelel a célnak. Ezt az értéket beírjuk a SCAN kód (ha a bekért értéket and-eljük 127-el akkor megkapjuk) által indexelt helyre. A megszakítás végén jeleznünk kell a billentyűzetnek, hogy feldolgoztuk az adatot, a 20H portra 20H-t kell kiküldeni. Mivel a megszakításból saját adatokat akarunk elérni, vissza kell tölteni a saját DS-t, ezért előtte valahol el is kell tárolni.
A megvalósítás a keyboard.pp -ben van. Egy különálló unit, így bármilyen más programban is felhasználható.

A falak melletti elcsúszás
Be kell vezetnünk egy állandót, ami meghatározza, milyen közel mehetünk egy falhoz (a programban HITDIST). Megvizsgáljuk X és Y irányban is, hogy a pozíciónktól ennyivel arrébb lévő elem a GridMap-en nem fal-e. Azt a mozgásösszetevőt (az ábrán dX illetve dY), amellyel már túl közel kerülnénk, egyszerűen nem adjuk hozzá a pozícióhoz, csak a másikat. Ezzel tulajdonképpen elcsúszunk a fal előtt. Pl.:
 

{Paraméterként megkapjuk, mennyit kéne  lépnünk X illteve Y irányban. A felső ábrán dX és dY} procedure Step(XStep,YStep : integer);begin    {X iranyban jo-e}        {Px és Py a néző világkoordinátái (Grid koordináta 64-szerese)}    if ((XStep<0) and (GridFree(Py shr UNITSHIFT,(Px-HITDIST) shr UNITSHIFT)))    or ((XStep>0) and (GridFree(Py shr UNITSHIFT,(Px+HITDIST) shr UNITSHIFT)))    then Px:=Px+XStep;    {Y iranyban}    if ((YStep<0) and (GridFree((Py-HITDIST) shr UNITSHIFT,Px shr UNITSHIFT)))    or ((YStep>0) and (GridFree((Py+HITDIST) shr UNITSHIFT,Px shr UNITSHIFT)))    then Py:=Py+YStep; end;

Az ajtók

Nézzük a legegyszerűbbet, a Wolf3D stílusút. Ezek az ajtók oldalra nyílnak, egy falnyi a szélességük, és mélységben mindig a fal felénél vannak.

Kirajzolásuk ugyanúgy RayCasting-el történik, mint a falaknál, némi különbséggel. Mivel azt akarjuk, hogy az ajtó fél fal mélységben legyen, csalnunk kell a sugár metszéspontjának meghatározásánál, így a távolság is nagyobb lesz:


Tehát mind X, mind Y irányban a metszésponthoz hozzá kell adni a sugár számolásánál használt lépésközök felét, így fogunk féltávhoz érni, ezeket a koordinátákat tekintjük metszéspontnak. Ezzel megvan a metszéspontunk, ami, ha teljesen zárva van az ajtó, akkor a helyes textúra-koordinátát is meghatározza. Viszont egy ajtó nem mindig van teljesen zárva.

Hogy az ajtó mennyire van nyitva, meghatározza, hogy még átlátunk-e ott, ekkor tovább kell vinnünk a sugarat, vagy az már az ajtó, és a metszés igazi, ajtó textúráját kell odarajzolni:

A door offset mondja meg, mennyire van zárva az ajtó. Ha tejesen zárva van, akkor az értéke 63 (mivel egy grid egység 64 kis egységből áll. 0..63), ha teljesen nyitva, akkor 0. A metszést akkor is meg fogjuk találni a raycasting során, ha az ajtó nincs zárva, ekkor meg kell vizsgálni, hogy tovább kell-e vinni a sugarat.

X irányú, azaz a GridMap-en vízszintes ajtók esetén a metszéspont X offsete, azaz a 63-al and-elt értéke határozza meg, hogy tényleg az ajtót metszettük, vagy tovább kell meni a sugárral. Ha az X offset nagyobb, mint a door offset, akkor tovább kell menni, hiszen átlátunk:

Pl. ezen a ronda ábrán az első sugár tényleg az ajtót metszi, hiszen az X offset kisebb, mint a door offset. A második sugár viszont már nem metszi az ajtót, annak ellenére, hogy a raycasting során metszésként találjuk meg, hiszen metszi azt a Grid-et ahol az ajtó van. Az X2 offset nagyobb, mint a door offset, ezért azt a sugarat tovább kell vinni, egészen amíg falat nem találunk.

Y irányú falak esetében a helyzet ugyanez, csak ott az Y koordinátákkal kell játszani.

Akkor már csak textúrázni kell az ajtót. Ha a két offset (a sugáré és az ajtóé) egyenlő, akkor pont az ajtólap sarkát kaptuk telibe, ott az ajtó  nulladik textúraoszlopa lesz. Egyel arréb az egyes, és így tovább. Tehát az U textúrakoordináta a két offset különbsége lesz.

Az alábbi programrészlet az X irányú falakat vizsgálja, tehát az X-es raycasting eljáráson belül van az alábbi részlet:
 
 

. . {Ha ajtot talaltunk}   if c='D' then  begin     i:=GetDoorIndex(MapX,MapY);  {megkapjuk, hogy az ajto hanyadik a                                  tömbben}     if (Doors[i].Side='X') then            {Ha X irányú}    begin       {az ajto melyebben van fel fallal, hozzáadjuk a lépésköz felét}       XOffset:=(XRounded+(DeltaX div (2*TABMUL))) and 63;      {Ha átlátunk, töröljük a kódot, tehát a rutin tovább fogja       vinni a sugarat}       if XOffset>Doors[i].offset then c:=' ';       end else c:=' '; {Ha Y irányú volt, akkor sem törődünk vele}    end;     if c=' ' then  {Ha töröltük a kódot, továbbvisszük a sugarat}    begin       {lepeskozok}       X:=X+DeltaX;       Y:=Y+DeltaY;    end;     until c<>' '; {amig falba, vagy ajtoba nem futunk}    {Ha ajtóba futottunk}     if c='D' then    begin       {a későbbi textúrázáshoz kell a különbség képzés}       XOffset:=Doors[i].offset-XOffset;      {az ajto melyebben van fel fallal, az Y-t is továbbvisszük       féllel}       Y:=(MapY shl UNITSHIFT)+(UNITSIZE shr 1);     end;

Na persze az ajtókat mozgatni is kell, ha éppen záródnak, vagy nyitódnak, és adataikat el kell tárolni. Ezt egy tömbben megtehetjük, ahol minden ajtóhoz megadjuk a helyét a GridMap-en, ez alapján tudjuk majd visszakeresni (GetDoorIndex a programrészletben), el kell tárolni az offsetjét, hogy éppen mit csinál (semmit, csukódik, záródik, vár, hogy zárhasson) és mindegyiknek kell egy számláló a zárás előtti várakozást számlálni, és kell hogy X vagy Y irányú.
 
 

TDoor=record   offset : byte; {0-nyitva, 63-zarva}   Side : char;   {X/Y}   MapX,MapY : integer;   Status : char; {Opening/Closing/Delay/Nothing}   Timer : integer; {ido, ahol szamolja meddig van zarva} end;

A mozgatást végző eljárást mindig meg kell hívogatni akárhányszor megrajzoltuk a képernyőt. Az eljárás nem bonyolult, ha záródik, akkor növeljük az offsetet, ha nyitódik, akkor csökkentjük, ha kinyitottunk átállunk várakozni, ha kivártuk amit kellett, átállunk zárásra, stb…
 
 

procedure UpdateDoors; var i : integer; begin    for i:=1 to DoorN do       case Doors[i].Status of          {Opening}          'O' : if Doors[i].offset-DOORSPEED>0                then Doors[i].offset:=Doors[i].offset-DOORSPEED                else                begin                   Doors[i].offset:=0;                   Doors[i].Status:='D';                end;          {Closing}          'C' :if Doors[i].offset+DOORSPEED<UNITSIZE-1                then Doors[i].offset:=Doors[i].offset+DOORSPEED                else                begin                   Doors[i].offset:=UNITSIZE-1;                   Doors[i].Status:='N';                end;          {Delay}          'D' : if Doors[i].Timer<DOORDELAY then Inc(Doors[i].Timer)                else                begin                   Doors[i].Status:='C';                   Doors[i].Timer:=0;                end;       end; end;

A DOORSPEED állandó határozza meg, hogy mennyi egységet nyitódjon egy-egy alkalommal, a DOORDELAY pedig, hogy mennyit várjon.

Az ajtókat persze ki is kell tudnunk nyitni. Attól függően, hogy merre néz a játékos, megpróbáljuk nyitni az előtte, mögötte, balra, vagy jobbra lévő gridpozíción lévő ajtókat, persze, ha ott egyáltalán ajtó van.

Most ennyit az ajtókról.

Magasságok interpolációja

További újdonság a programban, hogy nem számolunk minden képernyőoszlopra külön falmagasságot, hanem csak a fal elején és végén tesszük meg, közte lineárisan interpoláljuk Ettől egyenesebbek lesznek a falak, és ha közel megyünk akkor sem lesz olyan eszméletlen ronda a rajta lévő textúra. A programban ezt úgy oldjuk meg, hogy minden egyes sugár adatait eltároljuk. Hol metszett falat, milyen irányú falat, mennyi a távolság stb…
Ezen a tömbön végigmászva ki tudjuk szűrni, melyik sugarak tartoznak ugyanahhoz a falhoz (amik ugyanolyan map-koordinátákat birtokolnak), ezen sugármetszések közül csak az elsőre és az utolsóra számolunk pontos falmagasságot a távolságból, a közteseket lineárisan közelítjük.
 

Egy kis hibajavítás

Az előző program, és nyílván ez is tartalmaz jó néhány bug-ot, ezek közül nézzünk egyet, amit ebben a részben megpróbáltam korrigálni.
A hiba:

A hiba oka az, hogy egy hosszú egyenes fal során a program megtalálja minden egyes falegység láthatatlan élének kis darabját. Mivel a használt eljárás a metszés távolsága alapján dönti el, hogy mit rajzol ki, előfordulhat ,és elő is fordul, hogy a távolság nem kellően precíz számolása miatt néha téved, annál is inkább, mert a sugarakat is csak közelítjük.

A probléma megoldása, hogy az X falak keresésénél adjunk vissza végtelen távolságot, ha egy hosszabb Y fal közepén vagyunk, azaz ha egyel a talált fal fölött is fal van. Ugyanez az Y falak keresésénél csak fordítva.
 
 

{tavolsag visszaadasa} if (GridMap[(Y-DeltaY) shr UNITSHIFT,MapX]<>' ')   and (GridMap[(Y-DeltaY) shr UNITSHIFT,MapX]<>'D') then findXwall:=MaxInt else findXwall:=abs(((y-Py) shl TABSHIFT) div SinT[a]);

Tehát, ha a következő Y-on nem üres a hely (és nem ajtó),  (tehát fal van) akkor jó nagy távolságot adunk vissza, így biztos, hogy az Y falak keresése során visszaadott érték lesz a kisebb, és azt fogjuk kirajzolni.

Ugyanezt megtesszük az Y falak keresésénél, és a hiba ki van javítva.

Még folytatjuk...

A letölthetô a forráskód illetve a raycast.exe!