Guida ai giochi HTML5 senza lacrime

Daniele X. Moore
Daniele X. Moore

Introduzione

Quindi vuoi creare un gioco utilizzando Canvas e HTML5? Segui questo tutorial e sarai pronto per iniziare.

Il tutorial presuppone almeno un livello intermedio di conoscenza di JavaScript.

Innanzitutto puoi giocare oppure passare direttamente all'articolo e visualizzare il codice sorgente del gioco.

Creazione del canvas

Per disegnare cose, dobbiamo creare una tela. Poiché questa è una guida pratica, useremo 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');

Ciclo di gioco

Per simulare l'aspetto di un gameplay continuo e fluido, Vogliamo aggiornare il gioco e ridisegnare lo schermo più velocemente di quanto la mente umana e gli occhi possano percepire.

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

Per ora possiamo lasciare vuoti i metodi di aggiornamento e disegno. È importante sapere che setInterval() si occupa di chiamare periodicamente.

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

Hello world

Ora che il ciclo di gioco è in corso, aggiorniamo il metodo di disegno per disegnare effettivamente del testo sullo schermo.

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

Sembra interessante per i testi stazionari, ma poiché abbiamo già configurato un ciclo di gioco, dovremmo essere in grado di farlo muovere abbastanza facilmente.

var textX = 50;
var textY = 50;

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

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

Ora provaci. Se lo stai seguendo, dovrebbe essere in movimento, ma lasciando sullo schermo le volte precedenti. Prenditi un momento per individuare il motivo. Il motivo è che non stiamo cancellando lo schermo. Aggiungiamo quindi un codice per cancellare lo schermo al metodo di disegno.

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

Ora che hai del testo in movimento sullo schermo, sei a metà strada. Basta perfezionare i controlli, migliorare il gameplay, ritoccare la grafica. Ok, forse 1/7 del modo per avere un gioco vero, ma la buona notizia è che c'è molto altro nel tutorial.

Creazione del player in corso...

Crea un oggetto per conservare i dati del player ed essere responsabile di attività come il disegno. Qui creiamo un oggetto player utilizzando un semplice oggetto letterale per contenere tutte le informazioni.

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

Per il momento stiamo utilizzando un semplice rettangolo colorato per rappresentare il player. Quando tracciamo la partita, cancelleremo la tela e tragheremo il giocatore.

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

Controlli da tastiera

Utilizzo dei tasti di scelta rapida jQuery

Il plug-in jQuery Hotkeys semplifica notevolmente la gestione delle chiavi in più browser. Anziché piangere per problemi incomprensibili relativi a keyCode e charCode tra browser, possiamo associare eventi come questo:

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

È una grande opportunità per non doverti preoccupare dei dettagli delle chiavi e dei codici. Vogliamo semplicemente dire cose come "quando il player preme il pulsante Su, fai qualcosa". I tasti di scelta rapida di jQuery lo permettono.

Movimento del giocatore

Il modo in cui JavaScript gestisce gli eventi da tastiera è basato completamente sugli eventi. Ciò significa che non esiste una query integrata per verificare se una chiave non è attiva, perciò dovremo usare la nostra.

Ti starai chiedendo: "Perché non utilizzare semplicemente un modo di gestire le chiavi basato su eventi?". Il motivo è che la frequenza di ripetizione della tastiera varia da un sistema all'altro e non è vincolata alla tempistica del ciclo di gioco. Di conseguenza, il gameplay potrebbe variare notevolmente da un sistema all'altro. Per creare un'esperienza coerente, è importante che il rilevamento degli eventi della tastiera sia strettamente integrato nel ciclo di gioco.

La buona notizia è che ho incluso un wrapper JS a 16 righe che renderà disponibile le query sugli eventi. È chiamato key_status.js e puoi eseguire query sullo stato di una chiave in qualsiasi momento controllando keydown.left e così via.

Ora che abbiamo la possibilità di eseguire query per sapere se le chiavi non sono attive, possiamo utilizzare questo semplice metodo di aggiornamento per spostare il player.

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

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

Provaci.

Potresti notare che il player viene spostato fuori dallo schermo. Blocca la posizione del player per mantenerlo entro i limiti. Inoltre, il player sembra un po' lento, quindi aumentiamo anche la velocità.

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

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

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

Aggiungere altri input sarà altrettanto facile, quindi aggiungiamo qualche tipo di proiettile.

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

Aggiunta di altri oggetti di gioco

Proiettili

Aggiungiamo ora i proiettili per davvero. Prima di tutto, abbiamo bisogno di una raccolta in cui archiviarli tutti:

var playerBullets = [];

Successivamente, abbiamo bisogno di un costruttore per creare istanze bullet.

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

Quando il giocatore spara, dobbiamo creare un'istanza bullet e aggiungerla alla raccolta di punti elenco.

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

Ora dobbiamo aggiungere l'aggiornamento dei punti elenco alla funzione di passaggio di aggiornamento. Per evitare che la raccolta di punti elenco si riempia a tempo indeterminato, l'elenco di punti elenco viene filtrato in modo da includere solo quelli attivi. Questo ci permette anche di rimuovere i proiettili che si sono scontrati con un nemico.

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

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

Il passaggio finale consiste nel disegnare i punti elenco:

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

Nemici

Ora è il momento di aggiungere i nemici nello stesso modo in cui aggiungiamo i punti elenco.

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

Caricamento e disegno delle immagini in corso

È bello guardare tutte quelle scatole che volano in giro, ma avere delle immagini sarebbe ancora più bello. Il caricamento e il disegno di immagini su tela di solito sono errori. Per evitare dolori e infelizie, possiamo utilizzare una semplice classe di utilità.

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

  ...
}

Rilevamento delle collisioni

Abbiamo tutti questi deal in volo sullo schermo, ma non interagiscono tra loro. Per far sapere a tutti quando far esplodere, dovremo aggiungere un qualche tipo di rilevamento delle collisioni.

Utilizziamo un semplice algoritmo per il rilevamento delle collisioni rettangolare:

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

Vogliamo verificare un paio di conflitti:

  1. Proiettili dei giocatori => Navi nemiche
  2. Giocatore => Navi nemiche

Creiamo un metodo per gestire le collisioni che possiamo chiamare dal metodo di aggiornamento.

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

Ora dobbiamo aggiungere i metodi di esplosione al giocatore e ai nemici. In questo modo verranno segnalati per la rimozione e verrà aggiunta un'esplosione.

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

Audio

Per completare l'esperienza, aggiungeremo alcuni dolci effetti sonori. L'audio, come le immagini, può essere un po' complicato da usare in HTML5, ma grazie alla nostra magica formula sound.js, l'audio può essere reso estremamente semplice.

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

function Enemy(I) {
  ...

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

Sebbene l'API sia ora priva di lacrime, l'aggiunta di suoni è attualmente il modo più rapido per arrestare l'applicazione. Non è raro che l'intera scheda del browser rimossa o escluda l'intera scheda del browser, quindi prepara i fazzoletti.

Addio

Anche in questo caso, ecco la demo completa del gioco funzionante. Puoi anche scaricare il codice sorgente come codice postale.

Spero ti sia piaciuto apprendere le nozioni di base per creare un gioco semplice in JavaScript e HTML5. Programmando al giusto livello di astrazione, possiamo isolarci dalle parti più difficili delle API ed essere più resilienti di fronte ai cambiamenti futuri.

Riferimenti