Strona wykorzystuje ciasteczka by usprawnić komfort z jej korzystania. Korzystając ze strony akceptujesz naszą Politykę Ciasteczek. X

Podstawy silnika kafelkowego część 2: odnajdywanie ścieżki [zaawansowany]

W poprzedniej części omówiłem pokrótce przekształcenie izometryczne i jak uzyskać je w najłatwiejszy sposób. Tym razem, zgodnie z obietnicą zajmiemy się znajdywaniem ścieżki w silniku kafelkowym wykorzystując jak najprostszą metodę (aczkolwiek nie najszybszą). Nie wiem czy algorytm który tutaj przybliżę ma jakąś konkretną nazwę, ale na pewno jest jednym z najbardziej bezpośrednich spośród wszystkich dostępnych.
Dla wszystkich tych których nie interesują szczegóły przygotowałem klasę z całym algorytmem oraz dodatkowo plikiem FLA pokazującym jak go wykorzystać: PathFind.zip
W działaniu wygląda tak:
(Wybierz punkt startowy i końcowy naciskając na puste pola. Spacja by stworzyć nową mapę)

Działanie algorytmu jest całkiem proste i dzieli się na dwie fazy:
1. Poszukiwanie celu. Z punktu startowego krok po kroku sprawdzamy co raz większy obszar w poszukiwaniu docelowej pozycji, w międzyczasie zapisując do siatki aktualny krok iteracji. Numery iteracji będą nam później potrzebne by odtworzyć ścieżkę.
Urywek kodu z załączonego przykładu:

// ...
var vWrite:Vector.<Vector.<int>> = recreateTiles(tiles);
vWrite[startx][starty] = 1;
vWrite[endx][endy] = -1;
var vCheck:Vector.<Point>;
var vRead.<Point> = new Vector.<Point>();
			
var tile:Point;
var nStep:int = 2;
vRead.push( new Point(startx,starty) );
while(vRead.length != 0) {
				
	vCheck = vRead;
	vRead = new Vector.<Point>();
	for each(tile in vCheck) if(lookupTile(tile.x, tile.y, nStep, vRead,vWrite,vTile)) {
		return retracePath(tile.x,tile.y,nStep,vWrite);
	}
	nStep ++;
}
return null;
// ...
Funkcja lookupTile poszukuje kolejnych kafelek do iteracji. Jeśli któryś z 4 kierunków zawiera docelowy punkt, pętla zostaje zakończona i zwrócona zostaje znaleziona ścieżka. W przeciwnym wypadku w vWrite zapisywany jest aktualny krok, a do vRead wpisywana jest pozycja kolejnej kafelki do sprawdzenia.
2. Odtwarzanie ścieżki. Gdy już odnajdziemy punkt docelowy wystarczy krok po kroku zacząć wracać po zapisanych liczbach w vWrite by uzyskać naszą ścieżkę. Powiedzmy że doszliśmy do docelowej pozycji w 20 iteracji - teraz musimy zacząć się wracać zaczynając od znalezienia najbliższej kafelki z numerem 19, potem 18, 17, itd. Aż do 1 który będzie punktem startowym (0 powinny mieć kafelki które nigdy nie zostały sprawdzone).

Na koniec jeszcze tylko mała uwaga: algorytm można przyspieszyć wykonując go jednocześnie na punkcie startowym i końcowym, przez co ścieżka będzie odnaleziona gdy oba spotkają się w połowie drogi. Taki też sposób został użyty w dołączonej klasie.
Następnym razem przyjrzymy się idei "pola widzenia" w silnikach kafelkowych.

Podstawy Stage3D część I: wyświetlanie wielu obiektów [zaawansowany]

