Implementando o padrão de design de comando no Unity

imagem

Você já se perguntou como em jogos como Super Meat Boy a função de repetição é implementada? Uma das maneiras de implementá-lo é executar a entrada da mesma maneira que o reprodutor, o que, por sua vez, significa que a entrada precisa ser armazenada de alguma forma. Você pode usar o padrão de comando para isso e muito mais.

O modelo de comando também é útil para criar funções de desfazer e refazer em um jogo de estratégia.

Neste tutorial, implementamos o modelo de comando em C # e o usamos para guiar o caractere bot através de um labirinto tridimensional. No tutorial, você aprenderá:

  • Os princípios do padrão de comando.
  • Como implementar o padrão de comando
  • Como criar uma fila de comandos de entrada e atrasar sua execução.

Nota : supõe-se que você já esteja familiarizado com o Unity e tenha um conhecimento médio de C #. Neste tutorial, trabalharemos com o Unity 2019.1 e C # 7 .

Começando a trabalhar


Para começar, faça o download dos materiais do projeto . Descompacte o arquivo e abra o projeto Starter no Unity.

Vá para RW / Scenes e abra a cena principal . A cena consiste em um bot e um labirinto, bem como uma interface de usuário terminal que exibe instruções. O design do nível é feito na forma de uma grade, o que é útil quando movemos o bot visualmente pelo labirinto.


Se você clicar em Play , veremos que as instruções não funcionam. Isso é normal porque adicionaremos essa funcionalidade ao tutorial.

A parte mais interessante da cena é o GameObject Bot . Selecione-o na janela Hierarquia clicando nele.


No Inspetor, você pode ver que ele possui um componente Bot . Usaremos esse componente emitindo comandos de entrada.


Entendemos a lógica do bot


Vá para RW / Scripts e abra o script Bot no editor de código. Você não precisa saber o que está acontecendo no script Bot . Mas dê uma olhada em dois métodos: Move e Shoot . Novamente, você não precisa descobrir o que está acontecendo dentro desses métodos, mas precisa entender como usá-los.

Observe que o método Move recebe um parâmetro de entrada CardinalDirection . CardinalDirection é uma enumeração. Um elemento de enumeração do tipo CardinalDirection pode ser Up , Down , Right ou Left . Dependendo da CardinalDirection selecionada CardinalDirection bot se move exatamente um quadrado ao longo da grade na direção correspondente.


O método Shoot força o bot a disparar conchas que destroem as paredes amarelas , mas são inúteis contra outras paredes.


Por fim, dê uma olhada no método ResetToLastCheckpoint ; para entender o que ele está fazendo, olhe para o labirinto. Há pontos no labirinto chamado ponto de verificação . Para passar pelo labirinto, o bot precisa chegar ao ponto de controle verde .


Quando um bot pisa em um novo ponto de controle, ele se torna o último para ele. ResetToLastCheckpoint redefine a posição do bot, movendo-o para o último ponto de controle.


Embora não possamos usar esses métodos, vamos corrigi-lo em breve. Para começar, você precisa aprender sobre o padrão de design do Comando .

O que é o Command Design Pattern?


O padrão Command é um dos 23 padrões de design descritos no livro Design Patterns: Elements of Reusable Oriented Object Oriented, escrito por Gang of Four por Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides ( GoF , Gang of Four).

Os autores relatam que "o padrão Command encapsula a solicitação como um objeto, permitindo assim parametrizar outros objetos com solicitações diferentes, solicitações de fila ou log e oferecer suporte a operações reversíveis".

Uau! Como é isso?

Entendo que essa definição não é muito simples, então vamos analisá-la.

Encapsulamento significa que uma chamada de método pode ser encapsulada como um objeto.


O método encapsulado pode afetar muitos objetos, dependendo do parâmetro de entrada. Isso é chamado de parametrização de outros objetos.

O "comando" resultante pode ser salvo junto com outras equipes até que sejam executados. Esta é a fila de pedidos.


Fila de equipe

Finalmente, reversibilidade significa que as operações podem ser revertidas usando a função Desfazer.

OK, mas como isso se reflete no código?

A classe Command terá um método Execute , que recebe como parâmetro de entrada o objeto (pelo qual o comando é executado) chamado Receiver . Na verdade, o método Execute é encapsulado pela classe Command.

Muitas instâncias da classe Command podem ser passadas como objetos comuns, ou seja, elas podem ser armazenadas em estruturas de dados como uma fila, pilha, etc.

Para executar um comando, você deve chamar seu método Execute. A classe que inicia a execução é chamada Invoker .

O projeto atualmente contém uma classe vazia chamada BotCommand . Na próxima seção, implementaremos a implementação acima para permitir que o bot execute ações usando o modelo de comando.

Mover o bot


Implementação de padrão de comando


Nesta seção, implementamos o padrão de comando. Existem muitas maneiras de implementá-lo. Neste tutorial, abordaremos um deles.

Para começar, vá para RW / Scripts e abra o script BotCommand no editor. A classe BotCommand ainda BotCommand vazia, mas não por muito tempo.

Insira o seguinte código na classe:

  //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; } 

O que está acontecendo aqui?

  1. A variável commandName usada simplesmente para armazenar o nome do comando legível por humanos. Não é necessário usá-lo no modelo, mas precisaremos posteriormente no tutorial.
  2. O construtor do BotCommand recebe uma função e uma string. Isso nos ajudará a configurar o método Execute do objeto Command e seu name .
  3. O representante ExecuteCallback define o tipo de método encapsulado. O método encapsulado retornará nulo e aceitará como parâmetro de entrada um objeto do tipo Bot (componente Bot ).
  4. A propriedade Execute fará referência ao método encapsulado. Vamos usá-lo para chamar o método encapsulado.
  5. O método ToString é substituído para retornar a cadeia commandName . Isso é conveniente, por exemplo, para uso na interface do usuário.

Salve as alterações e pronto! Implementamos com sucesso o padrão de comando.

Resta usá-lo.

Team building


Abra o BotInputHandler na pasta RW / Scripts .

Aqui criaremos cinco instâncias do BotCommand . Essas instâncias encapsularão métodos para mover o GameObject Bot para cima, para baixo, para a esquerda e para a direita, bem como para fotografar.

Para implementar isso, insira o seguinte nesta classe:

  //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"); 

Em cada uma dessas instâncias, um método anônimo é passado para o construtor. Este método anônimo será encapsulado dentro do objeto de comando correspondente. Como você pode ver, a assinatura de cada um dos métodos anônimos atende aos requisitos especificados pelo delegado ExecuteCallback .

Além disso, o segundo parâmetro para o construtor é uma sequência que indica o nome do comando. Este nome será retornado pelo método ToString da instância do comando. Mais tarde vamos aplicá-lo à interface do usuário.

Nas quatro primeiras instâncias, métodos anônimos chamam o método Move no objeto bot . No entanto, seus parâmetros de entrada são diferentes.

Os MoveUp , MoveDown , MoveLeft e MoveRight passam os parâmetros Move CardinalDirection.Up , CardinalDirection.Down , CardinalDirection.Left e CardinalDirection.Right . Conforme mencionado na seção O que é padrão de design de comando , eles indicam direções diferentes para o GameObject Bot se mover.

Na quinta instância, o método anônimo chama o método Shoot para o objeto bot . Graças a isso, o bot disparará um shell durante a execução do comando.

Agora que criamos os comandos, precisamos acessá-los de alguma forma quando o usuário faz uma entrada.

Para fazer isso, BotInputHandler seguinte código no BotInputHandler , imediatamente após as instâncias 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; } 

O método HandleInput retorna uma instância do comando, dependendo da tecla pressionada pelo usuário. Salve suas alterações antes de prosseguir.

Aplicando comandos


Ótimo, agora é hora de usar as equipes que criamos. Vá para RW / Scripts novamente e abra o script SceneManager no editor. Nesta classe, você notará um link para uma variável uiManager do tipo UIManager .

A classe UIManager fornece métodos auxiliares úteis para a interface do usuário do terminal que usamos nesta cena. Se o método do UIManager for usado, o tutorial explicará o que ele faz, mas, em geral, para nossos propósitos, não é necessário conhecer sua estrutura interna.

Além disso, a variável bot refere-se ao componente bot anexado ao GameObject Bot .

Agora adicione o seguinte código à classe SceneManager , substituindo-o pelo comentário //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; } 

Uau, quanto código! Mas não se preocupe; finalmente estamos prontos para o primeiro lançamento real do projeto na janela do jogo.

Vou explicar o código mais tarde. Lembre-se de salvar as alterações.

Executando o jogo para testar o modelo de comando


Então agora é a hora de construir; Clique em Play no editor do Unity.

Você deve poder inserir comandos de movimentação usando as teclas WASD . Para inserir o comando de disparo, pressione a tecla F. Para executar comandos, pressione Enter .

Nota : até que o processo de execução seja concluído, não é possível inserir novos comandos.



Observe que as linhas são adicionadas à interface do usuário do terminal. As equipes na interface do usuário são indicadas por seus nomes. Isso é possível graças à variável commandName .

Observe também como a interface do usuário rola antes da execução e como as linhas são excluídas durante a execução.

Estudamos as equipes mais de perto


