Gra - samochód.

1. Wstęp

W niniejszym artykule chciałbym przybliżyć metodykę tworzenia gier we flashu z wykorzystaniem technik programowania obiektowego. Gra którą chciałbym przedstawić to jedno z najczęściej spotykanych zagadnień początkujących użytkowników flasha - sterowanie samochodzikiem widzianym z góry.

2. Od czego zacząć?

No najlepiej od samochodu ;)
Jakoś tak często mam że lubię najpierw sobie narysować co będę robił. Narysujmy więc prosty samochodzik skierowany w górę. Zapakujmy go w MovieClip o nazwie Car.
Teraz stwórzmy projekt.
W katalogu "car" tworzymu plik projektu (we FD empty project).
Zawsze tworzę następującą strukturę projektu (używam FlashDevelop):

- deploy
    car.swf
- resources
  - gfx
  - snd
  - docs
- src
  - classes
      - core
      - model
      - ui
  - fla
      car.fla
app.flp

Zawartość katalogów uzależniona jest od typu aplikacji. Niemal zawsze do katalogu core trafia klasa View. (Pewnie że można zrobić aplikację we flashu bez View ale co to za flash bez View ;) )
View zawsze umieszczam na głównej listwie, dzięki czemu mam kontrolę nad całością widoku.
To również pozwala mi utrzymać porządek na głównej listwie - właściwie jest tam tylko View.
Tworzymy więc pusty MovieClip o nazwie View i umieszczamy na głównej listwie.
Zawsze poza nieliczymi wyjątkami elementy umieszczam na pozycji (0, 0) więc gdy nie będę pisał inaczej to będzie to domyślna pozycja.
Podpinamy View pod klasę core.View, a Car pod klasę ui.car.Car. Oczywiście obie dziedziczą po MovieClip. Zarówno we Flash IDE jak i FlashDevelop ustawiamy ścieżkę do klas: we flashu względem .fla czyli ..\classes, w FD względem pliku projektu czyli src\classes
Uzbrajamy obie klasy w prywatny konstruktor żeby nikt nie próbował napisać new Car().
Teraz trzeba zmusić samochód aby reagował na naciśnięcia klawiszy.
No tak, ale co potem? lewo, prawo, gaz, hamulec, a jak samochód właściwie skręca?

3. Jak samochód się porusza czyli trochę matematyki

Aby ruch samochodu wyglądał w miarę realistycznie trzeba przyjrzeć się bliżej co się dzieje podczas jazdy gdy koła zostają skręcone. Gdy próbujemy rozwiązać jakiś problem ruchu patrzymy co się dzieje w krótkim przedziale czasu. Weźmy więc infinitezymalny przyrost czasu dt ;) między dwoma punktami t1 oraz t2. Załóżmy że samochód porusza się w następujący sposób:
W chwili t1 ma dane położenie, rotację i kąt skręcenia kół. W chwili t2 przesuwa się zgodnie z kierunkiem kół ze stałą (w tym małym przedziale czasu) prędkością v po czym zmienia kąt skrętu kół. Następną chwilę t3 wyznaczamy w analogiczny sposób. Ruch samochodu w momentach t1 i t2 może wyglądać tak jak na poniższym rysunku.

car_dt.jpg car_triangle.jpg

Widać z niego że najlepszym początkiem układu w przypadku samochodu będzie środek przedniej osi kół. Możemy więc od razu to poprawić w naszej grafice. Obok znajduje się powiększenie trójkąta który jest interesujący ze względu na dane które chcemy znaleźć.
h to ogległość między osiami kół
vdt to droga przebyta w tym czasie
Zakładając że znamy położenie, prędkość i skręt kół, możemy wyznaczyć położenie oraz przyrost rotacji samochodu w chwili t2. Prędkość w chwili t2 będziemy znali z dynamiki samochodu ale o tym później. To czego szukamy to kąt β.
Skorzystajmy z twierdzenia sinusów:

calc_car_kinematics.jpg

Otrzymaliśmy więc przyrost rotacji samochodu w zależności od:
- bieżącego skrętu kół (α)
- odległości pomiędzy osiami kół (h)
- przyrostem położenia (x)

4. Dlaczego samochód się porusza czyli trochę fizyki

Najważniejszym wzorem mechaniki klasycznej jest równanie wyrażające II zasadę dynamiki. Często podaje się je postaci iloczynu. Czasem gdy tłumaczę komuś fizykę zwracam uwagę na to, iż poprawna forma ma postać ilorazu:

