sexta-feira, 7 de maio de 2010

Double dispatch no tratamento de colisões em jogos

É recorrente em algumas aulas de programação orientada a objetos, meu professor dizer que é uma heresia perguntar a classe de um objeto.

Participo de um grupo de jogos e chegamos em um determinado ponto em que precisamos tratar as colisões. O problema aqui é um problema de engenharia mesmo, não algorítmico. Sabendo que existe colisão entre dois objetos, como fazemos para tratá-lo.

Vamos supor que temos as classes Jogador, Inimigo, Tiro e BonusDeVida. No caso todas essas classes herdam de uma classe em comum chamada ObjetoDeJogo.

Mostrarei um exemplo de como poderia ser a classe Jogador em C++ com uma solução não muito bonita.

class Jogador : public ObjetoDeJogo{
public:
virtual void trataColisao(ObjetoDeJogo* obj){
if((Inimigo *inimigo = dynamic_cast<<Inimigo> *>( obj )) != NULL){
cout << "Colidiu com inimigo!" << endl;
}
if((Tiro *tiro = dynamic_cast<<Tiro> *>( obj )) != NULL){
cout << "Colidiu com tiro!" << endl;
}
if((BonusDeVida *bonus = dynamic_cast<<BonusDeVida> *>( obj )) != NULL){
cout << "Colidiu com bonus!" << endl;
}
}
};

Deste modo funciona, porém estamos cometendo a heresia de perguntar a classe do objeto em que o Jogador está colidindo. É um método bem simples de resolvermos o problema, porém vai contra alguns princípios de orientação a objetos. Pela falta de polimorfismo desta abordagem, sofreríamos no futuro com algumas desvantagens.

Existiriam várias classes implementando esses métodos que tratam colisões e com o passar do tempo, os métodos ficariam bem grandes com a adição de novos tipos de colisão.
O tratamento de colisão não está muito bem encapsulado, caso fizessemos métodos para cada tipo de colisão, ainda assim teríamos que mexer no conjunto de "ifs" sempre que quisessemos adicionar uma nova colisão.

Uma solução interessante para o problema é o uso de double dispatch, a página da wikipedia explica bem a técnica e há uma implementação em C++, mas para completar a postagem vou mostrar como ficaria a classe Jogador usando a técnica de double dispatch.

class Jogador : public ObjetoDeJogo{
public:
virtual void trataColisao(ObjetoDeJogo* obj){
obj->trataColisao(this);
}
virtual void trataColisao(Inimigo* obj){
cout << "Colidiu com inimigo!" << endl;
}
virtual void trataColisao(Tiro* obj){
cout << "Colidiu com tiro!" << endl;
}
virtual void trataColisao(BonusDeVida* obj){
cout << "Colidiu com bonus!" << endl;
}
};


O C++ não implementa double dispatch nativamente, por isso devemos tomar alguns cuidados. Por exemplo, todos os tipos possíveis da função trataColisao devem existir na classe ObjetoDeJogo para que este método funcione do jeito esperado.

Após as classes terem sido criadas, podemos usar os objetos no loop principal do jogo, da seguinte maneira:

vector<ObjetoDeJogo *> objetosDeJogo;
objetosDeJogo.push_back(new Jogador());
objetosDeJogo.push_back(new Inimigo());
objetosDeJogo.push_back(new BonusDeVida());

for(int i = 0;i < objetosDeJogo.size();i++){
for(int j = i+1;j < objetosDeJogo.size();j++){
objetosDeJogo[i]->trataColisao(objetosDeJogo[j]);
objetosDeJogo[j]->trataColisao(objetosDeJogo[i]);
}
}


O loop principal continua o mesmo nos dois casos, usando dynamic_cast e double dispatch, porém com double dispatch aproveitamos melhor o polimorfismo que orientação a objetos nos oferece. Com double dispatch, se adicionarmos uma classe nova colidível, precisaríamos escrever uma assinatura nova do método trataColisão na classe ObjetoDeJogo e também implementar os métodos necessários nas classes que mudam algo com a colisão deste objeto de jogo novo.


O que você acha de double dispatch? Sabe resolver este problema de um jeito melhor?

0 comentários:

Postar um comentário