É hora de aprender o código que adicionamos na seção "Aplicando comandos":

  1. A lista botCommands armazena links para instâncias do BotCommand . Lembre-se de que, para economizar memória, podemos criar apenas cinco instâncias de comandos, mas pode haver várias referências a um comando. Além disso, a variável executeCoroutine refere-se a ExecuteCommandsRoutine , que controla a execução do comando.
  2. Update verifica se o usuário pressionou a tecla Enter; ExecuteCommands caso, chama ExecuteCommands , caso contrário, CheckForBotCommands é CheckForBotCommands .
  3. CheckForBotCommands usa o método estático HandleInput do BotInputHandler para verificar se o usuário concluiu a entrada e, em caso afirmativo, o comando é retornado . O comando retornado é passado para AddToCommands . No entanto, se os comandos forem executados, ou seja, se executeRoutine não executeRoutine nulo, ele retornará sem passar nada para AddToCommands . Ou seja, o usuário precisa esperar até a conclusão.
  4. AddToCommands adiciona um novo link à instância retornada do comando em botCommands .
  5. O método InsertNewText classe InsertNewText adiciona uma nova linha de texto à interface do usuário do terminal. Uma sequência de texto é uma sequência passada como um parâmetro de entrada. Nesse caso, passamos commandName para commandName .
  6. O método ExecuteCommandsRoutine inicia ExecuteCommandsRoutine .
  7. ResetScrollToTop no UIManager rola a interface do usuário do terminal para cima. Isso é feito imediatamente antes do início da execução.
  8. ExecuteCommandsRoutine contém um loop for que itera sobre os comandos dentro da lista botCommands e os executa um por um, passando o objeto bot para o método retornado pela propriedade Execute . Após cada execução, uma pausa é adicionada em segundos de CommandPauseTime .
  9. O método RemoveFirstTextLine do UIManager exclui a primeira linha de texto na interface do usuário do terminal, se existir. Ou seja, quando um comando é executado, seu nome é removido da interface do usuário.
  10. Depois que todos os comandos são botCommands é limpo e o bot é redefinido para o último ponto de interrupção usando ResetToLastCheckpoint . No final, executeRoutine null e o usuário pode continuar a digitar comandos.

Implementando os recursos Desfazer e Refazer


Execute a cena novamente e tente chegar ao ponto de controle verde.

Você notará que, embora não possamos cancelar o comando digitado. Isso significa que, se você cometer um erro, não poderá voltar antes de concluir todos os comandos inseridos. Você pode corrigir isso adicionando os recursos Desfazer e Refazer .

Volte ao SceneManager.cs e adicione a seguinte declaração de variável imediatamente após a declaração List para botCommands :

  private Stack<BotCommand> undoStack = new Stack<BotCommand>(); 

A variável undoStack é uma pilha (da família Coleções) que armazenará todas as referências a comandos que podem ser desfeitos.

Agora, adicionamos dois métodos UndoCommandEntry e RedoCommandEntry que executarão Desfazer e Refazer. Na classe SceneManager , SceneManager seguinte código após 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); } 

Vamos analisar o código:

  1. Se os comandos forem executados ou a lista botCommands vazia, o método UndoCommandEntry nada. Caso contrário, ele grava um link para o último comando digitado na pilha undoStack . Isso também remove o link para o comando da lista botCommands .
  2. O método RemoveLastTextLine do UIManager remove a última linha de texto da interface do usuário do terminal para que ela corresponda ao conteúdo de botCommands .
  3. Se a pilha undoStack vazia, o RedoCommandEntry não RedoCommandEntry nada. Caso contrário, ele extrai o último comando da parte superior do undoStack e o adiciona de volta à lista AddToCommands usando AddToCommands .

Agora vamos adicionar a entrada do teclado para usar essas funções. Dentro da classe SceneManager substitua o corpo Update método Update pelo seguinte 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. Quando você pressiona a tecla U , o método UndoCommandEntry é UndoCommandEntry .
  2. Quando você pressiona a tecla R , o método RedoCommandEntry é RedoCommandEntry .

Manuseio de Caixas de Borda


Ótimo, estamos quase terminando! Mas primeiro, precisamos fazer o seguinte:

  1. Ao inserir um novo comando, a pilha undoStack deve ser limpa.
  2. Antes de executar comandos, a pilha undoStack deve ser limpa.

Para implementar isso, primeiro precisamos adicionar um novo método ao SceneManager . Insira o seguinte método após CheckForBotCommands :

  private void AddNewCommand(BotCommand botCommand) { undoStack.Clear(); AddToCommands(botCommand); } 

Este método limpa undoStack e chama o método AddToCommands .

Agora substitua a chamada para AddToCommands dentro de CheckForBotCommands pelo seguinte código:

  AddNewCommand(botCommand); 

Em seguida, insira a seguinte linha após a if no método ExecuteCommands para limpar antes de executar os comandos undoStack :

  undoStack.Clear(); 

E finalmente terminamos!

Salve seu trabalho. Crie o projeto e clique no editor Play . Digite os comandos como antes. Pressione U para cancelar os comandos. Pressione R para repetir os comandos cancelados.


Tente chegar ao ponto de verificação verde.

Para onde ir a seguir?


Para saber mais sobre os padrões de design usados ​​na programação de jogos, recomendo que você estude os Padrões de programação de jogos de Robert Nystrom.

Para saber mais sobre técnicas avançadas de C #, faça o curso C # Collections, Lambdas e LINQ .

Tarefa


Como tarefa, tente chegar ao ponto de controle verde no final do labirinto. Eu escondi uma das soluções embaixo do spoiler.

Solução
  • moveUp × 2
  • moveRight × 3
  • moveUp × 2
  • moveLeft
  • atirar
  • moveLeft × 2
  • moveUp × 2
  • moveLeft × 2
  • moveDown × 5
  • moveLeft
  • atirar
  • moveLeft
  • moveUp × 3
  • atirar × 2
  • moveUp × 5
  • moveRight × 3

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


All Articles