aeqFbym.jpg

Jest tak dlatego gdyż to ruch ciała zostaje poddany działającym siłom. Tak to działa. Oczywiście możemy się zapytać jaki jest charakter siły gdy widzimy poruszające się ciało, ale to poprostu odwrócenie pytania. Fizyka ruchu pozostaje taka jaka jest.
Dlaczego samochód się porusza? Nie, nie dlatego że ma silnik. Dlatego że ma silnik to koła się kręcą. Samochód się porusza dzięki sile tarcia. Na samochód działa maksymalnie taka siła na jaką pozwala siła tarcia. Jeśli siła tarcia jest dostatecznie duża, to cała siła przekazywana na koła jest przekazywana na samochód jako całość. W przeciwnym wypadku maksymalna siła napędzająca samochód jest równa sile tarcia, reszta mocy silnika idzie na darcie gumy i asfaltu ;)
No tak, czyli samochód napędzany jest stałą siłą tarcia ale to oznacza, że samochód przyspieszałby w nieskończoność. Pomijając efekty relatywistyczne :) mamy jeszcze siłę oporu podłoża i powietrza. Efektywną siłę działającą na samochód możemy zatem zapisać w postaci:

totalForce.jpg

Siła oporu nie jest jednak stała - można założyć że rośnie proporcjonalnie do prędkości.

resistForce.jpg

Dlatego z przymróżeniem oka można powiedzieć że w takim ruchu im szybciej się poruszasz tym mniejszą prędkość możesz rozwinąć. To dlatego człowiek spada po pewnym czasie ze stałą prędkością, podobnie jak ze stałą prędkością bąbelki w piwie lecą do góry.

Jak to zastosować do naszego samochodu?
Wykorzystamy tu bardzo popularny tzw. algorytm Verleta
Prędkość to zmiana położenia w jednostce czasu. Potrzebujemy więc dwóch położeń.
Przyspieszenie to zmiana prędkości w jednostce czasu, a więc potrzebujemy trzeciego punktu. Oczywiście naszą jednostką czasu jest 1/FPS sekundy.
Oznaczmy te trzy punkty xp, x0 oraz xn.
Prędkość w punkcie x0 wynosi v0 = x0 - xp, natomiast w punkcie xn wynosi vn = xn - x0
Zatem w punkcie x0 możemy mówić o przyspieszeniu:
a0 = vn - v0 = (xn - x0) - (x0 - xp) = xn - 2*x0 + xp
Wynika z tego że znając przyspieszenie i dwa ostatnie położenia możemy wyznaczyć następne położenie.
xn = a0 + 2*x0 - xp

5. Implementacja

Najpierw zajmiemy się wykrywaniem interakcji czyli obsługą wciśnięcia klawiszy. Można by użyć listenera na obiekt Key i zaimplementować onKeyDown(). Problem w tym, że onKeyDown jest wywoływane nie tylko na naciśnięcie klawisza ale także z pewnym interwałem gdy jest on przytrzymany.
Zbudujmy więc klasę nasłuchującą i rozgłaszającą eventy klawiszy.
Do utworzonego obiektu tej klasy będziemy się rejestrować na nasłuchiwanie danego zestawu klawiszy.

import pl.arthwood.events.Delegate;
import pl.arthwood.events.Event;
import pl.arthwood.events.KeyWrapper;
import pl.arthwood.utils.UtilsArray;

class pl.arthwood.events.KeyController {
  public var onKey:Event;

  private var _keys:Array;
  private var _id:Number;
  private var _enabled:Boolean = true;
  private var _checkKeysDelegate:Delegate;

  public function KeyController(dontEnable_:Boolean) {
    onKey = new Event("onKey");
    _keys = new Array();
    _checkKeysDelegate = new Delegate(this, checkKeys);

    if (!dontEnable_) {
      enable();
    }
  }

  public function dispose():Void {
    clearInterval(_id);
  }

  public function enable():Void {
    _enabled = true;
    _id = setInterval(_checkKeysDelegate.callback, 20);
  }
  
  public function disable():Void {
    _enabled = false;
    clearInterval(_id);
  }
  
  public function get enabled():Boolean {
    return _enabled;
  }

