Implementación de la plantilla de estado en Unity

imagen

En el proceso de programación de entidades en el juego, surgen situaciones en las que deben actuar en diferentes condiciones de diferentes maneras, lo que sugiere el uso de estados .

Pero si decide usar la fuerza bruta, el código se convertirá rápidamente en un caos enredado con muchas declaraciones if-else anidadas.

Para una solución elegante a este problema, puede usar el patrón de diseño de estado. ¡Le dedicaremos este tutorial!

Del tutorial usted:

  • Aprenda los conceptos básicos de la plantilla de estado en Unity.
  • Aprenderá qué es una máquina de estado y cuándo usarla.
  • Aprende a usar estos conceptos para controlar el movimiento de tu personaje.

Nota : este tutorial es para usuarios avanzados; se asume que ya sabe cómo trabajar en Unity y tiene un nivel promedio de conocimiento de C #. Además, este tutorial utiliza Unity 2019.2 y C # 7.

Llegar al trabajo


Descargar materiales del proyecto . Descomprima el archivo zip y abra el proyecto de inicio en Unity.

Hay varias carpetas en el proyecto que lo ayudarán a comenzar. La carpeta Assets / RW contiene las carpetas Animaciones , Materiales , Modelos , Prefabs , Recursos , Escenas , Scripts y Sonidos , nombrados de acuerdo con los recursos que contienen.

Para completar el tutorial, trabajaremos solo con escenas y guiones .

Vaya a RW / Scenes y abra Main . En el modo Juego, verás un personaje en una capucha dentro de un castillo medieval.


Haga clic en Reproducir y observe cómo se mueve la cámara para ajustarse al marco del personaje . Por el momento, en nuestro pequeño juego no hay interacciones, trabajaremos en ellas en el tutorial.


Explora el personaje


En la jerarquía, seleccione Carácter . Echa un vistazo al inspector . Verá un componente con el mismo nombre que contiene la lógica de control de caracteres .


Abra Character.cs ubicado en RW / Scripts .

El script realiza muchas acciones, pero la mayoría de ellas no son importantes para nosotros. Por ahora, prestemos atención a los siguientes métodos.

  • Move : mueve el personaje, recibiendo valores de tipo speed flotación como la velocidad de movimiento y rotationSpeed Velocidad como la velocidad angular.
  • ResetMoveParams : este método restablece los parámetros utilizados para animar el movimiento y la velocidad angular del personaje. Se usa solo para limpiar.
  • SetAnimationBool : establece el parámetro de animación param de tipo Bool en valor.
  • CheckCollisionOverlap : recibe un tipo Vector3 y devuelve un bool que determina si hay colisionadores dentro del radio especificado desde el .
  • TriggerAnimation : TriggerAnimation el parámetro de animación del parámetro de entrada.
  • ApplyImpulse : ApplyImpulse pulso al carácter igual a la force parámetro de entrada force tipo Vector3 .

A continuación verá estos métodos. En nuestro tutorial, sus contenidos y trabajo interno no son importantes.

¿Qué son las máquinas de estado?


Una máquina de estados es un concepto en el que un contenedor almacena el estado de algo en un momento dado en el tiempo. Basado en los datos de entrada, puede proporcionar una conclusión dependiendo del estado actual, pasando este proceso a un nuevo estado. Las máquinas de estado se pueden representar como un diagrama de estado . La preparación de un diagrama de estado le permite pensar en todos los estados posibles del sistema y las transiciones entre ellos.

Máquinas de estado


Las máquinas de estado finito o FSM (máquina de estado finito) es una de las cuatro familias principales de máquinas . Los autómatas son modelos abstractos de máquinas simples. Se estudian en el marco de la teoría de los autómatas , la rama teórica de la informática.

En pocas palabras:

  • FSM consiste en una cantidad finita de condición . En un momento dado , solo uno de estos estados está activo .
  • Cada estado determina en qué estado entrará como salida en función de la secuencia recibida de información entrante .
  • El estado de salida se convierte en el nuevo estado activo. En otras palabras, hay una transición entre estados .


Para comprender mejor esto, considere el carácter de un juego de plataformas que está en el terreno. El personaje está en el estado de pie . Este será su estado activo hasta que el jugador presione el botón para que el personaje salte.

