Buenas tardes, queridos colegas!
Mi nombre es Alexander, soy desarrollador de juegos HTML5.
En una de las compañías donde envié mi currículum, me pidieron que completara una tarea de prueba. Acepté y, después de 1 día, envié como resultado el juego desarrollado de acuerdo con TOR HTML5.

Como estoy entrenando en programación de juegos, así como para un uso más eficiente de mi código, decidí que sería útil escribir un artículo de capacitación sobre el proyecto completado. Y dado que la prueba completa recibió una evaluación positiva y condujo a una invitación a una entrevista, probablemente mi decisión tiene derecho a existir y, posiblemente, ayudará a alguien en el futuro.
Este artículo dará una idea de la cantidad de trabajo suficiente para completar con éxito la tarea de prueba promedio para la posición HTML5 del desarrollador. El material también puede ser de interés para cualquiera que quiera familiarizarse con el marco Phaser. Y si ya está trabajando con Phaser y escribiendo en JS, vea cómo desarrollar un proyecto en TypeScript.
Entonces, ¡debajo de cat hay mucho código TypeScript!
Introduccion
Damos una breve declaración del problema.
- Desarrollaremos un juego HTML5 simple: un zapador clásico.
- Como herramientas principales usaremos phaser 3, mecanografiado y webpack.
- El juego estará diseñado para el escritorio y se ejecutará en el navegador.
Proporcionamos enlaces al proyecto final.
Enlaces a la demostración y fuente Y recuerda la mecánica del zapador, si de repente alguien olvida las reglas del juego. Pero dado que este es un caso poco probable, las reglas se colocan bajo el spoiler :)
Reglas de zapadorEl campo de juego consiste en celdas dispuestas en una mesa. Por defecto, cuando comienza el juego, todas las celdas están cerradas. Se colocan bombas en algunas de las celdas.
Al hacer clic izquierdo en una celda cerrada, se abre. Si hubo una bomba en una celda abierta, entonces el juego termina en derrota.
Si no había una bomba en la celda, se muestra un número dentro de ella, que indica la cantidad de bombas que hay en las celdas vecinas en relación con la apertura actual. Si no hay bombas cerca, entonces la celda se ve vacía.
Al hacer clic con el botón derecho en una celda cerrada, se marca una bandera. La tarea del jugador es organizar todas las banderas disponibles para que marquen todas las celdas minadas. Después de colocar todas las banderas, el jugador presiona el botón izquierdo del mouse en una de las celdas abiertas para verificar si ganó.
A continuación, vamos directamente al manual en sí. Todo el material se divide en pequeños pasos, cada uno de los cuales describe la implementación de una tarea específica en poco tiempo. Entonces, realizando pequeños objetivos paso a paso, al final crearemos un juego completo. Use la tabla de contenido si decide ir rápidamente a un paso específico.
1. Preparación
1.1 Plantilla de proyecto
Descargue la
plantilla de proyecto phaser predeterminada . Esta es la plantilla recomendada por el autor del marco y nos ofrece la siguiente estructura de directorios:
Para nuestro proyecto, no necesitamos el archivo
index.js
actual, así que elimínelo. Luego cree el directorio
/src/scripts/
y coloque el archivo vacío
index.ts
en él. Agregaremos todos nuestros scripts a esta carpeta.
También vale la pena tener en cuenta que al crear un proyecto para producción, se creará un directorio
dist
en la raíz, en el que se colocará la versión de lanzamiento.
1.2 Configuración de compilación
Utilizaremos webpack para el montaje. Como nuestra plantilla se preparó originalmente para trabajar con JavaScript, y escribimos en TypeScript, necesitamos realizar pequeños cambios en la configuración del recopilador.
En el
webpack/base.js
agregue la clave de
entry
, que indica el punto de entrada al construir nuestro proyecto, así como la configuración de
ts-loader
que describe las reglas para construir scripts TS:
También necesitaremos crear el archivo tsconfig.json en la raíz del proyecto. Para mí tiene el siguiente contenido:
{ "compilerOptions": { "module": "commonjs", "lib": [ "dom", "es5", "es6", "es2015", "es2017", "es2015.promise" ], "target": "es5", "skipLibCheck": true }, "exclude": ["node_modules", "dist"] }
1.3 Instalación de módulos
Instale todas las dependencias de package.json y agregue los módulos mecanografiado y ts-loader:
npm i npm i typescript --save-dev npm i ts-loader --save-dev
Ahora el proyecto está listo para comenzar el desarrollo. Tenemos 2 comandos a nuestra disposición que ya están definidos en la propiedad de
scripts
en el archivo
package.json
.
- Cree un proyecto para la depuración y ábralo en un navegador a través de un servidor local
npm start
- Ejecute la compilación para la venta y coloque la compilación de lanzamiento en la carpeta dist /
npm run build
1.4 Preparación de activos
Todos los recursos para este juego se descargan honestamente de
OpenGameArt (versión 61x61) y tienen la más amigable de las licencias llamadas
Feel free to use , que la página con el paquete nos dice cuidadosamente). Por cierto, ¡el código presentado en el artículo tiene la misma licencia! ;)
Eliminé la imagen del reloj del conjunto descargado y cambié el nombre del resto de los archivos para obtener nombres de cuadros que sean fáciles de usar. La lista de nombres y los archivos correspondientes se muestran en la pantalla a continuación.
A partir de los sprites resultantes, crearemos un atlas de formato
Phaser JSONArray
en el programa
TexturePacker (hay una versión gratuita más que suficiente, todavía no he conseguido trabajo) y
spritesheet.json
archivos
spritesheet.png
y
spritesheet.json
generados en el directorio
src/assets/
project.

