Padrão State
Padrões e Frameworks
Roberto A. Bittencourt
Jairo Calmon
Situação
• Suponha que estamos trabalhando com um pequeno
jogo plataforma.
• Nosso trabalho é implementar o herói
• O personagem deve responder à entrada de usuário
• Aperte B e ele deve pular.
Situação
• De maneira simples:
• Percebeu algum bug?
void Heroine::handleInput(Input input){ if (input == PRESS_B){
yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP); }
Situação
• Nada impede que ele pule no ar
• O jogador pode ficar apertando B e ele vai ficar
flutuando para sempre.
• Um forma simples de corrigir é adicionar uma flag
isJumping no herói para verificar se ele está pulando
void Heroine::handleInput(Input input){ if (input == PRESS_B){ if (!isJumping_){ isJumping_ = true; yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP); } } }
Situação
• Agora queremos que o herói se abaixe.
• Quando o jogador apertar “direcional para baixo” o
herói se abaixa enquanto no chão
• Ao soltar o botão o player volta a ficar de pé
void Heroine::handleInput(Input input){ if (input == PRESS_B){
// Jump if not jumping... }
else if (input == PRESS_DOWN){ if (!isJumping_){
setGraphics(IMAGE_DUCK); }
}
else if (input == RELEASE_DOWN){ setGraphics(IMAGE_STAND);
Situação
• Com esse código
o jogador pode
– Apertar para baixo para abaixar
– Apertar B para pular da posição abaixada
– Soltar para baixo enquanto no ar
• O herói vai mudar para a animação de “ficar em pé”
no meio do pulo
Situação
• Hora de adicionar uma outra flag:
void Heroine::handleInput(Input input){ if (input == PRESS_B){
if (!isJumping_ && !isDucking_){ // Jump...
} }
else if (input == PRESS_DOWN){ if (!isJumping_){
isDucking_ = true;
setGraphics(IMAGE_DUCK); }
}
else if (input == RELEASE_DOWN){ if (isDucking_){
isDucking_ = false;
setGraphics(IMAGE_STAND); }
Situação
• Seria legal se o herói fizesse um “dive attack” quando
o jogador aperta para baixo no meio de um pulo!
Situação
void Heroine::handleInput(Input input){ if (input == PRESS_B){
if (!isJumping_ && !isDucking_){ // Jump...
} }
else if (input == PRESS_DOWN){ if (!isJumping_){ isDucking_ = true; setGraphics(IMAGE_DUCK); } else { isJumping_ = false; setGraphics(IMAGE_DIVE); } }
else if (input == RELEASE_DOWN){ if (isDucking_){ // Stand... } }
Hora de achar
bug de novo
=/
Situação
• Embora não dê para pular enquanto pulando…
• … Dá pra pular enquanto “mergulhando”
• Vamos adicionar outro campo/flag?
• Ou que tal… Admitir que tem algo claramente
estranho em nossa abordagem?
• A gente nem colocou o “caminhar” ainda…
• Mas do jeito que as coisas tão indo…
FSM – Máquinas de Estados Finitas
• Meio frustrado, você tira tudo da mesa, deixa só uma
caneta e papel e começa a desenhar um diagrama…
FSM – Máquinas de Estados Finitas
• A essência das FSM são:
– Conjunto fixo de estados (standing, jumping, ducking,
diving…)
– A máquina pode estar apenas em um estado por vez
• Não queremos que nosso herói pule e antes ao mesmo tempo
• Um dos motivos para escolhermos usar uma FSM
– Entrada ou eventos são enviados à máquina
• No nosso caso apertar e soltar botões.
– Cada estado tem um conjunto de transições
• Associada com uma entrada/evento
FSM – Máquinas de Estados Finitas
• Quando uma entrada chega, se ela corresponder a
alguma transição a máquina muda para o estado
apontado.
• Exemplo:
– Apertar para baixo enquanto de pé faz a transição para o
estado abaixado (ducking)
– Apertar para baixo enquanto pulando faz a transição para
o estado “diving”
– E se chegar uma entrada que não tenha transição para o
estado?
FSM – Máquinas de Estados Finitas
• Resumindo:
–Estados
–Entradas
–Transições
Enums e Switches
• Um dos problemas no nosso exemplo até então é
que algumas combinações daqueles campos
booleanos não são válidos:
– isJumping e isDucking nunca deveriam ser ambos true
• Ter várias flags, sendo que apenas uma pode ser true
por vez pode sugerir que:
Enums e Switches
• No nosso caso, o Enum vai ser exatamente o
conjunto de estados
• Ao invés de um monte de flags, a classe Herói terá um campo
“State state”
enum State { STATE_STANDING, STATE_JUMPING, STATE_DUCKING, STATE_DIVING };Enums e Switches
• Mudamos também a ordem das condições
– No código anterior verificamos o input e depois o estado
– Agora a verificação do estado fica antes
void Heroine::handleInput(Input input){ switch (state_){ case STATE_STANDING: if (input == PRESS_B){ state_ = STATE_JUMPING; yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP); }
else if (input == PRESS_DOWN){ state_ = STATE_DUCKING; setGraphics(IMAGE_DUCK); } break; case STATE_JUMPING: if (input == PRESS_DOWN){ state_ = STATE_DIVING; setGraphics(IMAGE_DIVE); } break; case STATE_DUCKING: if (input == RELEASE_DOWN){ state_ = STATE_STANDING; setGraphics(IMAGE_STAND); } break; }
Melhorou?
Situação
void Heroine::handleInput(Input input){ if (input == PRESS_B){
if (!isJumping_ && !isDucking_){ // Jump...
} }
else if (input == PRESS_DOWN){ if (!isJumping_){ isDucking_ = true; setGraphics(IMAGE_DUCK); } else { isJumping_ = false; setGraphics(IMAGE_DIVE); } }
else if (input == RELEASE_DOWN){ if (isDucking_){ // Stand... } }
O código anterior
era assim
Enums e Switches
• Parece trivial, mas temos algumas melhorias em
relação ao código anterior
– O estado encontra-se apenas em um campo único
– Bugs reduzem?
• Código para gerenciar um estado está mais coeso
• Esta é uma das maneiras mais simples de se
implementar uma máquina de estados. Ok para
alguns usos. Porém…
Enums e Switches
• … Seu problema pode exigir um pouco mais do que
essa solução pode oferecer.
• Exemplo:
– Resolvemos adicionar uma funcionalidade que permite o
herói ficar abaixado por um tempo para carregar e liberar
um ataque especial.
Enums e Switches
• Então… enquanto abaixado é preciso manter e
atualizar o tempo de carga.
• Precisamos de um atributo em Herói que armazene
por quanto tempo foi carregado
• Assumindo que já há um método update em Herói
void Heroine::update(){ if (state_ == STATE_DUCKING){ chargeTime_++; if (chargeTime_ > MAX_CHARGE){ superBomb(); } }
Enums e Switches
• Precisamos também resetar o contador quando ele
começa a abaixar… Assim, modificamos handleInput:
void Heroine::handleInput(Input input){ switch (state_){ case STATE_STANDING: if (input == PRESS_DOWN){ state_ = STATE_DUCKING; chargeTime_ = 0; setGraphics(IMAGE_DUCK); }
// Handle other inputs... break;
// Other states... }
Enums e Switches
• Resumindo, para adicionar “charge attack” tivemos
que modificar dois métodos e adicionar um atributo
“chargeTime” em Herói.
• Apesar de só ser significativo para o estado abaixado
(“ducking”).
Padrão State
• Atingimos um ponto no nosso exemplo que
uma solução mais orientada a objetos
encaixaria melhor.
– Algumas vezes porém, tudo que precisamos é
apenas um “If”
• Como podemos deixar a solução mais POO?
• Sugestões?
Padrão State
• Que tal criamos uma interface para state?
• Cada pedaço de comportamento que é
dependente de estado vira um método na
interface.
public interface IHeroineState {
void handleInput(Heroine heroine, Input input); void update(Heroine heroine);
Padrão State
• Para cada estado, nós definimos uma classe
que implementa a interface.
• Os métodos definem o comportamento do
herói quando naquele estado
• Trocando em miúdos, pegamos cada case do
switch e movemos para sua própria classe
Padrão
State
class DuckingState extends public HeroineState {
public int chargeTime = 0;
public DuckingState(){}
public void handleInput(Heroine heroine, Input input) {
if (input == RELEASE_DOWN){
// Change to standing state...
heroine.setGraphics(IMAGE_STAND);
}
}
public void update(Heroine heroine) {
chargeTime++;
if (chargeTime > MAX_CHARGE){
heroine.superBomb();
Padrão State
• Delegue para o estado
• Perdemos aquele switch gigantesco
• Apenas informamos o estado inicial
class Heroine {
private HeroineState state;
public void handleInput(Input input){ state.handleInput(this, input); }
public void update(){ state.update(this); }
// Other methods... };
Padrão State
• Ráa! Perceberam que passei por cima de um detalhe
aqui, né?
• Para mudar os estados nos precisamos atribuir um
novo estado no atributo “state”.
• Mas de onde vêm esses objetos?
– Com enuns era simples: são tipos primitivos
• Mas agora nossos estados são classes. Precisamos de
um objeto para atribuir.
– Static States
Padrão State – Static states
• Se o objeto de estado não tem quaisquer atributos, ele
só precisa reimplementar os métodos para serem
chamados através de polimorfismo.
– Nesse caso, não há razões para termos mais de uma instância
– Afinal, todas as instâncias vão ser iguais mesmo…
• Nesse caso, você só precisa criar uma única instância
estática
– Mesmo se você tiver várias máquinas rodando ao mesmo
tempo, elas podem chamar a mesma instância (já que não há
nada “machine-specific”)
Padrão State – Static states
• Onde colocar a instância estática você escolhe.
• Ache um lugar que faça sentido na sua implementação.
• Qual outro padrão está em aplicação aqui?
• Quais são as vantagens e desvantagens de não colocar State ao
public class HeroineState implements IHeroineState { static StandingState standing = StandingState(); static DuckingState ducking = DuckingState(); static JumpingState jumping = JumpingState(); static DivingState diving = DivingState();
public HeroineState(){}
public void handleInput(Heroine heroine, Input input){} public void update(Heroine heroine){}
Padrão State – Static states
• Cada um desses atributos estáticos é uma instância
do estado que o jogo usa.
• Para fazer o herói pular, o estado “standing” poderia
ter algo assim:
• Nesse momento geralmente perguntam algo... O que?
• Irei explicar mais adiante...
public void handleInput(Heroine heroine, Input input) {
if (input == PRESS_B){
heroine.setState(HeroineState.jumping); heroine.setGraphics(IMAGE_JUMP);
} }
Padrão State – Instantiated states
• Porém... As vezes utilizar estados estáticos não dá liga...
• No nosso caso, por exemplo, temos um problema com o
estado “ducking”.
– Ele tem um atributo “chargeTime”, que é específico para o herói que
acontece de estar no estado “ducking”
• Coincidentemente pode acontecer de funcionar se só há um
herói.
– Mas e se adicionarmos multiplayer? Dois na mesma tela
– E se a IA quiser utilizar a máquina também?
Padrão State – Instantiated states
• No nosso exemplos, nós poderíamos ter que criar um objeto de
estado quando a transição fosse feita:
• Assim, cada FSM tenha sua própria instância do estado.
• Quando o estado é mais “stateful”, esta é a maneira.
• Quando o estado é mais “stateless”, prefere-se economizar
memória e ciclos de CPU para alocar objetos a cada mudança de
// Em StandingState...
public void handleInput(Heroine heroine, Input input) {
if (input == PRESS_DOWN){
heroine.setState(new DuckingState()); // ...
} }
Padrão State – “Eventos de Borda”
• O objetivo do padrão State é encapsular todo o comportamento e dados
para um estado em sua respectiva classe.
• Isso foi alcançado com certo sucesso, mas ainda falta alguns detalhes...
• Quando movemos a entrada de usuário e a atualização do tempo de
carregamento para DuckingState, teve um pedaço de código que ficou de
fora...
– No estado “standing”, quando o herói começa o ducking, ele faz alguma inicialização
// Em StandingState...
public void handleInput(Heroine heroine, Input input) {
if (input == PRESS_DOWN){ // Change state...
chargeTime_ = 0;
setGraphics(IMAGE_DUCK); }
Padrão State – “Eventos de Borda”
• O DuckingState é quem deve tomar conta de resetar esse
tempo e tocar a animação
– Afinal de contas é ele quem tem o atributo.
• E se os estados tivessem um “ação de entrada”?
public class DuckingState extends HeroineState {
public void enter(Heroine heroine){
chargeTime_ = 0;
heroine.setGraphics(IMAGE_DUCK);
}
// Other code...
};
Padrão State – “Eventos de Borda”
• De volta na classe do Herói, poderíamos realizar essa
chamada na mudança do estado.
• No código de StandingState faríamos apenas a chamada
//public void setState(HeroineState state){
public void changeState(HeroineState state){
this.state = state;
this.state.enter(this);
}
public void handleInput(Heroine heroine, Input input) {
if (input == PRESS_DOWN){
heroine.changeState(new DuckingState()); }