¡Buenas!

Bien, pues esta es la tercera parte de Creando un shooter en HTML5. Si has llegado aquí y no has leído las anteriores, te recomiendo que lo hagas antes de continuar: PARTE 1, PARTE 2.

 

PARTE 3: Creando el motor (2)

 

LAS ENTIDADES

En el anterior capítulo programamos uno de los elementos básicos del juego,que son los sprites. En este continuaremos esta secuencia, y crearemos lo realmente importante: Las entidades.

 

Llamaremos “entidades” a los elementos que tendrán algún tipo de interacción en el escenario: Véase, los muros, el jugador, los zombies, etc.

 

Estas entidades se compondrán de un objeto “prototipo” que les definirá las propiedades más importantes: El sprite que usa, la posición en la que está, el “origen” de sus coordenadas (ya lo explicaré más adelante), y una serie de métodos que actuarán como eventos: un evento que se ejecutará una sola vez cuando una instancia de dicha entidad se ha creado, un evento que se ejecutará contínuamente y de forma independiente en cada instancia, etc.

 

Pero, ¿instancia?… Las entidades, como ya dije, son sólo un “prototipo”, un molde que nos permitirá crear copias o instancias de sí misma en el escenario como tal. ¿Vale?

 

Digamos que tenemos una entidad llamada “zombie”. Definimos todas sus propiedades y eventos, pero podemos crear varias instancias de este zombie en la escena, cada una manteniendo dichas propiedades de manera individual.

 

1. Objeto “entidad”

Pues bien, hay que proceder a programar las entidades. Las entidades (al igual que las instancias) necesitan tener una serie de propiedades y eventos, que listaré a continuación:

  • El sprite
  • Subimagen actual del sprite (índice del sprite)
  • Velocidad de animación del sprite (cuánto aumentará el índice cada fotograma)
  • Posición (x, y) de la instancia
  • Origen relativo (x, y) del sprite con respecto a la posición de la instancia (ya lo explicaré más adelante)
  • Máscara de colisión (ya lo explicaré más adelante)
  • Eventos:
      • Al momento de crearse la instancia
      • Durante toda la ejecución del juego
      • Al colisionar con otra instancia

Toda esta información la almacenaremos dentro de una propiedad de la entidad a la que llamaremos Proto, y al momento de “crear una instancia en la escena”, copiaremos todas las propiedades de Proto en un objeto nuevo, ¡y voilá!

 

Peeero antes, algo muy importante: Una vez creada la instancia, ¿dónde la guardaremos para poderla ejecutar después? La respuesta es sencilla: En un arreglo “global”.

function Entity(sprite, container) {

    this.container = container;

    this.Proto = {

        sprite_index: sprite,
        image_index: 0,
        image_speed: 0,

        sprite_origin: {x: 0, y: 0},

        x: 0,
        y: 0,


        event_create: function() {},
        event_step: function() {},
        event_collision: function() {}

    };

}

Como vemos, el objeto Entity tiene sólo dos propiedades principales: Proto y containerProto, como ya dije, almacenará las propiedades y eventos que se copiarán a las instancias; y container, por otra parte, almacenará el arreglo en donde las instancias se guardarán. Un ejemplo de cómo funcionaría esto:

var instancias = [];
var Jugador = new Entity( sprites.player, instancias );

 

2. Función “instancia”

Vamos a la siguiente parte: Crear una instancia de una entidad. Para ello utilizaremos una función que reciba tres parámetros: La posición (x, y) donde crear esta instancia, y la entidad “padre” de la cual se creará dicha instancia. Esta función creará la instancia, la almacenará en el arreglo container de la entidad, y a su vez devolverá la instancia creada.

function instance_create(x, y, entity) {

    var instance = {};
    for(var i in entity.Proto) {
        instance[i] = entity.Proto[i];
    }
    instance.x = x;
    instance.y = y;
    instance.entity_index = entity;

    entity.container.push( instance );

    instance.event_create();
    return instance;
}

Como vemos, la lógica es muy simple: Crea un objeto nuevo y vacío, y copia las propiedades de Proto de la entidad en él. Después, modifica la posición por la que se ha pasado como parámetro, y se establece una propiedad adicional: entity_index, que almacenará la entidad “padre” de la que la instancia fue creada; principalmente para usarla comprobando que tipo de instancia es (si es un muro, un zombie, etc., por dar un ejemplo). Y por último, ejecuta el evento “create” de la instancia recién creada.

 

La forma de crear una entidad e instanciarla sería algo así:

var instancias = [];
var Jugador = new Entity( sprites.player, instancias );

var jug1 = instance_create(128, 96, Jugador);

 

3. Iniciar el juego

Vamos con la última parte de la entrada: Ejecutar las instancias creadas. Además, comenzaremos a programar el juego de verdad.

 