El estado Permanente identifica una pulsación de botón como una entrada importante y, como salida , cambia al estado de Salto .

Supongamos que hay un cierto número de tales estados de movimiento y un personaje solo puede estar en uno de los estados a la vez. Este es un ejemplo de FSM.

Máquinas de estado jerárquico


Considere un juego de plataformas con FSM, en el que varios estados comparten una lógica física común. Por ejemplo, puede moverse y saltar en los estados agachado y de pie . En este caso, varias variables entrantes conducen al mismo comportamiento y salida de información para dos estados diferentes.

En tal situación, sería lógico delegar el comportamiento general a algún otro estado. Afortunadamente, esto se puede lograr utilizando máquinas de estado jerárquicas .

En un FSM jerárquico, hay subestados que delegan información entrante sin procesar a sus subestados . Esto a su vez le permite reducir con gracia el tamaño y la complejidad de la FSM, manteniendo su lógica.

Plantilla de estado


En su libro Design Patterns: Elements of Reusable Object-Oriented Software, Erich Gamma, Richard Helm, Ralph Johnson y John Vlissidis ( The Gang of Four ) definieron la tarea de la plantilla del Estado de la siguiente manera:

“Debe permitir que el objeto cambie su comportamiento cuando cambia su estado interno. En este caso, parecerá que el objeto ha cambiado su clase ".

Para comprender mejor esto, considere el siguiente ejemplo:

  • Un script que recibe información entrante para la lógica del movimiento se adjunta a una entidad en el juego.
  • Esta clase almacena una variable de estado actual que simplemente se refiere a una instancia de la clase de estado .
  • La información entrante se delega a este estado actual, que la procesa y crea un comportamiento definido dentro de sí mismo. También maneja las transiciones de estado requeridas.

Por lo tanto, debido al hecho de que en diferentes momentos la variable de estado actual se refiere a diferentes estados, parecerá que la misma clase de script se comporta de manera diferente. Esta es la esencia de la plantilla de "Estado".

En nuestro proyecto, la clase de personaje antes mencionada se comportará de manera diferente dependiendo de los diferentes estados. ¡Pero necesitamos que se comporte!


En el caso general, hay tres puntos clave para cada clase de estado que permiten el comportamiento del estado en su conjunto:

  • Entrada : este es el momento en que una entidad ingresa a un estado y realiza acciones que deben realizarse solo una vez al ingresar al estado.
  • Salida : similar a la entrada: todas las operaciones de reinicio se realizan aquí, que deben realizarse solo antes de que cambie el estado.
  • Update Loop : Aquí está la lógica básica de actualización que se ejecuta en cada cuadro. Se puede dividir en varias partes, por ejemplo, un ciclo para actualizar la física y un ciclo para procesar la entrada del jugador.


Definir un estado y una máquina de estados


Vaya a RW / Scripts y abra StateMachine.cs .

La máquina de estado , como puede suponer, proporciona una abstracción para la máquina de estado. Tenga en cuenta que CurrentState ubicado correctamente dentro de esta clase. Almacenará un enlace al estado actual activo de la máquina de estado.

Ahora, para definir el concepto del estado , vayamos a RW / Scripts y abramos el script State.cs en el IDE.

El estado es una clase abstracta que usaremos como modelo del cual se derivan todas las clases de estados del proyecto. Parte del código en los materiales del proyecto ya está listo.

DisplayOnUI solo muestra el nombre del estado actual en la IU en pantalla. No es necesario que conozca su estructura interna, solo comprenda que recibe un enumerador de tipo UIManager.Alignment como parámetro de entrada, que puede ser Left o Right . La visualización del nombre del estado en la parte inferior izquierda o derecha de la pantalla depende de ello.

Además, hay dos variables protegidas, character y stateMachine . La variable de character refiere a una instancia de la clase Character , y stateMachine refiere a una instancia de la máquina de estados asociada con el estado.

Al crear una instancia de estado, el constructor enlaza character y stateMachine .

Cada una de las muchas instancias de Character en una escena puede tener su propio conjunto de estados y máquinas de estados.

Ahora agregue los siguientes métodos a State.cs y guarde el archivo:

 public virtual void Enter() { DisplayOnUI(UIManager.Alignment.Left); } public virtual void HandleInput() { } public virtual void LogicUpdate() { } public virtual void PhysicsUpdate() { } public virtual void Exit() { } 

