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!
Respostas:
Caramba - quando eles disseram que você deveria modelar suas entidades em componentes, não significavam que cada entidade fosse uma sacola de atributos baseados em enum!
O objetivo do design baseado em componentes é que, em vez de ter uma grande hierarquia em árvore de classes (
Penguin
herdada deFlightlessBird
herdada deBird
herdada deAnimal
herdada de ...) , como muitos jogos tradicionalmente, você constrói suas classes a partir de classes de componente que especifique quais propriedades essa classe possui. Por exemplo, suaPenguin
classe pode conter uma instância de umEatsFish
componente, mas nãoCanFly
.Veja aqui e aqui uma descrição melhor do design baseado em componentes do que eu jamais poderia dar (eu particularmente gosto deste artigo) .
Observe que os slides aos quais você vinculou sugeriram o uso de mix-ins para modelar componentes. O C # não suporta herança múltipla como o C ++, portanto, você não pode realmente "fazer" mix-ins. Você terá que compor suas aulas da maneira mais normal : usando interfaces e / ou composição.
Observe também que eles suportam o uso de um sistema de mensagens, que geralmente é considerado de design ruim (apesar de ser amplamente utilizado).
Finalmente, como você mencionou que está usando o XNA, sinto-me obrigado a mencionar que o XNA possui suporte interno para serviços . Isso resolverá os problemas de instanciação que você está tendo. E mais importante, isso realmente ajuda a manter seu código limpo e separado ... mas, por alguma razão ímpia, nunca parece ser mencionado em nenhum tutorial ou livro do XNA.
fonte
Eu li a apresentação da GDC Canadá por Marcin Chady algumas horas antes de ler sua pergunta e acho que você não entendeu o ponto dos atributos. Eu , pessoalmente, (eu não escrevi a apresentação, nem eu sei como ele realmente funciona na Radical Entertainment) acho que você deve criar os atributos necessários antes de realmente usá-los.
O ChangePositionBehaviour e o RenderBehaviour usam o atributo Position , mas nem o primeiro nem o segundo inicializam o atributo. Você deve inicializá-lo primeiro. Você precisa informar à entidade onde está localizada.
Deixe-me mostrar um exemplo simples de um clone do Breakout :
Você tem uma
Level
classe (é um nível de jogo = sala = mapa), que contém oBrick
objeto (entidade). ALevel
conhece, que a posição inicial doBrick
é na posição4.0f, 8.0f
(X, Y) e a sua velocidade é0.0f, 1.0f
. Se não o soubesse, não poderia ter sido um nível, porque você deve especificar os locais dos objetos em uma definição de nível (arquivo de dados de nível). Então, o que o nível faz? Ele cria umaBrick
entidade e logo depois disso (antes que qualquer comportamento seja chamado) define a posição doBrick
para o inicial.Então, você pode simplesmente continuar. E quando você precisa desenhar o
Brick
, você já sabe a posição dele. Quando você precisar alterar a posição dele (é um tijolo móvel, que se move do lado esquerdo da tela para o lado direito e depois para trás), basta:Espero que você tenha me entendido. É simples: os comportamentos dependem de atributos , portanto, os atributos já devem estar definidos antes de você avançar para
OnUpdate()
eOnMessage()
.A propósito: (como não posso comentar com minha reputação de novato no GameDev SE, estou adicionando este texto btw à minha resposta) Você não precisa de nenhuma herança múltipla, por isso não entendo qual é a principal resposta de BlueRaja - Danny Pflughoeft continua ... A apresentação da Teoria e da prática da arquitetura de componentes de objetos de jogo é totalmente transformável em C # sem problemas, assim como em qualquer outra linguagem OOP. E não sei o que há de errado na passagem de mensagens, porque neste exemplo, não há outra solução possível em que posso pensar (você não pode evitar o uso de mensagens se quiser que esse código seja genérico, na minha opinião). . Talvez você possa usar manipuladores de eventos personalizados e, em vez de mensagens, passar argumentos de evento. Também poderia funcionar.
fonte
Minha sugestão seria ter uma função de inicialização para os comportamentos. Quando você adiciona o comportamento à entidade, ele pode adicionar seus atributos. Ao inicializar, você procura os atributos necessários e gera um erro se eles não estiverem lá. Dessa forma, você pode adicioná-los todos em qualquer ordem, mas eles não procurarão os atributos até a inicialização.
Como alternativa, você pode usar sua infraestrutura do OnMessage para ignorar os atributos ausentes quando um comportamento é adicionado à entidade e, em seguida, ouvir todos os atributos necessários a serem adicionados no OnMessage para o comportamento. Se um atributo estiver ausente quando você tentar usar o comportamento, poderá gerar um erro ou simplesmente não executar o comportamento. O que for mais apropriado para o seu jogo.
Isso ajuda?
fonte