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

Podstawy Stage3D część IV: korzystanie z shaderów [ekspert]

Shadery są już nie odłączanym elementem w grafice 3D i to do tego stopnia, że nie możliwe jest wyświetlanie obrazu bez ustawienia chociaż najprostszego programu cieniującego. Dlatego tym razem opisze pokrótce jak pracować z shaderami na przykładzie oświetlenia i choć te można symulować na kilka różnych sposobów to właśnie shadery będą najłatwiejszym rozwiązaniem na dodanie świtała w naszym wirtualnym świecie. Programu cieniujące, czyli zestawy instrukcji przekazywanych karcie graficznej wymagają znajomości AGAL który jest zupełnie innym językiem od ActionScript i na jego opanowanie potrzebny byłby całkowicie osobny poradnik - tych na szczęście jest całkiem sporo w Internecie, dlatego na razie pominę szczegóły i skupię się tylko na absolutnym minimum potrzebnym do zastosowania prostego cieniowania.
Mając kod z poprzedniego artykuły, od razu zacznijmy od przygotowania Fragment Shader'a:
private const FRAGMENT_SHADER_LIGHT:String = "dp3 ft1, fc2, v1 n"+
"neg ft1, ft1 n"+
"max ft1, ft1, fc0 n"+
"mul ft2, fc4, ft1 n"+
"mul ft2, ft2, fc3 n"+
"add oc, ft2, fc1";
oraz Vertex Shader'a:
private const VERTEX_SHADER_LIGHT:String = "mov vt0, va0n"+
"m44 op, vt0, vc0n"+
"nrm vt1.xyz, va0.xyzn"+
"mov vt1.w, va0.wn"+	
"mov v1, vt1n" +
"mov v2, va1";
(Kod zapożyczony z http://blog.norbz.net/2012/04/stage3d-agal-from-scratch-part-vii-let-there-be-light)
Powyższe shadery będą rozjaśniać renderowane trójkąty zależnie od ich kąta i ustawienia względem kamery stwarzając wrażenie jakoby ta rzucała światło. Za nim cokolwiek zrobimy z tym kodem musimy go jeszcze skompilować do specjalnego "programu", który przygotujemy wykorzystując AGALMiniAssembler:
vertexAssembly.assemble(Context3DProgramType.VERTEX, VERTEX_SHADER_LIGHT, false);
fragmentAssembly.assemble(Context3DProgramType.FRAGMENT, FRAGMENT_SHADER_LIGHT, false);

programPairLight = renderContext.createProgram();
programPairLight.upload(vertexAssembly.agalcode, fragmentAssembly.agalcode);
(Nie zapomnij utworzyć zmiennej programPairLight)
W tym momencie mamy w naszej aplikacji dwa shadery: standardowy programPair oraz generujący oświetlenie programPairLight. Pozostanie nam jeszcze tylko zdecydować z którego będziemy korzystać podczas renderowania grafiki 3D wykorzystując funkcje setProgram w obiekcie Context3D. Aby łatwiej było widać różnicę w ich działaniu stworzymy osobny obiekt Box3D (nazwijmy go "box3DLight"), a następnie dopiszemy kod odpowiedzialny za jego wyświetlanie przed wywołaniem present() ale po wyrysowania "box3D":
boxMat = box3DLight.getMatrix3D();
boxMat.identity();
boxMat.append(rotMat);
boxMat.appendTranslation(posVec.x + 1, posVec.y + 1, posVec.z+1);

finalTransform.identity();
finalTransform.append(boxMat);
finalTransform.append(world);
finalTransform.append(camera);
finalTransform.append(projection);
	
renderContext.setProgram(programPairLight);
renderContext.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, finalTransform, true);

var p:Vector3D = camera.position.clone();
p.normalize();
var m3d:Matrix3D = finalTransform.clone();
m3d.invert();
p = m3d.transformVector(p);
p.normalize();
p.negate();

renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, Vector.<Number>([0,0,0,0]));
renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 1, Vector.<Number>([0,0,0,0]));
renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 2, Vector.<Number>([p.x,p.y,p.z,1]));
renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 3, Vector.<Number>([1, 1, 1, 1]));
renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 4, Vector.<Number>([0.6, 0.6, 0.6, 1]) );

box3DLight.render(renderContext);
Jest tego trochę, ale najważniejsze dla nas to to co się dzieje po linijce setProgram (operacje na "boxMat" i "finalTransform" to znane nam już transformacje z 3D do 2D). Termin "program" jest tutaj nieprzypadkowy bo shadery podobnie jak każdy inny program na komputerze wykonują pewne operacje matematyczne na zmiennych i stałych, aczkolwiek wykorzystując do tego bardzo nisko-poziomowy język. Dodatkowo w shaderach istnieją duże ograniczenia i tak np. zmienne mogą być tworzone tylko w programie i może ich być góra 8. Na szczęście inaczej sprawa ma się ze stałymi, ponieważ te możemy przekazywać z zewnątrz wykorzystując do tego funkcje setProgramConstantsFromVector oraz setProgramConstantsFromMatrix (również maksymalnie 8) i to właśnie dzięki nim do programu cieniującego przekażemy informacje o położeniu światła (zmienna "p") i jego kolorze.
Jeśli wszystko pójdzie zgodnie z planem powinniśmy zobaczyć coś takiego:
W tym przykładzie operujemy jedynie na dwóch shaderach, jednak nic nie stoi na przeszkodzie by stworzyć ich o wiele, wiele więcej a następnie wymieniać je funkcją setProgram gdy tylko chcemy, podobnie jak w Graphics wywołujemy funkcję lineStyle.
Całość do pobrania stąd: Tutorial3D_light.zip.

Podstawy Stage3D część III: korzystanie z rotacji [ekspert]

Kontynuując z poprzedniej części tutaj tym razem chce poruszyć temat łączenia rotacji. Każdy kto bez wcześniejszego kontaktu z 3D spróbował obrócić bryłę we Flash'owym Stage3D korzystając z funkcji appendRotation() na pewno prędzej czy później otrzymał wynik, którego się nie spodziewał. Do tego by jeszcze bardziej zniechęcić początkujących, w Internecie główną odpowiedział na owe zagadnienie jest "będziesz musiał skorzystać z liczb zespolonych (kwaternionów dokładnie)". Hmm? Dobra, nie wiem jak bardzo liczby zespolone przydają się do wyliczania obrotu, ale jeśli do rotacji 2D nie potrzebne są obliczenia 3D, to i zapewne w obrocie 3D da się przeżyć bez operacji 4D, a już szczególnie jeśli dopiero zaczynamy poruszać ten temat.
W każdy bądź razie, przejdźmy do sedna sprawy zaczynając od kodu z poprzedniego artykułu: Tutorial3D-2.zip, najpierw usuńmy wszystkie sześciany zostawiając tylko jedno pudełko, reszta nie będzie nam potrzebna. Przy okazji dodać można jeszcze zmienne do przechowywania jednego Vector3D i jednej Matrix3D - przydadzą się później.
Generalnie rotacje można dodać na dwa sposoby: lokalnie i globalnie - innymi słowy biorąc pod uwagę już istniejący obrót lub nie. Jakikolwiek sposób wybierzemy należy pamiętać, że na transformacje ma jeszcze wpływ istniejące przesunięcie, dlatego by ułatwić sobie pracę dobrze jest trzymać samą rotację w osobnej macierzy, a potem ją dodawać do transformacji naszego pudełka 3D. Tak więc zacznijmy od rotacji globalnej oraz utworzenia dodatkowej matrycy (jeśli jeszcze tego nie zrobiłeś):
private var rotMat:Matrix3D = new Matrix3D();
Następnie zmodyfikujemy kod odpowiedzialny za obsługę strzałek klawiatury tak by teraz dodawał obrót do matrycy rotMat:
switch(key&0x0000ff) {
	case 0x00000f: // forward
		rotMat.appendRotation(SPEED, Vector3D.X_AXIS);
		break;
	case 0x0000f0: // backward
		rotMat.appendRotation(-SPEED, Vector3D.X_AXIS);
		break;
}
switch(key&0x00ff00) {
	case 0x000f00: // left
		rotMat.appendRotation(SPEED, Vector3D.Y_AXIS);
		break;
	case 0x00f000: // right
		rotMat.appendRotation(-SPEED, Vector3D.Y_AXIS);
		break;
}
W tym momencie pozostaje nam już tylko dodać rotacje do obiektu sześcianu box3D zanim zostanie utworzona ostateczna transformacja w matrycy finalTransform:
var boxMat:Matrix3D = box3D.getMatrix3D();
boxMat.identity();
boxMat.append(rotMat);
W efekcie dostając:
Żeby uczynić ten przykład nieco ciekawszym i przy okazji sprawdzić, że faktycznie rotacja będzie niezależna od pozycji, możemy wykorzystać wcześniej wspomniany Vector3D. Nazwijmy go:
private var posVec:Vector3D = new Vector3D();
Jego manipulacja nie powinna być problemem na tym poziomie, dlatego wspomnę tylko aby dodać jego wartości w odpowiednim miejscu - zaraz po linijce boxMat.append(rotMat); należy dopisać:
boxMat.appendTranslation(posVec.x, posVec.y, posVec.z);
Otrzymując:
Tak więc globalna rotacja nie była szczególnie skomplikowanym zagadnieniem, ale pozostaje jeszcze kwestia lokalnej transformacji. Na szczęście i tutaj rozwiązanie jest trywialne i sprowadza się tylko do znalezienia prawidłowych osi w obiekcie:
switch(key&0x0000ff) {
	case 0x00000f: // forward
		rotMat.appendRotation(SPEED, rotMat.transformVector(Vector3D.X_AXIS));
		posVec.y += SPEED/50;
		break;
	case 0x0000f0: // backward
		rotMat.appendRotation(-SPEED, rotMat.transformVector(Vector3D.X_AXIS));
		posVec.y -= SPEED/50;
		break;
}
switch(key&0x00ff00) {
	case 0x000f00: // left
		rotMat.appendRotation(SPEED, rotMat.transformVector(Vector3D.Y_AXIS));
		posVec.x -= SPEED/50;
		break;
	case 0x00f000: // right
		rotMat.appendRotation(-SPEED, rotMat.transformVector(Vector3D.Y_AXIS));
		posVec.x += SPEED/50;
		break;
}
Wektor X_AXIS zostanie przekształcony do lokalnego systemu obiektu, dzięki czemu będziemy wiedzieli gdzie ma oś X w około której następnie dokonamy obrotu. W rezultacie jedna z krawędzi obracanego sześcianu będzie pozostawać nieruchomo, zależnie od wybranej osi:
Kod źródłowy: Tutorial3D_rot.zip

