Iniciando un juego en TypeScript

Me parece que uno de los mejores modos de aprender cualquier lenguaje es intentando hacer que se muevan cosas por pantalla.

Antes de empezar recordar que este tutorial forma parte de una cadena de tutoriales en las que pretendo simplemente probar tecnologías actuales. Aquí abajo podrás encontrarlos :

 

También  me gustaría que vieras este video de 2 minutos para entender dónde estamos y a dónde vamos

https://twitter.com/i/status/1214946355023634432


Mi equipo es Mac

macOS Catalina
Versión 10.15.2

MacBook Pro (15-inch, 2018)
Procesador 2,9 GHz Intel Core i9 de 6 núcleos
Memoria 32 GB 2400 MHz DDR4
Gráficos Intel UHD Graphics 630 1536 MB


Me parece que uno de los mejores modos de aprender cualquier lenguaje es intentando hacer que se muevan cosas por pantalla. Para enseñar también es perfecto porque la gente se pica y evoluciona mucho los juegos.

Voy a intentar crear una primera estructura de juego para comprobar cómo conectar los eventos de HTML (Listener) y temporizadores con TypeScript.

No lo voy a evolucionar mucho porque recordar que el objetivo es hacer un Arcanoid no un juego de ping-pong pero siempre me ha gustado hacer ejemplos descartables para aprender los conceptos y luego empezar con un proyecto limpio.

ADVERTENCIA IMPORTANTE: En lo que más tiempo he programado en mi vida ha sido C++ y Java por lo que por favor no os fiéis de lo que estoy haciendo porque es muy posible que, aunque funcione, no esté optimizado ni sea el mejor modo de hacerlo en el mundo JavaScript/TypeScript. Estoy recorriendo el camino del aprendizaje y no soy un experto. Seguro que en un tiempo puedo escribir un tutorial cuestionando cómo hago las cosas aquí.

En una primera búsqueda he encontrado a codigofacilito, al cual estoy agradecido, por su ejemplo:

Son una colección de videotutoriales que, si bien no he hecho mucho caso, me han valido de inspiración. Él lo hace en Javascript y yo lo voy a ir haciendo en TypeScript y así aprendo las diferencias y me veo obligado a traducir.

(Esto es una captura del vídeo de codigofacilito, gracias majo)

Lo primero que voy a hacer es crear un proyecto y construir un index.html.

Este html tendrá un canvas con id=areajuego

Tendrá un Script llamado logicadeluego.ts en el mismo directorio.

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <title>Titulo de la pagina</title>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
    <link rel='stylesheet' type='text/css' media='screen' href='main.css'>
    <script src='./index.ts'></script>
</head>
<body>
   <h1> Este es un ejemplo de pagina html 2</h1>

    <canvas id="areajuego"></canvas>
    <script src='./logicajuego.ts'></script>

</body>
</html>

Voy a crear una clase que represente el juego.

Luego una clase base llamada CComponenteVisual que es de la que dependerán el resto de los elementos. Cualquier elemento se basará en un rectángulo que pintaremos en principio de color negro.

class CComponenteVisual {
  x: number = 0;
  y: number = 0;
  width: number = 0;
  height: number = 0;
  color: string;

  pinta(ctx: CanvasRenderingContext2D) {
    ctx.fillStyle = "#000000";
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }

  constructor(x: number, y: number, width: number, height: number) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
  }
}

Y por “polimorfismo” podemos redefinir como se pinta cada una de las clases hijas. Esto es muy Java.

La clave es aprender cómo engancha el html y el TypeScript.

Tenemos un objeto window que con addEventListener le decimos que al cargar invoque una función main: arreglado.

En la función main creamos el objeto juego de la clase CJuego y le pasamos el contexto para pintar el canvas.

Primero recuperamos el canvas por id y luego invocamos al método getContext.

Como veis, muy sencillo.

window.addEventListener("load", main);

function main() {
  let juego: CJuego;

  let micanvas: HTMLCanvasElement = document.getElementById(
    "areajuego"
  ) as HTMLCanvasElement;

  let ctx: CanvasRenderingContext2D = micanvas.getContext("2d");

  juego = new CJuego(ctx);
  juego.creaElementos();
  juego.arranca();
}

Bueno, aquí tenemos el primer esqueleto de nuestro código.

