Trabajando en un Canvas con TypeScript (I)

THUMB

Una vez que hemos empezado a trabajar con TypeScript podemos empezar a ver lo mucho que nos ayuda en la programación orientada a objetos.
Un escenario en el que podemos trabajar fácilmente con orientación a objetos es en los juegos, y en este ejemplo veremos cómo crear nuestro primer juego en un canvas mediante TypeScript.

Estructura de archivos

Para este ejemplo he utilizado muy pocos archivos y una estructura sencilla que puedes copiar o mejorar según tu criterio.

image

Un archivo llamado index.html que va a estar vacío y es donde voy a incluir los archivos de JavaScript de mi juego.

  1: <!DOCTYPE html>
  2: <html>
  3: <head>
  4:     <title>Juego Typescript</title>
  5: </head>
  6: <body>
  7:  
  8:     <script src="ts/index.js"></script>
  9:     ...
  10:     ...
  11: </body>
  12: </html>
  13:  
  14:  

Un archivo index.ts en el que voy a inicializar el juego y en el que también podría crear una configuración específica si fuera necesario:

  1: /// <reference path="import.ts" />
  2:  
  3: module TSGame { 
  4:     window.onload = () => {
  5:         new Game();
  6:     }
  7: }
  8:  

Una carpeta llamada Models en la que pondré todas las clases necesarias para mi juego, por ejemplo: Canvas, Enemy, etc.

La carpeta Interfaces donde tendremos las interfaces que vayamos a necesitar utilizar en el proyecto.

Finalmente un archivo llamado import.ts que es el estándar de TypeScript en el que vamos a referenciar los archivos a utilizar en el proyecto de TS.

Nuestro juego

Para hacer un ejemplo sencillo, haremos un pequeño juego en canvas en el cual aparecen enemigos de forma aleatoria que hemos de esquivar.

Empezando el proyecto

Notas previas

Aunque muchas veces lo lógico es tener una configuración general del juego, para este ejemplo voy a configurar cada elemento en su misma clase, solo a modo de ejemplo.

También añadir que aunque podemos dar a cualquier propiedad el tipo que consideremos de HTML, muchas veces es mejor centrarse en una serie de tipos mínimos para nuestras propiedades. Yo suelo trabajar con todos los de typescript más el tipo Event y algún tipo simple.

Por ejemplo:

  1: // En vez de
  2: private canvas: HTMLCanvasElement;
  3:  
  4: // Usaré
  5: private canvas: any;

Canvas.ts

Esta clase va a gestionar la creación y configuración de nuestro canvas.

Inicializamos las propiedades necesarias de la clase:

  1: private canvas: any;
  2: private ctx: any;
  3: private width: number = 800;
  4: private height: number = 600;

Creamos un método simple que nos pinte el fondo:

  1: constructor() { 
  2:     this.canvas = document.createElement('canvas');
  3:     this.canvas.width = this.width;
  4:     this.canvas.height = this.height;
  5:     document.body.appendChild(this.canvas);
  6:  
  7:     this.ctx = this.canvas.getContext('2d');
  8:  
  9:     this.canvasStyling()
  10: }

Por último inicializamos todo en el constructor:

  1: constructor() { 
  2:     this.canvas = document.createElement('canvas');
  3:     this.canvas.width = this.size.width;
  4:     this.canvas.height = this.size.height;
  5:     document.body.appendChild(this.canvas);
  6:  
  7:     this.ctx = this.canvas.getContext('2d');
  8:  
  9:     this.canvasStyling()
  10: }

Ya tenemos un fabuloso canvas negro.

image

 

Personajes

Tenemos un elemento básico del que van a heredar los demás personajes.

  1: /// <reference path="../import.ts" />
  2:  
  3: module TSGame { 
  4:     'use strict';
  5:  
  6:     export class GameElement implements IGameElement { 
  7:  
  8:         public health: number;
  9:         public speed: number;
  10:         public color: string;
  11:         public image: any;
  12:         public x: number = 0;
  13:         public y: number = 0;
  14:         public size: number = 32;
  15:         public config;
  16:  
  17:         constructor() { 
  18:             this.health = this.config.health.max;
  19:             this.speed = this.config.speed.normal;
  20:             this.color = this.config.colors.normal;
  21:             this.image = new Image();
  22:             this.image.src = this.config.image;
  23:         }
  24:     }
  25: }

Previamente he creado una interfaz para la clase:

  1: /// <reference path="../import.ts" />
  2:  
  3: module TSGame { 
  4:     export interface IGameElement { 
  5:         health: number;
  6:         speed: number;
  7:         color: string;
  8:         image: any;
  9:         x: number;
  10:         y: number;
  11:         size: number;
  12:         config: {
  13:             health: { max: number; min: number; };
  14:             speed: { max: number; normal: number; min: number; };
  15:             colors: { normal: string; danger: string; bonus?: string; };
  16:             image: string;
  17:        };
  18:     }
  19: }

