Sterowanie obiektem za pomocą klawiatury
Canvas i JavaScript po raz kolejny
- Janek z czego się tak cieszysz?
- Właśnie zostałem ojcem, mam syna.
- To gratulacje! Jak się czuje żona?
- Nie wiem. Jeszcze jej o tym nie powiedziałem.
Niezidentyfikowany obiekt na niebie i klawiatura
Do tej pory wszystkie aplikacja z serii JavaScript+HTML+Canvas były proste. Staram się pokazywać tylko jedną rzecz i jak najbardziej ukrywać pozostałe elementy. Nie inaczej będzie tym razem. Przyjrzyjmy się przykładowi pokazanemu poniżej:
Sterowanie odbywa się za pomocą strzałek. Zachęcam do pobawienia się i posterowania dwoma elipsami udającymi UFO, a gdy nam się znudzi i uznamy, że jest to fajne - zapraszam do wyjaśnień.
Kod JavaScript do sterowania w grze
Poniżej zamieszczam pełny listing aplikacji, razem ze znacznikami HTML. Całość można skopiować, zapisać jako plik z rozszerzeniem *.htm lub *.html i uruchomić lokalnie.
<html>
<head><title>Sterowanie niezidentyfikowanym obiektem</title></head>
<body>
<h2>Sterowanie niezidentyfikowanym obiektem w HTML5 i Javascript</h2>
<figure>
<canvas id="paint1" width="640" height="320"></canvas>
<figcaption>Rys. 1 - Sterowanie niezidentyfikowanym obiektem.</figcaption>
</figure>
<script type="text/javascript">
function Paint(c){
var e = document.getElementById(c);
this.Canvas = e;
this.Ctx = e.getContext("2d");
this.X = e.offsetLeft;
this.Y = e.offsetTop;
this.Width = e.width;
this.Height = e.height;
this.KeyboardState = new Array();
document.addEventListener("keydown", this.KeyDown.bind(this), false);
document.addEventListener("keyup", this.KeyUp.bind(this), false);
window.setInterval(this.DrawFrame.bind(this), 35);//28 FPS
this.ObjectX = this.Width / 2.0;
this.ObjectY = this.Height / 2.0;
this.ObjectRX = 20;
this.ObjectScaleY = 0.5;
}
Paint.prototype.KeyUp = function(event){
this.KeyboardState[event.keyCode] = false;
if (this.IsAnyKeyPressed())
event.preventDefault();
}
Paint.prototype.KeyDown = function(event){
this.KeyboardState[event.keyCode] = true;
if (this.IsAnyKeyPressed())
event.preventDefault();
}
Paint.prototype.DrawFrame = function(){
if (this.KeyboardState[37])//left
this.ObjectX -= 4;
if (this.KeyboardState[39])//right
this.ObjectX += 4;
if (this.KeyboardState[38])//up
this.ObjectY -= 4;
if (this.KeyboardState[40])//down
this.ObjectY += 4;
if (this.IsAnyKeyPressed())
this.Draw();
}
Paint.prototype.IsAnyKeyPressed = function(){
if (this.KeyboardState[37] ||
this.KeyboardState[38] ||
this.KeyboardState[39] ||
this.KeyboardState[40])
return true;
else
return false;
}
Paint.prototype.Draw = function(){
var sky = this.Ctx.createLinearGradient(0, 0, 0, this.Height);
sky.addColorStop(0, "#6666DD");
sky.addColorStop(0.5, "#DAFEFC");
this.Ctx.save();
//Draw Sky
this.Ctx.fillStyle = sky;
this.Ctx.fillRect(0, 0, this.Width, this.Height);
//Draw UFO
this.Ctx.scale(1, this.ObjectScaleY);
this.Ctx.beginPath();
this.Ctx.fillStyle = "#101010";
this.Ctx.arc(this.ObjectX, this.ObjectY / this.ObjectScaleY,
this.ObjectRX, 0, 2*Math.PI);
this.Ctx.fill();
this.Ctx.beginPath();
this.Ctx.fillStyle = "#00AA00";
this.Ctx.arc(this.ObjectX, this.ObjectY / this.ObjectScaleY - 8,
2 * this.ObjectRX / 3, 0, 2*Math.PI);
this.Ctx.fill();
this.Ctx.restore();
}
//Dopiero po załadowaniu całej strony elementy będą poukładane
window.addEventListener("load", function(){
new Paint("paint1").Draw();
}, false);
</script>
</body>
</html>
Przejdźmy teraz do omówienia najważniejszych fragmentów.
Jak to wszystko działa?
Na początku, jak zwykle, tworzymy obiekt naszej planszy - Paint
. Tam ustawiamy kilka atrybutów (niektóre już powinny być znane - jeżeli nie, odsyłam do pozostałych artykułów z sekcji :
- Canvas - Element, na którym będziemy rysować.
- Ctx - Kontekst elementu, na którym będziemy rysować. Canvas należy traktować bardziej w kategoriach elementu HTML, podczas gdy Ctx w kategoriach płótna właściwego.
- X, Y, Width, Height - Kolejno: położenie X, Y, szerokość i wysokość płótna.
- KeyboardState - Stan klawiszy. Dla nas najważniejsze będą klawisze o numerach 37 (strzałka w lewo), 38 (strzałka w prawo), 39 (strzałka w górę), 40 (strzałka w dół).
- ObjectX, ObjectY - Współrzędne X i Y obiektu latającego.
- ObjectRX - Średnica obiektu latającego.
- ObjectScaleY - Proporcja wysokości i szerokości. Współczynnik 0.5 oznacza, że UFO będzie miało wysokość równą połowie szerokości. Osiągniemy w ten sposób efekt płaskiego talerza.
Pozostałe instrukcje w funckji Paint realizują obsługę zdarzeń. Są to funkcje:
- this.KeyDown - Obsługuje zdarzenie wciśnięcia klawisza. Metoda jest powiązana ze zdarzeniem keydown.
- this.KeyUp - Obsługuje zdarzenie zwolnienia klawisza. Metoda jest powiązana ze zdarzeniem keyup.
- this.DrawFrame - Obsługuje zdarzenie zegara. Jest ona powiązana z funkcją window.setInterval, która wyzwala metodę przekazaną w pierwszym parametrze co pewien czas. Czas zdefiniowany jest w milisekundach i przekazany w drugim parametrze.
Stany klawiszy i przesuwanie obiektu
Obsługa metod KeyDown oraz KeyUp jest bardzo prosta - przy wciśnięciu ustawiamy wartość w tablicy KeyboardState, przy zwolnieniu klawisza tę wartość resetujemy. W ten sposób zawsze mamy dostęp do aktualnego stanu wszystkich potrzebnych nam klawiszy. Pozostałe dwie linijki tych metod są pewnym dodatkiem. Sprawdzam, czy któryś z wciśniętych klawiszy jest strzałką (metoda IsAnyKeyPressed). Jeżeli tak, wyłączam domyślną funkcję obsługi tych klawiszy.
Zdecydowałem się blokować tylko te klawisze, które są kluczowe z punktu działania aplikacji.
Do przesuwania obiektu służy funkcja DrawFrame. Wywoływana jest ona co 35 milisekund, co powinno dawać 28 klatek animacji na sekundę. Zawartość funkcji jest prosta do zrozumienia: jeżeli klawisz strzałki w lewo jest wćiśnięty - zmniejsz współrzędną X obiektu, jeżeli w prawo - zwiększ współrzędną X obiektu. Analogicznie dla strzałek w górę i w dół. Po przetworzeniu stanu klawiszy i zmianie współrzędnych należy obszar gry odrysować. Znów uznałem, że nie ma potrzeby odrysowania jeżeli żaden z klawiszy nie był wciśnięty. Nie ma ruchu - niech sobie komputer odpocznie.
Do wyjaśnienia pozostała juz tylko funkcja rysująca.
Rysowanie obiektu
Technikę rysowania postanowiłem opisać oddzielnie, bo jest tam pewien trik. Nie ten z grdientem, bo on już się wcześniej pojawił i został opisany (Generowanie losowego terenu 2D). Chodzi o spłaszczanie okręgów.
Wśród funkcji rysujących różne obiekty w kontekście płótna próżno szukać elipsy. Nie ma nawet koła, które rysowane jest w postaci łuku obejmującego kąt 360°. Pisząc szeroko dostępne API należy zawsze dbać o jego rozmiar. Jeżeli jakaś operacja może być wykonana za pomocą innej (lub kilku innych) bez drastycznego spadku wydajności, warto rozważyć pominięcie jej. Łatwiej dobrze zrozumieć API, które ma 20 metod niż API, które ma tych metod 100. Łatwiej nad takim API zapanować od strony wykorzystującego, ale także piszącego. W związku z powyższym, koło reprezentowane jest przez łuk obejmujący kąt 360°, a elipsa jako przeskalowane koło. Reasumując: aby narysować elipsę o szerokości 20 pikseli i wysokości 10 pikseli można:
- narysować łuk o promieniu 20, obejmujący kąt 360°, w skali (1, 0.5) lub
- narysować łuk o promieniu 10, obejmujący kąt 360°, w skali (2, 1) lub
- zastosować jeszcze inne, mniej intuicyjne przekształcenia.
O bardziej zaawansowanych przekształceniach pewnie jeszcze kiedyś napiszę - teraz zajmijmy się aplikacją. Jest tam bowiem jeszcze jedna pułapka. Skala, definiowana przez metodę scale, obejmuje obszar całego płótna (kontekstu). Co więcej, operacje przekształcające kontekst kumulują się! Jeżeli zatem zastosujemy skalowanie zmniejszające dwa razy, obiekty będą podwójnie zmniejszone! Przykładowo, jeżeli zastosujemy skalę (1, 0.5) dwa razy otrzymamy skalę (1 * 1, 0.5 * 0.5), co da nam skalę wynikową w postaci (1, 0.25). Trzeci raz zastosowana skala to (1 * 1 * 1, 0.5 * 0.5 * 0.5), co nam daje (1, 0.125). Jeżeli w każdej klatce animacji zastosujemy operację skalowania, bardzo szybko uzyskamy skalę w postaci (1, 0.0000...), co w praktyce ukryje nam rysowany obiekt. W odpowiedzi na takie problemy powstała technika zapamiętywania i przywracania stanu kontekstu.
Nie jest to technika trudna, bo obsługiwana przez dwie metody: save, która kontekst zapamiętuje oraz restore, która kontekst przywraca. Każde wywołanie metody save zapamiętuje stan kontekstu na stosie, a każde wywołanie restore informacje o kontekście ze stosu pobiera. Stosowa implementacja pozwala zatem na stosowanie bardziej zaawansowanych technik, w których metody save muszą być zagnieżdżane. Zagnieżdżanie pociąga za sobą konieczność dbania o właściwe dopasowanie metod save oraz restore. Jeżeli kontekst został zapamiętany dwa razy, należy go też dwa razy przywrócić - chyba, że stosujemy super tajne, specjalne algorytmy - wtedy droga wolna.
Co dalej?
Pokazałem kolejny klocek, który jest szalenie przydatny podczas pisania gier (ale nie tylko) w HTML. Ten klocek to nie gra, choć tak go w tekście nazywałem. Do gry brakuje przeciwników, strzałów, albo chociaż jakiegoś zadania do wykonania. Postaram się zamieścić kolejne części tej już przyzwoitej serii. Zachęcam do dzielenia się spostrzeżeniami w komentarzach i do podsyłania pomysłów na kolejne wpisy.
Kategoria:HTMLJavaScriptCanvas
Brak komentarzy - bądź pierwszy