Zrozumieć symbol "Graphics"

Każdy kto spędził trochę czasu z ActionScript'em na pewno doskonale zna działanie MovieClip'ów jednak Flash pozwala również na stworzenie czegoś w miarę podobnego, ale zwanego symbolem "Graphics". Na pierwszy rzut oka mogą się po prostu wydawać uproszczonymi wersjami MovieClip'ów i jest to po części prawda ale w ich działaniu obowiązuje pewna o wiele prostsza zasada... w trakcie eksportu do SWF wszystkie obiekty "Graphics" zostają rozbite na kawałki, podobnie jak to robi polecenie "break apart" (CLTR+B) z tą różnicą, że wszystkie ustawienia i tween'y zostają nałożone na obiekty w środku. Z punktu widzenia ActionScript daje to nam ciekawe możliwości optymalizacji w sytuacjach gdy mamy do czynienia z zagnieżdżającymi się MovieClip'ami.
Weźmy na przykład scenariusz w którym chcemy, aby dwa obiekty krążyły w około siebie ale jednocześnie razem przesuwały się na nową pozycję:

Tak więc symbol Anim zawiera animację krążących obiektów, a tymczasem na scenie przesuwamy całość w prawo. Nic nadzwyczajnego, ale jeśli dodamy do tego chęć dostępu do owych krążących obiektów z poziomu ActionScript instynktownie pierwszym krokiem będzie zmiana Anim z "Graphics" na "MovieClip" by następnie nadać mu nazwę instancji i dalej odwoływać się przez nią by dotrzeć do dwóch obiektów w środku. To właśnie w takich sytuacjach wykorzystanie symbolu Graphics zamiast MovieClip pozwoli pozbyć się dodatkowego zagnieżdżenia, o ile oczywiście wiemy jak Graphics działa.
Proponuje przeprowadzić następującym eksperyment, stwórz na scenie dwa MovieClip'y z dwoma różniącymi się klatkami animacji, następnie nazwij je "mc1" oraz "mc2" (nazwy instancji, tak żeby można było się do nich odwołać z ActionScript) a całość dodatkowo zamknij w symbolu Graphics. Co się stanie jeśli ze sceny gdzie znajduje się owy symbol Graphics wywołamy poniższe funkcje?
mc1.gotoAndStop(1);
mc2.gotoAndStop(2);
Ponieważ Graphics zostanie rozbity i wszystkie wewnętrzne elementy wylądują na zewnątrz odwołania do "mc1" i "mc2" przejdą bez problemów.
Gotowy przykład do ściągnięcia tutaj: GraphExample.zip

Ograniczanie dostępu do wybranych stron przez ActionScript