Dwa tygodnie temu pisałem jak przystosować Flash Professional do obsługi Flash Player 11 przy okazji korzystając z przykładu w dokumentacji na wyświetlanie grafiki 3D (http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display3D/Context3D.html#includeExamplesSummary). Jednak skorzystanie z gotowego rozwiązania to jedno, a zrozumienie co właściwie robi i jak działa to już zupełnie inna sprawa - dlatego tym razem napiszę co nieco o podstawowych zagadnieniach Stage3D (jak to Adobe nazywa swoją akcelerację 3D we Flash'u).
Tak na wszelki wypadek cały kod z owego przykładu (wraz z dodatkowymi bibliotekami potrzebnymi do jego uruchomienia) znajduje się tutaj: Context3DExample.zip

Pierwszą cześć tego artykułu poświęcę na opis tworzenia i wyświetlania kilku obiektów naraz przy jednoczesnym zachowaniu możliwości ich transformacji. Dla ułatwienia sugeruje stworzyć osobną klasę dla sześcianów, kopiując już istniejący kod (z przykładu w dokumentacji) odpowiedzialny za tworzenie geometrii: IndexBuffer i VertexBuffer. Taka klasa może wyglądać tak:

private var vbBuffer:VertexBuffer3D;
private var ibIndex:IndexBuffer3D;
private var m3D:Matrix3D;
private var nCount:int;

public function Box3D(context:Context3D) {
			
            var triangles:Vector.<uint> = Vector.<uint>([...]);
	ibIndex = context.createIndexBuffer(triangles.length);
	ibIndex.uploadFromVector(triangles, 0, triangles.length);
			
	const dataPerVertex:int = 6;
	var vertexData:Vector.<Number> = Vector.<Number>([...]);
	vbBuffer = context.createVertexBuffer(vertexData.length / dataPerVertex, dataPerVertex);
	vbBuffer.uploadFromVector(vertexData, 0, vertexData.length / dataPerVertex);
					
	m3D = new Matrix3D();
	nCount = 12;	
}
				
public function render(context:Context3D):void {
	context.setVertexBufferAt( 0, vbBuffer, 0, Context3DVertexBufferFormat.FLOAT_3 );
	context.setVertexBufferAt( 1, vbBuffer, 3, Context3DVertexBufferFormat.FLOAT_3 );
	context.drawTriangles(ibIndex, 0, nCount);
}
				
public function getMatrix3D():Matrix3D {return m3D;}
Nie ma tutaj nic szczególnie skomplikowanego - warto jedynie pamiętać, że matryca m3D będzie przechowywać transformacje (pozycję, skalę) więc sześcian najlepiej zbudować na pozycji 0,0,0 a później go przestawić w wybrane miejsce za pomocą funkcji appendTranslation.
Może jeszcze tylko wyjaśnię, że funkcją setVertexBufferAt ustawiamy który VertexBuffer będzie wykorzystywany do wyrysowania trójkątów (funkcja drawTriangles), a wywoływana jest dwa razy bo za pierwszym razem przekazujemy wierzchołki, a za drugim kolory.
Tak więc przygotowanie klasy sześcianu jest relatywnie łatwe, problemy zaczynają się gdy chcemy je wyświetlić. Zakładając, że wszystkie te kostki będą przechowywane w wektorze vBox funkcję render w przykładzie Context3DExample należy przebudować na coś takiego:
private function render(event:Event):void {
	renderContext.clear(.3, .3, .3);
			
	for each(var box:Box3D in vBox) {
		finalTransform.identity();
		finalTransform.append(box.getMatrix3D());
		finalTransform.append(world);
		finalTransform.append(view);
		finalTransform.append(projection);
	renderContext.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, finalTransform, true);
				
		box.render(renderContext);
	}
			
	renderContext.present();
}
Tutaj już się robi trochę ciekawiej, a to za sprawą dwóch najważniejszych rzeczy: funkcji setProgramConstantsFromMatrix oraz konstrukcji matrycy finalTransform. Za pomocą setProgramConstantsFromMatrix ustawiamy matrycę (transformację) dla renderowanego obiektu i nie da się jej pominąć (a przynajmniej nie ma to sensu, bo nic nie zobaczymy na ekranie), podczas gdy finalTransform posłuży nam do przygotowanie owej matrycy.
Dla osób który nigdy nie miały do czynienia z grafiką 3D będzie to zapewne z początku trudne do zrozumienia, ale wszystko sprawdza się do faktu, że każdy obiekt (sześciany w naszym wypadku) tak naprawdę istnieje niezmodyfikowany przez cały czas działania aplikacji. Innymi słowy gdzieś tam tworzymy naszą kostkę, definiujemy jej trójkąty i nadajemy kolor, ale sama z siebie nie ma żadnej pozycji w przestrzeni - do tego wykorzystywane są właśnie matryce. Sęk w tym, że matryce również same z siebie nic konkretnego nie robią i nie mają żadnego bezpośredniego wpływy na geometrię aż do momentu gdy nie wykorzystamy setProgramConstantsFromMatrix by połączyć geometrię z transformacją. Jednak należy pamiętać, że jest to tylko pewnego rodzaju filtr - "Program" (osobny kod odpowiedzialny za renderowanie) bierze geometrię i przepuszcza ją przez matryce, a rezultat wyświetla na ekranie, nie modyfikując przy tym żadnych zmiennych.
Teraz jeśli chodzi o finalTransform - każdą klatkę animacji zaczynamy od wyzerowania tej matrycy (funkcja identity) by mieć pewność, że nie pozostaną tam jakieś inne transformacje. Ma to szczególne znacznie podczas operacji dodawania (append), ponieważ już istniejąca transformacja ma wpływ na późniejsze rezultaty, na przykład dodanie przesunięcia o 10 jednostek w lewo a potem obrotu o 90 stopni ustawi obiekt w innym miejscu niż gdybyśmy najpierw dodali obrót o 90 stopni a potem przesuniecie o 10 jednostek w lewo. Dokładnie jak w prawdziwym świecie, kierunek jaki obejmiemy na komendę "idź w lewo" będzie zależeć od tego w jaką stronę już spoglądamy. W każdym bądź razie, najlepiej samemu z tym poeksperymentować, pamiętając jedynie, że kolejność dodawani do siebie matryc ma ogromne znaczenie i dlatego właśnie w takiej a nie innej kolejności dodawane są do finalTransform matryce odpowiedzialne za: świat (world), pozycję kamery (view) oraz sposób wyświetlania (projection).

