Steering Behaviors – Seek
Hoje escrevi a primeira classe de Steering Behaviors, implementando o comportamento Seek.
Todos os steering behaviors na SofiaIA implementarão a seguinte interface:
class SteeringForce
{
public:
virtual math::Vector2D calculate() = 0;
virtual SteeringForce() {};
};
Comecei a enfrentar os primeiros problemas sobre como abstrair corretamente o universo do jogador ao implementar o comportamento Seek. O comportamento Seek descreve uma força que impulsiona um agente qualquer em direção à um alvo, que pode ou não ser móvel. É importante ressaltar que o comportamento seek não está preocupado em estacionar o veículo no alvo, só em direciona-lo a ele.Algumas considerações de projeto foram feitas ao implementar o seek:1. A classe Seek deveria ser imutável?Brian Goetz, em seu artigo “To mutate or not mutate?” ressalta os benefícios de classes imutáveis: thread-safety, seguras em meio a código mal implementado, ideais para serem usadas como chaves de maps, possibilidades de caching. Por outro lado, um dos requisitos de uma classe imutável é que seu valor permanece constante no decorrer de todo ciclo de vida do programa. As consequências disso, é que se seek for imutável, sempre que o agente ou alvo mudar de posição, um novo objeto de seek terá de ser criado. Como steering behaviors costumam a ser combinados, também significa dizer que, sempre que o alvo ou o agente mudassem de lugar, mudanças na árvore que combina os comportamentos também teriam que ser feitas.
Essa razão torna impraticável a aplicação de imutabilidade. Entretanto, optou-se por deixar Seek o mais imutável possível, ainda buscando simplificação do projeto e de conceitos. Optou-se então por adotar referências para o alvo e para o agente, tornando assim cada objeto da classe Seek válido apenas para um alvo e agente durante todo seu ciclo de vida.
2. Templates ou ABCs?
A segunda decisão de projeto envolve o uso de templates ou abstract base classes para definir o agente e o alvo. Esse é certamente um tema que terá de ser mais trabalhado. Vamos analisar cada um dos casos:
- O veículo: O veículo possui diversas características, geralmente descritas por um motor de física: massa, aceleração, direção, sentido. Para o Seek, um veículo pode ser simplificado como algo que contém somente uma velocidade instantânea, uma posição no espaço e uma velocidade máxima.
- O alvo: O único requisito necessário para um alvo existir é estar posicionado no espaço. Ou seja, ele deve ter uma coordenada em x e outra em y.
Embora, como ressalta Thomas Becker, exista tensão entre os paradigmas de OO e Generic programming, nenhum dos dois paradigmas será descartado para a SofiaIA.
Dentre as principais desvantagens das templates sobre OOP podemos citar o fato delas adicionarem um grande grau de complexidade ao código, de não serem bem suportadas por muitos compiladores e de poderem gerar um executável muito grande, devido ao fenômeno de code bloat. Como vantagens, podemos citar a não dependência do código sobre qualquer herarquia de classes, o que é desejável sob muitos aspectos.
O alvo, por exemplo, é um forte candidato ao uso de templates. O único requisito para um alvo ser um alvo é possuir uma posição no espaço. Ele não precisa estar em nenhuma hierarquia de classes, ou mesmo ser um veículo concreto (pode ser apenas um vetor, descrevendo uma posição desejada). Portanto, o alvo foi modelado através de templates.
Resta então a questão, o veículo deve ou não ser modelado através de templates? Minha resposta inicial a essa pergunta (e que foi implementada na SofiaIA) é que não. O veículo possui uma interface complexa, o que torna o uso de uma classe virtual desejável. Além disso, um veículo é um conceito que descreve um comportamento concreto. Essa decisão entretanto está bastante tênue. Meu conhecimento em programação genérica e seus paradigmas ainda não é grande o suficiente para eu descartar com segurança suficiente o seu uso, portanto, vou estudar melhor esse tema. Por hora, mantenho a abstract base class a seguir:
class Vehicle {
public:
virtual const Vector2D& getVelocity() const = 0;
virtual const Vector2D& getPosition() const = 0;
virtual float getMaxSpeed() const = 0;
};
Além de estudar, vou manter essa estrutura para os demais behaviors e ver o quanto e como a interface veículo cresce. Se preciso for, refaço as classes após um estudo mais detalhado de programação genérica.Finalmente, tentei integrar o comportamento Seek ao Batalha Estelar. Infelizmente, percebi que mais modificações teriam de ser feito ao protótipo. Comecei alterando a posição das naves para uma estrutura de vetor, ao invés de duas variáveis x e y. Essa simplificação do código será importante mais para frente (e é a maneira clássica de se lidar com o assunto).Renomeei a classe Player, que lidava com a movimentação dos jogadores, para HumanPlayer. Tornei a classe Player abstrata e então criei a classe AiPlayer. Lá comecei a integração do behavior.
O primeiro choque que senti, que já era esperado, foi a colisão das classes Vector2D da SofiaIA, com a classe Vector2D do próprio Batalha Estelar. Uma alternativa possível seria excluir a classe Vector2D do Batalha e basear apenas no do framework, mas não farei isso. Ao invés disso, estudarei formas de tornar essa conversão suave, preferencialmente praticamente invisível aos olhos do implementador da camada superior à SofiaIA (no caso, o próprio Batalha Estelar).
Outro choque foi o fato da classe Ship não estar nos moldes da interface Vehicle. Duas soluções possíveis para isso seriam: a) criar um Adapter que mapeia um Ship à um Vehicle;
b) adaptar a classe Ship para que implemente a ABC vehicle.
Ainda estou estudando qual das opções é a mais viável.
Uma última nota. No PontoV!, coloquei um link para download de todos os jogos desenvolvidos por mim, inclusive o Batalha Estelar. Lá, você pode baixar os fontes já com as modificações tratada no último artigo.