  private function checkKeys():Void {
    var vKW:KeyWrapper;
    var vCode:Number;
    var vIsDown:Boolean;

    for (var i:String in _keys) {
      vKW = KeyWrapper(_keys[i]);
      vCode = vKW.getCode();
      vIsDown = vKW.isDown;

      if (Key.isDown(vCode) && !vIsDown) {
        vKW.isDown = true;
        onKey.fire(vKW);
      }
      else if (!Key.isDown(vCode) && vIsDown) {
        vKW.isDown = false;
        onKey.fire(vKW);
      }
    }
  }

  public function addKey(key_:Number):Void {
    if (!isKeyRegistered(key_)) {
      _keys.push(new KeyWrapper(key_));
    }
  }

  public function setKeys(keys_:Array):Void {
    for (var i:String in keys_) {
      addKey(Number(keys_[i]));
    }
  }

  public function removeKey(key_:Number):Void {
    UtilsArray.removeItem(_keys, getKey(key_));
  }

  private function isKeyRegistered(key_:Number):Boolean {
    return Boolean(getKey(key_));
  }

  private function getKey(key_:Number):KeyWrapper {
    var vKW:KeyWrapper;

    for (var i:String in _keys) {
      if ((vKW = KeyWrapper(_keys[i])).getCode() == key_) {
        return vKW;
      }
    }

    return null;
  }
}

We wszystkich projektach używam modelu zdarzeń o którym można przeczytać tutaj nie będę więc go opisywał. Zastosujmy to z klasie samochodu.

import pl.arthwood.events.Delegate;
import pl.arthwood.events.KeyController;
import pl.arthwood.events.KeyWrapper;

class ui.car.Car extends MovieClip {
  private var _keyController:KeyController;

  private function Car() {
    _keyController = new KeyController();
    _keyController.onKey._add(new Delegate(this, onKey));
    _keyController.setKeys([Key.UP, Key.DOWN, Key.LEFT, Key.RIGHT]);
  }

  public function dispose():Void {
    _keyController.dispose();
  }

  private function onKey(kw_:KeyWrapper):Void {
    switch (kw_.getCode()) {
      case Key.UP:
        kw_.isDown? onKeyUP_down() : onKeyUP_up();
        break;
      
      case Key.DOWN:
        kw_.isDown? onKeyDOWN_down() : onKeyDOWN_up();
        break;
      
      case Key.LEFT:
        kw_.isDown? onKeyLEFT_down() : onKeyLEFT_up();
        break;
      
      case Key.RIGHT:
        kw_.isDown? onKeyRIGHT_down() : onKeyRIGHT_up();
        break;
    }
  }

  private function onKeyUP_down():Void {
  }

  private function onKeyUP_up():Void {
  }

  private function onKeyDOWN_down():Void {
  }

  private function onKeyDOWN_up():Void {
  }

  private function onKeyLEFT_down():Void {
  }

  private function onKeyLEFT_up():Void {
  }

  private function onKeyRIGHT_down():Void {
  }

  private function onKeyRIGHT_up():Void {
  }
}

Po utworzeniu obiektu możemy zarejestrować się jako słuchacz zdarzenia onKey. Następnie ustawiamy jakie klawisze nas interesują. Trzeba jednak pamiętać o ręcznym wyczyszczeniu interwału wewnątrz obiektu KeyController. Służy do tego metoda dispose(). Do opakowania klawisza możemy użyć Klasy KeyWrapper:

class pl.arthwood.events.KeyWrapper {
  public var isDown:Boolean = false;
  
  private var _code:Number;

  public function KeyWrapper(code_:Number) {
    _code = code_;
  }

  public function getCode():Number {
    return _code;
  }
}

To obiekt reprezentujący pojedynczy klawisz. Przechowuje kod i flagę czy jest on wciśnięty.

Teraz zajmiemy się silnikiem samochodu.
Silnik to obiekt który charakteryzuje pewna liczba _power z przedziału <0, _maxPower>. Dla uproszczenia dodanie gazu ustawia tę wartość od razu na maksymalną. Pierwotnie dodanie gazu powodowało wzrost tej liczby z pewnym interwałem o wartość określoną ułamkiem wartości maksymalnej. Jeśli ktoś ma ochotę może to przerobić. Zdjęcie nogi z gazu powoduje że silnik nie dostarcza mocy.

import pl.arthwood.events.Delegate;
import pl.arthwood.math.MathUtils;

class ui.car.Engine {
  private var _power:Number = 0;
  private var _maxPower:Number = 1;