Jak zwykle cały kod użyty w tym artykule jest spakowany i gotowy do pobrania stąd: Tutorial3D.zip

Silnik 3D dla początkujących III [zaawansowany]

W poprzednich częściach tego poradnika poświęconemu tworzeniu bardzo minimalistycznego silnika 3D wspomniałem jak stworzyć iluzję perspektywy oraz jak płynnie poruszać kamerą. Tym razem zajmiemy się obrotem widoku. Będzie to już ostatni rozdział tego poradnika gdyż dalsze zagłębianie się w temacie wymagać będzie znajomości bardziej zaawansowanych zagadnień, a wtedy już po prostu lepiej wziąć się za "prawdziwy" silnik (jak np. Alternativa3D lub Away3D). Do tego najnowsza wersja Flash'a z numerem 11 rozstała rozszerzona o akcelerację 3D co oznacza, że nic nie stoi na przeszkodzie by napisać w ActionScript gry na miarę naszych czasów jak Crysis albo Gears of War (szczególnie, że Adobe zaprezentowało Unreal Engine 3 działający we Flash'u).
No ale na to przyjdzie jeszcze czas, na razie zajmijmy się tą rotacją.

1. Za nim przejdę do konkretów do klasy utworzonej w poprzednich rozdziałach dopiszemy dwie stałe oraz jedną zmienną:

const SPEED:Number = 20;
const ROTATION:Number = Math.PI/16;
var a:Number = 0;
SPEED będzie prędkością poruszania się kamery, a ROTATION prędkością obrotu. Zmienna a posłuży do przechowywania aktualnego kąta obrotu.

2. Pamiętasz gdzie znajduje się kod odpowiedzialny za transformacje informacji 3D na 2D? Zmodyfikujemy go nieco by uwzględniał rotacje widoku:

var nx:Number;
var nz:Number;
var rx:Number;
var rz:Number;
var bx2:Array = new Array(bx.length);
var by2:Array = new Array(by.length);
var i:Number = 0;
while(i<bz.length) {
	nx = cx-bx[i];
	nz = cz-bz[i];
	rx = cx+( nx*Math.cos(-a) )-( nz*Math.sin(-a) );
	rz = cz+( nx*Math.sin(-a) )+( nz*Math.cos(-a) );
	bx2[i] = vw+( ((cx-rx) / (cz-rz) )*vw);
	by2[i] = vh+( ((cy-by[i]) / (cz-rz) )*vh);
	++i;
}
Właściwie różnica nie jest duża, dodałem jedynie parę zmiennych dla przejrzystości oraz wzór na obrót (wzdłuż osi Y, czyli tak samo jak w każdej grze FPS). Jeśli matematyka nie jest ci obca prawdopodobnie nie znajdziesz tutaj nic szokującego. W skrócie z każdego punktu wyliczany jest wektor przesunięcia (na przykładzie x: nx = cx-bx[i];), który następnie poddawany jest obrotowi (( nx*Math.cos(-a) )-( nz*Math.sin(-a) );) i ustawiany na nową pozycję ( rx = cx+...). Nowy punkt trafia do znanej nam już transformacji z 3D do 2D.

3. Teraz pozostaje nam jeszcze tylko zmodyfikować sposób poruszania się kamery tak aby korzystała ona z rotacji. W tym celu wrócimy do kodu odpowiedzialnego za przetwarzanie informacji o wciśniętych klawiszach na komendy w aplikacji, czyli switch(key). Przy okazji to również dobry moment by go trochę uprościć - stary kod w całości zastąp tym:

switch(key&0x0000ff) {
case 0x00000f:
		cz += Math.cos(-a)*SPEED;
		cx += Math.sin(-a)*SPEED;
	break;
	case 0x0000f0:
		cz -= Math.cos(-a)*SPEED;
		cx -= Math.sin(-a)*SPEED;
		break;
}
switch(key&0x00ff00) {
	case 0x000f00:
	a += ROTATION;
		break;
	case 0x00f000:
		a -= ROTATION;
		break;
}
W poprzedniej wersji switch sprawdzał wszystkie możliwe kombinacje zmiennej key, podczas gdy tutaj pierwszy switch wyciąga z key stan jedynie klawiszy góra i dół (0x000f0f&0x0000ff da w rezultacie 0x00000f a więc informacja o wciśniętym klawiszu lewo, 0x000f00, zostanie pominięta), a drugi klawiszy lewo i prawo.
Okej, więc teraz lepiej rozróżniamy wciśnięte klawisze, ale co właściwie się tutaj dzieje? Zaczynając od końca: klawisze lewo i prawo odpowiedzialne są za zmianę kąta obrotu, podczas gdy góra i dół za poruszanie się do przodu i do tyłu. Oczywiście teraz gdy mamy do czynienia z rotacją, definicja "do przodu" cały czas się zmienia stąd też potrzeba wykorzystania funkcji cos i sin które w tym przypadku wyliczają dla nas wektor przesunięcia zależny od aktualnego kąta.

4. Czas sprawdzić czy wszystko działa. Efekt końcowy powinien wyglądać mniej więcej tak :
(Poruszanie się na strzałkach)


A cała klasa do ściągnięcia jest tutaj: box3d_rot.zip

Od AS2 do AS3: duplicateMovieClip() w ActionScript 3 [zaawansowany]

Programiści którzy postanowią przejść na AS3 z AS2 prędzej czy później na pewno zauważą kompletny brak odpowiednika funkcji duplicateMovieClip() w najnowszej iteracji ActionScript'a. Może wydawać się to nieco dziwne, w końcu trudno sobie wyobrazić by Adobe nie było wstanie zaimplementować takiej funkcji - powodem ten decyzji jest prawdopodobnie chęć zabicia złych nawyków w użytkownikach AS3. W każdym razie, czasem po prostu trzeba zduplikować jakiś MovieClip dlatego poniżej prezentuje listę możliwych rozwiązań:

1. Własna klasa.
Myślę, że około 90% ludzi którzy szukają alternatywy dla duplicateMovieClip() powinni być zadowoleni z tego rozwiązania. W skrócie chodzi tutaj o stworzenie klasy podrzędnej MovieClip'a i powiązanie jej z grafiką którą chcemy powielić. Dla przykłady powiedzmy, że tworzymy aplikacje która w tle będzie miała spadające gwiazdy i oczywiście by uzyskać ten efekt stworzymy MovieClip jednej gwiazdy a następnie zaczniemy ją kopiować tyle razy ile jest to potrzebne. W tym celu należy udać się do właściwości MovieClip'a owej gwiazdy i zaawansowanych opcjach zaznaczyć "Eksportuj dla ActionScript".
Eksporotwanie dla ActionScript
Od teraz klasa "Star" zawiera grafikę gwiazdy i można ją duplikować do woli pisząc:

var mc:MovieClip = new Star();
addChild(mc);
I tyle.

2. Skorzystanie z funkcji loadBytes() w klasie Loader.
Więc metoda #1 jest całkiem przyjemna ale co jeśli chcemy zduplikować plik wczytany z zewnętrznego źródła? Zobaczmy jak może w takim wypadku wyglądać ładowanie grafiki:

import flash.display.Loader;
import flash.events.Event;
import flash.net.URLRequest;

lLoad = new Loader();
lLoad.contentLoaderInfo.addEventListener(Event.COMPLETE,onLoaded);
lLoad.load(new URLRequest("your_file.swf"));

function onLoaded(e:Event):void {
	addChild(lLoad.content);
}
Cóż, klasa Loader nie ma zbyt wielu funkcji i po załadowaniu pliku SWF możemy go praktycznie tylko dodać na scenę albo załadować inną grafikę. Oczywiście ponowne wczytanie grafiki z serwera odpada (aczkolwiek jeśli użytkownik ma włączone przechowywanie obiektów w pamięci podręcznej Flash skorzysta właśnie z niej zamiast nawiązywać połączenie jeszcze raz) dlatego duplikacji dokonamy wykorzystując loadBytes(). Trik polega na tym, że loadBytes() jest w stanie wczytać dowolny, obsługiwany przez load(), obiekt zapisany w ByteArray. Pytanie tylko jak uzyskać ByteArray wczytanego MovieClip'a? Okazuje się, że rozwiązanie jest bardzo proste:
function onLoaded(e:Event):void {
	addChild(lLoad.content);
	lLoad.loadBytes(lLoad.content.loaderInfo.bytes);
}
Wartość lLoad.content.loaderInfo.bytes przechowuje wczytaną animację jako ByteArray, więc wystarczy ją wrzucić do funkcji loadBytes by ponownie wywołać zdarzenie onLoaded i dostać kolejny MovieClip bez nawiązywania jakichkolwiek dodatkowych zewnętrznych połączeń.
Jest tylko jeden problem, zdarzenie onLoaded nie zostanie wywołane natychmiast... co oznacza, że wczytany MovieClip zostanie zduplikowany dopiero po kilku lub kilkunastu milisekundach (zależnie od wartości FPS).

3. Skorzystanie z wartości constructor w obiekcie wczytam przez Loader.
Alternatywnym rozwiązaniem do duplikowania SWF wczytanych z zewnątrz jest wykorzystanie wartości constructor dostępnej we wszystkich klasach Flash'a. Bez owijania w bawełnę korzysta się z niej w następujący sposób:

function onLoaded(e:Event):void {
	var d:DisplayObject = new lLoad.content["constructor"]();
	addChild(d);
}
(Zmienna constructor jest dynamiczna więc to jedyny sposób by się do niej dostać. Ponadto nie zapomnij o słowie kluczowym new!)
Niestety, nie jest to idealne rozwiązanie. Kod zdziała tylko wtedy gdy wczytana animacja będzie miała niestandardową "Klasę Dokumentu" (może to być cokolwiek, byle tylko pole "Klasa Dokumentu" nie było puste), co w praktyce oznacza, że to ty musisz być twórcą wczytywanego pliku.

By zaoszczędzić trochę na pracy zaimplementowałem te rozwiązania w jedną klasę - powinna być w stanie zduplikować każdy MovieClip (i nie tylko): foras_clone.zip
A korzysta się z niej w następujący sposób:

import foras.utils.ForCloner;
import flash.display.DisplayObject;
if(stage==null) return;
var fas_Clone:ForCloner = new ForCloner();
var d:DisplayObject = fas_Clone.cloneMovie(my_mc,onClone);
if(d!=null) {
	d.x = stage.stageWidth*Math.random();
	d.y = stage.stageHeight*Math.random();
	addChild(d);
}

function onClone(d:DisplayObject):void {
	d.x = stage.stageWidth*Math.random();
	d.y = stage.stageHeight*Math.random();
	addChild(d);
}
Ponieważ klasa ForCloner również korzysta z metody loadBytes() (o której pisałem w punkcie 2) potrzebna jest funkcja która zajmie się zdarzeniem wczytania pliku i właśnie dlatego cloneMovie() jej wymaga. Oczywiście może się zdarzyć, że wcale nie będzie potrzebna i cloneMovie() natychmiast zwróci zduplikowany MovieClip. Najczęściej jednak będzie to null i wtedy kopia animacji trafi do wybranej funkcji (w tym przykładzie onClone).
Jeszcze jedna uwaga na koniec, linijka if(stage==null) return; zatrzymuje kod przed ponowny wywołaniem - jeśli wszystko inne zawiedzie zduplikowana zostanie cała aplikacja, a to oznacza wczytanie również kodu drugi raz (z całości zostanie wyciągnięty tylko docelowy MovieClip, a reszta zostanie usunięta).

Silnik 3D dla początkujących II [zaawansowany]

