No processo de programação de entidades no jogo, surgem situações em que elas devem agir em condições diferentes e de maneiras diferentes, o que sugere o uso de
estados .
Mas se você decidir usar força bruta, o código rapidamente se transformará em caos emaranhado com muitas instruções if-else aninhadas.
Para uma solução simples para esse problema, você pode usar o padrão de design do Estado. Vamos dedicar este tutorial para ele!
No tutorial você:
- Aprenda o básico do modelo State no Unity.
- Você aprenderá o que é uma máquina de estado e quando usá-la.
- Aprenda a usar esses conceitos para controlar o movimento do seu personagem.
Nota : este tutorial é para usuários avançados; presume-se que você já saiba trabalhar no Unity e tenha um nível médio de conhecimento de C #. Além disso, este tutorial usa o Unity 2019.2 e C # 7.
Começando a trabalhar
Faça o download dos
materiais do projeto . Descompacte o
arquivo zip e abra o projeto
inicial no Unity.
Existem várias pastas no projeto que ajudarão você a começar. A pasta
Assets / RW contém as pastas
Animations ,
Materials ,
Models ,
Prefabs ,
Resources ,
Scenes ,
Scripts e
Sounds , nomeadas de acordo com os recursos que contêm.
Para concluir o tutorial, trabalharemos apenas com
cenas e
scripts .
Vá para
RW / Scenes e abra
Main . No modo de jogo, você verá um personagem em uma capa dentro de um castelo medieval.
Clique em
Reproduzir e observe como a
câmera se move para se ajustar ao quadro do
personagem . No momento, em nosso joguinho, não há interações, trabalharemos nelas no tutorial.
Explore o personagem
Na
hierarquia, selecione
Caractere . Confira o
Inspetor . Você verá um
componente com o mesmo nome que contém a lógica de controle de
caracteres .
Abra
Character.cs localizado em
RW / Scripts .
O script executa muitas ações, mas a maioria delas não é importante para nós. Por enquanto, vamos prestar atenção aos seguintes métodos.
Move
: move o personagem, recebendo valores do tipo speed
flutuação como velocidade de movimento e rotationSpeed
velocidade como velocidade angular.ResetMoveParams
: esse método redefine os parâmetros usados para animar o movimento e a velocidade angular do caractere. É usado apenas para limpeza.SetAnimationBool
: Define o param
animação param
do tipo Bool como valor.CheckCollisionOverlap
: recebe um
tipo Vector3
e retorna um bool
que determina se existem coletores dentro do raio especificado a partir do
.TriggerAnimation
: TriggerAnimation
o param
animação do parâmetro de entrada.ApplyImpulse
: ApplyImpulse
pulso ao caractere igual à force
parâmetro de entrada force
tipo Vector3
.
Abaixo você verá esses métodos. Em nosso tutorial, seu conteúdo e trabalho interno não são importantes.
O que são máquinas de estado
Uma máquina de estado é um conceito em que um contêiner armazena o estado de algo em um determinado momento no tempo. Com base nos dados de entrada, ele pode fornecer uma conclusão dependendo do estado atual, passando esse processo para um novo estado. Máquinas de estado podem ser representadas como um
diagrama de estado . A preparação de um diagrama de estados permite refletir sobre todos os estados possíveis do sistema e as transições entre eles.
Máquinas de estado
Máquinas de estado finito ou
FSM (máquina de estado finito) é uma das quatro principais famílias de
máquinas . Os autômatos são modelos abstratos de máquinas simples. Eles são estudados no âmbito da
teoria dos autômatos - o ramo teórico da ciência da computação.
Em poucas palavras:
- O FSM consiste em uma quantidade finita de condição . A qualquer momento , apenas um desses estados está ativo .
- Cada estado determina em qual estado entrará como uma saída com base na sequência recebida das informações recebidas .
- O estado de saída se torna o novo estado ativo. Em outras palavras, há uma transição entre estados .
Para entender melhor isso, considere o caráter de um jogo de plataforma que está no terreno. O personagem está no estado
Permanente . Este será o seu
estado ativo até o jogador pressionar o botão para que o personagem salte.
O estado
Permanente identifica o pressionar de um botão como uma
entrada significativa e, como
saída , alterna para o estado
Salto .
Suponha que exista um certo número desses estados de movimento e que um personagem possa estar apenas em um dos estados de cada vez. Este é um exemplo de FSM.
Máquinas de estado hierárquico
Considere um jogo de plataformas usando FSM, no qual vários estados compartilham uma lógica física comum. Por exemplo, você pode mover e pular nos estados
Agachado e
Parado . Nesse caso, várias variáveis recebidas levam ao mesmo comportamento e saída de informações para dois estados diferentes.
Em tal situação, seria lógico delegar o comportamento geral a algum outro estado. Felizmente, isso pode ser alcançado usando máquinas de estado
hierárquico .
Em um FSM hierárquico, há
subestados delegando informações
brutas de entrada em seus
subestados . Isso, por sua vez, permite reduzir graciosamente o tamanho e a complexidade do FSM, mantendo sua lógica.
Modelo de status
Em seu livro
Design Patterns: Elements of Reusable Oriented Object Software, Erich Gamma, Richard Helm, Ralph Johnson e John Vlissidis (
The Gang of Four ) definiram a
tarefa do modelo State da seguinte maneira:
“Ele deve permitir que o objeto mude seu comportamento quando seu estado interno mudar. Nesse caso, parece que o objeto mudou de classe. ”
Para entender melhor isso, considere o seguinte exemplo:
- Um script que recebe informações de entrada para a lógica do movimento é anexado a uma entidade no jogo.
- Essa classe armazena uma variável de estado atual que simplesmente se refere a uma instância da classe de estado .
- As informações recebidas são delegadas para esse estado atual, que as processa e cria um comportamento definido dentro de si. Ele também lida com as transições de estado necessárias.
Portanto, devido ao fato de que, em momentos diferentes, a variável de
estado atual se refere a estados diferentes, parece que a mesma classe de script se comporta de maneira diferente. Essa é a essência do modelo "Status".
Em nosso projeto, a classe
Character mencionada se comportará de maneira diferente, dependendo dos diferentes estados. Mas precisamos que ele se comporte!
No caso geral, existem três pontos principais para cada classe de estado que permitem o comportamento do estado como um todo:
- Entrada : é o momento em que uma entidade entra em um estado e executa ações que precisam ser executadas apenas uma vez ao entrar no estado.
- Saída : semelhante à entrada - todas as operações de redefinição são executadas aqui, que devem ser executadas apenas antes que o estado mude.
- Loop de atualização : Aqui está a lógica básica de atualização que é executada em cada quadro. Pode ser dividido em várias partes, por exemplo, um ciclo para atualizar a física e um ciclo para processar a entrada do jogador.
Definindo um estado e uma máquina de estado
Vá para
RW / Scripts e abra
StateMachine.cs .
A Máquina de Estado , como você pode imaginar, fornece uma abstração para a máquina de estado. Observe que o
CurrentState
localizado corretamente dentro desta classe. Ele armazenará um link para o estado atual da máquina de estado ativo.
Agora, para definir o conceito de
estado , vamos para
RW / Scripts e abra o script
State.cs no IDE.
State é uma classe abstrata que usaremos como
modelo a partir do qual todas as
classes de estados do projeto são derivadas. Parte do código nos materiais do projeto já está pronta.
DisplayOnUI
exibe apenas o nome do estado atual na interface do usuário na tela. Você não precisa conhecer seu dispositivo interno, apenas entenda que ele recebe um enumerador do tipo
UIManager.Alignment
como um parâmetro de entrada, que pode ser
Left
ou
Right
. A exibição do nome do status na parte inferior esquerda ou direita da tela depende disso.
Além disso, existem duas variáveis protegidas,
character
e
stateMachine
. A variável de
character
refere-se a uma instância da classe
Character e
stateMachine
refere-se a uma instância
da máquina de estados associada ao estado.
Ao criar uma instância de estado, o construtor vincula o
character
e
stateMachine
.
Cada uma das muitas instâncias de
Character
em uma cena pode ter seu próprio conjunto de estados e máquinas de estados.
Agora adicione os seguintes métodos ao
State.cs e salve o arquivo:
public virtual void Enter() { DisplayOnUI(UIManager.Alignment.Left); } public virtual void HandleInput() { } public virtual void LogicUpdate() { } public virtual void PhysicsUpdate() { } public virtual void Exit() { }
Esses métodos virtuais definem os principais pontos de status descritos acima. Quando
a máquina de estados faz uma transição entre estados, chamamos
Exit
para o estado anterior e
Enter
novo
estado ativo .
HandleInput
,
LogicUpdate
e
PhysicsUpdate
juntos definem
um loop de atualização .
HandleInput
lida com a entrada do player.
LogicUpdate
processa a lógica básica, enquanto o
PhyiscsUpdate
processa os cálculos da lógica e da física.
Agora abra
StateMachine.cs novamente, adicione os seguintes métodos e salve o arquivo:
public void Initialize(State startingState) { CurrentState = startingState; startingState.Enter(); } public void ChangeState(State newState) { CurrentState.Exit(); CurrentState = newState; newState.Enter(); }
Initialize
configura a máquina de estado, definindo
CurrentState
como
CurrentState
e chamando
Enter
para ela. Isso inicializa a máquina de estado, definindo pela primeira vez o estado ativo.
ChangeState
lida com transições de
estado . Ele chama
Exit
para o antigo
CurrentState
antes de substituir sua referência por
newState
. No final, chama
Enter
para
newState
.
Assim, definimos o
estado e a
máquina de estado .
Criando estados de movimento
Veja o diagrama de estados a seguir, que mostra os diferentes
estados de movimento da essência
do jogador no jogo. Nesta seção, implementamos o modelo "Status" para o
movimento mostrado na figura
FSM :
Preste atenção aos estados de movimento, como
Permanente ,
Ducking e
Jumping , bem como como os dados recebidos causam transições entre os estados. Este é um FSM hierárquico no qual
Grounded é um
subestado dos
subestados Ducking e
Standing .
Retorne ao Unity e vá para
RW / Scripts / States . Lá você encontrará vários arquivos C # com nomes que terminam em
State .
Cada um desses arquivos define uma classe, cada uma das quais é herdada do
State
. Portanto, essas classes definem os estados que usaremos no projeto.
Agora abra
Character.cs na pasta
RW / Scripts .
Role acima do arquivo
#region Variables
e adicione o seguinte código:
public StateMachine movementSM; public StandingState standing; public DuckingState ducking; public JumpingState jumping;
Este
movementSM
refere-se a uma máquina de estado que processa a lógica de movimento da instância
Character
. Também adicionamos links para três estados que implementamos para cada tipo de movimento.
Vá para
#region MonoBehaviour Callbacks
no mesmo arquivo. Adicione os seguintes métodos
MonoBehaviour e salve
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(); }
- Em
Start
código cria uma instância da Máquina de Estado e a atribui a movementSM
, além de instanciar vários estados de movimento. Ao criar cada um dos estados de movimento, passamos referências à instância Character
usando a this
, bem como a instância MovementSM. No final, chamamos Initialize
para o movementSM
e passamos Standing
como o estado inicial. - No método
Update
, chamamos HandleInput
e LogicUpdate
para o CurrentState
da máquina LogicUpdate
. Da mesma forma, em FixedUpdate
chamamos PhysicsUpdate
para o CurrentState
da máquina PhysicsUpdate
. Em essência, isso delega tarefas para um estado ativo; esse é o significado do modelo "Status".
Agora precisamos definir o comportamento dentro de cada um dos estados de movimento. Prepare-se, haverá muito código!
Empresa Permanente
Retorne para
RW / Scripts / States na janela Projeto.
Abra
Grounded.cs e observe que esta classe possui um construtor que corresponde ao construtor
State
. Isso é lógico porque essa classe é herdada dela. Você verá a mesma coisa em todas as outras classes de
estado .
Adicione o seguinte 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); }
Aqui está o que acontece aqui:
- Redefinimos um dos métodos virtuais definidos na classe pai. Para preservar toda a funcionalidade que possa existir no pai, chamamos o método
base
com o mesmo nome de cada método substituído. Este é um modelo importante que continuaremos a usar. - A próxima linha,
Enter
define horizontalInput
e verticalInput
seus valores padrão. - Dentro da
Exit
como mencionado acima, chamamos o método ResetMoveParams
para redefinir ao mudar para outro estado. - No método
HandleInput
, as variáveis horizontalInput
e verticalInput
HandleInput
valores dos eixos de entrada horizontal e vertical. Graças a isso, o jogador pode controlar o personagem usando as teclas W , A , S e D. - No
PhysicsUpdate
fazemos uma chamada Move
, passando as variáveis horizontalInput
e verticalInput
multiplicadas pelas velocidades correspondentes. Na speed
variável speed
a velocidade do movimento é armazenada e, na rotationSpeed
, a velocidade angular.
Agora abra o
Standing.cs e preste atenção ao fato de ele herdar do
Grounded
. Isso aconteceu porque, como dissemos acima,
ficar em pé é um subestado da
Grounded . Existem diferentes maneiras de implementar esse relacionamento, mas neste tutorial usamos herança.
Adicione os seguintes métodos de
override
e salve o 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); } }
- Em
Enter
configuramos as variáveis herdadas do Grounded
. Aplique o MovementSpeed
e RotationSpeed
personagem para speed
e rotationSpeed
. Então eles se relacionam, respectivamente, com a velocidade normal de movimento e a velocidade angular destinada à essência do personagem.
Além disso, as variáveis para armazenar entradas de crouch
e jump
são redefinidas para false. - Dentro do
HandleInput
, as variáveis de crouch
e jump
armazenam a entrada do jogador para agachamentos e saltos. Se na cena principal o jogador pressionar a tecla Shift, o agachamento será definido como verdadeiro. Da mesma forma, um jogador pode usar a tecla Espaço para jump
. - No
LogicUpdate
verificamos as variáveis de crouch
e jump
do tipo bool
. Se crouch
for true, o movementSM.CurrentState
muda para character.ducking
. Se o jump
for verdadeiro, o estado mudará para character.jumping
.
Salve e monte o projeto e clique em
Play . Você pode se mover pela cena usando as teclas
W ,
A ,
S e
D. Se você tentar pressionar
Shift ou
Espaço , ocorrerá um comportamento inesperado, porque os estados correspondentes ainda não foram implementados.
Tente mover-se sob os objetos da tabela. Você verá que, devido à altura do colisor do personagem, isso não é possível. Para que o personagem faça isso, você precisa adicionar um comportamento de agachamento.
Subimos debaixo da mesa
Abra o script
Ducking.cs . Observe que
Ducking
também herda da classe
Grounded
pelos mesmos motivos que
Standing
. Adicione os seguintes métodos de
override
e salve o 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 causa a alternância da animação de agachamento é definido como agachado, o que ativa a animação de agachamento. As propriedades character.CrouchSpeed
e character.CrouchRotationSpeed
recebem os valores de speed
e rotation
, que retornam o movimento e a velocidade angular do personagem ao se mover em um agachamento .
Em seguida, character.CrouchColliderHeight
define o tamanho do colisor do personagem, que retorna a altura desejada do colisor ao se agachar. No final, o belowCeiling
redefinido para falso. - Dentro de
Exit
o parâmetro de animação de agachamento é definido como falso. Isso desativa a animação de agachamento. Em seguida, a altura normal do colisor é definida, retornada por character.NormalColliderHeight
. - Dentro de
HandleInput
variável crouchHeld
define o valor de entrada do player. Na cena principal , segurar Shift define crouchHeld
como true. - Dentro de
PhysicsUpdate
variável belowCeiling
recebe um valor passando um ponto no formato Vector3
com a cabeça do objeto de jogo do personagem para o método CheckCollisionOverlap
. Se houver uma colisão perto deste ponto, isso significa que o personagem está sob algum tipo de teto. - Internamente, o
LogicUpdate
verifica se crouchHeld
ou belowCeiling
é verdadeiro. Se nenhum deles for verdadeiro, o movementSM.CurrentState
alterado para character.standing
.
Crie o projeto e clique em
Play . Agora você pode se mover pela cena. Se você pressionar
Shift , o personagem se sentará e você poderá se mover no agachamento.
Você também pode subir sob a plataforma. Se você liberar
Shift enquanto estiver sob as plataformas, o personagem ainda estará agachado até que ele deixe seu abrigo.
Suba!
Abra
Jumping.cs . Você verá um método chamado
Jump
. Não se preocupe com como isso funciona; basta entender que é usado para que o personagem possa pular, levando em consideração a física e a animação.
Agora adicione os métodos de
override
comuns e salve o 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
SoundManager
reproduz o som do salto. grounded
redefinido para seu valor padrão. No final, Jump
é chamado. - Dentro do
PhysicsUpdate
ponto PhysicsUpdate
ao lado das pernas do personagem é enviado para o CheckCollisionOverlap
, o que significa que, quando o personagem estiver no chão, o grounded
será definido como verdadeiro. - No
LogicUpdate
, se o grounded
for verdadeiro, chamamos TriggerAnimation
para ativar a animação de touchdown, o som do touchdown é reproduzido e o movementSM.CurrentState
muda para character.standing
.
Portanto, concluímos a implementação completa do deslocamento do FSM usando
o modelo "State" . Crie o projeto e execute-o. Pressione
espaço para fazer o personagem pular.
Para onde ir a seguir?
Os
materiais do projeto têm um projeto preliminar e um projeto finalizado.
Apesar de sua utilidade, as máquinas de estado têm limitações. Máquinas de estado simultâneas e máquinas de autômato de empilhamento podem lidar com algumas dessas limitações. Você pode ler sobre eles no livro de Robert Nystrom
Game Programming Patterns .
Além disso, o tópico pode ser explorado mais profundamente, explorando as
árvores de comportamento usadas para criar entidades mais complexas no jogo.