Sou estudante universitário e o desenvolvimento de jogos é um pequeno hobby para mim. Estou escrevendo um novo jogo e tenho procurado uma nova abordagem para desenvolver o mecanismo de jogo para torná-lo mais flexível (e permitir fornecer uma base sólida para futuros empreendimentos). Eu encontrei alguns artigos sobre como, em vez de depender de herança profunda para representar entidades de jogo (que podem levar a nós pesados de raízes e folhas em sua árvore de herança, bem como herança de diamante em alguns casos), uma abordagem melhor é modelar sua lógica em componentes (melhor ainda, comportamentos e atributos).
Aqui está uma apresentação que ilustra isso: http://www.gdcvault.com/free/gdc-canada-09 "Teoria e prática da arquitetura de componentes de objetos de jogo ..."
Estou implementando isso usando XNA e C #. Eu tenho uma classe de entidade:
class Entity
{
private Game1 mGame;
private Dictionary<AttributeType, object> mAttributes;
private Dictionary<BehaviorType, Behavior> mBehaviors;
public Entity(Game game)
{
mGame = game;
mAttributes = new Dictionary<AttributeType, object>();
mBehaviors = new Dictionary<BehaviorType, Behavior>();
}
public Game GetGame()
{
return mGame;
}
//Attributes
public void SetAttribute<T>(AttributeType attributeType, T value)
{
if (mAttributes.ContainsKey(attributeType))
{
mAttributes[attributeType] = value;
OnMessage<T>(MessageType.ATTRIBUTE_UPDATED, attributeType, value);
}
else
{
mAttributes.Add(attributeType, value);
OnMessage<T>(MessageType.ATTRIBUTE_CREATED, attributeType, value);
}
}
public T GetAttribute<T>(AttributeType attributeType)
{
if (!mAttributes.ContainsKey(attributeType))
{
throw new KeyNotFoundException("GetAttribute: Attribute with type: " + attributeType.ToString() + " not found.");
}
return (T)mAttributes[attributeType];
}
public bool HasAttribute(AttributeType attributeType)
{
return mAttributes.ContainsKey(attributeType);
}
//Behaviors
public void SetBehavior(BehaviorType behaviorType, Behavior behavior)
{
if (mBehaviors.ContainsKey(behaviorType))
{
mBehaviors[behaviorType] = behavior;
}
else
{
mBehaviors.Add(behaviorType, behavior);
}
}
public Behavior GetBehavior(BehaviorType behaviorType)
{
if (!mBehaviors.ContainsKey(behaviorType))
{
throw new KeyNotFoundException("GetBehavior: Behavior with type: " + behaviorType.ToString() + " not found.");
}
return mBehaviors[behaviorType];
}
public bool HasBehavior(BehaviorType behaviorType)
{
return mBehaviors.ContainsKey(behaviorType);
}
public Behavior RemoveBehavior(BehaviorType behaviorType)
{
if (!mBehaviors.ContainsKey(behaviorType))
{
throw new KeyNotFoundException("RemoveBehavior: Behavior with type: " + behaviorType.ToString() + " not found.");
}
Behavior behavior = mBehaviors[behaviorType];
mBehaviors.Remove(behaviorType);
return behavior;
}
public void OnUpdate(GameTime gameTime)
{
foreach (Behavior behavior in mBehaviors.Values)
{
behavior.OnUpdate(gameTime);
}
}
public void OnMessage<T>(MessageType messageType, AttributeType attributeType, T data)
{
foreach (Behavior behavior in mBehaviors.Values)
{
behavior.OnMessage<T>(messageType, attributeType, data);
}
}
}
Onde "AttributeType" e "BehaviorType" são apenas enumerações:
public enum AttributeType
{
POSITION_2D,
VELOCITY_2D,
TEXTURE_2D,
RGB_8888
}
A classe de comportamento é uma superclasse de todos os outros comportamentos; comportamentos devem ser independentes e modulares (por exemplo, eu posso ter um SpriteBatchRenderBehavior que sabe apenas como renderizar uma entidade para o SpriteBatch, que deve ser utilizável em qualquer novo jogo que use essa estrutura).
abstract class Behavior
{
//Reference to owner to access attributes
private Entity mOwner;
public Behavior(Entity owner)
{
mOwner = owner;
}
public Entity GetOwner()
{
return mOwner;
}
protected void SetAttribute<T>(AttributeType attributeType, T value)
{
mOwner.SetAttribute<T>(attributeType, value);
}
protected T GetAttribute<T>(AttributeType attributeType)
{
return (T)mOwner.GetAttribute<T>(attributeType);
}
public abstract void OnUpdate(GameTime gameTime);
public abstract void OnMessage<T>(MessageType messageType, AttributeType attributeType, T data);
}
Portanto, uma entidade possui um conjunto de atributos rotulados (POSITION_2D, TEXTURE_2D, etc), cada comportamento pode acessar os atributos de seus proprietários por meio de métodos get / set. Estou com um problema em que os atributos da minha entidade precisam ser compartilhados entre comportamentos (não tenho certeza se isso é realmente um problema). Basicamente, se eu tenho um comportamento que altera a posição de uma entidade no mundo do jogo, ele modifica o atributo de posição da entidade. Em seguida, adiciono o SpriteBatchDrawBehavior à entidade, que depende da posição da entidade (para que ela saiba onde desenhar a entidade). Meu problema é que, se eu adicionar o comportamento do draw antes do comportamento do movimento, não há garantia de que a entidade já tenha adquirido um atributo de posição (nesse caso, uma exceção de chave não encontrada seria lançada).
Eu sempre posso garantir que a entidade tenha uma posição adicionando um comportamento que garanta que ela terá uma posição antes de adicionar um comportamento de empate (ou qualquer outro comportamento que dependa desse atributo); no entanto, isso tornaria esses comportamentos acoplados de uma maneira que não deveria ser.
A outra opção é herdar o comportamento de desenho de um comportamento de movimento que herda de um comportamento de posição (mas isso nos leva de volta ao problema da herança de diamantes, etc., e classes filho que têm pouca ou nenhuma relevância REAL para suas classes pai, outras do que ter dependências semelhantes).
Mais uma opção é fazer uma verificação nos meus comportamentos, como: "Se meu proprietário não tem uma posição, crie um atributo de posição para ele", mas isso levanta a questão: qual valor padrão deve ser escolhido para o novo atributo ? etc etc.
Alguma sugestão? Estou negligenciando alguma coisa?
Obrigado!