W pierwszej części poradnika opisałem jak stworzyć podstawy silnika 3D który będzie w stanie wygenerować trójwymiarowy sześcian, jednak bez możliwości poruszania się taki statyczny obraz zapewne nie był zbyt imponujący. Dlatego w tej część zajmiemy się mechanizmami kontroli kamery.
Zacznijmy od dodania do naszego konstruktora box3d dwóch zdarzeń związanych z klawiaturę:
stage.addEventListener(KeyboardEvent.KEY_DOWN,eventKeyDown);
stage.addEventListener(KeyboardEvent.KEY_UP,eventKeyUp);
(Nie zapomnij zaimportować flash.events.KeyboardEvent!)
Nasłuchiwacze zostały powiązane z wartością stage ponieważ w przypadku innych obiektów trzeba byłoby najpierw ustawić na nich "focus" co na razie jest nam kompletnie nie przydatne i stage w zupełności wystarczy. W każdym razie, musimy jeszcze stworzyć funkcje obsługujące owe zdarzenia:
private function eventKeyDown(e:KeyboardEvent):void {

}
private function eventKeyUp(e:KeyboardEvent):void {

}
W tym momencie sugerował bym dopisać do którejś z nich linijkę trace(e.keyCode); by upewnić się, że Flash poprawnie przechwytuje naciśnięte klawisze.
W następny kroku zajmiemy się już właściwym wyłapywaniem klawiszy i tłumaczeniem ich na odpowiedni ruch kamerą, i choć całość można byłoby załatwić pisząc po prostu:
if(e.keyCode == Keyboard.LEFT) cx -= 20;
else if(e.keyCode == Keyboard.RIGHT) cx += 20;
Takie rozwiązanie jest niestety nie tylko ograniczające ale w dodatku niezbyt płynne. Dlatego w naszym silniku zastosujemy nieco bardziej zaawansowaną metodę opartą o operacje bitowe - tylko za nim ją opisze, dodaj do klasy nową zmienną: var key:Number = 0; której używać będziemy w następujący sposób:
private function eventKeyDown(e:KeyboardEvent):void {
	switch(e.keyCode) {
		case Keyboard.UP:
			key = key | 0x00000f;
			break;
		case Keyboard.DOWN:
			key = key | 0x0000f0;
			break;
		case Keyboard.LEFT:
			key = key | 0x000f00;
			break;
		case Keyboard.RIGHT:
			key = key | 0x00f000;
			break;
	}
}
(Nie zapomnij importować flash.ui.Keyboard!)
Okej, musze przyznać, że na pierwszy rzut oka wygląda to trochę bez sensu (szczególnie dla osób które nie miały styczności z operacjami bitowymi lub liczbami szesnastkowymi), ale za parę chwil wszystko stanie się jasne. Owy kod przypisuje pewne wartości do zmiennej key zależnie od wciśniętego klawisza - wciskając klawisze strzałek "do góry" i "w lewo" key będzie mieć wartość "0x000f0f". Rezultat ten otrzymujemy poprzez operacje bitowe na liczbach szesnastkowych, a więc dla przykładu jeśli weźmiemy liczby "0x000f00" oraz "0x00000f" i wykonamy na nich operację | (OR) dostaniemy "0x000f0f". Ta kreska | jest znana jako bitowe "lub" i przypomina w pewnych sytuacjach dodawanie - 100|1 da wynik 101 (więcej informacji znajdziesz w dokumentacji ActionScript'a). W każdym razie, dla nas ważne jest tylko to, że key będzie przypominał trochę jakby listwę ze światełkami gdzie 0 = zgaszona lampka (klawisz nie jest wciśnięty) a f = zapalona lampka (klawisz jest wciśnięty). 0x00ffff = wszystkie klawisze wciśnięte, 0x00f00f0 = tylko klawisz w dół i w prawo, 0x000000 = żaden klawisz nie jest wciśnięty itd.
Jednak sam eventKeyDown służy nam jedynie do zapalania światełek. Do pełnego obrazu brakuje nam jeszcze przeciwnej funkcji czyli:
private function eventKeyUp(e:KeyboardEvent):void {
	switch(e.keyCode) {
		case Keyboard.UP:
			key = key ^ 0x00000f;
			break;
		case Keyboard.DOWN:
			key = key ^ 0x0000f0;
			break;
		case Keyboard.LEFT:
			key = key ^ 0x000f00;
			break;
		case Keyboard.RIGHT:
			key = key ^ 0x00f000;
			break;
	}
}
W tym wypadku bitowe ^ (XOR) działa trochę jak odejmowanie, a więc operacja " 0x000f0f ^ 0x00000f" zwróci "0x000f00".
Najgorsze za nami, teraz już mamy w pełni funkcjonalną zmienną key która sygnalizuje nam jakie klawisze są w użyciu i pozostaje nam jedynie przetłumaczyć je na ruch kamery. W tym celu przejdź do funkcji render (utworzyliśmy ją w poprzedniej część poradnika) i przed linijką graphics.clear(); dopisz:
switch(key) {
	case 0x00000f: // ^
		cz += 20;
	break;
	case 0x0000f0: // v
		cz -= 20;
		break;
	case 0x000f00: // <
		cx -= 20;
		break;
	case 0x00f000: // >
		cx += 20;
		break;
}
To już prawie koniec. Skompiluj aplikacje i sprawdź czy wszystko działa poprawnie. Jeśli wcześniej próbowałeś poruszać kamerą bezpośrednio z poziomu eventKeyDown i eventKeyUp na pewno zauważyłeś teraz poprawę (a jeśli nie, proponuję powrót do owego rozwiązania i porównanie z aktualnym).
W każdym razie, została jeszcze jedna mała rzecz - poruszanie się po skosie. Używanie dwóch klawiszy naraz ustala wartość key na liczbę którą nasz switch(key) nie obsługuje, dlatego na koniec musimy go jeszcze rozbudować o cztery dodatkowe warunki:
	case 0x000f0f:
		cz += 20;
		cx -= 20;
		break;
	case 0x00f00f:
		cz += 20;
		cx += 20;
		break;
	case 0x000ff0:
		cz -= 20;
		cx -= 20;
		break;
	case 0x00f0f0:
		cz -= 20;
		cx += 20;
		break;
I tyle. W razie czego całą klasę można pobrać stąd: box3d_2.zip
A sama aplikacja zobaczyć tutaj: box3d.swf