En el capítulo anterior habíamos programado algo en el archivo game.js, pero era sólo para realizar una prueba. Para el juego de verdad, comenzaremos desde aquí, imaginando que dicho archivo está en blanco.

 

Ahora sí, comenzaremos añadiendo los sprites al juego. Usaremos una constante que almacene estos sprites en un objeto.

const sprites = {
    player: new Sprite( 'source/sprites/spr_player.png', 8 )
};

(por el momento cargaremos sólo el jugador)

Y ahora añadiremos el evento “load” al cuerpo del documento, para cargar el canvas, su render y comenzar a dibujar el juego en él.

window.addEventListener( 'load', function() {
    //Obtener el elemento <canvas> del juego y su CanvasRenderingContext2D
    const Canvas = document.getElementById( 'game' );
    const Render = Canvas.getContext( '2d' );
    const FPS    = 30; //FPS del juego
}

Todo el siguiente código que pondré, debe ir dentro de este mismo evento load para que se ejecute correcamente.

 

Lo siguiente que haremos será crear un arreglo que cumplirá la función que ya mencioné antes: Almacenar las instancias del juego. De pasó crearé la primera entidad: El jugador.

    var entities = [];

    var objPlayer = new Entity( sprites.player, entities );

Ahora bien , algo que no mencioné antes: Para modificar una propiedad o evento de una entidad, puedes hacerlo de dos maneras: Modificando dicha propiedad en Proto desde la misma entidad, o después de crear una instancia, puedes modificar directamente la misma. Por ejemplo:

        objPlayer.Proto.sprite_origin = {x:33, y: 33};
        objPlayer.Proto.event_step = function() {

            this.x++;

        }
        
        instance_create(0, 0, objPlayer).sprite_origin = { x: 0, y: 0 };

(Ignoremos este último código código, es sólo un ejemplo xDxd)

 

Lo siguiente que haremos será iniciar un interval que “limpie” el canvas rendering del juego, y a su vez ejecute todas las instancias del mismo:

    window.setInterval(function() {
        Render.clearRect( 0, 0, Canvas.width, Canvas.height );
        for(var i = 0; i < entities.length; i++) {
            var instance = entities[i];
            instance.event_step();
        }
    }, 1000/FPS );

Peeero, aún hay un problema, y es que aún no hemos dibujado en pantalla el sprite de las instancias, Y AQUÍ ES donde explicaré la dichosa propiedad sprite_origin.

 

Esta propiedad define las coordenadas relativas al sprite que “se unirán” con las coordenadas reales de la instancia. Algo como muestra esta imagen:

¿Y cómo lo dibujamos? Pues fácil, con la función draw_sprite() que ya habíamos creado anteriormente.

    window.setInterval(function() {

        Render.clearRect( 0, 0, Canvas.width, Canvas.height );
        for(var i = 0; i < entities.length; i++) {

            var instance = entities[i];

            draw_sprite(Render, 
                instance.sprite_index, 
                Math.floor(instance.image_index), 
                instance.x - instance.sprite_origin.x, 
                instance.y - instance.sprite_origin.y
            );
            instance.image_index += instance.image_speed;

            instance.event_step();

        }


    }, 1000/FPS );

Lo último que haremos será dejar en funcionamiento un par de instancias para probar el progreso de este capítulo. Creemos una nueva entidad y sus instancias (por fuera del interval pero aún dentro del evento load):

    var objPlayer = new Entity( sprites.player, entities );
        objPlayer.Proto.sprite_origin = {x:33, y: 33};

        objPlayer.Proto.event_create = function() {


            //Obtener una dirección aleatoria entre 0 y 2PI, sólo con ángulos
            //múltiplos de PI/4.
            var dir = Math.floor(8 * Math.random()) ;
            this.direction = dir * Math.PI/4; //Esta propiedad no está predefinida, es una "única" de esta entidad.

            //Definir la subimagen correspondiente al ángulo.
            this.image_index = dir;

        };
        objPlayer.Proto.event_step = function() {

            //Mover el jugador con una velocidad de 3, hacia el ángulo generado.
            this.x += Math.cos(this.direction) * 3;
            this.y -= Math.sin(this.direction) * 3;

            //Mantener al jugador siempre dentro de la pantalla.
            if(this.x < 0)
                this.x = Canvas.width;
            if(this.y < 0) this.y = Canvas.height; if(this.x > Canvas.width)
                this.x = 0;
            if(this.y > Canvas.height)
                this.y = 0;

        };


    var i = 0;
    while(i < 5) {
        //Crear cinco instancias de objPlayer en posiciones aleatorias dentro de la escena
        
        var pos_x = Math.floor( Math.random() * Canvas.width );
        var pos_y = Math.floor( Math.random() * Canvas.height );
        instance_create(pos_x, pos_y, objPlayer);
        i++;
    }

Por este capítulo, esto sería todo. Puedes observar el progreso en este enlace.

 

PARTE 4.

 

Y ahora sí, ¡Saludos!