  public function Engine(maxPower_:Number) {
    _maxPower = isNaN(maxPower_) ? _maxPower : maxPower_;
  }

  public function turnOn():Void {
    setPower(_maxPower);
  }

  public function turnOff():Void {
    setPower(0);
  }

  private function setPower(power_:Number) {
    var vPower:Number = getPower();

    _power = MathUtils.getLimitedValue(power_, 0, _maxPower);
  }

  public function getPower():Number {
    return _power;
  }

  public function getMaxPower():Number {
    return _maxPower;
  }

  public function getRelativePower():Number {
    return _power/_maxPower;
  }
}

MathUtils.getLimitedValue() zwraca liczbę przekazaną jako pierwszy argument obciętą do przedziału podanego jako drugi i trzeci argument.
Do konstruktora klasy Car dodajemy:

_engine = new Engine();

Pamiętając o deklaracji _engine (deklarujemy jako typ Engine) i imporcie ui.car.Engine.
Gdy mamy zbudowany silnik ze swoim API, możemy obsłużyć zdarzenia klawiszy w klasie Car:

private function onKeyUP_down():Void {
  _engine.turnOn();
}

private function onKeyUP_up():Void {
  _engine.turnOff();
}

Teraz spróbujmy napisać podobną klasę dla układu kierowniczego.
Na naciśnięcie klawiszy LEFT, RIGHT koła powinny ulegać skręceniu.
Podobnie jak w przypadku silnika użyłem stopniowego zwiększania wychylenia kół w pewnym interwale. Nic nie stoi jednak na przeszkodzie (i nie będzie miało to wielkiego wpływu na grywalność), aby wraz z naciśnięciem klawisza, koło odchyliło się od razu do pozycji maksymalnej. Tutaj drobna uwaga. Ponieważ koła są elementem widoku, postanowiłem oś przednią i tylną opisać klasą Axis dziedziczącą po MovieClip. Możemy to samo zrobić z silnikiem jeśli chcemy aby był widoczny i np. czasem dymił :)
Każde koło opakowujemy w jeden MovieClip (Wheel) wycentrowany względem środka koła (bo taka będzie rotacja), a następnie po dwa koła pakujemy do MovieClipa Axis i centrujemy względem punktu między kołami. Teraz pod Axis podpinamy klasę ui.car.Axis.
Koła wewnątrz Axis nazywamy "_wheelLeft" i "_wheelRight", natomiast same osie w klasie Car nazywamy "_axisFront" i "_axisRear". Wszystkie powyższe pola opatrujemy w identyfikator dostępu private. Pamiętajmy że koła są typu MovieClip, natomiast osie deklarujemy jako Axis. Oczywiście sama deklaracja niczego nam nie zmienia - obiekt jest tym czym jest. Łatwiej nam sie będzie poprostu pisało (podpowiedzi + debug w czasie kompilacji).
A oto klasa reprezentująca oś i spełniająca postawione wyżej wymagania:

import pl.arthwood.events.Delegate;
import pl.arthwood.math.MathUtils;

class ui.car.Axis extends MovieClip {
  public var maxWheelsTurn:Number = Math.PI/6;

  private var _wheelsAngle:Number = 0;
  private var _wheelLeft:MovieClip;
  private var _wheelRight:MovieClip;

  private function Axis()	{
  }
  
  public function setWheelsAngle(angle_:Number):Void {
    _wheelLeft._rotation = _wheelRight._rotation = MathUtils.toDeg(
      _wheelsAngle = MathUtils.getLimitedValue(angle_, -maxWheelsTurn, maxWheelsTurn)
    );
  }

  public function turnLeft():Void {
    setWheelsAngle(-maxWheelsTurn);
  }

  public function turnRight():Void {
    setWheelsAngle(maxWheelsTurn);
  }

  public function normalize():Void {
    setWheelsAngle(0);
  }

  public function getAngle():Number {
    return _wheelsAngle;
  }	
}

Teraz w klasie Car implementujemy obsługę klawiszy bocznych:

private function onKeyLEFT_down():Void {
  _axisFront.turnLeft();
}

private function onKeyLEFT_up():Void {
  _axisFront.normalize();
}

private function onKeyRIGHT_down():Void {
  _axisFront.turnRight();
}

private function onKeyRIGHT_up():Void {
  _axisFront.normalize();
}