Estos métodos virtuales definen los puntos de estado clave descritos anteriormente. Cuando la máquina de estados realiza una transición entre estados, llamamos a Exit para el estado anterior e Enter nuevo estado activo .

HandleInput , LogicUpdate y PhysicsUpdate juntos definen un ciclo de actualización . HandleInput maneja la entrada del jugador. LogicUpdate procesa lógica básica, mientras que PhyiscsUpdate procesa PhyiscsUpdate lógica y física.

Ahora abra StateMachine.cs nuevamente, agregue los siguientes métodos y guarde el archivo:

 public void Initialize(State startingState) { CurrentState = startingState; startingState.Enter(); } public void ChangeState(State newState) { CurrentState.Exit(); CurrentState = newState; newState.Enter(); } 

Initialize configura la máquina de estado estableciendo CurrentState en startingState y llamando a Enter para ello. Esto inicializa la máquina de estado, por primera vez configurando el estado activo.

ChangeState maneja las transiciones de estado . Llama a Exit para el CurrentState anterior antes de reemplazar su referencia con newState . Al final, llama a Enter para newState .

Por lo tanto, configuramos el estado y la máquina de estados .

Crear estados de movimiento


Echa un vistazo al siguiente diagrama de estado, que muestra los diferentes estados de movimiento de la esencia del jugador en el juego. En esta sección, implementamos la plantilla "Estado" para el movimiento que se muestra en la figura FSM :


Preste atención a los estados de movimiento, es decir, de pie , agachándose y saltando , así como a cómo los datos entrantes provocan transiciones entre los estados. Este es un FSM jerárquico en el que Grounded es un subestado para los subestados Ducking y Standing .

Regrese a Unity y vaya a RW / Scripts / States . Allí encontrará varios archivos C # con nombres que terminan en Estado .

Cada uno de estos archivos define una clase, cada una de las cuales se hereda de State . Por lo tanto, estas clases definen los estados que utilizaremos en el proyecto.

Ahora abra Character.cs desde la carpeta RW / Scripts .

Desplácese sobre el archivo #region Variables y agregue el siguiente código:

 public StateMachine movementSM; public StandingState standing; public DuckingState ducking; public JumpingState jumping; 

Este movementSM refiere a una máquina de estados que procesa la lógica de movimiento para la instancia de Character . También agregamos enlaces a tres estados que implementamos para cada tipo de movimiento.

Vaya a #region MonoBehaviour Callbacks en el mismo archivo. Agregue los siguientes métodos MonoBehaviour y luego guarde

 private void Start() { movementSM = new StateMachine(); standing = new StandingState(this, movementSM); ducking = new DuckingState(this, movementSM); jumping = new JumpingState(this, movementSM); movementSM.Initialize(standing); } private void Update() { movementSM.CurrentState.HandleInput(); movementSM.CurrentState.LogicUpdate(); } private void FixedUpdate() { movementSM.CurrentState.PhysicsUpdate(); } 

  • En Start código crea una instancia de la Máquina de estado y la asigna a movementSM , y también crea una instancia de varios estados de movimiento. Al crear cada uno de los estados de movimiento, pasamos referencias a la instancia de Character utilizando la this , así como la instancia de motionSM. Al final, llamamos Initialize para movementSM y pasamos Standing como estado inicial.
  • En el método de Update , llamamos a HandleInput y LogicUpdate para LogicUpdate de la máquina movementSM . Del mismo modo, en FixedUpdate llamamos a PhysicsUpdate para el CurrentState de la máquina movementSM . En esencia, esto delega tareas a un estado activo; Este es el significado de la plantilla "Estado".

Ahora necesitamos establecer el comportamiento dentro de cada uno de los estados de movimiento. Prepárate, ¡habrá mucho código!

Firmeza permanente


Regrese a RW / Scripts / States en la ventana Proyecto.

Abra Grounded.cs y observe que esta clase tiene un constructor que coincide con el constructor State . Esto es lógico porque esta clase hereda de ella. Verá lo mismo en todas las demás clases de estado .

