Comportamento / Atributo GameComponent Architecture

7

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!


fonte
11
FYI: parece que você está tentando escrever C ++ em C #. Não usamos getters como GetGame (). Procure "propriedades" e simplifique sua vida.
3Dave

Respostas:

5

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 ( Penguinherdada de FlightlessBirdherdada de Birdherdada de Animalherdada 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, sua Penguinclasse pode conter uma instância de um EatsFishcomponente, mas não CanFly.

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.

BlueRaja - Danny Pflughoeft
fonte
Eu quero manter esse quadro o mais portátil possível; criar meu jogo usando o XNA GameServices não amarra meu jogo ao XNA? Ou a estrutura GameService é simples o suficiente para ser implementada por conta própria para uso em outras plataformas (android, iphone, etc.). Além disso, na apresentação que referenciei, eles mostram um componente de saúde que pega uma referência a um atributo relacionado do proprietário do componente: "GetAttribute (HEALTH_KEY);", e foi por isso que escolhi modelar meus atributos dessa maneira.
@ Travis: Se você deseja torná-lo portátil para android / iphone, C # não é uma boa escolha. Você pode procurar em bibliotecas móveis de plataforma cruzada para Java ou C ++. No entanto, se C # é tudo que você sabe, fique com isso e se preocupe em criar um jogo por enquanto. Preocupe-se com a portabilidade mais tarde - criar um jogo já é difícil o suficiente, sem a necessidade de aprender um novo idioma!
BlueRaja - Danny Pflughoeft 13/01
Ah, haha, eu sei que C # certamente não é o melhor para portabilidade, no entanto, os conceitos usados ​​neste design de mecanismo de jogo são portáteis para qualquer plataforma (que é o que eu estou focando). Eu tenho uma noção muito boa de escrever jogos usando o Android ndk, open gl es 2.0 e as c ++ directx 10 libs, estou escrevendo este jogo em C # e XNA porque é para um evento do Microsoft Windows Phone no campus.
Você pode explicar como alteraria o código de Travis? Ainda estou lutando para entender o design de jogos com base em componentes, e os artigos aos quais você vinculou são os mesmos que já vi muitas vezes antes; eles são muito altos e vagos para realmente ajudar a entender o padrão (para mim, de qualquer maneira). Do meu entendimento do design baseado em componentes, parece que Travis está no caminho certo. Você pode ser mais específico sobre onde você acha que ele deu errado e como o código dele pode ser melhorado?
Michael
1

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 Levelclasse (é um nível de jogo = sala = mapa), que contém o Brickobjeto (entidade). A Levelconhece, que a posição inicial do Brické na posição 4.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 uma Brickentidade e logo depois disso (antes que qualquer comportamento seja chamado) define a posição do Brickpara o inicial.

Brick.SetAttribute<Vector2>(AttributeType.POSITION_2D, new Vector2(4, 8))
Brick.SetAttribute<Vector2>(AttributeType.VELOCITY_2D, new Vector2(0, 1))

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:

Vector2 position = GetAttribute<Vector2>(AttributeType.POSITION_2D);
Vector2 speed = GetAttribute<Vector2>(AttributeType.VELOCITY_2D);
position += speed;
SetAttribute<Vector2>(AttributeType.POSITION_2D, position);

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()e OnMessage().

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.

TomsonTom
fonte
0

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?

Smelch
fonte
Gosto da sua ideia de ter os comportamentos inicializados após a construção para verificar se existem atributos necessários. Acho que posso tentar fazer isso e ver como funciona. Na verdade, tive a mesma idéia com o uso dos métodos OnMessage para notificar comportamentos sobre a adição de novos atributos; mas parece que essas duas soluções ainda dependem da ordem em que os componentes são adicionados / inicializados. Com o sistema de mensagens, se um componente adicionar um atributo, uma mensagem única será enviada a todos os comportamentos; no entanto, comportamentos relevantes podem não ter sido adicionados à entidade ainda, sem a mensagem.
@TravisSein Os adicionados após o envio da mensagem devem verificar o atributo quando são adicionados, assim, se ele existia antes de ser bom. Se for criado depois, receberá a mensagem.