class CJuego {
  puntuacion: number = 0;
  arrancado: boolean = true;
  velocidad = { x: 1, y: 1 };
  elementos = [];
  tama = { x: 100, y: 200 };

  ctx: CanvasRenderingContext2D;

  constructor(ctx: CanvasRenderingContext2D) {
    this.ctx = ctx;
  }

  creaElementos(): void {
    this.elementos.push(new CRaqueta("der", 10, 10, 2, 10));
    this.elementos.push(new CRaqueta("izq", 20, 20, 2, 10));
    this.elementos.push(new CPelota(30, 30, 5, 5));
    this.elementos.push(new CAreaJuego(40, 40, this.tama.x, this.tama.y));

    this.elementos.forEach(element => {
      element.pinta(this.ctx);
    });
  }

  arranca(): void {}
}

class CComponenteVisual {
  x: number = 0;
  y: number = 0;
  width: number = 0;
  height: number = 0;
  color: string;

  pinta(ctx: CanvasRenderingContext2D) {
    ctx.fillStyle = "#000000";
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }

  constructor(x: number, y: number, width: number, height: number) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
  }
}

class CRaqueta extends CComponenteVisual {
  lado: string;

  constructor(
    lado: string,
    x: number,
    y: number,
    width: number,
    height: number
  ) {
    super(x, y, width, height);
    this.lado = lado;
  }
}

class CPelota extends CComponenteVisual {
  constructor(x: number, y: number, width: number, height: number) {
    super(x, y, width, height);
    this.width = width;
    this.height = height;
  }
}

class CAreaJuego extends CComponenteVisual {
  borde: boolean;

  constructor(x: number, y: number, width: number, height: number) {
    super(x, y, width, height);
    this.width = width;
    this.height = height;
  }
}

window.addEventListener("load", main);

function main() {
  let juego: CJuego;

  let micanvas: HTMLCanvasElement = document.getElementById(
    "areajuego"
  ) as HTMLCanvasElement;

  let ctx: CanvasRenderingContext2D = micanvas.getContext("2d");

  juego = new CJuego(ctx);
  juego.creaElementos();
  juego.arranca();
}

Y vemos que simplemente se pintan los objetos dentro del canvas.

Le voy a completar un poco el código y añadir soporte para el movimiento de las raquetas.

class CJuego {
  puntuacion: number = 0;
  arrancado: boolean = true;
  velocidad = { x: 1, y: 1 };
  elementos = [];
  tama = { x: 0, y: 0 };

  limites: { ancho: number; alto: number; margen: number };

  ctx: CanvasRenderingContext2D;

  constructor(ctx: CanvasRenderingContext2D) {
    this.ctx = ctx;
    this.tama.x = ctx.canvas.width;
    this.tama.y = ctx.canvas.height;

    this.limites = {
      ancho: ctx.canvas.width,
      alto: ctx.canvas.height,
      margen: 10
    };
  }

  creaElementos(): void {
    this.elementos.push(
      new CRaqueta(
        "der",
        this.ctx.canvas.width - this.limites.margen,
        20,
        2,
        10,
        this.limites
      )
    );

    this.elementos.push(
      new CRaqueta("izq", this.limites.margen, 20, 2, 10, this.limites)
    );
    this.elementos.push(new CPelota(30, 30, 5, 5, this.limites));

    this.elementos.forEach(element => {
      element.pinta(this.ctx);
    });
  }

  arranca(documento: Document): void {
    this.elementos.forEach(element => {
      // recorremos todos los elementos graficos
      if (element instanceof CRaqueta) {
        // añadimos listeners a raquetas

        document.addEventListener("keydown", (e: KeyboardEvent) => {
          let tecla = e.charCode || e.keyCode; // depende del navegador la variable usada

          if (element.lado === "izq") {
            // a la raqueta izquierda la controlamos con a y z
            if (tecla == 65) {
              console.log("es A");
              element.borra(this.ctx);
              element.moverArriba();
              element.pinta(this.ctx);
            }
            if (tecla == 90) {
              element.borra(this.ctx);
              element.moverAbajo();
              element.pinta(this.ctx);
            }
          }

          if (element.lado === "der") {
            // a la raqueta derecha la controlamos con k y m
            if (tecla == 75) {
              element.borra(this.ctx);
              element.moverArriba();
              element.pinta(this.ctx);
            }
            if (tecla == 77) {
              element.borra(this.ctx);
              element.moverAbajo();
              element.pinta(this.ctx);
            }
          }
        });
      }
    }); // instance
  } // arranca
}

