Padrão Command
Padrões e Frameworks
Roberto A. Bittencourt
Jairo Calmon
Situação-Problema
• Vamos manter este exemplo simples =)
• Nosso personagem executa 3 ações:
– Ao clicar em um área, o player vai para a posição
do mouse.
– Ao apertar key1 (A), o player dá um giro de 360°
– Ao apertar key2 (S), o player muda de cor.
• Todas as ações possuem respectivos tempo de
execução.
Situação-Problema
• Exemplo em um jogo conhecido: The Sims.
– Você quer que seu Sim acorde, faça a cama, use o
banheiro, tome um banho, vá para a cozinha e
tome o café da manhã.
– Você clica em cada uma das ações, e essas ações
são enfileiradas no canto superior da tela. O Sim
passa a executá-las.
Situação-Problema
• Começaremos com uma possível implementação da nossa nave
public class Nave : MonoBehavior { ITween iTween = new ITween(); public Nave(){}
public void move(Vector3 pos){
iTween.MoveTo(gameObject, pos, 2.0f); }
public void shoot(){
iTween.RotateAdd (gameObject, new Vector3 (0, 0, 360), 2.0f); }
int flag = 0;
public void gather(){
iTween.ColorTo (gameObject, new Color (1, flag, flag), 1.0f); flag = flag == 1 ? 0 : 1;
}
public bool isBusy(){
return iTween.tweens.Count > 0; }
Situação-Problema
• O Tween é basicamente um interpolador.
• Exemplo: iTween.MoveTo(gameObject, pos, 2.0f);
– gameObject é o objeto que guardas as informações gráficas do Sprite como
posição, orientação, translação, cor etc.
– Então o tween pode pegar a posição atual do gameObject. Passamos uma
nova posição “pos”.
– E passamos também o tempo de duração da interpolação.
• Podemos acessar as interpolações em “iTween.tweens”
• Criamos um método “isBusy” que verifica se algum tween
está em execução, o que nos permite controlar a execução
das ações.
Situação-Problema
• Agora uma possível implementação da nossa aplicação
public class CommandDemo {
public Nave nave;
// Update is called once per frame
void Update () {
if (Input.GetKeyDown(KeyCode.Mouse0)){
Vector3 mp = Camera.main.ScreenToWorldPoint(Input.mousePosition);
mp = new Vector3(mp.x, mp.y, 0.0f);
nave.move(mp);
}
else if(Input.GetKeyDown(KeyCode.A)){
nave.shoot();
}
else if(Input.GetKeyDown(KeyCode.S)){
nave.gather();
}
}
}
Situação-Problema
• Alguns problemas:
– As teclas estão “hardcoded”. Isso tira a
possibilidade de customização.
– Os comandos atuais, não respeitam os comandos
anteriores em execução.
Situação-Problema
• “AHHHHH! Já sei!!! Vou usar o método isBusy para prevenir execuções!”
public class CommandDemo { public Nave nave;
// Update is called once per frame void Update () {
if(!nave.isBusy()){
if (Input.GetKeyDown(KeyCode.Mouse0)){
Vector3 mp = Camera.main.ScreenToWorldPoint(Input.mousePosition); mp = new Vector3(mp.x, mp.y, 0.0f);
nave.move(mp); } else if(Input.GetKeyDown(KeyCode.A)){ nave.shoot(); } else if(Input.GetKeyDown(KeyCode.S)){ nave.gather(); } } } }
Situação-Problema
• Bom... Parou de interromper... =)
• ... Mas perdemos comandos =(
• “Ah... Mas é claro. Temos que salvar essas
chamadas para quando a ação anterior acabar
de ser executada”.
Situação-Problema
• “Que tal se eu salvar o input?”
public class CommandDemo { public Nave nave;
List<KeyCode> codes = new List<KeyCode>(); void Update () {
if (Input.GetKeyDown(KeyCode.Mouse0)) codes.add(KeyCode.Mouse0); else if(Input.GetKeyDown(KeyCode.A)) codes.add(KeyCode.A);
else if(Input.GetKeyDown(KeyCode.S)) codes.add(KeyCode.S);
if(!nave.isBusy()){ processInput(codes.popFront()); } } void processInput(KeyCode k){ if (k == KeyCode.Mouse0){ Vector3 mp = Camera.main.ScreenToWorldPoint(Input.mousePosition); mp = new Vector3(mp.x, mp.y, 0.0f);
nave.move(mp); }
else if(k == KeyCode.A) nave.shoot(); else if(k == KeyCode.S) nave.gather(); }
Situação-Problema
• Será que funciona?
• Quase.
• Apesar dos comandos esperarem para ser
executados... A posição do mouse está errada.
• Hmm... A posição deveria ter sido armazenada no
momento da captura da entrada. Mas onde iríamos
armazená-la? Ihh... Complicou.
Que tal…
• Que tal criarmos uma interface para a chamada de
um método, e as classes implementariam poderiam
salvar os dados que quisessem! =D
Command
• O padrão Command visa justamente isso!
• Segundo GoF:
“Encapsular uma solicitação como um objeto, permitindo desta forma
parametrizar clientes com diferentes solicitações, enfileirar ou fazer o
registro (log) de solicitações e suportar operações que podem ser
desfeitas.”
“Commands é uma implementação de callbacks em POO”.
• Segundo GPP:
Definindo interface e implementação
• Criamos uma interface (provavelmente a mais comum do planeta)
• Criamos uma classe que encapsula os dados e a chamada ao método.
• Note que ShootCommand, neste exemplo, é bem específico – e geralmente
interface ICommand { void execute(); }
class ShootCommand implements ICommand { Nave nave;
public ShootCommand(Nave nave){ this.nave = nave;
}
public void execute(){ nave.shoot();
} }
Definindo interface e implementação
• O GatherCommand é bem parecido:
class GatherCommand implements ICommand { Nave nave;
public GatherCommand(Nave nave){ this.nave = nave;
}
public void execute(){ nave.gather(); }
Definindo interface e implementação
• O MoveCommand é semelhante mas armazena dados adicionais:
class MoveCommand implements ICommand { Nave nave;
Vector3 pos;
public MoveCommand(Nave nave, Vector3 pos){ this.nave = nave;
this.pos = pos; }
public void execute(){ nave.move (pos); }
Implementação Inicial (Retomando)
• Retomando a implementação inicial
public class DemoSemCommand { public Nave nave;
// Update is called once per frame void Update () {
if (Input.GetKeyDown(KeyCode.Mouse0)){
Vector3 mp = Camera.main.ScreenToWorldPoint(Input.mousePosition); mp = new Vector3(mp.x, mp.y, 0.0f);
nave.move(mp); } else if(Input.GetKeyDown(KeyCode.A)){ nave.shoot(); } else if(Input.GetKeyDown(KeyCode.S)){ nave.gather(); } } }
Implementação Inicial com Command
• Introduzindo o Command
public class DemoCommand { public Nave nave;
// Update is called once per frame void Update () {
ICommand cmd = null;
if (Input.GetKeyDown(KeyCode.Mouse0)){
Vector3 mp = Camera.main.ScreenToWorldPoint(Input.mousePosition); mp = new Vector3(mp.x, mp.y, 0.0f);
cmd = new MoveCommand(nave, mp); } else if(Input.GetKeyDown(KeyCode.A)){ cmd = new ShootCommand(nave); } else if(Input.GetKeyDown(KeyCode.S)){ cmd = new GatherCommand(nave, mp); } if(cmd) cmd.execute();
Tudo igual?
• Tudo permanece igual…
O que muda então?
• Tudo permanece igual…
• Mas… o que ganhamos com isso?
– Nós transformamos uma chamada em um objeto.
– Isso facilita realizar algumas atividades:
• Postergar a execução da chamada.
• Empilhar ou Enfileirar a execução da chamada.
• Cancelar a execução da chamada.
• Com algum cuidado: Desfazer e refazer a execução da chamada.
• …
Command sem Fila (Retomando)
• Sem fila:
public class DemoCommand { public Nave nave;
void Update () {
ICommand cmd = null;
if (Input.GetKeyDown(KeyCode.Mouse0)){
Vector3 mp = Camera.main.ScreenToWorldPoint(Input.mousePosition); mp = new Vector3(mp.x, mp.y, 0.0f);
cmd = new MoveCommand(nave, mp); } else if(Input.GetKeyDown(KeyCode.A)){ cmd = new ShootCommand(nave); } else if(Input.GetKeyDown(KeyCode.S)){ cmd = new GatherCommand(nave, mp); } if(cmd) cmd.execute(); } }
Implementação com Fila - Command
• Para implementar o empilhar é simples:
public class DemoCommand { public Nave nave;
private Queue<ICommand> commands = new Queue<ICommand>(); void Update () {
ICommand cmd = null;
if (Input.GetKeyDown(KeyCode.Mouse0)){
Vector3 mp = Camera.main.ScreenToWorldPoint(Input.mousePosition); mp = new Vector3(mp.x, mp.y, 0.0f);
cmd = new MoveCommand(nave, mp); } else if(Input.GetKeyDown(KeyCode.A)){ cmd = new ShootCommand(nave); } else if(Input.GetKeyDown(KeyCode.S)){ cmd = new GatherCommand(nave, mp); } if(cmd) commands.Enqueue(cmd);