Spis treści:

Kategoria:HTMLJavaScriptCanvas


Sterowanie obiektem za pomocą klawiatury

Canvas i JavaScript po raz kolejny

Idzie facet przez wieś i uśmiecha się od ucha do ucha.
- 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.
Kontynuując serię wpisów na temat Canvas zmierzam powoli do napisania prostej gry. Wiemy już jak wykonywać podstawowe operacje graficzne, wiemy jak obługiwać zdarzenia myszy, jak wykrywać proste obiekty pod wskaźnikiem myszy, jak wygenerować losowy teren. Pokazałem też kilka innych przydatnych technik. Nie pokazałem jednak nigdzie obsługi klawiatury. Najwyższy na to czas, bo jest to chyba podstawowy interfejs wejściowy do większości gier. Trudno sobie wyobrazić kierowanie samochodem za pomocą myszki, niełatwo też spotkać platformówkę, w której główna postać nie poruszałaby się w reakcji na naciśnięcie klawiszy strzałek. Wiele innych gier również silnie zależy od wejścia klawiaturowego. Przekonamy się zaraz, że nie jest to nieosiągalne dla przeciętnego człowieka.

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:

Rys. 1 - Sterowanie niezidentyfikowanym obiektem.

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.

<!DOCTYPE html>
<html>
<head><title>Sterowanie niezidentyfikowanym obiektem</title></head>
<body>
<h2>Sterowanie niezidentyfikowanym obiektem w HTML5 i Javascript</h2>
<figure>
<canvas id="paint1width="640height="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

, 2013-12-20

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
bardo ciekawe , można dzięki takim wpisom dostrzec wędkę..
Bardzo dziękuję za jasne tłumaczenie z dobrze dobranym przykładem!
Dzieki za te informacje. były kluczowe
Dobrze wyjaśnione, dzięki !