Przewodnik po grach HTML5

Daniel X. Moore
Daniel X. Moore

Wstęp

Chcesz utworzyć grę w Canvas i HTML5? Postępuj zgodnie z instrukcjami w tym samouczku, a szybko ruszysz w drogę.

W samouczku zakłada się, że wiedza o języku JavaScript jest średnio zaawansowana.

Możesz najpierw zagrać w grę lub przejść bezpośrednio do artykułu i wyświetlić kod źródłowy gry.

Tworzę obszar roboczy

Aby rysować, musimy utworzyć płótno. Ponieważ jest to przewodnik No Tears, użyjemy jQuery.

var CANVAS_WIDTH = 480;
var CANVAS_HEIGHT = 320;

var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
                      "' height='" + CANVAS_HEIGHT + "'></canvas>");
var canvas = canvasElement.get(0).getContext("2d");
canvasElement.appendTo('body');

Pętla gry

Aby symulować wygląd płynnej rozgrywek, chcemy zaktualizować grę i narysować jej ekran tak szybko, jak człowiek i oko nie są w stanie tego dostrzec.

var FPS = 30;
setInterval(function() {
  update();
  draw();
}, 1000/FPS);

Na razie możemy zostawić pole aktualizacji i metody rysowania puste. Pamiętaj, że usługa setInterval() będzie okresowo do nich dzwonić.

function update() { ... }
function draw() { ... }

Hello world

To już pętla gry, więc zaktualizujmy metodę rysowania, by rysować na ekranie tekst.

function draw() {
  canvas.fillStyle = "#000"; // Set color to black
  canvas.fillText("Sup Bro!", 50, 50);
}

To świetnie, jeśli chodzi o tekst nieruchomy, ale ponieważ mamy już skonfigurowaną pętlę gry, powinno być możliwe łatwe poruszanie się po niej.

var textX = 50;
var textY = 50;

function update() {
  textX += 1;
  textY += 1;
}

function draw() {
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Sprawdź się. Jeśli go śledzisz, obraz powinien się poruszać, ale też pozostawać w miejscu wcześniejszego wyświetlenia na ekranie. Zastanów się, dlaczego tak jest. Dzieje się tak, ponieważ nie czyścimy ekranu. Dodajmy kod do czyszczenia ekranu w metodzie rysowania.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Tekst się przesuwa, więc jesteś w połowie drogi do prawdziwej gry. Wystarczy, że poprawisz elementy sterujące, rozgrywkę i grafikę. Choć może się to dziać,

Tworzenie odtwarzacza

Utwórz obiekt do przechowywania danych odtwarzacza i odpowiedzialny za takie rzeczy jak rysowanie. Tutaj tworzymy obiekt odtwarzacza za pomocą prostego literału obiektu do przechowywania wszystkich informacji.

var player = {
  color: "#00A",
  x: 220,
  y: 270,
  width: 32,
  height: 32,
  draw: function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  }
};

Na razie do oznaczenia gracza używamy prostego, kolorowego prostokąta. Podczas rysowania w grze wyczyścimy obszar roboczy i wyrysujemy gracza.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  player.draw();
}

Sterowanie za pomocą klawiatury

Używanie kluczy skrótów jQuery

Wtyczka jQuery Hotkeys znacznie ułatwia obsługę kluczy w przeglądarkach. Zamiast płakać nad nieczytelnymi problemami z keyCode i charCode w różnych przeglądarkach, możemy powiązać je w następujący sposób:

$(document).bind("keydown", "left", function() { ... });

Nie trzeba też martwić się o to, które klucze mają poszczególne kody. Chcemy po prostu móc mówić: „Gdy odtwarzacz naciśnie przycisk w górę, coś zrobi”.

Ruch gracza

Sposób, w jaki JavaScript obsługuje zdarzenia z klawiatury, jest całkowicie zależny od zdarzeń. Oznacza to, że nie ma wbudowanego zapytania do sprawdzania, czy klucz jest wyłączony, więc będziemy musieli użyć własnego.

Być może pytasz: „Dlaczego nie po prostu chcesz użyć metody obsługi kluczy opartej na zdarzeniach?”. Wynika to z tego, że częstotliwość powtórzeń klawiatury różni się w zależności od systemu i nie jest powiązana z czasem pętli gry, więc rozgrywka może się znacznie różnić w zależności od systemu. Aby zapewnić spójne wrażenia, warto zadbać o ścisłą integrację wykrywania zdarzeń klawiatury z pętlą gry.

Dobra wiadomość jest taka, że dołączyliśmy 16-wierszowe kod JavaScript, dzięki któremu zapytania o zdarzenia będą dostępne. Nazywa się one key_status.js i w każdej chwili możesz sprawdzić stan klucza, sprawdzając keydown.left itp.

Wiesz już, czy klucze są nieczynne, więc możemy użyć tej prostej metody aktualizacji, by poruszać odtwarzaczem.

function update() {
  if (keydown.left) {
    player.x -= 2;
  }

  if (keydown.right) {
    player.x += 2;
  }
}

Sprawdź się.

Odtwarzacz można zsunąć poza ekran. Ograniczmy pozycję gracza, by mieścił się w jej granicach. Poza tym odtwarzacz wydaje się działać wolno, więc podkręćmy też tempo.