2. Creando escenas
2.1 Punto de entrada
Comenzamos el desarrollo creando el punto de entrada descrito en la configuración del paquete web.
Dado que el juego que tenemos está diseñado para el escritorio y ocupará toda la pantalla, utilizamos audazmente todo el ancho y alto del navegador para los campos de
width
y
height
.
El campo de
scene
es actualmente una matriz vacía y lo arreglaremos.
2.2 Escena de inicio
Cree la clase de la primera escena en el
src/scripts/scenes/StartScene.ts
:
export class StartScene extends Phaser.Scene { constructor() { super('Start'); } public preload(): void { } public create(): void { } }
Para una herencia válida de
Phaser.Scene
pasamos el nombre de la escena como parámetro al constructor de la clase padre.
Esta escena combinará la funcionalidad de precarga de recursos y la pantalla de inicio, invitando al usuario al juego.
Por lo general, en mis proyectos, un jugador pasa por dos escenas antes de llegar a la inicial, en este orden:
Boot => Preload => Start
Pero en este caso, el juego es tan simple, y hay tan pocos recursos que no hay razón para colocar la precarga en una escena separada y aún más lo hace el cargador de
Boot
inicial separado.
Cargaremos todos los activos en el método de
preload
. Para poder trabajar con el atlas creado en el futuro, debemos realizar 2 pasos:
- obtener archivos
png
y json
atlas usando require
:
- cárguelos en el método de
preload
de la escena de inicio:
2.3 Textos de la escena inicial
En la escena inicial, quedan 2 cosas por hacer:
- dile al jugador cómo comenzar el juego
- iniciar el juego por iniciativa del jugador
Para cumplir con el primer punto, primero creamos dos enumeraciones al comienzo del archivo de escena para describir los textos y sus estilos:
Y luego cree ambos textos como objetos en el método de
create
. Permítame recordarle que el método de
create
de escenas en
Phaser
se llamará solo después de cargar todos los recursos en el método de
preload
y esto es bastante adecuado para nosotros.
En otro proyecto más grande, podríamos poner los textos y estilos en archivos locales de json o en configuraciones separadas, pero dado que ahora solo tenemos 2 líneas, considero que este paso es redundante y en este caso sugiero no complicar nuestras vidas, limitándonos a los listados al comienzo del archivo de escena.
2.4 Transición al nivel del juego
Lo último que haremos en esta escena antes de continuar es rastrear el evento de clic del mouse para lanzar al jugador al juego:
Escena de 2.5 niveles
A juzgar por el parámetro
"Game"
pasado al método
this.scene.start
ya
this.scene.start
que era hora de crear una segunda escena, que procesaría la lógica principal del juego. Cree el archivo
src/scripts/scenes/GameScene.ts
:
export class GameScene extends Phaser.Scene { constructor() { super('Game'); } public create(): void { } }
En esta escena, no necesitamos el método de
preload
, porque Ya hemos cargado todos los recursos necesarios en la escena anterior.
2.6 Configuración de escenas en el punto de entrada
Ahora que ambas escenas están creadas, agréguelas a nuestro punto de entrada
src/scripts/index.ts
:
3. objetos del juego
Entonces, la clase
GameScene
implementará la lógica de nivel de juego. ¿Y qué esperamos del nivel de juego zapador? Visualmente, esperamos ver un campo de juego con celdas cerradas. Sabemos que el campo es una tabla, lo que significa que tiene un número dado de filas y columnas, en varias de las cuales se colocan cómodamente bombas. Por lo tanto, tenemos suficiente información para crear una entidad separada que describa el campo de juego.
3.1 Tablero de juego
Cree el archivo
src/scripts/models/Board.ts
en el que
src/scripts/models/Board.ts
la clase
Board
:
import { Field } from "./Field"; export class Board extends Phaser.Events.EventEmitter { private _scene: Phaser.Scene = null; private _rows: number = 0; private _cols: number = 0; private _bombs: number = 0; private _fields: Field[] = []; constructor(scene: Phaser.Scene, rows: number, cols: number, bombs: number) { super(); this._scene = scene; this._rows = rows; this._cols = cols; this._bombs = bombs; this._fields = []; } public get cols(): number { return this._cols; } public get rows(): number { return this._rows; } }
Hagamos que la clase sea la sucesora de Phaser.Events.EventEmitter para acceder a la interfaz para registrar y llamar a eventos, que necesitaremos en el futuro.
Una matriz de objetos de la clase
Field
se almacenará en la propiedad privada
_fields
. Implementaremos este modelo más tarde.
Configuramos propiedades numéricas privadas
_rows
y
_cols
para indicar el número de filas y columnas del campo de juego. Crea
_rows
públicos para leer
_rows
y
_cols
.
El campo
_bombs
nos dice la cantidad de bombas que deberán generarse para el nivel. Y en el parámetro
_scene
pasamos una referencia al objeto de la escena del juego
GameScene
, en el que crearemos una instancia de la clase
Board
.
Vale la pena señalar que transferimos el objeto de escena al modelo solo para su posterior transmisión a las vistas, donde lo usaremos solo para mostrar la vista. El hecho es que el phaser usa directamente el objeto de escena para renderizar sprites y, por lo tanto, nos obliga a proporcionar un enlace a la escena actual al crear prefabricados de sprites, que desarrollaremos en el futuro. Y por nosotros mismos, aceptaremos el acuerdo de que transferimos el enlace a la escena solo para su uso posterior como motor de visualización y aceptaremos que no llamaremos directamente a los métodos personalizados de la escena en modelos y vistas.
Una vez que hayamos decidido la interfaz de creación del tablero, propongo inicializarla en la escena de nivel, finalizando la clase
GameScene
:
Llevamos los parámetros del tablero a constantes al comienzo del archivo de escena y los pasamos al constructor del
Board
al crear una instancia de esta clase.
3.2 Modelo de celda
El tablero consta de celdas, que desea mostrar en la pantalla. Cada celda debe colocarse en la posición correspondiente, determinada por la fila y la columna.
Las celdas también se seleccionan como una entidad separada. Cree el archivo
src/scripts/models/Field.ts
en el que
src/scripts/models/Field.ts
la clase que describe la celda:
import { Board } from "./Board"; export class Field extends Phaser.Events.EventEmitter { private _scene: Phaser.Scene = null; private _board: Board = null; private _row: number = 0; private _col: number = 0; constructor(scene: Phaser.Scene, board: Board, row: number, col: number) { super(); this._init(scene, board, row, col); } public get col(): number { return this._col; } public get row(): number { return this._row; } public get board(): Board { return this._board; } private _init(scene: Phaser.Scene, board: Board, row: number, col: number): void { this._scene = scene; this._board = board; this._row = row; this._col = col; } }
Cada celda debe tener métricas de fila y columna en las que se encuentra. Configuramos los parámetros
_board
y
_scene
para establecer enlaces a los objetos del tablero y la escena. Implementamos getters para leer los
_row
,
_col
y
_board
.
3.3 Vista de celda
Se crea la celda abstracta y ahora queremos visualizarla. Para mostrar una celda en la pantalla, debe crear su vista. Cree el archivo
src/scripts/views/FieldView.ts
y coloque la clase de vista en él:
import { Field } from "../models/Field"; export class FieldView extends Phaser.GameObjects.Sprite { private _model: Field = null; constructor(scene: Phaser.Scene, model: Field) { super(scene, 0, 0, 'spritesheet', 'closed'); this._model = model; this._init(); this._create(); } private _init(): void { } private _create(): void { } }
Tenga en cuenta que convertimos esta clase en descendiente de
Phaser.GameObjects.Sprite
. En términos de fase, esta clase se ha convertido en un sprite prefabricado. Es decir, obtuve la funcionalidad del objeto de juego del sprite, que ampliaremos aún más con nuestros propios métodos.
Veamos el constructor de esta clase. Aquí, en primer lugar, debemos llamar al constructor de la clase padre con los siguientes conjuntos de parámetros:
- enlace al objeto de la escena (como advertí en la sección 3.1: el phaser requiere que nos vinculemos a la escena actual para renderizar sprites)
- coordenadas
y
en lienzo - la clave de cadena para la que está disponible el atlas, que cargamos en el método de
preload
de la escena de inicio - la clave de cadena de fotograma en este atlas que desea seleccionar para mostrar el sprite
Establezca una referencia al modelo (es decir, una instancia de la clase
Field
) en la propiedad privada
_model
.
También iniciamos prudentemente 2
_create
_init
y
_create
actualmente vacíos, que implementaremos un poco más tarde.
3.4 Crear un sprite en una clase de vista
Entonces, se creó la vista, pero ella todavía no sabe cómo dibujar un sprite. Para colocar el sprite con el marco que necesitamos en el lienzo, deberá modificar nuestro propio método privado
_create
:
3.5 Posicionamiento de sprites
Por el momento, todos los sprites creados se colocarán en las coordenadas (0, 0) del lienzo. También necesitamos colocar cada celda en su posición correspondiente en el tablero. Es decir, al lugar que corresponde a la fila y columna de esta celda. Para hacer esto, necesitamos escribir un código para calcular las coordenadas de cada instancia de la clase
FieldView
.
Agregue la propiedad
_position
a la clase, que es responsable de las coordenadas finales de la celda en el campo de juego:
Dado que queremos alinear el tablero y, en consecuencia, las celdas en él, en relación con el centro de la pantalla, también necesitamos la propiedad
_offset
, que indica el desplazamiento de esta celda en particular en relación con los bordes izquierdo y superior de la pantalla. Agréguelo con un captador privado:
Por lo tanto, nosotros:
this._scene.cameras.main.width
ancho total de la pantalla en this._scene.cameras.main.width
.- Obtuvimos el ancho total de la placa multiplicando el número de celdas por el ancho de una celda:
this._board.cols * this.width
. - Quitando el ancho del tablero del ancho de la pantalla, obtuvimos un lugar en la pantalla, no ocupado por el tablero.
- Dividiendo el número resultante por 2, obtuvimos el valor de sangría a la izquierda y a la derecha del tablero.
- Al cambiar cada celda por el valor de esta sangría, garantizamos la alineación de toda la placa a lo largo del eje
x
.
Realizamos acciones absolutamente similares para obtener desplazamiento vertical.
Queda por agregar el código necesario en el método
_init
:
Las propiedades
this.x
,
this.y
,
this.width
y
this.height
aquí son las propiedades heredadas de la clase
Phaser.GameObjects.Sprite
. Cambiar las propiedades de
this.x
this.y
conduce a la correcta colocación del sprite en el lienzo.
3.6 Crear una instancia de FieldView
Cree una vista en la clase
Field
:
3.7 Mostrar los campos del tablero.
Volvamos a la clase
Board
, que es esencialmente una colección de objetos
Field
y creará celdas.
_create
código de creación de la placa en un método
_create
separado y llamaremos a este método desde el constructor. Sabiendo que en el método
_create
no solo crearemos celdas,
_createFields
el código para crear celdas en un método
_createFields
separado.
Es en este método que crearemos el número deseado de celdas en un bucle anidado:
Es la primera vez que se ejecuta el ensamblado para la depuración con el comando
npm start
Asegúrese de que en el centro de la pantalla se espera que veamos 64 celdas en 8 filas.
3.8 Fabricando bombas
Anteriormente, informé que en el método
_create
de la clase
Board
, no solo crearemos campos. Que mas También habrá la creación de bombas y la configuración de las células creadas para el número de bombas vecinas. Comencemos con las bombas mismas.
Necesitamos colocar N bombas en el tablero en celdas aleatorias. Describimos el proceso de creación de bombas con un algoritmo aproximado:
En cada iteración del bucle, obtendremos una celda aleatoria de la propiedad
this._fields
hasta que creemos tantas bombas como se especifica en el campo
this._bombs
. Si la celda recibida está vacía, instalaremos una bomba en ella y actualizaremos el contador de las bombas necesarias para la generación.
Para generar un número aleatorio, utilizamos el método estático
Phaser.Math.Between
.
No olvides escribir la llamada a
this._createBombs();
en el archivo
Board.ts
this._createBombs();
al final del método
_create
Como ya notó, para que este código funcione correctamente, debe refinar la clase
Field
agregando el getter
empty
y el método
setBomb
.
Agregue un campo privado
_value
a la
_value
Field, que regulará el contenido de la celda. Aceptamos los siguientes acuerdos.
Siguiendo estas reglas, desarrollaremos métodos en la clase
Field
que funcionen con la propiedad
_value
:
3.9 Configuración de valores
Las bombas están ordenadas y ahora tenemos todos los datos para establecer los valores numéricos en todas las celdas que lo requieren.
Permítame recordarle que de acuerdo con las reglas del zapador, la celda debe tener el número que corresponde al número de bombas ubicadas al lado de esta celda. En base a esta regla, escribimos el pseudocódigo correspondiente.
En la clase
Board
, cree un nuevo método y traduzca el pseudocódigo especificado en código real:
Veamos cuál de las interfaces que utilizamos no está implementada.
getClosestFields
agregar el método
getClosestFields
para obtener las celdas vecinas.
¿Cómo identificar las células vecinas?
Por ejemplo, considere cualquier celda del tablero que no esté en el borde, es decir, no en la fila extrema y no en la columna extrema. Dichas celdas tienen un número máximo de vecinos: 1 en la parte superior, 1 en la parte inferior, 3 a la izquierda y 3 a la derecha (incluidas las celdas en diagonal).
Por lo tanto, en cada una de las celdas vecinas, los indicadores
_row
y
_col
no difieren en más de 1. Esto significa que podemos especificar de antemano la diferencia entre los parámetros
_row
y
_col
con el campo actual. Agregue una constante al comienzo del archivo a la descripción de la clase:
Y ahora podemos agregar el método faltante, en el que recorreremos esta matriz:
No olvide verificar la variable de
field
en cada iteración, ya que no todas las celdas en el tablero tienen 8 vecinos. Por ejemplo, la celda superior izquierda no tendrá vecinos a su izquierda, y así sucesivamente.
Queda por implementar el método
getField
y agregar todas las llamadas necesarias al método
_create
en la clase
Board
4. Manejo de eventos de entrada
4.1 Seguimiento de eventos de clic del mouse
Por el momento, el tablero está completamente inicializado, tiene bombas y hay celdas con números, pero todas están cerradas y no hay forma de abrirlas. Corregiremos esto e implementaremos la apertura de celdas haciendo clic en el botón izquierdo del mouse.Primero, necesitamos rastrear este mismo clic. En la clase, FieldView
agregue el _create
siguiente código al final del método :
En phaser, puede suscribirse a objetos del espacio de nombres para diferentes eventos Phaser.GameObjects
. En particular, suscribiremos al evento click ( pointerdown
) el prefabricado del propio sprite, es decir, un objeto de una clase FieldView
heredada de Phaser.GameObjects.Sprite
.Pero antes de hacer esto, debemos indicar explícitamente que el sprite es potencialmente interactivo, es decir, generalmente necesita escuchar la entrada del usuario sobre él. Debe hacer esto llamando al método setInteractive
sin parámetros en el propio sprite, lo que hicimos en el ejemplo anterior.Ahora que nuestro sprite se ha vuelto interactivo, volvamos a la clase Board
en el lugar donde se crean los nuevos objetos modelo Field
, es decir, al método _createFields
y registremos la devolución de llamada para los eventos de entrada para la vista:
Una vez que hayamos establecido que al hacer clic en el sprite queremos ejecutar el método _onFieldClick
, necesitamos implementarlo. Pero eliminaremos la lógica de procesar el clic de la clase Board
. Se cree que es mejor procesar el modelo según la entrada y, en consecuencia, cambiar sus datos en un controlador separado, cuya similitud es la clase de la escena del juego GameScene
. Por lo tanto, debemos reenviar el evento de clic más lejos, desde la clase Board
a la escena misma. Entonces haremos:
Aquí no solo lanzamos el evento de clic como lo fue, sino que también especificamos qué clic fue. Esto será útil en el futuro, cuando en la clase de escena procesaremos cada opción de manera diferente. Por supuesto, sería posible enviar el evento de clic tal como está, pero simplificaremos el código de escena, dejando parte de la lógica con respecto al evento en sí en la clase Field
.Bueno, ahora volvamos a la clase de la escena del juego GameScene
y agreguemos un _create
código al final del método que rastrea los eventos de un clic en las celdas:
4.2. Procesamiento de clic izquierdo
Procedemos a implementar el procesamiento de eventos de clic del mouse. Y comience abriendo las celdas. Las celdas deben abrirse presionando el botón izquierdo. Y antes de comenzar a programar, expresemos las condiciones que deben cumplirse:- Al hacer clic en una celda cerrada, debe abrirse
- si hay una mina en una celda abierta, el juego se pierde
- si no hay minas o valores en la celda abierta, entonces min no está en las celdas vecinas, en este caso necesita abrir todas las celdas vecinas y continuar así hasta que el valor aparezca en la celda abierta
- cuando haces clic en una celda abierta, debes verificar si todas las banderas están configuradas correctamente y, de ser así, finalizar el juego con una victoria
Y ahora, para simplificar la comprensión de la funcionalidad requerida, traducimos la lógica anterior en pseudocódigo:
Ahora tenemos una comprensión de lo que necesita ser programado. Implementamos el método _onFieldClickLeft
:
Y luego, como siempre, finalizaremos las clases Field
e Board
implementaremos los métodos que llamamos en el controlador.Indicamos 3 estados posibles de la celda en la enumeración States
, agregamos un campo _state
e implementamos un captador para cada estado posible:
Ahora que tenemos estados que indican si la celda está cerrada o no, podemos agregar un método open
que cambiará el estado:
Cada cambio en el estado del modelo debe desencadenar un evento que informa esto. Por lo tanto, presentamos un método privado adicional _setState
en el que se implementará toda la lógica del cambio de estado. Este método se llamará en todos los métodos públicos del modelo, que deberían cambiar su estado.Agregue una bandera booleana _exploded
para indicar explícitamente exactamente el objeto Field que se hizo explotar:
Ahora abra la clase Board
e implemente el método en ella openClosestFields
. Este método es recursivo y su tarea será abrir todos los campos vecinos vacíos en relación con la celda aceptada en el parámetro.El algoritmo será el siguiente: :
Y esta vez ya tenemos todas las interfaces necesarias para la implementación completa de este método:
Agregue un captador completed
a la clase Board
para indicar la colocación correcta de banderas en el tablero. ¿Cómo podemos determinar si un tablero se ha limpiado con éxito? El número de campos marcados correctamente debe ser igual al número total de bombas en el tablero.
Este método filtra la matriz _fields
por getter completed
, que debe indicar la validez de la marca de campo. Si la longitud de la matriz filtrada (en la que solo caen correctamente los campos marcados, de los cuales el captador completed
ya es responsable de la clase Field
) es igual al valor del campo _bombs
(es decir, el número de bombas en el tablero), entonces volvemos true
, en otras palabras, consideramos que el juego ganó.Tampoco nos importa la oportunidad de abrir todo el tablero con una sola llamada, lo que tenemos que hacer al final del nivel. También agregaremos esta característica a la clase Board
:
Queda por agregar un captador completed
a la clase misma Field
. ¿En qué caso el campo se considerará borrado con éxito? Si está minado y marcado. Ambos captadores necesarios ya están allí y podemos agregar este método:
Para completar el procesamiento del clic izquierdo del mouse, crearemos un método _onGameOver
en el que deshabilitemos el seguimiento de los eventos del tablero y le mostraremos al jugador todo el tablero. Más adelante también agregaremos un código de representación del informe de finalización de estado basado en el parámetro status
.
4.3 Visualización de campo
Antes de comenzar a procesar el clic derecho, aprenderemos a volver a dibujar las celdas recién abiertas.Anteriormente en la clase, Field
desarrollamos un método _setState
que dispara un evento change
cuando cambia el estado del modelo. Usaremos esto y en la clase FieldView
rastrearemos este evento:
Específicamente convertimos el método intermedio en una _onStateChange
devolución de llamada del evento de cambio de modelo. En el futuro, necesitaremos verificar cómo se cambió el modelo para comprender si es necesario realizarlo _render
.Para mostrar el sprite actual de una celda en un nuevo estado, debe cambiar su marco. Como cargamos el atlas como activos, podemos llamar al método setFrame
para cambiar el marco actual a uno nuevo.Para obtener el marco en una línea, utilizamos astutamente el getter _frameName
, que ahora debe implementarse. Primero, describimos todos los valores posibles que puede tomar un marco de celda.Obtuvimos una descripción de todos los estados y ya tenemos todos los métodos del modelo, gracias a los cuales se pueden obtener estos estados. Vamos a obtener una pequeña configuración al comienzo del archivo:
Las claves en este objeto serán los valores de los marcos, y los valores de estas claves son las devoluciones de llamada que devuelven un resultado booleano. En base a esta configuración, podemos desarrollar un método para obtener el marco deseado (es decir, la clave de la configuración):
Por lo tanto, mediante una simple enumeración en un bucle, revisamos todas las claves del objeto de configuración y llamamos a cada devolución de llamada a su vez. La función que nos devuelve primero true
indicará que la clave key
en la iteración actual es el marco correcto para el estado actual del modelo.Si ninguna clave es adecuada, entonces para el estado predeterminado consideraremos un campo abierto con un valor _value
, ya States
que no configuramos este estado en la configuración .Ahora podemos probar completamente el clic izquierdo en los campos del tablero y verificar cómo se abren las celdas y qué se muestra después de abrirlas.4.4 Procesamiento de clic derecho
Como en el caso de crear el controlador de clic izquierdo, primero definimos claramente la funcionalidad esperada. Al hacer clic derecho, debemos marcar la celda seleccionada con una bandera. Pero hay ciertas condiciones.- Solo se puede marcar un campo cerrado que no está marcado actualmente
- Si el campo está marcado, entonces un clic derecho nuevamente eliminará la bandera del campo
- Al configurar / eliminar una bandera, es necesario actualizar el número de banderas disponibles en el nivel y mostrar el texto con el número actual
Traduciendo estas condiciones en pseudocódigo, obtenemos las siguientes líneas de comentarios:
Ahora podemos traducir este algoritmo en llamadas a los métodos que necesitamos, incluso si aún no se han desarrollado:
Aquí también comenzamos un nuevo campo _flags
, que al comienzo del nivel del juego es igual al número de bombas en el tablero, ya que al comienzo del juego no se ha establecido una sola bandera. Este campo está obligado a actualizarse con cada clic derecho, ya que en este caso la bandera se agrega o se elimina del tablero. Agrega un Board
captador a la clase countMarked
:
Establecer y eliminar el indicador es un cambio en el estado del modelo Field
, por lo que implementamos estos métodos en la clase correspondiente de manera similar al método open
:
Permítame recordarle que _setState
activará un evento change
que se rastrea en la vista y, por lo tanto, el sprite se volverá a dibujar automáticamente esta vez cuando cambie el modelo.Al probar la funcionalidad desarrollada, seguramente encontrará que cada vez que hace clic con el botón derecho del mouse, se abre un menú contextual. Agregue el código que deshabilita este comportamiento al constructor de la escena del juego:
4.5 Objeto GameSceneView
Para mostrar la interfaz de usuario en la escena del juego, crearemos una clase GameSceneView
y la colocaremos src/scripts/views/GameSceneView.ts
.En este caso, actuaremos de una manera diferente a la creación FieldView
y no haremos de esta clase un prefabricado y un heredero GameObjects
.En este caso, necesitamos generar los siguientes elementos desde la vista de escena:- texto en el número de banderas
- botón de salida
- Mensaje de estado de finalización del juego (ganar / perder)
Hagamos que cada elemento de la IU sea un campo separado en la clase GameSceneView
.Vamos a preparar un trozo. enum Styles { Color = '#008080', Font = 'Arial' } enum Texts { Flags = 'FLAGS: ', Exit = 'EXIT', Success = 'YOU WIN!', Failure = 'YOU LOOSE' }; export class GameSceneView { private _scene: Phaser.Scene = null; private _style: {font: string, fill: string}; constructor(scene: Phaser.Scene) { this._scene = scene; this._style = {font: `28px ${Styles.Font}`, fill: Styles.Color}; this._create(); } private _create(): void { } public render() { } }
Agregue texto con el número de banderas.
Este código colocará el texto que necesitamos en una posición con sangría de 50 px desde los lados superior e izquierdo y lo configurará al estilo especificado. Además, el método setOrigin
establece el punto de pivote del texto en las coordenadas (0, 1). Esto significa que el texto se alineará con su borde izquierdo.Agrega un mensaje de estado.
Colocamos el texto de estado en el centro de la pantalla y lo alineamos con el centro de la línea llamando setOrigin
al parámetro 0.5 para la coordenada x. Además, de manera predeterminada, este texto debe estar oculto, ya que solo lo mostraremos al finalizar el juego.Cree un botón de salida, que en esencia es también un objeto de texto.
Ponemos el botón en la esquina superior derecha de la pantalla y lo usamos nuevamente setOrigin
para alinear el texto esta vez con su borde derecho. Hacemos que el botón sea interactivo y agregamos una devolución de llamada al evento de clic, que envía al jugador a la escena inicial. Por lo tanto, le damos al jugador la oportunidad de salir del nivel en cualquier momento.Queda por desarrollar un método render
para actualizar correctamente todos los elementos de la interfaz de usuario y agregar llamadas a todos los métodos creados en _create
.
Dependiendo de la propiedad pasada en el parámetro, actualizamos la IU, mostrando los cambios necesarios.Cree una representación en la escena del juego en la clase GameScene y escriba la llamada al método _render siempre que sea necesario con el significado:
5. Animaciones
¿Qué tipo de fanático de crear un juego, incluso tan simple como el nuestro, si no hay animaciones en él? Además, desde que comenzamos a estudiar el phaser, conozcamos las características más básicas de las animaciones y consideremos la funcionalidad de los gemelos. Los gemelos se implementan en el marco mismo y no se requieren bibliotecas de terceros.Agregue 2 animaciones al juego: llenando el tablero con celdas al comienzo y volteando la celda en la apertura. Comencemos con el primero de estos.5.1 Animación de relleno de tablero
Nos aseguramos de que todas las celdas del tablero vuelen en su lugar desde el borde superior izquierdo de la pantalla. Al comenzar el nivel del juego, debemos mover todas las celdas a la esquina superior izquierda de la pantalla y para que cada celda comience la animación del movimiento a sus coordenadas correspondientes.En la clase, FiledView
agregue la _create
llamada al final de los métodos _animateShow
:
B implementamos el nuevo método que necesitamos. En él, como acordamos anteriormente, es necesario realizar 2 cosas:- mueva la celda detrás de la esquina superior izquierda para que no sea visible en la pantalla
- iniciar el movimiento gemelo a las coordenadas deseadas con el retraso correcto
Dado que la esquina superior izquierda del lienzo tiene coordenadas (0, 0), si configuramos la celda con las coordenadas iguales a sus valores negativos de ancho y alto, esto colocará la celda detrás de la esquina superior izquierda y la ocultará de la pantalla. Por lo tanto, completamos nuestra primera tarea.Alcanzarás el segundo objetivo llamando al método _moveTo
.
Para crear una animación, usamos la propiedad de escena tweens
. En su método, add
pasamos el objeto de configuración con la configuración:- La propiedad
targets
aquí debe contener como valor aquellos objetos del juego a los que desea aplicar efectos de animación. En nuestro caso, este es un enlace this
al objeto actual, ya que es un prefabricado del sprite. - El segundo y tercer parámetro pasamos las coordenadas del destino.
- La propiedad
duration
es responsable de la duración de la animación, en nuestro caso, 600 ms. - Parámetros
ease
y easeParams
establecer la función de relajación. - En el campo de retraso, sustituimos el valor del segundo argumento, que se genera para cada celda individual, teniendo en cuenta su posición en el tablero. Esto se hace para que las células no salgan volando al mismo tiempo. En cambio, cada celda aparecerá con un ligero retraso en relación con la anterior.
- Finalmente,
onComplete
colocamos una devolución de llamada en la propiedad , que se llamará al final de la acción de interpolación.
Es razonable envolver al gemelo en una promesa para que en el futuro pueda acoplar maravillosamente diferentes animaciones, por lo que colocaremos una llamada a la función en la devolución de llamada que resolve
indica la ejecución exitosa de la animación.5.2 Animaciones de volteo de celda
Será genial si, cuando se abrió la celda, se reprodujo el efecto de su inversión. ¿Cómo podemos lograr esto?La apertura de una celda se realiza actualmente cambiando el marco cuando se llama al método _render
en la vista. Si verificamos el estado del modelo en este método, veremos si la celda estaba abierta. Si la celda estaba abierta, inicie la animación en lugar de mostrar instantáneamente un nuevo marco de inversión.
Para obtener el efecto deseado, utilizaremos la transformación del sprite a través de la propiedad scale
. Si escalamos el sprite a lo largo del eje x
a cero con el tiempo , eventualmente se reducirá, conectando los lados izquierdo y derecho. Y viceversa, si escala el sprite a lo largo del eje x
de cero a su ancho completo, lo estiramos a su tamaño completo. Implementamos esta lógica en el método _animateFlip
.
Por analogía con el método, _moveTo
implementamos _scaleTo
:
En este método, como parámetro, tomamos el valor de la escala, que usaremos para cambiar el tamaño del sprite en ambas direcciones y pasarlo como un segundo parámetro al objeto de configuración de animación. Todos los demás parámetros de configuración ya nos son familiares de la animación anterior.¡Ahora comenzaremos el proyecto para probar y después de la depuración consideraremos que nuestro juego está completo y la tarea de prueba completada! :)
¡Agradezco sinceramente a todos por haber llegado a este momento conmigo!Conclusión
Colegas, me alegraría mucho si el material presentado en el artículo les resulta útil y pueden utilizar estos o los enfoques descritos en sus propios proyectos. Siempre puede recurrir a mí con cualquier pregunta, tanto en este artículo como en la programación por fases o trabajar en gamedev en general. ¡Agradezco la comunicación y me alegrará hacer nuevos conocidos e intercambiar experiencias!Y tengo una pregunta para ti ahora mismo. Como estoy creando video tutoriales sobre el desarrollo de juegos, naturalmente acumulé una docena de estos pequeños juegos. Cada juego abre el marco a su manera. Por ejemplo, en este juego tocamos el tema de los gemelos, pero hay muchas otras características, como física, mapa de mosaicos, columna vertebral, etc.A este respecto, la pregunta es: ¿te gustó este artículo y, de ser así, te interesaría seguir leyendo artículos como este, pero sobre otros pequeños juegos clásicos? Si la respuesta es sí, con mucho gusto traduciré los materiales de mis videos tutoriales a formato de texto y continuaré publicando nuevos manuales con el tiempo, pero para otros juegos. Traigo la encuesta correspondiente.¡Gracias a todos por su atención! Estaré encantado de recibir comentarios y hasta pronto!