Nie ma co się oszukiwać, cokolwiek pojawi się w Internecie może być skopiowane i rozpowszechniane bez zgody właściciela i Flash nie jest tutaj wyjątkiem. Na szczęście w przeciwieństwie do zdjęcie czy muzyki, z odrobiną ActionScript można ograniczyć działanie aplikacji Flash tylko do wybranych stron.
Zadanie jest proste, pobierzemy adres z którego został uruchomiony nasz Flash i porównamy z adresem który chcielibyśmy aby był:
function checkDomain(...domains):Boolean {
	if(domains == null || domains.length==0) return false;
	
	var sList:String = ".*"+domains.join("|.*");
	var sURLswf:String = stage.loaderInfo.url;
	var sRegPattern:String = "^http(|s)://("+sList+")/";
	var regPattern:RegExp = new RegExp(sRegPattern,"i");
	
	return regPattern.test(sURLswf);
}
Funkcja checkDomain pobiera nazwy domen po przecinku i porównuje je z aktualnym adresem pliku SWF - jeśli któraś z podanych domen pasuje do URL, zwracane jest true. Do porównania wykorzystywane są wyrażenia regularne które nie są tematem tego artykuły, dlatego tylko pokrótce wyjaśnię, że zmienna "sURLswf" jest porównywana do schematu ze zmiennej "sRegPattern", a to z kolei została utworzona z listy dostarczonych domen.
Tyle w zasadzie wystarczy, ale istnieje jeszcze szansa, że nasz plik SWF zostanie wczytany przez zewnętrzny Loader, a wtedy "loaderInfo.url" zwróci oryginalni adres pliku pomimo że sam Loader jest na innej stronie. Rozwiązanie? Banalne, wystarczy do funkcji checkDomain dopisać:
var sURLLoader:String = stage.loaderInfo.loaderURL;
...
return regPattern.test(sURLswf) && regPattern.test(sURLLoader);
I gotowe, teraz już nikt nie uruchomi naszej aplikacji na niepowołanych serwerach.
Należy jednak pamiętać, że funkcja ta nie chroni nas przed tzw. hot-linkowaniem, czyli na wczytywaniu plików z naszego serwera, na zupełnie innej stronie WWW. To jednak już jest poza możliwościami Flash'a i powinno być zabezpieczone po stronie serwera - polecam poszukać informacji na temat hot-linkowania i "htaccess".
Co jednak jeśli nasz hosting po prostu nie oferuje takich zabezpieczeń?
Jest jeden trik który może nam pomóc, ale niestety nie gwarantuje 100% skuteczności - właściwie to nie jestem pewien czy w ogóle gwarantuje jakąkolwiek szansę na sukces. Za nim jednak wyjaśnię o co chodzi, upewnij się że podczas embedowania pliku SWF na swojej stronie ustawisz parametr allowScriptAccess na "sameDomain". Ten prosty zabieg pozwoli aplikacji Flash na wywoływanie poleceń JavaScript, a to z kolei pozwoli nam odczytać aktualny adres URL z paska przeglądarki. W tym celu należy do kodu ActionScript dopisać:
var  sURLTop:String;
if(ExternalInterface.available) {
	try {
		sURLTop = ExternalInterface.call("function(){return window.location.href}") as String;
	}catch(e:Error) {
		 sURLTop = "";
	}
} else {
	sURLTop = "";
}
W tym momencie osoby trzecie już nie będą w stanie zrobić wiele (jeśli wszystko poszło zgodnie z planem) - pozwalając na wykonanie JavaScript'u dostaniemy w naszym kodzie adres który od razu odrzucimy, a z kolei blokując go otrzymamy w kodzie Error który również potraktujemy jako brak dostępu (w końcu na naszej stronie JavaScript jest dostępny). To rozwiązanie byłoby idealne ale nie mamy gwarancji, że osoby przeglądające naszą stronę będą mieć JavaScript włączony...

Precyzyjne porównanie do typu klasy bez wykorzystywania 'is' i 'instanceof'

W ActionScript2 i 3 istnieje możliwość sprawdzenia czy dany obiekt należy do pewnej klasy - w AS2 jest to słowo instanceof a w AS3 is i oba spełniają swoja zadanie całkiem nieźle, jednak należy pamiętać, że oba zwrócą 'true' nawet jeśli docelowy obiekt nie jest dokładnie z tej samej klasy, tylko ją rozszerza. Innymi słowy porównanie obiektu MovieClip do Sprite zwróci 'true':
var mc:MovieClip = new MovieClip();
trace(mc is Sprite); //true
Tak więc co zrobić gdy chcemy sprawdzić, że wybrany obiekt faktycznie należy do docelowej klasy i nic po za tym? Rozwiązania są dwa:
1. Funkcja getQualifiedClassName.
Czyli najprostsze podejście - wyciągamy nazwę klasy w postaci String i porównujemy ją do nazwy której szukamy.
var mc:MovieClip = new MovieClip();
trace(getQualifiedClassName(mc) == "MovieClip"); //true
trace(getQualifiedClassName(mc) == "Sprite"); //false
2. Porównanie konstruktorów.
To już temat nieco bardziej zaawansowany, a do tego możliwy tylko w AS3. Nie jest to do końca jasno napisane, ale każdy obiekt w ActionScript3 ma pole o nazwie 'constructor' zawierające odniesienie do konstruktora użytego przy tworzeniu obiektu, a ponieważ w AS konstruktor w każdej klasie może być tylko jeden wystarczy je porównać:
var mc:MovieClip = new MovieClip();
var con:Object = MovieClip.prototype.constructor;
trace(mc.constructor == con); //true
Przy czym pamiętać tutaj należy o kilku rzeczach:
- ponieważ 'prototype' i 'constructor' są przydzielone dynamicznie ich czas dostępu będzie wolniejszy niż ten z wykorzystaniem funkcji getQualifiedClassName.
- aczkolwiek w odpowiednio dużej pętli, zrzucając uprzednio konstruktor do lokalnej zmiennej, czas wykonania może być nawet o połowę szybszy.
- mc.constructor w FlashDevelop (ale nie Flash Professional) najprawdopodobniej wyrzuci błąd, który można obejść rzutując mc do klasy Object:
var con:Object = MovieClip.prototype.constructor;
trace((mc as Object).constructor == con); //true
Niestety, rzutowanie spowolni czas wykonania o jakieś 10%.

Interface'y we Flash'u [podstawowy]

Interfejsami w programowaniu nazywamy abstrakcyjne definicje metod które sam z siebie nic nie robią po za narzucaniem implementującym je klasą pewnej składni. Innymi słowy nazwę funkcji i jej argumenty definiujemy w jednym miejscu a w drugim implementujemy (docelowej klasie) jej działanie. Nie wątpliwie są szybsze i łatwiejsze sposoby by tworzyć funkcje we Flash’u jednak interfejsy mają jedną szczególna cechę powodującą, że są wręcz nie zastąpione w specyficznych scenariuszach. Tą cechą jest możliwość rzutowania obiektów do implementowanych przez nich klas, a co za tym idzie będziemy mogli się odwołać do definiowanych metod bez dokładnej znajomości całego obiektu.
Myślę, że najlepiej będzie to przedstawić na przykładzie, zaczynając od samego początku: powiedzmy, że tworzymy grę w której informacje przekazujmy przez wyskakujące okienka zawierające przycisk “OK”.
public class MyPopup extends MovieClip {
	private var myButton:OKButton;
	public function MyPopup() {
		myButton = new OKButton(this);
		addChild(myButton);
	}

	public function close():void {
		this.visible = false;
	}
}
public class OKButton extends MovieClip {
	private var myPopup:MyPopup;
	public function OKButton(popup:MyPopup) {
		myPopup = popup;
		this.addEventListener(MouseEvent.CLICK,onMouse);
	}
	private function onMouse(e:MouseEvent):void {
		myPopup.close();
	}
}
Z początku wszystko może wydawać się w porządku, ale jeśli tylko zaczniemy rozbudowywać nasz grę o kolejne okienka, ograniczenia powyższego kodu staną się oczywiste - co zrobić gdy dodamy MyPopup2 ale będziemy chcieli wykorzystać ten sam przycisk? Czy dopisanie do konstruktora kolejnego argumentu ( OKButton(popup:MyPopup, popup2:MyPopup2) {} ) wystarczy? A co jeśli później dodamy kolejne okienka? Jednym rozwiązaniem jest na pewno utworzenie klasy podrzędnej wspólnej dla wszystkich okienek, odwołując się do niej zamiast wszystkich nadrzędnych klas z osobna. Jednak klasa bazowa może przysporzyć we Flash’u kłopotów których nie będę tutaj opisywał. Myślę, że w tym momencie jest już jasne do czego zmierzam - najlepszym rozwiązaniem będzie wykorzystanie interfejsów. Tak więc stwórzmy osobny plik AS:
public interface IPopup {
	function close():void
}
A istniejące klasy przeróbmy w następujący sposób:
public class MyPopup extends MovieClip implements IPopup{
	private var myButton:OKButton;
	public function MyPopup() {
		myButton = new OKButton(this);
		addChild(myButton);
	}

	public function close():void {
		this.visible = false;
	}
}
public class OKButton extends MovieClip {
	private var myPopup:IPopup;
	public function OKButton(popup:IPopup) {
		myPopup = popup;
		this.addEventListener(MouseEvent.CLICK,onMouse);
	}
	private function onMouse(e:MouseEvent):void {
		myPopup.close();
	}
}
Różnica jest niewielka, ale możliwości o wiele większe. Od teraz MyButton może się komunikować ze wszystkimi stworzonymi okienkami bez potrzeby poznania ich klasy, wystarczy że implementują ten sam interfejs.

Debugowanie w lokalnym środowisku [podstawowy]

Ostatnim razem wspomniałem o kilku trikach debugowania w aplikacjach który już zostały wydane na świat - tym razem zerkniemy na debugowanie w lokalnym środowisku, w trakcie produkcji. Oczywiście wszystko w tym wypadku zależy od nas id od tego jak przygotujemy aplikację, jednak jest parę uniwersalnych rozwiązań które na pewno zawsze się przydadzą:

1. Pomiar prędkości działania
Pomijając fakt, że zawsze warto programować aplikacje biorąc pod uwagę również słabsze komputery, pomiar prędkości jest dobrym sposobem by przetestować poprawność algorytmów, szczególnie jeśli z góry znam czas jaki powinny osiągnąć. Do pomiarów najlepiej dodać pętle by łatwiej było dostrzec różnice w czasie:

public static function measureProcessTime(fun:Function, repeat:int = 10):Number {
	var time:Number = getTimer();
	for(var i:int = 0; i < repeat; i++) {
		fun();
	}
	return getTimer() - time;
}
Warto dodać, że w tym wypadku bardzo przydają się lokalne funkcje które we Flash'u można utworzyć dosłownie wszędzie i zawsze będą działać, wystarczy dowolny kod zamknąć w następujący sposób:
var someVar:Number = 0;
someVar = 1;
function debug() {
	var otherVar:Number = 10;
	someVar += otherVar;
}
trace(measureProcessTime(debug));
}

2. Wędrowanie przez pętle.
Kolejny sposób na testowanie algorytmów. Najprościej mówiąc chodzi tutaj o wyświetlanie informacji tylko na wybranym punkcie pętli ale tak, żeby w każdej chwili można było zmienić jego położenie. Przykładowa implementacja:

private static var DEBUG_COUNT:int = 0;
private static var DEBUG_STEP:int = 0;
public static function nextStep():void {
	DEBUG_STEP++;
}
public static function prevStep():void {
	DEBUG_STEP--;
	if(DEBUG_STEP<0) DEBUG_STEP=0;
}
public static function resetDebug():void {
	DEBUG_COUNT = 0;
}
public static function countDebug():Boolean {
	DEBUG_COUNT++;
	return  DEBUG_COUNT == DEBUG_STEP;
}
Funkcje nextStep i prevStep należy podpiąć pod klawisze strzałek by można było się poruszać po pętli. Funkcja resetDebug powinna się pojawić przed rozpoczęciem testowanej pętli. I w końcu countDebug która zwróci true gdy doliczy do wybranego kroku. Choć nie ma tutaj nic nadzwyczajnego, wędrowanie po pętli ułatwia debugowanie w nie porównywalnym stopniu. W działaniu, z wykorzystaniem algorytmu rysowania linii może to wyglądać tak:
(Użyj klawiszy góra i dół by wybrać krok algorytmu)

3. Nakładka graficzna.
Chyba nie ma wątpliwości, że wyświetlanie informacji w postaci przejrzystej grafiki zamiast listy zmiennych jest zawsze o niebo przyjemniejsze, dlatego każda porządna aplikacja powinna dawać możliwość rysowania (z wykorzystaniem klasy Graphics) ponad wszystkimi obiektami. Osiągnąć to można trzymając całą aplikację w jednym MovieClipie, coś na rodzaj "zbiornika", a tuż nad nim umieścić obiekt, np. Sprite, który będzie pusty, ale do którego zawsze będzie dostęp z każdej lokacji w aplikacji:
public static debug:Sprite;
public function DocumentClass() {
	var main:MyApplication = new MyApplication();
	addChild(main);

	var debug:Sprite = new Sprite();
	addChild(debug);
}
Teraz wystarczy napisać DocumentClass.debug.graphics by mieć dostęp do grafiki widocznej ponad całą aplikacją. Osobiście często korzystam z tego by sprawdzić rozmiary obiektów znajdujących się na ekranie:

Debugowanie po stronie użytkownika [podstawowy]

