Come visto nei capitoli precedenti, nonostante JavaScript non sia propriamente un linguaggio Object Oriented, possiamo sfruttare l'ereditarietà di primo livello tramite prototype. Vediamo quindi come sfruttare l'ereditarietà per eliminare il codice ripetuto e includere il codice riusabile in un unico oggetto GameObj.
function GameObj(x, y) {
// variabili di uso comune inizializzate dal costruttore
this.x = x;
this.y = y;
this.animSpeed = 1;
this.curFrame = 0;
this.xOffset = 0;
this.yOffset = 0;
// questa funzione imposta l'offset di
// rendering al centro dello sprite
this.OffsetCenter = function() {
this.xOffset = this.sprite.w/2;
this.yOffset = this.sprite.height/2;
}
// cicla una lista di gameobject
// ritorna l'id di un oggetto con cui
// collide il bounding box
this.GetCollision = function(gameobj_list, x, y) {
var len = gameobj_list.length;
for(var i = 0; i < len; i++){
if(this.bbox.CollidesAt(gameobj_list[i].bbox, x, y)) {
return gameobj_list[i];
}
}
return null;
}
// metodo di draw per GameObject semplici
this.__DrawSimple = function() {
game.ctx.drawImage(this.sprite, this.x - game.viewX - this.xOffset, this.y - game.viewY - this.yOffset);
}
// metodo di draw per GameObject animati
this.__DrawAnimated = function() {
var ox = Math.floor(this.curFrame) * this.sprite.w;
game.ctx.drawImage(this.sprite, ox, 0, this.sprite.w,this.sprite.height,this.x - game.viewX - this.xOffset, this.y - game.viewY - this.yOffset, this.sprite.w, this.sprite.height);
}
// imposta lo sprite e seleziona l'evento draw corretto
this.SetSprite = function(sprite) {
this.sprite = sprite;
if(sprite.frames > 0) {
// imposta l'evento draw animato
this.Draw = this.__DrawAnimated;
} else {
//imposta l'evento draw semplice e cancella l'evento animazione
this.Draw = this.__DrawSimple;
this.UpdateAnimation = function(){};
}
}
// aggiorna i frames dell'animazione in base a animSpeed,
// che può essere sia positiva che negativa.
this.UpdateAnimation = function() {
this.curFrame += this.animSpeed;
if(this.animSpeed > 0) {
var diff = this.curFrame - this.sprite.frames;
if(diff >= 0) {
this.curFrame = diff;
// al termine dell'animazione, chiama questa funzione
this.OnAnimationEnd();
}
}
else if(this.curFrame < 0) {
this.curFrame = (this.sprite.frames + this.curFrame) - 0.0000001;
// al termine dell'animazione, chiama questa funzione
this.OnAnimationEnd();
}
}
// distrugge l'istanza corrente eliminandola dalla lista dei
// gamobjects e attiva l'evento OnDestroy
this.Destroy = function() {
this.OnDestroy();
game.gameobjects.splice(game.gameobjects.indexOf(this), 1);
}
// inizializza le variabili degli eventi non generici con funzioni vuote
this.Draw = function(){}
this.Update = function(){}
this.OnAnimationEnd = function(){}
this.OnDestroy = function(){}
//aggiunge questo gameobject alla lista di gameobjects
game.gameobjects.push(this);
}
Il prossimo passo, è definire l'array gameobject in ResetLevel
this.ResetLevel = function() {
//...
this.gameobjects = [];
}
Generalizziamo le funzioni Update, EndLoop e Draw di Game
:
// aggiorna tutti i gameobjects
this.Update = function() {
if(this.level > 0) {
for(var i = 0; i < this.gameobjects.length; i ++) {
this.gameobjects[i].Update();
}
}
}
// aggiorna tutte le animazioni
this.EndLoop = function() {
if(this.level > 0) {
for(var i = 0; i < this.gameobjects.length; i ++) {
this.gameobjects[i].UpdateAnimation();
}
}
}
// disegna le tiles e i gameobjects
this.Draw = function() {
//...
// al posto di player.Draw()
for(var i = 0; i < this.gameobjects.length; i ++) {
this.gameobjects[i].Draw();
}
//...
}
In questo modo stabiliamo che, a ogni loop, gestiremo tutto ciò che viene dichiarato nei rispettivi metodi di tutti i game object.
Ereditarietà dei game object
Definiamo una nuova funzione, che chiamiamo Inherit
, che ci consente di creare nuovi oggetti eredi di GameObj
function Inherit(obj, parent) {
// impostiamo GameObj come parent predefinito
if(parent == undefined)
parent = GameObj;
//imposta il prototype per ereditare dal parent
obj.prototype = Object.create(parent.prototype);
obj.prototype.constructor = obj;
}
Facciamo le opportune modifiche all'oggetto Player
, per ereditare da GameObj. Intanto aggiungiamo il codice relativo ai colpi subiti e impostiamo un invulnerabilità di qualche secondo dopo l'impatto, mostrata visivamente da un l'effetto grafico "trasparenza".
Ecco la function completa:
function Player() {
// imposto le coordinate di partenza
this.xStart = game.canvas.width/2;
this.yStart = game.canvas.height/2-80;
this.x = this.xStart;
this.y = this.yStart;
// chiamo il costruttore del parent
GameObj.call(this, this.x, this.y);
//imposto lo sprite e il metodo Draw
this.SetSprite(game.sprPlayerRun, true);
this.width = this.sprite.w;
this.height = this.sprite.height;
this.xOffset = Math.floor(this.width/2);
this.yOffset = this.height;
this.animSpeed = 0.2;
this.maxSpeed = 5;
this.hSpeed = 0;
this.vSpeed = 0;
this.gravity = 0.4;
this.scaling = 1;
this.lives = 5;
this.shotTime = 0;
this.canShot = true;
// variabile che indica se il personaggio è stato colpito
this.hit = false;
//durata dell'invulnerabilità dopo il colpo
this.hitTimer = 0;
//variabili trasparenza
this.hitAlpha = 0;
this.hitAlphaTimer = 30;
this.bbox = new BoundingBox(this.x - this.xOffset, this.y - this.yOffset, this.width, this.height);
this.Draw = function() {
game.ctx.save();
// se è stato colpito, utilizza la trasparenza "a intermittenza"
if(this.hit) {
if(this.hitAlpha)
game.ctx.globalAlpha = 0.3;
else
game.ctx.globalAlpha = 0.6;
}
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.sprite.height, this.sprite.w, this.sprite.height);
game.ctx.restore();
}
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;
this.sprite = game.sprPlayerIdle;
this.curFrame = 0;
}
}
if(Inputs.GetKeyPress("Z") && this.GetCollision(game.blocks, 0 , 1)){
this.vSpeed -= 9;
}
this.vSpeed += this.gravity;
var collides = false;
for(var a = Math.abs(this.vSpeed); a > 0; a-=Math.abs(this.gravity)) {
if(this.vSpeed > 0) {
if(!this.GetCollision(game.blocks, 0, a)) {
this.y += a;
break;
} else {
collides = true;
}
} else {
if( !this.GetCollision(game.blocks, 0 , -a)) {
this.y -= a;
break;
} else {
collides = true;
}
}
}
if(collides) {
this.vSpeed = 0;
}
var collides = false;
for(var a = Math.abs(this.hSpeed); a > 0; a--) {
if(this.hSpeed > 0) {
if(!this.GetCollision(game.blocks, a , 0)) {
this.x += a;
break;
}
else collides = true;
} else {
if( !this.GetCollision(game.blocks, - a , 0)) {
this.x -= a;
break;
}
else collides = true;
}
}
if(this.hSpeed != 0) {
// sposto il player
this.scaling = (this.hSpeed < 0) ? -1 : 1;
if(this.sprite != game.sprPlayerRun) {
this.sprite = game.sprPlayerRun;
this.curFrame = 0;
}
this.animSpeed = 0.1 + Math.abs( this.hSpeed / this.maxSpeed * 0.12);
}
if(this.vSpeed > 0){
this.sprite = game.sprPlayerFall;
this.curFrame = 0;
} else if(this.vSpeed < 0) {
this.sprite = game.sprPlayerJump;
this.curFrame = 0;
}
// aggiorna le variabii relative allo stato di "colpito"
if(this.hit) {
this.hitTimer--;
this.hitAlphaTimer--;
if(this.hitAlphaTimer < 0) {
this.hitAlpha = !this.hitAlpha;
this.hitAlphaTimer = 10;
}
if(this.hitTimer <= 0){
this.hit = false;
}
}
this.bbox.Move(this.x - this.xOffset, this.y - this.yOffset);
//se il player cade sotto il livello, muore
if(this.y > game.areaH) {
this.Die();
}
var targetX = Math.clamp(this.x - game.canvas.width/2, 0, game.areaW - game.canvas.width);
var targetY = Math.clamp(this.y - game.canvas.height/2, 0, game.areaH - game.canvas.height);
// muove la telecamera verso il personaggio, in modo smussato
game.viewX = Math.floor(Math.lerp(game.viewX, targetX, 0.2));
game.viewY = Math.floor(Math.lerp(game.viewY, targetY, 0.2));
}
//imposta lo status in "colpito" per un certo tempo
this.Hit = function() {
this.hit = true;
this.hitTimer = 140;
this.hitAlpha = true;
this.hitAlphaTimer = 10;
// riduce le vite di 1
this.lives--;
if(this.lives <= 0) {
this.Die();
}
//simula un contraccolpo verso l'alto
this.vSpeed = Math.clamp(this.vSpeed-5, -5, 0);
}
// riporta il personaggio all'inizio del livello
// azzera le vite e il punteggio
this.Die = function() {
this.x = this.xStart;
this.y = this.yStart;
this.lives = 5;
game.score = 0;
}
}
Inseriamo appena sotto la funzione Player, il seguente codice:
// Player eredita da GameObj
Inherit(Player);