Przewodnik po grach HTML5

Daniel X. Moore
Daniel X. Moore

Wprowadzenie

Chcesz tworzyć gry za pomocą Canvas i HTML5? Postępuj zgodnie z tym samouczkiem, a w krótkim czasie zaczniesz tworzyć własne gry.

Ten samouczek zakłada, że znasz język JavaScript na poziomie co najmniej średnio zaawansowanym.

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

Tworzenie obszaru roboczego

Aby coś narysować, musimy utworzyć płótno. Ponieważ jest to przewodnik bez łez, 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ć płynną i ciągłą rozgrywkę, chcemy aktualizować grę i odświeżać ekran szybciej niż ludzki umysł i oko mogą to ogarnąć.

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

Na razie możemy pozostawić puste metody update i draw. Ważne jest, aby wiedzieć, że setInterval() okresowo dzwoni do tych użytkowników.

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

Hello world

Teraz, gdy mamy już pętlę gry, zaktualizujmy metodę rysowania, aby narysować na ekranie tekst.

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

To świetne rozwiązanie w przypadku stałego tekstu, ale ponieważ mamy już pętlę gry, powinniśmy móc łatwo ją przemieścić.

var textX = 50;
var textY = 50;

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

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

Spróbuj. Jeśli idziesz za mną, powinieneś widzieć, że wskaźnik się porusza, ale zostawia też poprzednie pozycje, w których był wyświetlany na ekranie. Zastanów się, dlaczego tak się dzieje. Dzieje się tak, ponieważ nie czyścimy ekranu. Dodaj kod oczyszczania ekranu do metody draw.

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

Teraz, gdy tekst porusza się na ekranie, jesteś już w połowie drogi do stworzenia prawdziwej gry. Po prostu dopraczono sterowanie, ulepszono rozgrywkę i poprawiono grafikę. Może 1/7 drogi do prawdziwej gry, ale dobrą wiadomością jest to, że samouczek jest znacznie dłuższy.

Tworzenie odtwarzacza

Utwórz obiekt do przechowywania danych gracza i odpowiadania za takie czynności jak rysowanie. Tutaj tworzymy obiekt player za pomocą prostego literału obiektu, który przechowuje wszystkie informacje.

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 używamy prostego kolorowego prostokąta, który reprezentuje odtwarzacz. Gdy rysujemy grę, czyścimy płótno i rysujemy gracza.

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

Sterowanie za pomocą klawiatury

Korzystanie z skrótów klawiszowych jQuery

Plik JavaScript z skrótami klawiszowymi znacznie ułatwia obsługę klawiszy w różnych przeglądarkach. Zamiast rozpaczać się nad nierozszyfrowanymi problemami z usługami keyCodecharCode w różnych przeglądarkach, możemy związać zdarzenia w taki sposób:

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

Nie musisz się martwić, które klucze mają które kody. Chcemy tylko móc powiedzieć coś w stylu „gdy gracz naciśnie przycisk w górę, zrób coś”. Skróty klawiszowe jQuery to umożliwiają.

Ruch gracza

Sposób obsługi zdarzeń związanych z klawiaturą przez JavaScript zależy całkowicie od zdarzeń. Oznacza to, że nie ma wbudowanego zapytania do sprawdzania, czy klucz jest niedostępny, więc musimy użyć własnego.

Możesz się zastanawiać, dlaczego nie użyć metody obsługi kluczy opartej na zdarzeniach. Ponieważ częstotliwość powtarzania klawiszy jest różna w zależności od systemu i nie jest powiązana z czasem trwania pętli gry, rozgrywka może się znacznie różnić w zależności od systemu. Aby zapewnić spójne wrażenia, ważne jest, aby wykrywanie zdarzeń klawiatury było ściśle zintegrowane z pętlą gry.

Dobra wiadomość jest taka, że dodałem 16-wierszową osłonkę JS, która umożliwi wykonywanie zapytań do zdarzeń. Nazywa się on key_status.js i w każdej chwili możesz sprawdzić stan klucza, sprawdzając keydown.left itp.

Teraz, gdy możemy zapytać, czy klawisze są wciśnięte, możemy użyć tej prostej metody aktualizacji, aby przesuwać odtwarzacz.

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

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

Śmiało, sprawdź.

Możesz zauważyć, że odtwarzacz można przesunąć poza ekran. Zablokujmy pozycję gracza, aby nie wychodził poza obszar. Dodatkowo odtwarzacz wydaje się trochę wolny, więc zwiększmy też jego szybkość.

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 kolejnych danych wejściowych będzie równie proste, więc dodajmy 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 kolejnych obiektów gry

Pociski

Teraz dodamy pociski. Najpierw potrzebujemy kolekcji, w której będziemy je przechowywać:

var playerBullets = [];

Następnie potrzebujemy konstruktora do tworzenia przykładów pocisków.

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;
}

Gdy gracz strzela, powinniśmy utworzyć pocisk i dodać go do kolekcji pociskó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 musimy dodać do funkcji aktualizacji kroku aktualizację punktów. Aby zapobiec nieograniczonemu wypełnianiu się kolekcji punktów, filtrujemy listę punktów, aby zawierała tylko aktywne punkty. Dzięki temu możemy też usuwać pociski, które uderzyły we wroga.

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

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

Ostatnim krokiem jest narysowanie punktów:

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

Wrogowie

Teraz nadszedł czas na dodanie wrogów w sposób podobny do tego, w jaki dodaliśmy 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 oglądać te wszystkie latające pudełka, ale jeszcze fajniej byłoby mieć do nich obrazy. Ładowanie i rysowanie obrazów na płótnie to zwykle łzawe przeżycie. Aby uniknąć tego bólu i cierpienia, możemy użyć prostej klasy pomocniczej.

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

Mamy na ekranie różne elementy, ale nie oddziałują one na siebie. Aby określić, kiedy wszystko ma wybuchnąć, musimy dodać pewien rodzaj wykrywania kolizji.

Użyjemy prostego algorytmu 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;
}

Chcemy sprawdzić kilka kolizji:

  1. Kule gracza => Statki wroga
  2. Gracz> Statki wroga

Utwórzmy metodę do obsługi kolizji, którą można wywołać 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 musimy dodać metody eksplozji do gracza i wrogów. Spowoduje to ich oznaczenie 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 dopełnić całości, dodamy kilka fajnych efektów dźwiękowych. Dźwięki, podobnie jak obrazy, mogą być trudne do użycia w HTML5, ale dzięki naszej magicznej formule sound.js można je stosować bardzo prosto.

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

function Enemy(I) {
  ...

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

Chociaż interfejs API jest teraz bezpłatny, dodanie dźwięku jest obecnie najszybszym sposobem na zablokowanie aplikacji. Nierzadko dźwięki są wyciszane lub wyłączane w całym oknie przeglądarki, więc przygotuj chusteczki.

Farewell

Oto pełna wersja demo działającej gry. Możesz też pobrać kod źródłowy w formacie ZIP.

Mam nadzieję, że spodobało Ci się poznawanie podstaw tworzenia prostej gry w JavaScript i HTML5. Programowanie na odpowiednim poziomie abstrakcji pozwala nam odizolować się od trudniejszych części interfejsów API, a także chronić przed przyszłymi zmianami.

Odniesienia