Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Player principale e collisioni

Come gestire le animazioni e il controllo dei personaggi e aggiungere al game engine la nozione di collisione.
Come gestire le animazioni e il controllo dei personaggi e aggiungere al game engine la nozione di collisione.
Link copiato negli appunti

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

Sprite del personaggio principale

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.

Il personaggio disegnato

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.

ALT_TEXT

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.

ALT_TEXT

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)

ALT_TEXT

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.

Ti consigliamo anche