Trabajando en un Canvas con TypeScript (II)

bannerpost

En el artículo anterior ya vimos las partes más básicas para crear un juego e TypeScript, ahora que tenemos eso vamos a crear la lógica del juego y terminar con los detalles finales.

Generación de enemigos

Recordando que ya tenemos la clase Enemy hecha, solo tenemos que crear la lógica del juego que tienen detrás.

  • Generar enemigos en posiciones aleatorias.
  • Mover estos enemigos por la pantalla.
  • Aumentar el número de enemigos a medida que pase el tiempo. 
  • Destruir las instancias cuando dejen de ser necesarias.
  • Restar vida al tocar un enemigo.

 

Generar enemigos en posiciones aleatorias

Primero de todo añadimos las propiedades que vamos a necesitar en Game.ts:

    1: private enemies: Array<Enemy> = []; // Array con los enemigos activos
    2: private time: number = 0; // Inicializo el tiempo
    3: private minTime: number = 3000; // Cada cuanto tiempo generamos enemigos
    4: private score: number = 0; // Puntuación obtenida
    5: private maxEnemies: number = 2; // Número de enemigos que generamos (cambiará)

En el método update, comprobamos que cada X tiempo vamos a tener que mirar que pasa con los enemigos.

Lo que hace este código es mirar si ha pasado el tiempo configurado y si es así, guardar el nuevo timestamp, generar enemigos y añadir algo de puntuación:

    1: private update(): void { 
    2:     ...
    3:     // Check enemies generation and destruction
    4:     var now = Date.now();
    5:     if (now - this.time > this.minTime) {
    6:         this.time = now;
    7:         this.score += 100;
    8:         this.checkEnemies();
    9:     }
   10:  
   11: }

En el método checkEnemies, de momento vamos a generar todos los enemigos para este turno, dependerán del número de enemigos máximo que pueda hacer y se guardarán en el array de enemigos activos:

    1: private checkEnemies(): void {
    2:     for (var i: number = 0; i < this.maxEnemies; i++) {
    3:         this.enemies.push(this.createRandomEnemy());
    4:     }
    5: }
    6:  
    7: private createRandomEnemy(): Enemy {
    8:     var enemy = new Enemy();
    9:     enemy.x = Math.floor(Math.random() * this.canvas.width * 0.8) + (this.canvas.width - this.canvas.width * 0.8);
   10:     enemy.y = Math.floor(Math.random() * this.canvas.height * 0.8) + (this.canvas.height - this.canvas.height * 0.8);
   11:     return enemy;
   12: }

Solo nos queda modificar el método draw para que se pinten los enemigos activos:

    1: private draw(): void {
    2:     ...
    3:     this.drawHealth(this.hero);
    4:     for (var i: number = 0; i < this.enemies.length; i++) {
    5:         this.drawElement(this.enemies[i]);
    6:     }
    7: }

Deberías tener algo parecido a esto:

task 

Mover estos enemigos por la pantalla

Vamos a basar este movimiento en una dirección al azar que cambia cada minTime.

En el modelo de enemy añadimos una propiedad pública que solo va a tener esta clase (no hace falta en GameElement) y la inicializamos:

    1: module TSGame { 
    2:     'use strict';
    3:  
    4:     export class Enemy extends GameElement { 
    5:  
    6:         public direction;
    7:  
    8:         constructor() { 
    9:            ...
   10:              this.direction = {
   11:                 x: 1,
   12:                 y: 1
   13:             }
   14:         }
   15:     }
   16: }

El siguiente paso es que en la función update de la clase Game.ts vamos a añadir la llamada a dos métodos, moveEnemy que va a mover todos los enemigos en su correspondiente dirección y changeEnemyDirection que cada minTime va a generar otra dirección aleatoria:

    1: private update(): void { 
    2:     // Check hero movement
    3:     ...
    4:  
    5:     for (var i: number = 0; i < this.enemies.length; i++) {
    6:         this.moveEnemy(this.enemies[i]);
    7:     }
    8:  
    9:     // Check enemies generation and destruction
   10:     if (now - this.time > this.minTime) {
   11:         ...
   12:  
   13:         for (var i: number = 0; i < this.enemies.length; i++) {
   14:             this.changeEnemyDirection(this.enemies[i]);
   15:         }
   16:     }
   17:  
   18: }

