Iniciando un juego en TypeScript

0
2623

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


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

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

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

Por favor ingrese su nombre aquí

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

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad