Implementando o modelo de status no Unity

imagem

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.

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


All Articles