¿Y que hacen estos dos métodos? pues lo comentado, mover al enemigo y generar una dirección aleatoria (-1, 0, 1)

    1: private moveEnemy(enemy: Enemy): void {
    2:     enemy.x += enemy.speed * enemy.direction.x;
    3:     enemy.y += enemy.speed * enemy.direction.y;
    4: }
    5:  
    6: private changeEnemyDirection(enemy: Enemy): void {
    7:     enemy.direction.x = (Math.floor(Math.random() * (3) + 1) - 2);
    8:     enemy.direction.y = (Math.floor(Math.random() * (3) + 1) - 2);
    9: }

Aumentar el número de enemigos a medida que pase el tiempo

Para aumentar la dificultad, a medida que gane puntos vamos a aumentar el número de enemigos.

Podemos hacer que por ejemplo cada 400 puntos añada un enemigo más:

    1: private update(): void { 
    2:     // Check hero movement
    3:     ...
    4:     // Check enemies generation and destruction
    5:     if (now - this.time > this.minTime) {
    6:         ...
    7:  
    8:         if (this.score % 4 === 0) {
    9:             this.maxEnemies++;
   10:         }
   11:     }
   12: }

Destruir las instancias cuando dejen de ser necesarias

Una vez el enemigo desaparece de nuestra visión queremos destruirlo.

Podríamos tener en cuenta diferentes factores, pero debería ser suficiente con borrar el objeto del array al salirse de los bordes de la pantalla:

    1: private checkEnemies(): void {
    2:     for (var i: number = 0; i < this.enemies.length; i++) {
    3:         if (this.enemies[i].x < 0 || this.enemies[i].x > this.canvas.width || this.enemies[i].y < 0 || this.enemies[i].y > this.canvas.height) {
    4:             this.enemies.splice(i, 1);
    5:         }
    6:     }
    7:  
    8:     ...
    9: }

En este punto veremos algo así:

task

Restar vida al tocar un enemigo

El último punto de este capítulo va a ser restar vida cada vez que toquemos un enemigo.

Modificamos en el método update la parte en la que aplicamos el movimiento a cada enemigo y vamos a aprovechar ese bucle para mirar también si toca al héroe:

    1: private update(): void { 
    2:    // Check hero movement
    3:    ...
    4:  
    5:    for (var i: number = 0; i < this.enemies.length; i++) {
    6:        ...
    7:        this.checkDamage(this.enemies[i]);
    8:    }
    9:  
   10:    // Check enemies generation and destruction
   11:    ...
   12: }

Y este método lo que hace mirar que se cumplan las condiciones mencionadas, en caso de hacerlo resta algo de vida al enemigo:

    1: private checkDamage(enemy: Enemy): void {
    2:     if (this.hero.x < enemy.x + this.hero.size && this.hero.x > enemy.x - this.hero.size
    3:         && this.hero.y < enemy.y + this.hero.size && this.hero.y > enemy.y - this.hero.size) {
    4:         this.hero.health -= 0.1;
    5:     }
    6: }

Con todo esto ya tenemos un juego funcionando, en el próximo capítulo:

  • Comprobaremos la puntuación.
  • Comprobaremos la vida del personaje.
  • Añadiremos algunos efectos.
  • Veremos los últimos detalles y sugerencias.