// clase base para todos los componentes basados area cuadrada
class CComponenteVisual {
  x: number = 0;
  y: number = 0;
  width: number = 0;
  height: number = 0;
  color: string = "#000000";
  limites: { ancho: number; alto: number; margen: number };

  pinta(ctx: CanvasRenderingContext2D) {
    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }

  borra(ctx: CanvasRenderingContext2D) {
    ctx.fillStyle = "#FFFFFF";
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }

  constructor(
    x: number,
    y: number,
    width: number,
    height: number,
    limites: any
  ) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.limites = limites;
  }
}

class CRaqueta extends CComponenteVisual {
  lado: string;

  velocidad: number = 5;

  constructor(
    lado: string,
    x: number,
    y: number,
    width: number,
    height: number,
    limites: any
  ) {
    super(x, y, width, height, limites);
    this.lado = lado;
  }

  moverArriba() {
    if (this.y > 0) {
      this.y -= this.velocidad;
    }
  }

  moverAbajo() {
    if (this.y < this.limites.alto) {
      this.y += this.velocidad;
    }
  }
}

class CPelota extends CComponenteVisual {
  constructor(
    x: number,
    y: number,
    width: number,
    height: number,
    limites: any
  ) {
    super(x, y, width, height, limites);
    this.color = "#FF0000";
  }

  pinta(ctx: CanvasRenderingContext2D) {
    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }
}

window.addEventListener("load", main);

function main() {
  let juego: CJuego;

  let micanvas: HTMLCanvasElement = document.getElementById(
    "areajuego"
  ) as HTMLCanvasElement;

  let ctx: CanvasRenderingContext2D = micanvas.getContext("2d");

  juego = new CJuego(ctx);
  juego.creaElementos();
  juego.arranca(document);
}

También podemos cambiar dar un borde al canvas simplemente con estilos en la página índex.html o en la css.

Así quedaría el ejemplo donde podemos mover las dos raquetas con a-z y m-k.

Es un poco cutre porque borro el cuadro anterior (pinto uno blanco) antes de pintar el siguiente, ocupémonos en cada tutorial de una cosa distinta.

        if (element.lado === "der") {
            // a la raqueta derecha la controlamos con k y m
            if (tecla == 75) {
              element.borra(this.ctx);
              element.moverArriba();
              element.pinta(this.ctx);
            }
            if (tecla == 77) {
              element.borra(this.ctx);
              element.moverAbajo();
              element.pinta(this.ctx);
            }

Ahora vamos a mover la pelota.

Esto ya tiene un poquito más de historia porque tenemos que hacer que a intervalos se refresque el juego.

La clave para animar es ejecutar una función recurrentemente. Esto lo podemos hacer de dos modos. El primero, que refleja el ejemplo, es que cada n-milisegundos específicos se actualicen las posiciones y el pintado. La segunda es utilizando la función requestAnimationFrame para que sea el navegador el que se ocupe, en base a las capacidades gráficas, de refrescar las veces que creas necesarias (tendrás tu que ocuparte de calcular el tiempo que ha pasado). Os recomiendo visitar este enlace para ver el segundo modo https://developer.mozilla.org/es/docs/Web/API/Window/requestAnimationFrame. Aquí una captura del segundo modo del enlace sugerido.

Bueno, en nuestro juego, usamos el intervalo, para lo que tenemos que invocar a una función de este modo.

 juego = new CJuego(ctx);
  juego.creaElementos();
  juego.arranca(document);
  juego.timer = setInterval(juego.anima, 200);
}

Si luego queremos parar la animación tenemos que quedarnos con el id del timbre. En mi caso lo guardo en la propia clase juego.

Mi primera sorpresa es que no podría definir el “método” anima del modo clásico, como lo haría en cualquier clase sino que lo tengo que definir de este modo. Luego lo puedo utilizar en mis clases como cualquier método normal.

  anima = () => {
    // console.log("Animamos con pelota " + this.pelota);
    this.pelota.borra(this.ctx);
    this.pelota.actualizaPosicion();
    this.pelota.pinta(this.ctx);

    if (this.repeticiones++ <= 5) {
      console.log("Repeticiones " + this.repeticiones);
    } else this.paraJuego();
  };