Agregue el siguiente código:

 public override void Enter() { base.Enter(); horizontalInput = verticalInput = 0.0f; } public override void Exit() { base.Exit(); character.ResetMoveParams(); } public override void HandleInput() { base.HandleInput(); verticalInput = Input.GetAxis("Vertical"); horizontalInput = Input.GetAxis("Horizontal"); } public override void PhysicsUpdate() { base.PhysicsUpdate(); character.Move(verticalInput * speed, horizontalInput * rotationSpeed); } 

Esto es lo que pasa aquí:

  • Redefinimos uno de los métodos virtuales definidos en la clase padre. Para preservar toda la funcionalidad que puede existir en el padre, llamamos al método base con el mismo nombre de cada método anulado. Esta es una plantilla importante que continuaremos usando.
  • La siguiente línea, Enter establece horizontalInput y verticalInput sus valores predeterminados.
  • Dentro de Exit nosotros, como se mencionó anteriormente, llamamos al método ResetMoveParams para restablecerlo cuando se cambia a otro estado.
  • En el método HandleInput , las variables horizontalInput y verticalInput HandleInput valores de los ejes de entrada horizontal y vertical. Gracias a esto, el jugador puede controlar al personaje usando las teclas W , A , S y D.
  • En PhysicsUpdate realizamos una llamada Move , pasando las variables horizontalInput y verticalInput multiplicadas por las velocidades correspondientes. En la speed variable speed la velocidad de movimiento se almacena, y en velocidad de rotationSpeed , la velocidad angular.

Ahora abra Standing.cs y preste atención al hecho de que hereda de Grounded . Sucedió porque, como dijimos anteriormente, Standing es un subestado para Grounded . Hay diferentes formas de implementar esta relación, pero en este tutorial usamos la herencia.

Agregue los siguientes métodos de override y guarde el script:

 public override void Enter() { base.Enter(); speed = character.MovementSpeed; rotationSpeed = character.RotationSpeed; crouch = false; jump = false; } public override void HandleInput() { base.HandleInput(); crouch = Input.GetButtonDown("Fire3"); jump = Input.GetButtonDown("Jump"); } public override void LogicUpdate() { base.LogicUpdate(); if (crouch) { stateMachine.ChangeState(character.ducking); } else if (jump) { stateMachine.ChangeState(character.jumping); } } 

  • En Enter configuramos las variables heredadas de Grounded . Aplica la speed MovementSpeed y RotationSpeed speed RotationSpeed personaje a la speed y speed rotationSpeed . Luego se relacionan, respectivamente, con la velocidad normal de movimiento y la velocidad angular destinada a la esencia del personaje.

    Además, las variables para almacenar la entrada de crouch y jump se restablecen a falso.
  • Dentro de HandleInput , las variables crouch y jump almacenan la entrada del jugador para las sentadillas y saltos. Si en la escena Principal el jugador presiona la tecla Mayús, la posición en cuclillas se establece en verdadera. Del mismo modo, un jugador puede usar la tecla Espacio para jump .
  • En LogicUpdate verificamos las variables LogicUpdate y jump de tipo bool . Si crouch es verdadero, entonces movementSM.CurrentState cambia a character.ducking . Si jump es verdadero, el estado cambia a character.jumping .

Guarde y ensamble el proyecto, luego haga clic en Reproducir . Puede moverse por la escena con las teclas W , A , S y D. Si intenta presionar Mayús o Barra espaciadora , se producirá un comportamiento inesperado, porque los estados correspondientes aún no están implementados.


Intenta moverte debajo de los objetos de la mesa. Verás que, debido a la altura del colisionador del personaje, esto no es posible. Para que el personaje haga esto, debes agregar un comportamiento en cuclillas.

Subimos debajo de la mesa