Gracias a esto, podemos crear las clases de Enemy de Hero y lo que queramos, de una manera bastante sencilla:

  1: /// <reference path="../import.ts" />
  2:  
  3: module TSGame { 
  4:     'use strict';
  5:  
  6:     export class Hero extends GameElement { 
  7:  
  8:         constructor() { 
  9:             this.config = {
  10:                 health: {
  11:                     max: 100,
  12:                     min: 0
  13:                 },
  14:  
  15:                 speed: {
  16:                     max: 20,
  17:                     normal: 10,
  18:                     min: 1
  19:                 },
  20:  
  21:                 colors: {
  22:                     normal: 'blue',
  23:                     danger: 'red',
  24:                     bonus: 'yellow'
  25:                 },
  26:  
  27:                 image: '../../images/hero.png'
  28:             }
  29:  
  30:             super();
  31:         }
  32:     }
  33: }
  1: /// <reference path="../import.ts" />
  2:  
  3: module TSGame { 
  4:     'use strict';
  5:  
  6:     export class Enemy extends GameElement { 
  7:         constructor() { 
  8:             this.config = {
  9:                 health: {
  10:                     max: 10,
  11:                     min: 0
  12:                 },
  13:  
  14:                 speed: {
  15:                     max: 5,
  16:                     normal: 5,
  17:                     min: 1
  18:                 },
  19:  
  20:                 colors: {
  21:                     normal: 'green',
  22:                     danger: 'red'
  23:                 },
  24:  
  25:                 image: '../../images/enemy.png'
  26:             }
  27:  
  28:             super();
  29:         }
  30:     }
  31: }

Para probar que todo está como esperamos, inicializamos la clase Game que luego modificaremos:

  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:  
  11:         constructor() { 
  12:             this.initialize();
  13:         }
  14:  
  15:         public initialize(): void { 
  16:             this.canvas = new Canvas();
  17:             this.hero = new Hero();
  18:             var enemy = new Enemy();
  19:             this.drawElement(this.hero, 100, 100);
  20:             this.drawElement(enemy, 200, 100);
  21:         }
  22:  
  23:         public drawElement(element: IGameElement, x: number, y: number) {
  24:             this.canvas.ctx.drawImage(element.image, x, y); 
  25:         }
  26:     }
  27: }

image

Movimiento de los personajes

Vamos a tener dos tipos de movimiento, uno mediante las teclas para Hero y uno aleatorio para Enemy.

Hero:

Nos interesan las teclas de las flechas, por lo que preparamos un enum con ellas:

  1: enum Keys { Left = 37, Up = 38, Right = 39, Down = 40 };

Para saber qué tecla se ha pulsado asociamos los eventos necesarios y nos guardamos las teclas activas:

  1: private bindEvents(): void { 
  2:     this.keysDown = {};
  3:  
  4:     addEventListener('keydown', e => { 
  5:         // arrows: 37, 38, 39, 40
  6:         if (e.keyCode >= Keys.Left && e.keyCode <= Keys.Down) {
  7:             this.keysDown[Keys[e.keyCode]] = true;
  8:         }
  9:     }, false);
  10:  
  11:     addEventListener('keyup', e => {
  12:         if (e.keyCode >= Keys.Left && e.keyCode <= Keys.Down) {
  13:             this.keysDown[Keys[e.keyCode]] = false;
  14:         }
  15:     }, false);
  16: }

Además, cada vez que pulsemos una tecla, vamos a hacer el movimiento correspondiente. Para ello creamos ya el gameloop (al ser un ejemplo usaremos requestAnimationFrame sin tener en cuenta la compatibilidad).

En update actualizamos la posición del personaje y en draw lo dibujamos:

  1: constructor() { 
  2:     this.initialize();
  3:     this.gameLoop();
  4: }
  5:  
  6: private gameLoop(): void { 
  7:     window.requestAnimationFrame(() => this.gameLoop());
  8:     this.update();
  9:     this.draw();
  10: }
  11:  
  12: private update(): void { 
  13:     var keys = this.hero.keysDown;
  14:     var moveUnits = this.hero.size * 1 / this.hero.speed + this.hero.speed;
  15:     if (keys.Left === true) { 
  16:         this.hero.x -= (this.hero.x > 0) ? moveUnits : 0;
  17:     }
  18:  
  19:     if (keys.Right === true) {
  20:         this.hero.x += (this.hero.x < this.canvas.width - this.hero.size) ? moveUnits : 0;
  21:     }
  22:  
  23:     if (keys.Down === true) {
  24:         this.hero.y += (this.hero.y < this.canvas.height - this.hero.size) ? moveUnits : 0;
  25:     }
  26:  
  27:     if (keys.Up === true) {
  28:         this.hero.y -= (this.hero.y > 0) ? moveUnits : 0;
  29:     }
  30: }
  31:  
  32: private draw(): void {
  33:     this.canvas.clearCanvas();
  34:     this.drawElement(this.hero);
  35: }

Y este sería el resultado obtenido:

task

Para terminar este primer artículo vamos a mostrar la vida que tiene nuestro protagonista, una barra de 100 que irá cambiando de color según nuestro estado. Para ello añadimos una última función en el método draw():

  1: private draw(): void {
  2:     this.canvas.clearCanvas();
  3:     this.drawElement(this.hero);
  4:     this.drawHealth(this.hero);
  5: }
  6:  
  7: public drawHealth(element: IGameElement): void {
  8:     this.canvas.ctx.beginPath();
  9:     this.canvas.ctx.rect(10, 10, element.health, 10);
  10:     this.canvas.ctx.fillStyle = element.color;
  11:     this.canvas.ctx.fill();
  12: }

En la siguiente parte vamos a crear los diferentes enemigos y a controlar la vida del personaje.

¿Habías probado alguna vez a utilizar TypeScript para trabajar en un canvas o un juego? ¿No te parece mucho más claro el código que tenemos? ¿Sabías que muchas de las librerías famosas para crear juegos en JavaScript son compatibles con TypeScript?

_____________________________

Quique Fernández

Technical Evangelist Intern

@CKGrafico