¿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