Abra el script Ducking.cs . Tenga en cuenta que Ducking también hereda de la clase Grounded por los mismos motivos que Standing . Agregue los siguientes métodos de override y guarde el script:

 public override void Enter() { base.Enter(); character.SetAnimationBool(character.crouchParam, true); speed = character.CrouchSpeed; rotationSpeed = character.CrouchRotationSpeed; character.ColliderSize = character.CrouchColliderHeight; belowCeiling = false; } public override void Exit() { base.Exit(); character.SetAnimationBool(character.crouchParam, false); character.ColliderSize = character.NormalColliderHeight; } public override void HandleInput() { base.HandleInput(); crouchHeld = Input.GetButton("Fire3"); } public override void LogicUpdate() { base.LogicUpdate(); if (!(crouchHeld || belowCeiling)) { stateMachine.ChangeState(character.standing); } } public override void PhysicsUpdate() { base.PhysicsUpdate(); belowCeiling = character.CheckCollisionOverlap(character.transform.position + Vector3.up * character.NormalColliderHeight); } 

  • Dentro de Enter parámetro que provoca que la animación de sentadilla se active se pone en cuclillas, lo que permite la animación de sentadilla. Las propiedades character.CrouchSpeed y character.CrouchRotationSpeed tienen asignados los valores de speed y rotation , que devuelven el movimiento del personaje y la velocidad angular cuando se mueve en cuclillas .

    Siguiente character.CrouchColliderHeight CrouchColliderHeight establece el tamaño del colisionador del personaje, que devuelve la altura deseada del colisionador cuando se pone en cuclillas. Al final, belowCeiling restablece a falso.
  • Dentro de Exit el parámetro de animación en cuclillas se establece en falso. Esto deshabilita la animación en cuclillas. Luego se establece la altura normal del colisionador, devuelta por character.NormalColliderHeight .
  • Dentro de HandleInput variable crouchHeld establece el valor de entrada del reproductor. En la escena Principal , mantener Shift establece crouchHeld en verdadero.
  • Dentro de PhysicsUpdate variable belowCeiling asigna un valor al pasar un punto en formato Vector3 con la cabeza del objeto del juego del personaje al método CheckCollisionOverlap . Si hay una colisión cerca de este punto, esto significa que el personaje está bajo algún tipo de techo.
  • Internamente, LogicUpdate comprueba si crouchHeld o belowCeiling es verdadero. Si ninguno de ellos es cierto, entonces movementSM.CurrentState cambia a character.standing .

Construye el proyecto y haz clic en Reproducir . Ahora puedes moverte por la escena. Si presionas Shift , el personaje se sentará y podrás moverte en la sentadilla.

También puedes subir debajo de la plataforma. Si sueltas Shift mientras estás debajo de las plataformas, el personaje seguirá en cuclillas hasta que abandone su refugio.


¡Alzate!


Open Jumping.cs . Verá un método llamado Jump . No te preocupes por cómo funciona; es suficiente entender que se usa para que el personaje pueda saltar teniendo en cuenta la física y la animación.

Ahora agregue los métodos de override habituales y guarde el script

 public override void Enter() { base.Enter(); SoundManager.Instance.PlaySound(SoundManager.Instance.jumpSounds); grounded = false; Jump(); } public override void LogicUpdate() { base.LogicUpdate(); if (grounded) { character.TriggerAnimation(landParam); SoundManager.Instance.PlaySound(SoundManager.Instance.landing); stateMachine.ChangeState(character.standing); } } public override void PhysicsUpdate() { base.PhysicsUpdate(); grounded = character.CheckCollisionOverlap(character.transform.position); } 

  • Dentro de Enter Singleton SoundManager reproduce el sonido del salto. Luego a grounded restablece a su valor predeterminado. Al final, se llama Jump .
  • Dentro de PhysicsUpdate punto PhysicsUpdate al lado de las piernas del personaje se envía a CheckCollisionOverlap , lo que significa que cuando el personaje está en el suelo, grounded se establecerá en verdadero.
  • En LogicUpdate , si la grounded es verdadera, llamamos a TriggerAnimation para habilitar la animación de la toma de contacto, se reproduce el sonido de la toma de contacto y el movementSM.CurrentState cambia a character.standing .

Entonces, en esto hemos completado la implementación completa del desplazamiento FSM utilizando la plantilla "Estado" . Construye el proyecto y ejecútalo. Presiona la barra espaciadora para hacer que el personaje salte.


¿A dónde ir después?


Los materiales del proyecto tienen un proyecto borrador y un proyecto terminado.

A pesar de su utilidad, las máquinas de estado tienen limitaciones. Las máquinas de estado concurrente y las máquinas Pushdown Automaton pueden manejar algunas de estas limitaciones. Puedes leer sobre ellos en el libro de Robert Nystrom Game Programming Patterns .

Además, el tema se puede explorar más profundamente al examinar los árboles de comportamiento utilizados para crear entidades más complejas en el juego.

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


All Articles