Introducción
¿Qué programador no ha soñado con participar en la creación de un videojuego? No sé vosotros, pero a mí siempre me ha gustado la idea de desarrollar un pequeño juego, ya desde los tiempos del Commodore 64 o del Sinclair Spectrum, que me ponía a copiar el código "Basic" de las revistas típicas sobre informática de la época para hacer algún juego simple.
En la actualidad, gracias a los distintos motores gráficos y Frameworks disponibles, está más a la alcance de la mano, aunque el desarrollo de un videojuego es algo complejo e involucra el trabajo de muchas personas.
Sin embargo, un solo desarrollador puede embarcarse en un proyecto "indie" gracias a herramientas destinadas para ello. En el presente artículo voy a comentar cómo comenzar a crear un pequeño videojuego para HTML5 usando el Framework Phaser 3. Solamente se necesitan conocimientos de Javascript / TypeScript.
¿Qué es Phaser 3?
Phaser 3 es una herramienta de programación, más concretamente, una librería Javascript, creada por Richard Davey, destinada a desarrollar juegos de una forma rápida para navegadores web, válida tanto para versiones de escritorio como móvil, y pueden ser compilados para usarlos y publicarlos como aplicación móvil. El juego se carga en un Canvas o en WebGL.
Canvas es un elemento HTML que es utilizado para dibujar gráficos, hacer composiciones de fotos o incluso realizar animaciones.
WebGL es una evolución de Canvas que permite cargar gráficos 3D.
Conceptos básicos
Antes de comenzar, pasamos a definir una serie de conceptos generales básicos que se tienen que conocer sobre la programación de videojuegos:
- Game Object: Es la clase base que se extiende a todos los Objetos del juego
- Game Loop: Casi todos los juegos corren bajo un bucle infinito capaz de detectar los eventos y cambios que se producen en el juego.
- Escena: Un juego en Phaser 3 está compuesto por diferentes escenas, donde cada una tiene su propia lógica del juego. El juego irá cambiando de escenas según una serie de condicionantes.
- Sprite: Simplemente es una imagen, o un conjunto de imágenes agrupadas en un solo archivo. Son los personajes, enemigos, etc.
- Tile Set: Conjunto de imágenes, separadas por cuadrículas, que forman los objetos del juego, el fondo, el mapa, etc.
- Máscara de colisión: Zona del sprite que puede colisionar con otras máscara de colisión.
Toda la documentación de phaser se puede encontrar en su GitHub: https://photonstorm.github.io/phaser3-docs/
Configuración del proyecto
Lo más formal para ejecutar Phaser es tener instalado Node.js y NPM, junto con un buen editor de código, como por ejemplo "Visual Studio Code".
En él instalaremos las extensiones "Live Server" y "Code runner", para poder ejecutar el proyecto.
Creamos la carpeta de nuestro proyecto y ejecutamos desde la terminal los siguientes comandos:
- npm init -y (Crear el package.json)
- npm i phaser (Instalación de las dependencias phaser en el proyecto)
- npm i -g phaser3-cli-gamma (Instalación global)
O simplemente añadimos a nuestro archivo "index.html" la librería de phaser3:
<script src="//cdn.jsdelivr.net/npm/phaser@3.51.0/dist/phaser.min.js"></script>
Obteniéndola de:
A parte, crearemos los siguientes archivos:
- En la raíz creamos un index.html.
- También un archivo jsconfig.json.
- Creamos una carpeta y el archivo src/main.js.
- Dentro creamos la carpeta y el archivo src/scenes/Firstscene.js (Primera escena).
- Creamos una carpeta assets/ para los archivos multimedia del proyecto.
- Creamos una carpeta def/ y metemos el archivo phaser.d.ts (Para las definiciones de typescript).
La estructura inicial del proyecto sería la siguiente:
index.html
El contenido del index quedaría así:
Donde se crea la capa con el contenedor del juego y se enlazan la librería phaser.min.js de node_modules así como el archivo main.js (que se especificará como type="module".
Ya tenemos preparada nuestra arquitectura básica para comenzar el desarrollo.
Configuración del juego
En el archivo main.js codificaremos las opciones del juego. Comenzaremos exponiendo las opciones básicas:
main.js
Una vez establecida las opciones, creamos la instancia del juego:
- const game = new Phaser.Game(config);
Crear escena
Ahora vamos a configurar la clase de la primera escena. Cada escena puede tener los siguientes métodos:
- init (Inicializa datos)
- preload (Precarga de assets)
- create (Instanciar los assets)
- update (Bucle del juego)
Quedando la clase de la siguiente manera:
Los parámetros time y delta del update, hacen referencia al tiempo transcurrido desde que se inició la escena (time), y el tiempo que pasa entre cada iteración, respectivamente (delta), este último usado para controlar el refresco en los ordenadores más antiguos.
Si lanzamos index.html bajo "Live Server", para ello le clicamos con el botón derecho encima del archivo desde Visual Studio Code y elegimos la opción "Ejecutar con Live Server", se nos abrirá la siguiente página web:
Añadiendo un sprite
En este caso vamos a añadir un personaje a la escena. Nada más y nada menos que nuestra mascota más tecnológica:
Para ello, añadimos la imagen a la carpeta assets/, (ya más adelante organizaremos bien la estructura de las imágenes).
Para comenzar, precargamos al personaje en el método preload de la siguiente manera:
- this.load.image('doggy50','./assets/doggy50.png');
Donde doggy50 será el objeto, y ./assets/doggy50.png es la ruta de la imagen.
También podemos indicar la ruta raíz de los sprites, y así no hay que ir indicándolo en cada uno:
- this.load.path = './assets/';
- this.load.image('doggy50','doggy50.png');
Incluso puedo ahorrarme indicar el nombre y extensión del sprite, si los hago coincidir, de las siguiente manera:
- this.load.path = './assets/';
- this.load.image('doggy50');
Lógicamente si tenemos que cargar varios sprites en pantalla no vamos a ir añadiéndolos uno a uno, para ello le pasamos un array de sprites de la siguiente manera:
- this.load.image(['doggy50']);
Ahora añadiremos el sprite al canvas, esto lo haremos dentro del método create:
- this.doggy50 = this.add.image(100, 100, 'doggy50').setInteractive();
.setInteractive() para a interactuar con el sprite.
Donde 100, 100, serán las coordenadas X,Y desde el vértice superior izquierdo del lienzo, posicionando el centro de la imagen. El código quedaría de la siguiente forma:
Si quisiéramos cambiar el punto de agarre del sprite sería con:
- this.doggy50.setOrigin(0,0);
Donde (0,0) es el vértice superior izquierdo del sprite y el (1,1) sería el inferior derecho.
Si tenemos activo "Live Server" los cambios se irán aplicando en el juego a medida que desarrollamos, sino, pues volvemos a ejecutar el index.html bajo "Live Server" y nos generaría una página web con el siguiente aspecto:
A continuación enumero algunas propiedades básicas del sprite:
- this.doggy50.flipX = true; (Girar la sprite horizontalmente).
- this.doggy50.flipY = true; (Girar la sprite verticalmente).
- this.doggy50.setVisible(0); (0, ocultar sprite, 1 mostrar sprite).
- this.doggy50.setScale(scalaX,scalaY);(Escalar sprite)
- this.doggy50.setAlpha(1); (Transparencia)
- this.doggy50.setTint(color en hexadecimal); (solo funciona con WebGL)
- this.doggy50.x = numero; (Posición en la escena)
- this.doggy50.y = numero; (Posición en la escena)
- this.doggy50.angle = 0; (Giro del sprite en grados).
- this.doggy50.setDepth(0); (Profundidad de los sprites, de 0 en adelante).
Se puede imprimir en console log el objeto, así se pueden ver todos los métodos y propiedades.
Moviendo el sprite con el teclado
Controlar nuestro personaje con el teclado es bastante sencillo con Phaser 3 ya que posee un gestor de teclado incorporado. En este casos, vamos a definir el movimiento con los cursores.
Para ello, creamos un objeto que contenga las propiedades de los cursores, de esta forma:
- this.cursors = this.input.keyboard.createCursorKeys();
De este modo, ya podemos comprobar si las teclas arriba, abajo, izquierda o derecha e incluso la barra espaciadora y el shift del teclado han sido pulsadas. En el método update, que es el Loop de la escena, podemos poner un código similar a este:
Nota: Ahora mismo no funcionaría el movimiento inclinado, para ello deberíamos hacer antes una anidación de IF .. ELSE con la lógica de ambas teclas pulsadas. Con lo que sabemos hasta el momento, podríamos hacer que nuestro personaje se moviera en todas las direcciones de la siguiente manera:
Si os fijáis hemos añadido también el giro (flip) del sprite según la dirección de las flechas pulsadas. (Más adelante modificaremos y optimizaremos este códgio, tanto el control del teclado como la animación del personaje).
Controlando cualquier tecla
Lógicamente también podemos controlar la pulsación de cualquier tecla. Esto se consigue de la siguiente manera:
const keys = Phaser.Input.Keyboard.KeyCodes;
this.keyZ = this.input.keyboard.addKey(keys.Z);
this.keyZ.on('down', () => {
console.log("Has presionado la tecla Z");
});
Si ponemos esta función en el método "create" solamente se ejecutará una vez.
Para que funcione en el "update" haríamos lo siguiente:
if (Phaser.Input.Keyboard.JustDown(this.keyZ)){
console.log("Has presionado Z");
}
También se pueden bindear varias teclas a la vez, para no ir de una en una, con el método "addkeys".
Añadiendo hoja de sprites y físicas al personaje
Hasta ahora nuestro personaje era simplemente una imagen que se podía mover por la pantalla en cualquier dirección. Sin embargo, ahora vamos a convertirlo en una entidad que posea física, animación, movimiento y que reconozca las colisiones.
Si queremos hacer que el personaje parezca que ande, lo mejor será encadenar una serie de sprites en un único archivo (hoja de sprites) incorporando los diferentes estados que posea para emular su forma de caminar.
Así que en vez de cargar una única imagen fija, cargaremos dicha hoja de sprites en preload() de la siguiente manera:
function preload ()
{
this.load.spritesheet('doggysprite',
'assets/doggysprite.png',
{ frameWidth: 50, frameHeight: 60 }
);
}
La propiedad "frameWidth" indica el ancho de cada sprite individual que conforman la hoja de sprites, recordemos que todos deberán tener el mismo ancho. Y "frameHeight" corresopnde a la altura de la hoja completa.
Ejemplo:
Una vez hecho esto dotaremos de física a nuestro personaje principal.
Volviendo a nuestro archivo main.js, añadiremos la siguiente propiedad a la configuración general del juego:
physics: {
default: 'arcade',
arcade: {
gravity: { y: 300 },
debug: false
}
},
y ahora regresando al archivo Firstscene.js declararemos los siguiente en el método create():
// Creamos el personaje en la posicíon X:Ancho/2 Y:Altura y le añadimos físicas.
this.player = this.physics.add.sprite(this.sys.game.canvas.width/2, this.sys.game.canvas.height-100, 'doggysprite');
// Hacemos que en las caídas tenga un pequeño rebote
this.player.setBounce(0.2);
// El personaje colisionará con los bordes del juego
this.player.setCollideWorldBounds(true);
Nota: "this.sys.game.canvas.width" es la anchura del lienzo. Y se posiciona desde el vértice inferior izquierdo. Le quitamos unos píxeles para que al empezar el juego el player tenga una pequeña caída.
Ahora haremos que el juego entienda nuestra hoja de sprites y pueda seleccionarlos correctamente:
Y lo llamaremos desde create(). Ahora solo nos queda modificar el movimiento del personaje en el update() de la siguiente manera:
Añadiendo el fondo del juego
Es tan simple como añadir otro sprite pero sin ningún tipo de interacción y colocado con la profundidad adecuada. Usaremos el siguiente recurso:
Fuente: Vector de Dibujos animados creado por upklyak - www.freepik.es
La precargamos:
this.load.image('background', 'assets/background.png');
Y la creamos posicionándola desde el centro del lienzo:
this.background = this.add.image(this.sys.game.canvas.width/2, this.sys.game.canvas.height/2, 'background');
Nota: En este punto hemos reconfigurado la anchura del lienzo en el main.js, para dotarlo de más amplitud. También podríamos usar el método this.background.setScale(x,y); para modificar su escala.
Añadiendo disparos
A continuación haremos que nuestro héroes dispare una serie de burbujas hacia arriba, por ello, cambiaremos la gravedad general del juego (en la configuración inicial), para solamente aplicársela al "player", ya que el disparo irá hacia arriba y no queremos que le afecte la gravedad, sin embargo, si le dejaremos establecido la posibilidad de interactuar con otros objetos de la escena (futuros enemigos).
Antes de nada, modificaremos también la hoja de sprites de nuestro personaje para añadirle una vacuna, que es por donde saldrán el disparo, en nuestro caso serán unas burbujas.
También crearemos nuestro sprite del disparo:
Creando grupos
Cuando vamos a tener el mismo sprite repetido varias veces en la pantalla se debe crear un grupo de imágenes que iremos controlando. Para ello como siempre precargamos el sprite:
- this.load.image("bullet", "assets/bullet.png");
Ahora crearemos un método para crear el grupo de imágenes en create():
this.bullets = this.physics.add.group({
defaultKey: 'bullet',
// maxSize: 1000
});
Nota: maxsize es para limitar la cantidad de sprites que se pueden crear de ese grupo. En nuestro caso queremos que no se gasten las balas, así que lo comentamos.
Ahora creamos el método que genere el disparo:
Posicionamos correctamente desde dónde queremos que nazca la bala (burbuja en nuestro caso).
Ya para finalizar esta parte del tutorial, volvemos a modificar el control por teclado para añadir el disparo, quedando el código de la siguiente manera:
Si os fijáis hemos cambiado el control de la tecla SPACE, primero hemos cambiado la condición para que si dejas pulsada la tecla no aparezcan un sin fin de disparos, si no que detallamos el tiempo que transcurre entre la generación continuada de burbujas. Y ya por último se ha añadido la llamada a fire pasándole como parámetro el objeto del personaje para que pueda obtener su posición en el momento de pulsar la tecla.
El juego luciría como se muestra en el siguiente gif:
Continuará...
En el próximo post añadiremos a los enemigos (virus que irán cayendo), explicaremos las colisiones, el sistema de puntuación y la pantallas de título y game over del juego. Optimizaremos el código creando clases para el personaje y los enemigos.
El código actual del juego lo tenéis en GitHub:
Segunda entrega del tutorial de phaser3:
Si te ha gustado, ¡síguenos en Twitter para próximas entregas!
Enlaces de interés
- API de phaser3: https://photonstorm.github.io/phaser3-docs/index.html
- Documentación de phaser3: https://rexrainbow.github.io/phaser3-rex-notes/docs/site/keyboardevents/
- Free assets para juegos: https://itch.io/game-assets/free