Debugowanie to potoczna nazwa procesu usuwania błędów (lub po prostu nie pożądanego działania) z kodu źródłowego. Programując w Flash Professional lub FlashDevelop mamy dostęp do narzędzie debuggera które swoją rolę sprawuje całkiem nieźle i na pewno nie jednej osobie pomogło już usunąć błędy we własnych projektach. Niestety debugger jest narzędziem dostępnym jedynie twórcom aplikacji którzy jednak nie zawsze są w stanie przewidzieć (i sprawdzić) wszystkie możliwe scenariusze działania które podejmie docelowy odbiorca. Dlatego dla pewności zawsze warto zaimplementować system wyłapywania błędów od strony korzystających z aplikacji użytkowników - w tym celu przyjrzymy się kilku najpopularniejszym mechanizmom.

1. Główna pętla.
Ten, zdawać by się mogło, trywialny element powinien być częścią każdej szanującej się aplikacji i to nie tylko dlatego, że ułatwia debugowanie ale również czyni kod przejrzystszym i mniej podatnym na wycieki pamięci. Ale czym właściwie jest "główna pętla"? We Flash'u sprowadza się do posiadania tylko JEDNEGO zdarzenia "ENTER_FRAME" w całej aplikacji, co oznacza, że wszystkie inny wykonywane funkcja będą wywodzić się bezpośrednio (lub pośrednio) z głównej pętli. Takie podejście ułatwia wyłapywanie błędów w nieporównywalnym stopniu bo wystarczy, że kod głównej pętli obejmiemy słowami kluczowymi try i catch, a już żaden wyrzucony Error nie umknie naszej uwadze. Główna pętla z try i catch może wyglądać na przykład tak:

public function render(e:Event):void {
	try {
		processPlayer();
		processEnemy();
		processMap();
	} catch(e:Error) {
		trace("Error!");
	}
}

2. Dziennik.
Chociaż wyłapywanie błędów jest bardzo ważne, to jednak samo w sobie może okazać się mniej istotne niż na przykład okoliczności w których doszło do wyrzucenia wyjątku, dlatego drugim bardzo ważnym elementem każdej aplikacji powinien być dziennik zapisujący wykonywanie wszystkich kluczowych funkcji przez użytkownika - w jakim stanie rozpoczął korzystanie z aplikacji (wersja Flash, system operacyjny itd.), jakich przycisków używał, przez jakie okienka przeszedł itp. A jeśli będzie to gra, to na pewno na jakiej jest mapie, w jakiej jest pozycji i z czym miał już do czynienia. Przechowując wszystkie te informacji łatwiej będzie nam odtworzyć warunki w jakich użytkownik otrzymał błąd.
Klasa dziennika jest zawsze najprostszym elementem do zaimplementowania:

public class Log {
	private static var log:String = "";
	public function Log() {}
	public static function add(s:String):void {
		log += s+"n";
	}
	public static function toClipboard():void {
		System.setClipboard(log);
	}
}

3. Konsola.
Chyba każdy gracz chociaż raz korzystał z konsoli w grach PC, choćby po to żeby sobie wpisać "kody na nieśmiertelność". Konsola to potoczne określenie (zwykle ukrytej) linii komend w aplikacji, pozwalająca użytkownikowi wymusić działanie pewnych, z góry ustalonych funkcji. Zaimplementowanie dobrej konsoli to już nieco roboty, na szczęście w Internecie można znaleźć sporo gotowych rozwiązań, jak na przykład: Flash Console. Zdecydowanie każdy powinien mieć linie komend w swojej aplikacji - jeden raz wystarczy by już na zawsze się w niech zakochać.

Na koniec gdy nasza aplikacja jest już gotowa, warto w głównej pętli pozbyć się funkcji trace i zastąpić ją czymś co pozwoli nam otrzymać informacje o ewentualnych błędach, na przykład wysyłając je sobie na e-mail.

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 silnika kafelkowego część 1: widok izometryczny [podstawowy]