Salvando este punto, lo demás es bastante normal. Aquí os dejo el código que hace que la bola ya sea capaz de moverse un poco en la pantalla.

class CJuego {
  puntuacion: number = 0;
  arrancado: boolean = true;
  velocidad = { x: 1, y: 1 };
  elementos = [];
  tama = { x: 0, y: 0 };
  limites: { ancho: number; alto: number; margen: number };
  ctx: CanvasRenderingContext2D;
  pelota: CPelota = new CPelota(30, 30, 5, 5, this.limites);
  repeticiones: number = 0;
  timer: any;

  constructor(ctx: CanvasRenderingContext2D) {
    this.ctx = ctx;
    this.tama.x = ctx.canvas.width;
    this.tama.y = ctx.canvas.height;

    this.limites = {
      ancho: ctx.canvas.width,
      alto: ctx.canvas.height,
      margen: 10
    };
  }

  creaElementos(): void {
    console.log("Creamos elementos");

    this.elementos.push(
      new CRaqueta(
        "der",
        this.ctx.canvas.width - this.limites.margen,
        20,
        2,
        10,
        this.limites
      )
    );

    this.elementos.push(
      new CRaqueta("izq", this.limites.margen, 20, 2, 10, this.limites)
    );

    this.elementos.push(this.pelota);

    this.elementos.forEach(element => {
      element.pinta(this.ctx);
    });
  }

  arranca(documento: Document): void {
    console.log("Arrancamos juego");

    this.elementos.forEach(element => {
      // recorremos todos los elementos graficos
      if (element instanceof CRaqueta) {
        // añadimos listeners a raquetas

        document.addEventListener("keydown", (e: KeyboardEvent) => {
          let tecla = e.charCode || e.keyCode; // depende del navegador la variable usada

          if (element.lado === "izq") {
            // a la raqueta izquierda la controlamos con a y z
            if (tecla == 65) {
              console.log("es A");
              element.borra(this.ctx);
              element.moverArriba();
              element.pinta(this.ctx);
            }
            if (tecla == 90) {
              element.borra(this.ctx);
              element.moverAbajo();
              element.pinta(this.ctx);
            }
          }

          if (element.lado === "der") {
            // a la raqueta derecha la controlamos con k y m
            if (tecla == 75) {
              element.borra(this.ctx);
              element.moverArriba();
              element.pinta(this.ctx);
            }
            if (tecla == 77) {
              element.borra(this.ctx);
              element.moverAbajo();
              element.pinta(this.ctx);
            }
          }
        });
      }
    }); // instance
  } // arranca

  anima = () => {
    // console.log("Animamos con pelota " + this.pelota);
    this.pelota.borra(this.ctx);
    this.pelota.actualizaPosicion();
    this.pelota.pinta(this.ctx);

    if (this.repeticiones++ <= 5) {
      console.log("Repeticiones " + this.repeticiones);
    } else this.paraJuego();
  };

  hayColision(): boolean {
    return false;
  }

  paraJuego() {
    console.log("Paramos el juego " + this.pelota);
    clearInterval(this.timer);
  }
}

// clase base para todos los componentes basados area cuadrada
class CComponenteVisual {
  x: number = 0;
  y: number = 0;
  width: number = 0;
  height: number = 0;
  color: string = "#000000";
  limites: { ancho: number; alto: number; margen: number };

  pinta(ctx: CanvasRenderingContext2D) {
    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }

  borra(ctx: CanvasRenderingContext2D) {
    ctx.fillStyle = "#FFFFFF";
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }

  constructor(
    x: number,
    y: number,
    width: number,
    height: number,
    limites: any
  ) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.limites = limites;
  }
}

class CRaqueta extends CComponenteVisual {
  lado: string;

  velocidad: number = 5;

  constructor(
    lado: string,
    x: number,
    y: number,
    width: number,
    height: number,
    limites: any
  ) {
    super(x, y, width, height, limites);
    this.lado = lado;
  }

  moverArriba() {
    if (this.y > 0) {
      this.y -= this.velocidad;
    }
  }

  moverAbajo() {
    if (this.y < this.limites.alto) {
      this.y += this.velocidad;
    }
  }
}

