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.