Możemy teraz uruchomić aplikację i pobawić się kołami :)
OK. Mamy silnik, kierownicę, tylko samochód nam nie chce jeździć :/.
Tchnijmy w niego życie.
Nadszedł czas aby zastosować to o czym mówiliśmy rozważając ruch i napęd samochodu. We flashu najbardziej popularną metodą implementacji ruchu jest modyfikacja położenia w każdej iteracji przejścia do następnej klatki. Oczywiście wiadomo że żadnego przejścia być nie musi na listwie. Każdy MovieClip we flashu ma taki wewnętrzny licznik, który tykając wywołuje na sobie metodę onEnterFrame(). Póki jest pusta nic się nie dzieje, a jak ją zaimplementujemy to jest weselej ;)
Wróćmy na chwilę do fizyki ruchu. Doszliśmy do wniosku że równanie ruchu naszego samochodu ma postać:

motion_eq1.jpg

gdzie F to całkowita siła działająca na samochód, Fe to siła napędzająca koła pochodząca od silnika i równa co do wartości sile tarcia,
k to współczynnik oporu powietrza i nawierzchni a v prędkość samochodu
Zgodnie z konkluzją rozdziału 4. aby dostać tor ruchu samochodu, musimy w każdej iteracji odnajdować następny punkt znając poprzedni, bieżący oraz przyspieszenie.
Zbudujmy klasę która sama będzie operowała dwoma punktami dając nam każdy następny gdy powiemy jej jakie przyspieszenie mamy obecnie. Klasa ma dwa pola _xp i _x0 oraz metodę update do której przekazujemy przyspieszenie. Zwraca nam ona następny punkt, automatycznie aktualizując wartość swoich pól.

class pl.arthwood.physics.motion.Verlet {
  private var _xp:Number = 0;
  private var _x0:Number = 0;

  public function Verlet(x_:Number, v_:Number) {
    _x0 = x_ || _x0;
    _xp = (_x0 - v_) || _xp;
  }

  public function update(a_:Number):Number {
    var vX:Number = a_ + 2*_x0 - _xp;

    _xp = _x0;
    return _x0 = vX;
  }

  public function get x():Number {
    return _x0;
  }

  public function get v():Number {
    return _x0 - _xp;
  }
}

Tworzymy więc nowe pole typu Verlet w klasie Car i inicjalizujemy w konstruktorze:

_verlet = new Verlet();

Ponieważ interesują nas tylko zmiany położenia możemy zostawić położenie początkowe z wartością 0 (domyślna wartość obiektu Verlet). Prędkości też nie przekazujemy bo początkowo samochód jest w spoczynku. Przekazanie jakiejś prędkości spowodowałoby startowego kopniaka :) Jeszcze jedna uwaga w temacie kinematyki we flashu. Dla ułatwienia przyjmujemy za umowną jednostkę czasu 1/FPS sekundy. Możemy pójść dalej i określić czas jako liczbę niemianowaną. Dzięki temu położenie, prędkość i przyspieszenie mierzymy w tych samych jednostkach - pikselach. To podobnie jak np. w mechanice relatywistycznej gdzie często przyjmujemy że c = 1. Wtedy masę wyrażamy w eV.
Doszliśmy więc do najbardziej interesującego momentu - implementacji ruchu. Oto jak przedstawia się metoda aktualizująca położenie onEnterFrame (klasa Car):

private function onEnterFrame():Void {
  var v:Number = _roundTo(_verlet.v, 2);
  var vWA:Number = _axisFront.getAngle();
  var a:Number = Math.PI/2 - vWA;
  var o:Number = _toRad(_rotation - 90) + vWA;

  _verlet.update((_engine.getPower() - Environment.resistance*v)/_m);
  _rotation += _toDeg(Math.atan(Math.cos(a)/(Math.sin(a) + _axisDistance/v)));
  _x += v*Math.cos(o);
  _y += v*Math.sin(o);
}

Do metody _verlet.update trafia przyspieszenie będące ilorazem efektywnej siły oraz masy samochodu. Jak widać stała oporu brana jest ze statycznej zmiennej klasy Environment:

class model.Environment {
  private static var DEFAULT_RESISTANCE:Number = 0.1;
  private static var _resistance:Number = DEFAULT_RESISTANCE;

  private function Environment() {
  }

  public static function resetResistance():Void {
    _resistance = DEFAULT_RESISTANCE;
  }

  public static function get resistance():Number {
    return _resistance;
  }

  public static function set resistance(resistance_:Number):Void {
    _resistance = Math.max(0, Math.min(1, resistance_));
  }
}