W tym trzyczęściowym artykule postaram się przybliżyć kilka najbardziej podstawowych zagadnień z silników opartych o kratki - czy też kafelki (tile-based). Owe silniki istnieją już od bardzo dawna i wydawać by się mogło, że już wszystko w tej sprawie zostało powiedziane, jednak wciąż wiele osób ma z nimi problemy, nierzadko komplikując trywialne zagadnienia.
Jednym z takich zagadnień jest widok izometryczny (czasem nazywanym "widokiem po skosie") będący specyficznym rodzajem transformacji 3D bez dodatku perspektywy, często wykorzystywanym w starszych grach strategicznych. Chociaż definicja mówi o transformacji 3D takowa wcale nie jest wymagana i można ją zastąpić czymś o wiele prostszym, co niestety nie wszyscy próbują zrobić i dlatego w Internecie można całkiem sporo podobnych artykułów, które jednak najpierw tłumaczą podstawy 3D - nie potrzebnie.
Całość można sprowadzić do pojedynczej funkcji:
function getTile(nTargetX:Number,nTargetY:Number,nWidth:Number,nHeight:Number):Point {
	var nScale:Number = nWidth/nHeight;
	var nTransY:Number = (nScale*nTargetY-nTargetX)/2;
	var nTransX:Number = nTargetX+nTransY;
	var nTmpY:Number = Math.round( nTransY/(nHeight*nScale) );
	var nTmpX:Number = Math.round( nTransX/nWidth );	
	var nTileX:Number = (nTmpX-nTmpY)*nWidth;
	var nTileY:Number = (nTmpX+nTmpY)*nHeight;
	return new Point(nTileX,nTileY);
}
W efekcie dostając:
(Zielone kółko to Point zwrócony przez getTile)
Gotowy przykład: tile_example.zip

I to w zasadzie wszystko, dalej tylko jeszcze poświecę parę słów na wyjaśnienie działania funkcji getTile krok po kroku:
1.
var nScale:Number = nWidth/nHeight;
Skala będzie nam potrzebna przy transformacji by wyrównać wysokość do szerokości (ponieważ algorytm działa tylko w skali 1:1).
2.
var nTransY:Number = (nScale*nTargetY-nTargetX)/2;
var nTransX:Number = nTargetX+nTransY;

W tym miejscu następuje transformacja punktu na ekranie, na punkt w rzucie izometrycznym.
Najczęściej mówiąc o rzucie izometrycznym mamy na myśli przestrzeń obróconą o kąt 45 stopni, w taki sposób że ruchowi w prawo nie będzie odpowiadać samo +X, ale raczej pewna wartość X plus częściowo również i Y. Najlepiej zobrazować to na przykładzie:

Jak widać przesunięcie kursora w prawo, przesuwa punkt transformacji w prawo tylko po części, ponieważ dochodzi do tego jeszcze przesunięcie do góry. Uzbrojeni w ta wiedzę, możemy spróbować napisać generalny wzór - skoro przesunięcie w prawy (+X) daje nam również trochę przesunięcia do góry (-Y) można powiedzieć, że Y maleje wraz ze wzrostem X, czyli innymi słowy Y = -nTargetX. Jednak należy pamiętać, że nie jest to transformacja x=>y (inaczej mieli byśmy do czynienia z obrotem o 90 stopni), stąd też do równania należy dołączyć również dzielnie przez 2 (bo 90 stopni podzielone przez 2 to nasze 45 stopni... w wielkim uproszczeniu). Teraz gdy już znamy Y, obliczenie X jest trywialne i wystarczy, że odejmiemy wartość Y od docelowego X (by pokonany dystans był identyczny z tym przed transformacją), a ponieważ Y już jest ujemne wstawiamy znak dodawania Y = nTargetX+nTransY;. Przy okazji, plus i minus można w tych równaniach zamienić miejscami by uzyskać transformację dla obrotu -45 stopni.
3.
var nTmpY:Number = Math.round( nTransY/(nHeight*nScale) );
var nTmpX:Number = Math.round( nTransX/nWidth );
var nTileX:Number = (nTmpX-nTmpY)*nWidth;
var nTileY:Number = (nTmpX+nTmpY)*nHeight;

Tutaj zmieniamy wybrany punkt na podpadający pod niego kafelek, poprzez "skurczenie" całej przestrzeni o wysokość i szerokość kratki, następnie usunięcie informacji po przecinku i przywrócenia całości do "normalnych rozmiarów". Ponieważ uwielbiam przykłady o to następny: jeśli szerokość kafelki numer #0 wynosi 20 pikseli, to wszystko pomiędzy 0 a 20 pikseli będzie podpadać właśnie pod nią, tak więc: jak sprawdzić do której kafelki należy siódma piksela? Bierzemy owe 7, dzielimy przez 20 i dostajemy 0.35. Z rezultatu usuwamy wszystko po przecinku i dostajemy naszą kratkę #0. Jeszcze spróbujmy tego samego dla pikseli 27; 27/20 = 1.35; usuwamy liczby po przecinku i dostajemy #1, co się zgadza bo dwudziesta siódma piksela nie należy do kratki #0.

Uff, starczy na dzisiaj. Za dwa tygodnie napisze jak w najłatwiejszy sposób zaimplementować znajdywanie ścieżki w silnikach kafelkowych.


Starsze posty>>>