function update() {
  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

Dodawanie większej liczby danych wejściowych będzie równie proste, dlatego dodamy do tego jakieś pociski.

function update() {
  if (keydown.space) {
    player.shoot();
  }

  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

player.shoot = function() {
  console.log("Pew pew");
  // :) Well at least adding the key binding was easy...
};

Dodawanie większej liczby obiektów gry

Pociski

Dodajmy pociski. Po pierwsze, potrzebujemy kolekcji, w której zostaną one zapisane w tych usługach:

var playerBullets = [];

Następnie potrzebny jest konstruktor do utworzenia wystąpień listy punktowanej.

function Bullet(I) {
  I.active = true;

  I.xVelocity = 0;
  I.yVelocity = -I.speed;
  I.width = 3;
  I.height = 3;
  I.color = "#000";

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.active = I.active && I.inBounds();
  };

  return I;
}

Kiedy gracz strzela, należy utworzyć punktor i dodać go do kolekcji punktorów.

player.shoot = function() {
  var bulletPosition = this.midpoint();

  playerBullets.push(Bullet({
    speed: 5,
    x: bulletPosition.x,
    y: bulletPosition.y
  }));
};

player.midpoint = function() {
  return {
    x: this.x + this.width/2,
    y: this.y + this.height/2
  };
};

Teraz do funkcji kroku aktualizacji musimy dodać aktualizację punktorów. Aby zapobiec zapełnianiu listy punktorów w nieskończoność, filtrujemy listę punktorów tak, aby zawierała tylko aktywne punktory. Pozwala to też usuwać pociski, które zdarzyły się z wrogiem.

function update() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.update();
  });

  playerBullets = playerBullets.filter(function(bullet) {
    return bullet.active;
  });
}

Ostatni krok to wyrównanie punktorów.

function draw() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.draw();
  });
}

Wrogowie

Czas dodawać wrogów w ten sam sposób, w jaki dodajesz pociski.

  enemies = [];

function Enemy(I) {
  I = I || {};

  I.active = true;
  I.age = Math.floor(Math.random() * 128);

  I.color = "#A2B";

  I.x = CANVAS_WIDTH / 4 + Math.random() * CANVAS_WIDTH / 2;
  I.y = 0;
  I.xVelocity = 0
  I.yVelocity = 2;

  I.width = 32;
  I.height = 32;

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.xVelocity = 3 * Math.sin(I.age * Math.PI / 64);

    I.age++;

    I.active = I.active && I.inBounds();
  };

  return I;
};

function update() {
  ...

  enemies.forEach(function(enemy) {
    enemy.update();
  });

  enemies = enemies.filter(function(enemy) {
    return enemy.active;
  });

  if(Math.random() < 0.1) {
    enemies.push(Enemy());
  }
};

function draw() {
  ...

  enemies.forEach(function(enemy) {
    enemy.draw();
  });
}

Wczytywanie i rysowanie obrazów

Fajnie jest patrzeć, jak te wszystkie pudełka latają dookoła, ale zdjęcia dla nich są jeszcze fajniejsze. Wczytywanie i rysowanie obrazów na płótnie zwykle jest uciążliwym doświadczeniem. Aby temu zapobiec, możemy skorzystać z prostych zajęć praktycznych.

player.sprite = Sprite("player");

player.draw = function() {
  this.sprite.draw(canvas, this.x, this.y);
};

function Enemy(I) {
  ...

  I.sprite = Sprite("enemy");

  I.draw = function() {
    this.sprite.draw(canvas, this.x, this.y);
  };

  ...
}

Wykrywanie kolizji

Na ekranie pojawiają się różne umowy, ale nie wchodzą ze sobą w interakcję. Aby dać znać wszystkim, kiedy wybuchać, trzeba dodać jakiś rodzaj wykrywania kolizji.

Przyjrzyjmy się prostemu algorytmowi wykrywania kolizji prostokątnych:

function collides(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}

Musimy sprawdzić kilka kolizji:

  1. Punktory gracza => statki wroga
  2. Gracz => Statki wroga

Stwórzmy metodę obsługi kolizji, które można wywoływać z metody update.

function handleCollisions() {
  playerBullets.forEach(function(bullet) {
    enemies.forEach(function(enemy) {
      if (collides(bullet, enemy)) {
        enemy.explode();
        bullet.active = false;
      }
    });
  });

  enemies.forEach(function(enemy) {
    if (collides(enemy, player)) {
      enemy.explode();
      player.explode();
    }
  });
}

function update() {
  ...
  handleCollisions();
}

Teraz trzeba dodać metody eksplozji do gracza i wrogów. Spowoduje to oznaczenie ich do usunięcia i dodanie eksplozji.

function Enemy(I) {
  ...

  I.explode = function() {
    this.active = false;
    // Extra Credit: Add an explosion graphic
  };

  return I;
};

player.explode = function() {
  this.active = false;
  // Extra Credit: Add an explosion graphic and then end the game
};

Dźwięk

Aby wzbogacić prezentację, dodamy kilka ciekawych efektów dźwiękowych. Korzystanie z dźwięków w HTML5 może być trudne, tak jak w przypadku grafiki, ale dzięki naszej technologii sound.js można to zrobić bardzo prosto.

player.shoot = function() {
  Sound.play("shoot");
  ...
}

function Enemy(I) {
  ...

  I.explode = function() {
    Sound.play("explode");
    ...
  }
}

Chociaż interfejs API nie pęka, dodanie dźwięków jest obecnie najszybszym sposobem na awarie aplikacji. Często dźwięki wycinają się lub kasują całą kartę przeglądarki, więc przygotuj chusteczki.

Pożegnanie

Oto pełna wersja demonstracyjna gry. Możesz też pobrać kod źródłowy w formacie ZIP.

Mam nadzieję, że nauka podstaw tworzenia prostych gier w językach JavaScript i HTML5 przypadła Ci do gustu. Dzięki programowaniu na odpowiednim poziomie abstrakcji możemy uwolnić się od trudniejszych części interfejsów API i uchronić się przed przyszłymi zmianami.

Odniesienia