¿Alguna vez te has preguntado cómo se implementa la función de reproducción en juegos como
Super Meat Boy ? Una forma de implementarlo es llevar a cabo la entrada de la misma manera que lo hizo el jugador, lo que, a su vez, significa que la entrada debe almacenarse de alguna manera. Puede usar
el patrón de Comando para esto y mucho más.
La plantilla de Comando también es útil para crear funciones Deshacer y Rehacer en un juego de estrategia.
En este tutorial, implementamos la plantilla de Comando en C # y la usamos para guiar al personaje bot a través de un laberinto tridimensional. Del tutorial aprenderás:
- Los fundamentos del patrón de comando.
- Cómo implementar el patrón de comando
- Cómo crear una cola de comandos de entrada y retrasar su ejecución.
Nota : se supone que ya está familiarizado con Unity y tiene un conocimiento promedio de C #. En este tutorial trabajaremos con Unity 2019.1 y C # 7 .
Llegar al trabajo
Para comenzar, descargue los
materiales del proyecto . Descomprima el archivo y abra el proyecto
Starter en Unity.
Vaya a
RW / Scenes y abra la escena
Principal . La escena consta de un bot y un laberinto, así como una interfaz de usuario de terminal que muestra instrucciones. El diseño del nivel se realiza en forma de cuadrícula, lo cual es útil cuando movemos visualmente el bot a través del laberinto.
Si hace clic en
Reproducir , veremos que las instrucciones no funcionan. Esto es normal porque agregaremos esta funcionalidad al tutorial.
La parte más interesante de la escena es el GameObject
Bot . Selecciónelo en la ventana Jerarquía haciendo clic en él.
En el Inspector, puede ver que tiene un componente
Bot . Utilizaremos este componente emitiendo comandos de entrada.
Entendemos la lógica del bot.
Vaya a
RW / Scripts y abra el script
Bot en el editor de código. No necesita saber qué está sucediendo en el script de
Bot . Pero eche un vistazo a dos métodos:
Move
y
Shoot
. Una vez más, no tiene que descubrir qué está sucediendo dentro de estos métodos, pero debe comprender cómo usarlos.
Observe que el método
Move
recibe un parámetro de entrada
CardinalDirection
.
CardinalDirection
es una enumeración. Un elemento de enumeración de tipo
CardinalDirection
puede ser
Up
,
Down
,
Right
o
Left
. Dependiendo de
CardinalDirection
seleccionada
CardinalDirection
bot se mueve exactamente un cuadrado a lo largo de la cuadrícula en la dirección correspondiente.
El método
Shoot
obliga al bot a disparar proyectiles que destruyen las
paredes amarillas , pero son inútiles contra otras paredes.
Finalmente, eche un vistazo al método
ResetToLastCheckpoint
; para entender lo que está haciendo, mira el laberinto. Hay puntos en el laberinto llamados
puntos de control . Para pasar el laberinto, el bot necesita llegar al punto de control
verde .
Cuando un bot pisa un nuevo punto de control, se convierte en el
último para él.
ResetToLastCheckpoint
restablece la posición del bot, moviéndolo al último punto de control.
Si bien no podemos usar estos métodos, lo solucionaremos pronto. Para comenzar, debe aprender sobre el patrón de diseño de
Comando .
¿Qué es el patrón de diseño de comandos?
El patrón Command es uno de los 23 patrones de diseño descritos en el libro
Design Patterns: Elements of Reusable Object-Oriented Software escrito por The Gang of Four por Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides (
GoF , Gang of Four).
Los autores informan que "el patrón de Comando encapsula la solicitud como un objeto, lo que nos permite parametrizar otros objetos con diferentes solicitudes, solicitudes de cola o registro, y admitir operaciones reversibles".
Wow! Como es eso
Entiendo que esta definición no es muy simple, así que analicémosla.
Encapsulación significa que una llamada al método se puede encapsular como un objeto.
El método encapsulado puede afectar muchos objetos dependiendo del parámetro de entrada. Esto se llama
parametrización de otros objetos.
El "comando" resultante se puede guardar junto con otros equipos hasta que se ejecuten. Esta es la
cola de solicitudes.
Cola de equipoFinalmente, la
reversibilidad significa que las operaciones se pueden revertir utilizando la función Deshacer.
OK, pero ¿cómo se refleja esto en el código?
La clase
Command tendrá un método
Execute , que recibe como parámetro de entrada el objeto (por el cual se ejecuta el comando) llamado
Receiver . Es decir, de hecho, el método Execute está
encapsulado por la clase Command.
Muchas instancias de la clase Command se pueden pasar como objetos ordinarios, es decir, se pueden almacenar en estructuras de datos como una cola, una pila, etc.
Para ejecutar un comando, debe llamar a su método Execute. La clase que comienza la ejecución se llama
Invoker .
El proyecto actualmente contiene una clase vacía llamada
BotCommand
. En la siguiente sección, implementaremos la implementación de lo anterior para permitir que el bot realice acciones utilizando la plantilla de Comando.
Mover el bot
Implementación del patrón de comando
En esta sección, implementamos el patrón de Comando. Hay muchas formas de implementarlo. En este tutorial cubriremos uno de ellos.
Para comenzar, vaya a
RW / Scripts y abra el script
BotCommand en el editor. La clase
BotCommand
todavía
BotCommand
vacía, pero no por mucho tiempo.
Inserte el siguiente código en la clase:
¿Qué está pasando aquí?
- La variable
commandName
usa simplemente para almacenar el nombre del comando legible por humanos. No es necesario usarlo en la plantilla, pero lo necesitaremos más adelante en el tutorial. - El constructor de
BotCommand
recibe una función y una cadena. Esto nos ayudará a configurar el método Execute
del objeto Command y su name
. - El delegado
ExecuteCallback
define el tipo de método encapsulado. El método encapsulado devolverá nulo y aceptará como parámetro de entrada un objeto de tipo Bot
(componente Bot ). - La propiedad
Execute
se referirá al método encapsulado. Lo usaremos para llamar al método encapsulado. - El método
ToString
se reemplaza para devolver la cadena commandName
. Esto es conveniente, por ejemplo, para usar en la interfaz de usuario.
Guarde los cambios y listo. Hemos implementado con éxito el patrón de comando.
Queda por usarlo.
Trabajo en equipo
Abra
BotInputHandler en la carpeta
RW / Scripts .
Aquí crearemos cinco instancias de
BotCommand
. Estas instancias encapsularán métodos para mover el GameObject Bot hacia arriba, abajo, izquierda y derecha, así como para disparar.
Para implementar esto, inserte lo siguiente en esta clase:
En cada una de estas instancias,
se pasa
un método anónimo al constructor. Este método anónimo se encapsulará dentro del objeto de comando correspondiente. Como puede ver, la firma de cada uno de los métodos anónimos cumple con los requisitos especificados por el delegado
ExecuteCallback
.
Además, el segundo parámetro para el constructor es una cadena que indica el nombre del comando. Este nombre será devuelto por el método
ToString
de la instancia de comando. Más tarde lo aplicaremos para la interfaz de usuario.
En las primeras cuatro instancias, los métodos anónimos llaman al método
Move
en el objeto
bot
. Sin embargo, sus parámetros de entrada son diferentes.
Los
MoveUp
,
MoveDown
,
MoveLeft
y
MoveRight
pasan los parámetros de
Move
CardinalDirection.Up
,
CardinalDirection.Down
,
CardinalDirection.Left
y
CardinalDirection.Right
. Como se menciona en la sección
Qué es el patrón de diseño de comandos , indican diferentes direcciones para que se mueva el GameObject Bot.
En la quinta instancia, el método anónimo llama al método
Shoot
para el objeto
bot
. Gracias a esto, el bot disparará un shell durante la ejecución del comando.
Ahora que hemos creado los comandos, necesitamos acceder de alguna manera cuando el usuario realiza una entrada.
Para hacer esto,
BotInputHandler
siguiente código en
BotInputHandler
, inmediatamente después de las instancias de comando:
public static BotCommand HandleInput() { if (Input.GetKeyDown(KeyCode.W)) { return MoveUp; } else if (Input.GetKeyDown(KeyCode.S)) { return MoveDown; } else if (Input.GetKeyDown(KeyCode.D)) { return MoveRight; } else if (Input.GetKeyDown(KeyCode.A)) { return MoveLeft; } else if (Input.GetKeyDown(KeyCode.F)) { return Shoot; } return null; }
El método
HandleInput
devuelve una instancia del comando dependiendo de la tecla presionada por el usuario. Guarde sus cambios antes de continuar.
Aplicando comandos
Genial, ahora es el momento de usar los equipos que creamos. Vaya a
RW / Scripts nuevamente y abra el script
SceneManager en el editor. En esta clase, notará un enlace a una variable
uiManager
de tipo
UIManager
.
La clase
UIManager
proporciona métodos de ayuda útiles para la
interfaz de usuario del
terminal que usamos en esta escena. Si se utiliza el método de
UIManager
, el tutorial explicará lo que hace, pero en general para nuestros propósitos no es necesario conocer su estructura interna.
Además, la variable
bot
refiere al componente bot conectado al GameObject
Bot .
Ahora agregue el siguiente código a la clase
SceneManager
, reemplazándolo con el comentario
//1
:
Wow, cuánto código! Pero no te preocupes; finalmente estamos listos para el primer lanzamiento real del proyecto en la ventana del juego.
Explicaré el código más tarde. Recuerde guardar los cambios.
Ejecutar el juego para probar la plantilla de Comando
Así que ahora es el momento de construir; Haga clic en
Reproducir en el editor de Unity.
Debería poder ingresar comandos de movimiento con las
teclas WASD . Para ingresar el comando de disparo, presione la tecla
F. Para ejecutar comandos, presione
Entrar .
Nota : hasta que se complete el proceso de ejecución, no es posible ingresar nuevos comandos.
Observe que las líneas se agregan a la interfaz de usuario del terminal. Los equipos en la interfaz de usuario se indican con sus nombres. Esto es posible gracias a la variable
commandName
.
Además, observe cómo se desplaza la IU antes de la ejecución y cómo se eliminan las líneas durante la ejecución.
Estudiamos los equipos más de cerca.
Es hora de aprender el código que agregamos en la sección "Aplicación de comandos":
- La lista
botCommands
almacena enlaces a instancias de BotCommand
. Recuerde que para ahorrar memoria, solo podemos crear cinco instancias de comandos, pero puede haber varias referencias a un comando. Además, la variable executeCoroutine
refiere a ExecuteCommandsRoutine
, que controla la ejecución del comando. Update
comprueba si el usuario ha presionado la tecla Intro; si es así, llama a ExecuteCommands
; de lo contrario, se llama a ExecuteCommands
.CheckForBotCommands
utiliza el método estático HandleInput
del BotInputHandler
para verificar si el usuario ha completado la entrada y, de ser así, se devuelve el comando. El comando devuelto se pasa a AddToCommands
. Sin embargo, si los comandos se ejecutan, es decir si executeRoutine
no executeRoutine
nulo, regresará sin pasar nada a AddToCommands
. Es decir, el usuario debe esperar hasta su finalización.AddToCommands
agrega un nuevo enlace a la instancia devuelta del comando en botCommands
.- El método
InsertNewText
de la clase InsertNewText
agrega una nueva línea de texto a la interfaz de usuario del terminal. Una cadena de texto es una cadena que se pasa como parámetro de entrada. En este caso, le pasamos commandName. - El método
ExecuteCommandsRoutine
inicia ExecuteCommandsRoutine
. ResetScrollToTop
desde UIManager
desplaza la IU del terminal hacia arriba. Esto se hace justo antes del inicio de la ejecución.ExecuteCommandsRoutine
contiene un bucle for
que itera sobre los comandos dentro de la lista botCommands
y los ejecuta uno por uno, pasando el objeto bot
al método devuelto por la propiedad Execute
. Después de cada ejecución, se agrega una pausa en CommandPauseTime
segundos.- El método
RemoveFirstTextLine
de UIManager
elimina la primera línea de texto en la interfaz de usuario del terminal, si existe. Es decir, cuando se ejecuta un comando, su nombre se elimina de la interfaz de usuario. - Después de completar todos los comandos
botCommands
se borra y el bot se restablece al último punto de interrupción usando ResetToLastCheckpoint
. Al final, executeRoutine
null
y el usuario puede continuar ingresando comandos.
Implementación de las funciones Deshacer y Rehacer
Ejecute la escena nuevamente e intente llegar al punto de control verde.
Notará que si bien no podemos cancelar el comando ingresado. Esto significa que si comete un error, no puede regresar hasta completar todos los comandos que ingresa. Puede solucionar esto agregando las funciones
Deshacer y
Rehacer .
Vuelva a
SceneManager.cs y agregue la siguiente declaración de variable inmediatamente después de la Declaración de
lista para
botCommands
:
private Stack<BotCommand> undoStack = new Stack<BotCommand>();
La variable
undoStack
es una
pila (de la familia Colecciones) que almacenará todas las referencias a comandos que se pueden deshacer.
Ahora agregamos dos métodos
UndoCommandEntry
y
RedoCommandEntry
que ejecutarán Deshacer y Rehacer. En la clase
SceneManager
,
SceneManager
siguiente código después de
ExecuteCommandsRoutine
:
private void UndoCommandEntry() {
Analicemos el código:
- Si se ejecutan comandos o la lista
botCommands
vacía, el método UndoCommandEntry
nada. De lo contrario, escribe un enlace al último comando ingresado en la pila undoStack
. Esto también elimina el enlace al comando de la lista de botCommands
. - El método
RemoveLastTextLine
de UIManager
elimina la última línea de texto de la interfaz de usuario del terminal para que coincida con el contenido de botCommands
. - Si la pila
undoStack
vacía, entonces RedoCommandEntry
no hace nada. De lo contrario, extrae el último comando de la parte superior de undoStack
y lo agrega de nuevo a la lista de AddToCommands
usando AddToCommands
.
Ahora agregaremos la entrada del teclado para usar estas funciones. Dentro de la clase
SceneManager
reemplace el cuerpo del método
Update
con el siguiente código:
if (Input.GetKeyDown(KeyCode.Return)) { ExecuteCommands(); } else if (Input.GetKeyDown(KeyCode.U))
- Cuando presiona la tecla U , se
UndoCommandEntry
método UndoCommandEntry
. - Cuando presiona la tecla R , se
RedoCommandEntry
método RedoCommandEntry
.
Manejo de cajas de borde
¡Genial, ya casi hemos terminado! Pero primero, debemos hacer lo siguiente:
- Al ingresar un nuevo comando, la pila
undoStack
debe undoStack
. - Antes de ejecutar comandos, la pila
undoStack
debe undoStack
.
Para implementar esto, primero debemos agregar un nuevo método a
SceneManager
. Inserte el siguiente método después de
CheckForBotCommands
:
private void AddNewCommand(BotCommand botCommand) { undoStack.Clear(); AddToCommands(botCommand); }
Este método borra
undoStack
y luego llama al método
AddToCommands
.
Ahora reemplace la llamada a
AddToCommands
dentro de
CheckForBotCommands
con el siguiente código:
AddNewCommand(botCommand);
Luego inserte la siguiente línea después de la
if
dentro del método
ExecuteCommands
para borrar antes de ejecutar los comandos
undoStack
:
undoStack.Clear();
¡Y finalmente hemos terminado!
Guarda tu trabajo. Construye el proyecto y haz clic en el editor
Play . Ingrese los comandos como antes. Presione
U para cancelar los comandos. Presione
R para repetir los comandos cancelados.
Intenta llegar al punto de control verde.
¿A dónde ir después?
Para aprender más sobre los patrones de diseño utilizados en la programación de juegos, te recomiendo que estudies los
Patrones de programación de juegos de Robert Nystrom.
Para obtener más información sobre las técnicas avanzadas de C #, tome el curso de
Colecciones de C #, Lambdas y el curso
LINQ .
Tarea
Como tarea, intenta llegar al punto de control verde al final del laberinto. Escondí una de las soluciones debajo del spoiler.
Solución- moveUp × 2
- moveRight × 3
- moveUp × 2
- moverse a la izquierda
- disparar
- moveLeft × 2
- moveUp × 2
- moveLeft × 2
- moveDown × 5
- moverse a la izquierda
- disparar
- moverse a la izquierda
- moveUp × 3
- disparar × 2
- moveUp × 5
- moveRight × 3