Con questa lezione finalmente entriamo nel vivo dell'azione sviluppando il gioco vero e proprio e iniziamo creando il personaggio principale (il player).
Iniziamo col creare l'animazione che realizziamo, come abbiamo visto, servendoci di una strip con tutti i fotogrammi (frame) affiancati. Se ancora non le avete, potete scaricare le risorse grafiche riportate in allegato.
Carichiamo quindi le immagini relative al personaggio e un'immagine per il background all'interno della funzione Game
, nel file main.js
, come visto nelle lezioni precedenti:
//ResourceHandler.LoadSprite(src, subimages, callback);
this.sprPlayerIdle = rh.LoadSprite("img/playerIdle.png",2);
this.sprPlayerIdleShot = rh.LoadSprite("img/playerShot.png",1);
this.sprPlayerRun = rh.LoadSprite("img/playerRun.png",6);
this.sprPlayerJump = rh.LoadSprite("img/playerJump.png",1);
this.sprPlayerJumpShot = rh.LoadSprite("img/playerJumpShot.png",1);
this.sprPlayerFall = rh.LoadSprite("img/playerFall.png",1);
this.sprPlayerFallShot = rh.LoadSprite("img/playerFallShot.png",1);
this.background1 = rh.LoadSprite("img/sky.png", 1);
Definiamo quindi l'oggetto player all'interno di main.js
function Player(){
this.sprite = game.sprPlayerRun;
this.curFrame = 0;
this.animSpeed = 0.2;
this.width = this.sprite.w;
this.height = this.sprite.height;
this.xStart = game.canvas.width/2;
this.yStart = game.canvas.height/2-60;
this.x = this.xStart;
this.y = this.yStart;
this.xOffset = Math.floor(this.width/2);
this.yOffset = this.height;
this.Draw = function(){
game.ctx.save();
game.ctx.translate(this.x-game.viewX,this.y-game.viewY);
game.ctx.scale(this.scaling, 1);
var ox = Math.floor(this.curFrame) * this.width;
game.ctx.drawImage(this.sprite, ox, 0,
this.sprite.w, this.sprite.height,
-this.xOffset, -this.yOffset,
this.sprite.w, this.sprite.height);
game.ctx.restore();
}
}
Definiamo, fin da subito, gran parte delle variabili locali che ci serviranno:
Variabile | Descrizione |
---|---|
sprite | Immagine contenente tutti i frames dell'animazione corrente |
curFrame | Il frame corrente |
width | Larghezza del singolo frame ( non bisogna utilizzare sprite.width perchè questa variabile corrisponde alla larghezza totale dell'immagine e non del singolo frame) |
height | Altezza del singolo frame |
xStart, yStart | Coordinate di partenza |
x, y | Coordinate attuali del personaggio |
xOffset, yOffset | Offset con cui disegneremo l'immagine |
Definiamo anche la funzione Draw che svolgerà le seguenti azioni:
- Salva l'impostazione del context attuale;
- Trasla il context in base alle coordinate del personaggio, meno la X della view (che serve a simulare una telecamera: nel nostro caso seguirà il personaggio e influirà sulla traslazione dell'intera scena);
- Scala orizzontalmente il context, in base all'orientamento del personaggio;
- Calcola la posizione X del frame corrente nell'intera strip di animazione.
- Disegna solamente la porzione di immagine relativa al frame corrente utilizzando la funzione DrawImage, nella sua forma estesa (maggiori info su W3C)
- Ripristina le impostazioni del context
Ricordiamoci di inizializzare le seguenti variabili nell'oggetto Game
:
this.viewX = 0;
this.viewY = 0;
Quindi inseriamo nella function Draw dell'oggetto Game, il draw del background e la funzione Draw del player. Poi per testare, creiamo un istanza dell'oggetto player e vediamo se viene renderizzata a schermo
this.ResetLevel = function() {
//...
this.player = null;
}
this.LoadLevel = function(lev) {
//...
if(lev == 0) {
//...
}
else{
//carica un livello di gioco
this.player = new Player();
}
}
this.Draw = function() {
//...
if(lev == 0) {
//...
} else {
//disegna il fondale "sky.png" riempiendo il canvas
this.ctx.drawImage(this.background1, 0, 0, this.canvas.width, this.canvas.height);
//livello di gioco
this.player.Draw();
}
}
Se non abbiamo commesso errori, una volta entrati nel primo livello, cliccando "New game" dovrebbe essere disegnato il personaggio a schermo.
I Movimenti
Il passo successivo è quello di dare vita al nostro personaggio, facendo in modo che risponda agli input da tastiera. Aggiungiamo qualche variabile al nostro oggetto Player
:
this.maxSpeed = 5;
this.hSpeed = 0;
this.vSpeed = 0;
Quindi aggiungiamo la funzione Update:
this.Update = function() {
if(Inputs.GetKeyDown(KEY_RIGHT)) {
if(this.hSpeed < 0) this.hSpeed = 0;
if(this.hSpeed < this.maxSpeed) this.hSpeed += 0.4;
}
else if(Inputs.GetKeyDown(KEY_LEFT)) {
if(this.hSpeed > 0) this.hSpeed = 0;
if(this.hSpeed > -this.maxSpeed) this.hSpeed -= 0.4;
}
else{
this.hSpeed/=1.1;
if(Math.abs(this.hSpeed) < 1) {
this.hSpeed = 0;
}
}
if(this.hSpeed != 0){
// sposto il player
this.x += this.hSpeed;
}
}
Il primo if
, verifica che il tasto KEY_RIGHT
(Freccia a destra) della tastiera sia premuto. Quindi controlla che la velocità orizzontale sia minore di 0 (in tal caso il personaggio si stava muovendo a sinistra):
if(this.hSpeed < 0) this.hSpeed = 0;
se true
: imposta la sua velocità a 0
, ovvero ferma il personaggio per fare in modo che inizi a muoversi verso destra.
Se la velocità orizzontale è entro i limiti della velocità massima:
if(this.hSpeed < this.maxSpeed) this.hSpeed+=0.4;
incrementa la velocità orizzontale (che assumerà valori positivi sempre maggiori, quindi è una velocità orizzontale verso destra).
Lo stesso discorso si applica alla pressione del tasto KEY_LEFT
, ma con segni opposti, dato che si tratta di una velocità negativa sull'asse x.
Se nessuno dei due tasti è premuto, la velocità orizzontale viene ridotta dividendola per un valore (nel nostro caso 1.1) e se il suo valore assoluto è minore di 1 (utilizziamo Math.abs
in modo che il segno +/-
non influisca sul controllo della variabile), imposta a 0 la velocità del player.
Infine, se la velocità orizzontale è diversa da 0, spostiasmo la X del personaggio di un numero di pixel, pari alla sua velocità.
L'ultimo passo, è quello di aggiungere player.Update
all'interno dell'evento Update dell'oggetto Game
:
//aggiorna tutto
this.Update = function() {
if(this.level > 0) {
this.player.Update();
}
}
Se avviamo il gioco, dovremmo avere la possibilità di spostare il personaggio sull'asse X
premendo i tasti della tastiera.
Poiché un personaggio paralizzato non è tanto bello da vedere, vediamo subito come applicare le animazioni e definiamo la funzione UpdateAnimation
, che gestirà il flusso dei frames dell'animazione.
this.UpdateAnimation = function() {
this.curFrame += this.animSpeed;
if(this.animSpeed > 0) {
var diff = this.curFrame - this.sprite.frames;
if(diff >= 0){
this.curFrame = diff;
}
}
else if(this.curFrame < 0){
this.curFrame = (this.sprite.frames + this.curFrame) - 0.0000001;
}
}
Aggiungiamo all'oggetto Game, una funzione EndLoop, che verrà chiamata nella funzione GameLoop, subito dopo il draw.
All'interno di EndLoop
, eseguiamo UpdateAnimation
del personaggio
//aggiorna animazioni
this.EndLoop = function(){
if(this.level > 0){
this.player.UpdateAnimation();
}
}
this.GameLoop = function() {
//..
this.Draw();
this.EndLoop();
//..
}
In questo modo, il personaggio eseguirà continuamente l'animazione di corsa. Dobbiamo perciò impostare lo sprite di Idle
(personaggio fermo), quando non sta effettivamente correndo e l'animazione di corsa quando hSpeed
è diversa da 0
Aggiungiamo alcune righe di codice alla funzione Update del player:
this.Update = function() {
if(Inputs.GetKeyDown(KEY_RIGHT)) {
//...
}
else if(Inputs.GetKeyDown(KEY_LEFT)) {
//...
}
else{
//x...
if(Math.abs(this.hSpeed) < 1) {
// ...
//imposto lo sprite del personaggio fermo
this.sprite = game.sprPlayerIdle;
this.curFrame = 0;
}
}
//...
if(this.hSpeed != 0) {
// sposto il player
this.x += hSpeed;
// orientamento orizzontale dello sprite
this.scaling = (this.hSpeed < 0) ? -1 : 1;
//cambio sprite
if(this.sprite != game.sprPlayerRun) {
this.sprite = game.sprPlayerRun;
this.curFrame = 0;
}
this.animSpeed = 0.1 + Math.abs( this.hSpeed / this.maxSpeed * 0.12);
}
}
Quando impostiamo l'animazione della corsa, dobbiamo anche impostare scaling orizzontale (variabile this.scaling
) in base alla velcocità orizzontale.
Inoltre determiniamo la velocità di animazione, in base ad hSpeed
, in modo da avere un animazione più veloce, man mano che hSpeed
aumenta.
Collisioni
Perché il personaggio interagisca con il mondo circostante, abbiamo bisogno di introdurre il meccanismo delle collisioni.
Esistono diverse tecniche per farlo, più o meno elaborate e realistiche. In questa guida utilizzeremo per semplicità il metodo del Bounding Box. Con questo sistema, si dovrà approssimare ogni forma ad un rettangolo, in modo da poter verificare in modo semplice l'intersezione tra due forme.
Creiamo un nuovo file bbox.js
e all'interno inseriamo il seguente codice
function BoundingBox(x,y,w,h){
this.x = x;
this.y = y;
this.width = w;
this.height = h;
this.Move = function(x,y){
this.x = x;
this.y = y;
}
}
Dichiariamo quindi 2 variabili per indicare la posizione (x, y) e due per le dimensioni (width
, height
) del nostro rettangolo.
Aggiungiamo una funzione Move
, per semplificare il codice nel caso dovessimo spostare il nostro BoundingBox
Per verificare la collisione con un altro bounding box, possiamo inserire la seguente funzione
this.Collides = function(b){
return !(this.x + this.width < b.x || b.x + b.width < this.x ||
this.y + this.height < b.y || b.y + b.height < this.y);
}
L'argomento b
della funzione è un altra istanza di BoundingBox
.
Questa funzione utilizza le seguenti condizione per verificare se i rettangoli NON si intersecano
no_collisione = (x1+w1<x2 or x2+w2<x1 or y1+h1<Y2 or y2+h2<y1)
Ecco un immagine per spiegare la prima condizione X1+W1<X2
(le altre si possono immaginare di conseguenza)
Per completare, aggiungiamo altre 2 funzioni
//dx: deltax , dy: deltay
this.CollidesAt = function(b, dx, dy){
return !(this.x + dx + this.width < b.x || b.x + b.width < this.x + dx || this.y + this.height + dy < b.y || b.y + b.height < this.y + dy);
}
this.CollidesPosition = function(b, x, y){
return !(x + this.width < b.x || b.x + b.width < x || y + this.height < b.y || b.y + b.height < y);
}
Funzione | Descrizione |
---|---|
CollidesAt | Verifica una collisione col bounding box "b" spostando temporaneamente il bounding box di una quantità relativa (dx, dy) |
CollidesPosition | Non tiene conto della posizione x,y attuale del bounding box, ma utilizza temporaneamente le coordinate assolute passate come argument, per verificare la collisione con b |
Esistono numerose librerie per collisioni poligonali e simulazioni fisiche, come Box2DWeb o PhysicsJS a cui daremo un occhiata nel capitolo conclusivo.