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 equipeFinalmente,
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:
O que está acontecendo aqui?
- 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. - 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
. - 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 ). - A propriedade
Execute
fará referência ao método encapsulado. Vamos usá-lo para chamar o método encapsulado. - 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:
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
:
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":
- 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. Update
verifica se o usuário pressionou a tecla Enter; ExecuteCommands
caso, chama ExecuteCommands
, caso contrário, CheckForBotCommands
é CheckForBotCommands
.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.AddToCommands
adiciona um novo link à instância retornada do comando em botCommands
.- 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
. - O método
ExecuteCommandsRoutine
inicia ExecuteCommandsRoutine
. ResetScrollToTop
no UIManager
rola a interface do usuário do terminal para cima. Isso é feito imediatamente antes do início da execução.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
.- 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. - 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() {
Vamos analisar o código:
- 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
. - 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
. - 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))
- Quando você pressiona a tecla U , o método
UndoCommandEntry
é UndoCommandEntry
. - 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:
- Ao inserir um novo comando, a pilha
undoStack
deve ser limpa. - 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