Stałej oporu k odpowiada pole _resistance i podobnie jak k musi należeć do przedziału <0, 1>.
Rotacja samochodu odbywa się zgodnie ze wzorem otrzymanym w punkcie 3. Jak widać przesunięcie jest liczbowo równe prędkości samochodu, natomiast stała odległość pomiędzy osiami określona jest zmienną _axisDistance. Dodajmy ją do klasy i zdefiniujmy w konstruktorze:

_axisDistance = _axisRear._y - _axisFront._y;

Po wstępnych przeliczeniach kątów odpowiednie wartości są wstawiane do wyprowadzonych wzorów. Zmienna lokalna a odpowiada kątowi α czyli rotacji kół w układzie odniesienia samochodu i służy do zmiany rotacji samochodu, natomiast o odpowiada rotacji samochodu, w układzie odniesienia związanym z View i służy do przesunięcia samochodu w odpowiednim kierunku. Użyłem także pewnego skrótu na użytek optymalizacji, a mianowicie zdefiniowałem kilka pól będących referencjami do metod pomocniczych:

  private var _roundTo:Function = MathUtils.roundTo;
  private var _toRad:Function = MathUtils.toRad;
  private var _toDeg:Function = MathUtils.toDeg;

Przydatną rzeczą jest hamulec. Hamulec to nic innego jak nagłe zwiększenie oporu. Samochód jako poruszający się obiekt nie wie czy to nawierzchnia się zmieniła, czy zaczął wiać przeciwny wiatr czy tarcze hamulcowe zaczęły pracować. Objawia się to poprostu dodaniem stałego składnika do równania ruchu. Zakładając że siła hamowania jest stała zdefiniujmy zmienną:

private var _brakePower:Number = 0.6;

oraz flagę:

private var _brakesOn:Boolean = false;

Następnie dodajmy ten czynnik do równania ruchu:

_verlet.update((_engine.getPower() - Environment.resistance*v - Number(_brakesOn)*_brakePower)/_m);

i uwzględnijmy hamowanie w obsłudze klawisza Key.DOWN:

private function onKeyDOWN_down():Void {
  _brakesOn = true;
}

private function onKeyDOWN_up():Void {
  _brakesOn = false;
}

Możemy także określić wartość siły hamulca w konstruktorze:

private var _m:Number = 1;

private function Car(mass_:Number, brakePower_:Number) {
  _m = (mass_ || _m);
  _brakePower = (brakePower_ || _brakePower);
  // ...
}

Przy okazji uwzględniamy masę samochodu.
Mimo że nie wywołujemy jawnie konstruktora Car, to moglibyśmy zbudować klasę car.Ford

import ui.car.Car;

class ui.car.Ford extends Car {
  private function Ford() {
    super(4);
  }
}

i podpiąć pod inny samochód.
Możemy dla wygody uzbroić nasz zamochód w użyteczne API:

  public function getVelocity():Number {
    return _verlet.v;
  }

  public function getPower():Number {
    return _engine.getPower();
  }

  public function getMaxPower():Number {
    return _engine.getMaxPower();
  }

Pozwala to na przykład zbudować wyświetlacz pokazujący stan samochodu na desce rozdzielczej.

6. Podsumowanie

Otrzymaliśmy w pełni funkcjonalny engine do tworzenia tego typu gier. Od pomysłowości programisty zależy teraz stworzenie jak najlepszej wersji. Nic nie stoi na przeszkodzie aby dodać mapę wraz z jej automatycznym przesuwaniem, dodanie na niej nieprzejezdnych elementów, zaimplementowanie kolizji z nimi, czy też kałuż (zwiększenie oporu). Można również wstawić na scenę dwa samochody z obsługą różnych klawiszy i wyścigi gotowe :)
Mam nadzieję że wielu z Was ten artykuł pomógł w zrozumieniu metodologii tworzenia gier we flashu i przybliżył nieco temat OOP w języku ActionScript.

7. Klasy pomocnicze

class pl.arthwood.math.MathUtils {
  public static function roundTo(n:Number, pos:Number):Number {
    var v_d:Number = Math.pow(10, pos);

    return Math.round(n*v_d)/v_d;
  }

  public static function toDeg(rad_:Number):Number {
    return 180*(rad_/Math.PI);
  }

  public static function toRad(deg_:Number):Number {
    return deg_*(Math.PI/180);
  }
}
swf do pobrania