class CPelota extends CComponenteVisual {
  incrementoX = 5;
  incrementoY = 5;

  constructor(
    x: number,
    y: number,
    width: number,
    height: number,

    limites: any
  ) {
    super(x, y, width, height, limites);
    this.color = "#FF0000";
  }

  pinta(ctx: CanvasRenderingContext2D) {
    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }

  actualizaPosicion() {
    console.log("x e y ", this.x, this.y);
    this.x += this.incrementoX;
    this.y += this.incrementoY;
  }
}

window.addEventListener("load", main);

function main() {
  let juego: CJuego;

  let micanvas: HTMLCanvasElement = document.getElementById(
    "areajuego"
  ) as HTMLCanvasElement;

  let ctx: CanvasRenderingContext2D = micanvas.getContext("2d");

  juego = new CJuego(ctx);
  juego.creaElementos();
  juego.arranca(document);
  juego.timer = setInterval(juego.anima, 200);
}

Y este es el resultado que obtenemos.

Para mejorar este juego solamente tendríamos que cambiar el incremento X e Y de signo cada vez que tocase una pared permitida o una raqueta.

También podríamos cambiar el valor de ese incremento en base al punto donde tocase a la raqueta, en el centro o en el borde, y así variaríamos los ángulos. Incluso, podríamos incrementar la velocidad en base al tiempo que llevemos jugando ( ejemplo, contar llamadas a función y: incremento += llamadas/1000 ).

Por último, tendríamos que parar el juego cuando tocase uno de los bordes detrás de la raqueta o superase la posición de la misma.

Como veis, para aprender el lenguaje ya no tiene más interés y os lo dejo a vosotros.

Ahora nos quedaría ver si hay frameworks más adecuados para hacer esto, cómo gestionar la persistencia en el servidor de un juego y si lo que hacemos es óptimo o no.

 

Aquí te dejo el siguiente tutorial:

Conexión HTTP cliente-servidor con TypeScript

Comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

He leído y acepto la política de privacidad

Información básica acerca de la protección de datos

  • Responsable: IZERTIS S.A.
  • Finalidad: Envío información de carácter administrativa, técnica, organizativa y/o comercial sobre los productos y servicios sobre los que se nos consulta.
  • Legitimación: Consentimiento del interesado
  • Destinatarios: Otras empresas del Grupo IZERTIS. Encargados del tratamiento.
  • Derechos: Acceso, rectificación, supresión, cancelación, limitación y portabilidad de los datos.
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad

Creador y propietario de AdictosAlTrabajo.com, Director General de Autentia S.L., Profesor asociado en IE Business School, inversor en StartUps y mentor de emprendedores. Ingeniero Técnico de Telecomunicaciones y Executive MBA por IE Business School 2007. Twitter: Follow @rcanalesmora Autor de los Libros: Planifica tu éxito: de aprendiz a empresario, Informática profesional, las reglas no escritas para triunfar en la empresa, Conceptos ágiles aplicados a distintas áreas de una empresa y Conversaciones con CEOs y CIOs sobre Transformación Digital y Metodologías Ágiles. ¡Descárgalos gratis aquí! Puedes consultar mi CV y alguna de mis primeras aplicaciones (de los 90) aquí.

¿Quieres publicar en Adictos al trabajo?

Te puede interesar

22/06/2026

Juan Antonio Jiménez Torres

Vamos a instrumentalizar un SDD para un microservicio bancario. En este artículo sobre SDD - Spec Driven Development, instrumentalizado sobre OpenSpec vamos a construir una PoC de algo más serio que una tabla periódica. Un microservicio bancario muy simplificado que gestione cuentas y permita hacer transferencias

16/06/2026

Juan Antonio Jiménez Torres

Este es el segundo artículo sobre SDD, Spec Driven Development, instrumentalizado sobre OpenSpec. Pero esta vez nos acercaremos usando softwere gratuito. Por un lado OpenCode en lugar de ClaudeCode y usando el modelo gratuito Gemma4 de Google en lugar de otros de pago.

08/06/2026

Juan Antonio Jiménez Torres

Aprenderemos sobre SDD – Spec Driven Development instrumentalizado sobre OpenSpec. Haremos un primer acercamiento usando ClaudeCode con Sonnet, para luego, en un artículo posterior pasarnos a la versión gratuita de OpenCode con Gemma4