Este es el código completo de la clase Game.ts y la siguiente imagen muestra su funcionamiento:

    1: /// <reference path="../import.ts" />
    2:  
    3: module TSGame { 
    4:     'use strict';
    5:  
    6:     export class Game { 
    7:  
    8:         private canvas: any;
    9:         private hero: Hero;
   10:         private enemies: Array<Enemy> = [];
   11:         private time: number = 0;
   12:         private minTime: number = 2000;
   13:         private score: number = 0;
   14:         private maxEnemies: number = 2;
   15:  
   16:         constructor() { 
   17:             this.initialize();
   18:             this.gameLoop();
   19:         }
   20:  
   21:         public initialize(): void { 
   22:             this.canvas = new Canvas();
   23:             this.hero = new Hero();
   24:             this.hero.y = this.canvas.height / 2;
   25:         }
   26:  
   27:         private gameLoop(): void { 
   28:             window.requestAnimationFrame(() => this.gameLoop());
   29:             this.update();
   30:             this.draw();
   31:         }
   32:  
   33:         private update(): void { 
   34:             // Check hero movement
   35:             var keys = this.hero.keysDown;
   36:             var moveUnits = this.hero.size * 1 / this.hero.speed + this.hero.speed;
   37:             if (keys.Left === true) { 
   38:                 this.hero.x -= (this.hero.x > 0) ? moveUnits : 0;
   39:             }
   40:  
   41:             if (keys.Right === true) {
   42:                 this.hero.x += (this.hero.x < this.canvas.width - this.hero.size) ? moveUnits : 0;
   43:             }
   44:  
   45:             if (keys.Down === true) {
   46:                 this.hero.y += (this.hero.y < this.canvas.height - this.hero.size) ? moveUnits : 0;
   47:             }
   48:  
   49:             if (keys.Up === true) {
   50:                 this.hero.y -= (this.hero.y > 0) ? moveUnits : 0;
   51:             }
   52:  
   53:             for (var i: number = 0; i < this.enemies.length; i++) {
   54:                 this.moveEnemy(this.enemies[i]);
   55:                 this.checkDamage(this.enemies[i]);
   56:             }
   57:  
   58:             // Check enemies generation and destruction
   59:             var now = Date.now();
   60:             if (now - this.time > this.minTime) {
   61:                 this.time = now;
   62:                 this.score += 100;
   63:                 this.checkEnemies();
   64:  
   65:                 for (var i: number = 0; i < this.enemies.length; i++) {
   66:                     this.changeEnemyDirection(this.enemies[i]);
   67:                 }
   68:  
   69:                 if (this.score % 4 === 0) {
   70:                     this.maxEnemies++;
   71:                 }
   72:             }
   73:         }
   74:  
   75:         private draw(): void {
   76:             this.canvas.clearCanvas();
   77:             this.drawElement(this.hero);
   78:             this.drawHealth(this.hero);
   79:             for (var i: number = 0; i < this.enemies.length; i++) {
   80:                 this.drawElement(this.enemies[i]);
   81:             }
   82:         }
   83:  
   84:         public drawElement(element: IGameElement): void {
   85:             this.canvas.ctx.drawImage(element.image, element.x, element.y); 
   86:         }
   87:  
   88:         public drawHealth(element: IGameElement): void {
   89:             this.canvas.ctx.beginPath();
   90:             this.canvas.ctx.rect(10, 10, element.health, 10);
   91:             this.canvas.ctx.fillStyle = element.color;
   92:             this.canvas.ctx.fill();
   93:         }
   94:  
   95:         private checkEnemies(): void {
   96:             for (var i: number = 0; i < this.enemies.length; i++) {
   97:                 if (this.enemies[i].x < 0 || this.enemies[i].x > this.canvas.width || this.enemies[i].y < 0 || this.enemies[i].y > this.canvas.height) {
   98:                     this.enemies.splice(i, 1);
   99:                 }
  100:             }
  101:  
  102:             for (var i: number = 0; i < this.maxEnemies; i++) {
  103:                 this.enemies.push(this.createRandomEnemy());
  104:             }
  105:         }
  106:  
  107:         private createRandomEnemy(): Enemy {
  108:             var enemy = new Enemy();
  109:             enemy.x = Math.floor(Math.random() * this.canvas.width * 0.8) + (this.canvas.width - this.canvas.width * 0.8);
  110:             enemy.y = Math.floor(Math.random() * this.canvas.height * 0.8) + (this.canvas.height - this.canvas.height * 0.8);
  111:             return enemy;
  112:         }
  113:  
  114:         private moveEnemy(enemy: Enemy): void {
  115:             enemy.x += enemy.speed * enemy.direction.x;
  116:             enemy.y += enemy.speed * enemy.direction.y;
  117:         }
  118:  
  119:         private changeEnemyDirection(enemy: Enemy): void {
  120:             enemy.direction.x = (Math.floor(Math.random() * (3) + 1) - 2);
  121:             enemy.direction.y = (Math.floor(Math.random() * (3) + 1) - 2);
  122:         }
  123:  
  124:         private checkDamage(enemy: Enemy): void {
  125:             if (this.hero.x < enemy.x + this.hero.size && this.hero.x > enemy.x - this.hero.size
  126:                 && this.hero.y < enemy.y + this.hero.size && this.hero.y > enemy.y - this.hero.size) {
  127:                 this.hero.health -= 0.1;
  128:             }
  129:         }
  130:     }
  131: }

 

final

_____________________________

Quique Fernández

Technical Evangelist Intern

@CKGrafico