Implementación del patrón de diseño de comandos en Unity

imagen

¿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 equipo

Finalmente, 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:

  //1 private readonly string commandName; //2 public BotCommand(ExecuteCallback executeMethod, string name) { Execute = executeMethod; commandName = name; } //3 public delegate void ExecuteCallback(Bot bot); //4 public ExecuteCallback Execute { get; private set; } //5 public override string ToString() { return commandName; } 

¿Qué está pasando aquí?

  1. 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.
  2. 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 .
  3. 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 ).
  4. La propiedad Execute se referirá al método encapsulado. Lo usaremos para llamar al método encapsulado.
  5. 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:

  //1 private static readonly BotCommand MoveUp = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Up); }, "moveUp"); //2 private static readonly BotCommand MoveDown = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Down); }, "moveDown"); //3 private static readonly BotCommand MoveLeft = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Left); }, "moveLeft"); //4 private static readonly BotCommand MoveRight = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Right); }, "moveRight"); //5 private static readonly BotCommand Shoot = new BotCommand(delegate (Bot bot) { bot.Shoot(); }, "shoot"); 

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 :

  //1 private List<BotCommand> botCommands = new List<BotCommand>(); private Coroutine executeRoutine; //2 private void Update() { if (Input.GetKeyDown(KeyCode.Return)) { ExecuteCommands(); } else { CheckForBotCommands(); } } //3 private void CheckForBotCommands() { var botCommand = BotInputHandler.HandleInput(); if (botCommand != null && executeRoutine == null) { AddToCommands(botCommand); } } //4 private void AddToCommands(BotCommand botCommand) { botCommands.Add(botCommand); //5 uiManager.InsertNewText(botCommand.ToString()); } //6 private void ExecuteCommands() { if (executeRoutine != null) { return; } executeRoutine = StartCoroutine(ExecuteCommandsRoutine()); } private IEnumerator ExecuteCommandsRoutine() { Debug.Log("Executing..."); //7 uiManager.ResetScrollToTop(); //8 for (int i = 0, count = botCommands.Count; i < count; i++) { var command = botCommands[i]; command.Execute(bot); //9 uiManager.RemoveFirstTextLine(); yield return new WaitForSeconds(CommandPauseTime); } //10 botCommands.Clear(); bot.ResetToLastCheckpoint(); executeRoutine = null; } 

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":

  1. 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.
  2. Update comprueba si el usuario ha presionado la tecla Intro; si es así, llama a ExecuteCommands ; de lo contrario, se llama a ExecuteCommands .
  3. 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.
  4. AddToCommands agrega un nuevo enlace a la instancia devuelta del comando en botCommands .
  5. 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.
  6. El método ExecuteCommandsRoutine inicia ExecuteCommandsRoutine .
  7. ResetScrollToTop desde UIManager desplaza la IU del terminal hacia arriba. Esto se hace justo antes del inicio de la ejecución.
  8. 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.
  9. 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.
  10. 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() { //1 if (executeRoutine != null || botCommands.Count == 0) { return; } undoStack.Push(botCommands[botCommands.Count - 1]); botCommands.RemoveAt(botCommands.Count - 1); //2 uiManager.RemoveLastTextLine(); } private void RedoCommandEntry() { //3 if (undoStack.Count == 0) { return; } var botCommand = undoStack.Pop(); AddToCommands(botCommand); } 

Analicemos el código:

  1. 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 .
  2. 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 .
  3. 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)) //1 { UndoCommandEntry(); } else if (Input.GetKeyDown(KeyCode.R)) //2 { RedoCommandEntry(); } else { CheckForBotCommands(); } 

  1. Cuando presiona la tecla U , se UndoCommandEntry método UndoCommandEntry .
  2. 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:

  1. Al ingresar un nuevo comando, la pila undoStack debe undoStack .
  2. 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

Source: https://habr.com/ru/post/463953/


All Articles