Ponto V!

Home Arquitetura Programação Artemis .Net - Uma implementação OpenSource do padrão Entity System para .Net
Thiago Dias Pastor
Artemis .Net - Uma implementação OpenSource do padrão Entity System para .NetImprimir
Escrito por Thiago Dias Pastor

Nascido como um port da sensacional API “Java-Artemis”, nosso homônimo “Artemis .Net” é um Entity System de alta performance voltada para jogos.

Nosso objetivo inicialmente era portar completamente a API Java (algo que conseguimos na versão 1.0), e em seguida extender e adicionar novos conceitos/funcionalidades a medida que os usuarios forem fornecendo feedbacks.

No momento estamos muito próximos de lançar a versão 1.2 que já engloba diversas features e paradigmas que nos distanciam bastante da versão original.

Entity System

Antes de mais nada, precisamos entender o que é um entity system!!

Entity System é uma maneira de organizar a lógica de um jogo tendo como maior princípio promover o desacoplamente entre agentes e os comportamentos dos mesmos.

Os componentes básicos deste paradigma sâo:

  • Entity: É um “objeto” que representa uma determinada “coisa” no jogo. Para cada casa, arvore, inimigo .... nos temos uma entidade associada.
  • Component: Define um comportamente que pode ser associado a uma Entidade. Ex: Uma casa pode ter os componentes Renderable (ser renderizada/ ter uma representacao visual), Physics (participar da simulaçao fisica), Health (ter uma quandtidade de energi?!)
  • EntitySystem: Um sistema que processa entidades de acordo com os componentes que ela possui. Ex: Um Render System processaria todas as entidades que possuem o componente Renderable desenhando-as na tela.

“Fazer um jogo” se resume a criar componentes e sistemas que os processam.

Explicar o por que eu uma estrutura simples assim oferece diversas vantagens em relação a tradicional orientação a objetos mereceria diversos artigos, algo que felizmente já existe =P !!!. Seguem algumas boas referencias sobre o assunto:

Artemis .Net

A Artemis foi contruida inteiramente em C# usando apenas as APIs do framework .Net, desta forma é possível suportar praticamente todos os ecossistemas da Microsoft:

  • PC / .Net Framework 3 em diante
  • Windows RT / Windows 8
  • Windows Phone 7 e 8
  • Xbox 360
  • Mono (Linux)
  • Monotouch (IOS)
  • MonoDroid (Android)

Diferenças com relaçao a versão original

Suportamos todas as funcionalidades da versão original e algumas novidades (o significado destas funcionalidades ficarão mais claros ao fim do artigo):

  • Suporte extenso a multithreading
  • Pode ser expandido completamente
  • Foco mantido principalmente em desempenho
  • Inclui modelos de sistemas especializados para cenários comuns
  • Inclui um sistema de comunicação entre sistemas baseado em Blackboard (quadro negro)
  • Blackboard também é usado para compartilhar objetos comuns entre sistemas
  • Expõe eventos para interceptar adição e remoção de entidades e/ou componentes
  • Fornecce um pool de entidades e componentes, que permite re-uso de objetos, minimizando atividade do garbage collector, melhorando assim o desempenho.
  • Contém sistemas fora do padrão baseados em filas ao invés de aspectos
  • Contém um novo paradigma de sistema que não possui dependência com componentes e entidades
  • Uso extremo de atributos do .Net
  • Pequenos itens, como preencher o EntityWorld com entidades completas (possivelmente lendo de recursos externos), liga/desliga de sistemas e entidades, tags para entidades, atalhos para API, etc.

Usando Artemis

Ao inves de explicar detalhadamente cada um dos conceitos, é mais interessante/didático mostrar código e ir comentando cada uma das novidades que aparecerem.

Caso apareça algo que não foi explicado, aguarde até o fim do artigo que “eventualmente” a explicação virá. (caso ela nunca apareça, faça um comentario no fim do artigo)

Conceitos Básicos
  • Entidades sâo apenas uma coleção de componentes
  • Componentes são Dados.
  • Sistemas processam Componentes, criando comportamentos.
Criando Componentes

Componentes são classes que podem extender da interface IComponent ou da classe abstrata ComponentPoolable (para componentes usados com muita frequencia). Neste segundo caso, pode-se usar o Data Attribute ArtemisComponentPool para especificar o comportamento do Pool.

Segue abaixo um exemplo:

//Add this Attribute and extend ComponentPoolable if you want your Component to use Artemis Component Pool
[Artemis.Attributes.ArtemisComponentPool(InitialSize=5,Resizes=true, ResizeSize=20, isSupportMultiThread=false)]
class Velocity : ComponentPoolable
{
    private float velocity;
    private float angle;
    public Velocity() { }
    public Velocity(float vector)        
    {
        velocity = vector;
    }
        
    public Velocity(float velocity, float angle)        
    {
        this.velocity = velocity;
        this.angle = angle;
    }

    public float Speed 
    {
        get { return velocity;}
        set { velocity = value; }
    }

    public float Angle 
    {
        get { return angle; }
        set { angle = value;}
    }
        
    public void AddAngle(float a)        
    {
        angle = (angle + a) % 360;
    }

    public float AngleAsRadians        
    {
        get { return (float)Math.PI * angle / 180.0f; }
    }

    //obligatory for poolable Components
    public void Cleanup() 
    {
        coords = Vector3.Zero;
    }
}

Segue a seguir um exemplo mostrando como criar uma entidade que contem este componente:

Entity e = world.CreateEntity();

e.AddComponent(new Transform(200,400));

// use AddComponentFromPool if the Component extend from ComponentPoolable
e.AddComponentFromPool(new Velocity(2.4f,0.9f)); 

// always call Refresh() when adding/removing components! This Actually Adds an entity in the world
e.Refresh();

O objeto Entity contem varios metodos intuitivos como Delete(), GetComponent(), RemoveComponent() que devem ser usados quando necessário.

Criando um sistema para processar estas entidades

Todo sistema deve herdar de um dos modelos a seguir:

  • EntitySystem – o modelo mais simples, não precisa de nenhum componente, bom para tarefas como colisão (é a classe base para todos os outros sistemas)
  • EntityProcessingSystem – um modelo para processar muitas entidades, atrelado a componentes
  • IntervalEntitySystem – um modelo simples, não atrelado a componentes, proessa periodicamente, baseado no delta gerado pelo World
  • IntervalEntityProcessingSystem - um modelo simples, atrelado a componentes, proessa periodicamente, baseado no delta gerado pelo World
  • ParallelEntityProcessingSystem – um modelo para processar entidades em paralelo, atrelado a componentes
  • QueueProcessingSystem – não atrelado a componentes, é preciso adicionar as entidades explicitamente a fila e o sistema as processa
  • HybridQueueProcessingSystem – processa entidaeds em fila ou usando a forma comum
  • QueueProcessingSystemThreadSafe – Similar ao QueueProcessingSystem, mas este éThreadSafe (além de incluir métodos estaticos para receber as entidades para processamento)
  • IntervalTagSystem – um modelo simples, não atrelado a componentes, processa uma entidade com a tag periodicamente, baseado no delta do World
  • DelayedEntitySystem – um modelo simples que inicia o processamente após um tempo informado, baseado no delta do World
  • DelayedEntityProcessingSystem - um modelo simples que inicia o processamente após um tempo informado, baseado no delta do World e utilizando componentes
  • Muitos outros, veja a lista completa

Segue um exemplo de um sistema que processa todas as entidades que contem os componentes Velocity e Transform, a fim de criar um comportamento de movimentação:

//Add this attribute so the EntityWorld knows the systems it should execute, use the Layer to determine execution order
[Artemis.Attributes.ArtemisEntitySystem(GameLoopType = GameLoopType.Update, Layer = 1)]
public class MovementSystem : EntityProcessingSystem 
{
    ComponentMapper velocityMapper;
    ComponentMapper transformMapper;

    public MovementSystem(): 
        base(Aspect.All(typeof(Transform), typeof(Velocity))) 
    { 
    }

    public override void Initialize() 
    {
        velocityMapper = new ComponentMapper(world);
        transformMapper = new ComponentMapper(world);
    }

    public override void Process(Entity e) 
    {
        Velocity velocity = velocityMapper.Get(e);

        float v = velocity.Speed;

        Transform transform = transformMapper.Get(e);

        float r = velocity.AngleAsRadians;

        float xn = transform.X + (TrigLUT.Cos(r) * v * world.Delta);
        float yn = transform.Y + (TrigLUT.Sin(r) * v * world.Delta);

        transform.SetLocation(xn, yn);
    }
}

Todo sistema pode receber o atributo ArtemisEntitySystem que especifica os seguintes parametros de configuração:

  • GameLoopType: Especifica se o sistema deve ser executado durante a fase de draw ou de update
  • Layer (int): É possivel ordenar a execucao dos sistemas, o Artemis garante que os sistemas nas camadas (layers) com valores menores serão executados antes do sistemas nas camadas maiores.
  • ExecutionType: Se o sistema pode ou não rodar em uma Thread separada.

O ComponentMapper é um objeto que agiliza (em termos de performance) a recuperação de um componente de uma entidade.

Inicialização

Durante a inicialização do jogo, deve-se construir e carregar o EntityWord.

O método InitializeAll é bastante importante pois ele irá adicionar os Systems criados no Entity World (caso o usuario não utilize os Data Attributes mostrados anteriormente, o metodo InitializeAll NÃO irá adicionar os sistemas no mundo, neste caso o usuario deve chamar o método world.SetSystem<>() manualmente).

var world = new EntityWorld();

world.InitializeAll();

Durante o Update do seu jogo, chame os metodos Update e Draw do entity world:

world.Update(elapsed.Milliseconds);

Em resumo:

  1. Crie diversos Componentes
  2. Crie diversos sistemas que processam estes components
  3. Crie entidades e adicione estes componentes a elas.
  4. Adicione estas entidades no mundo.
  5. Chame os metodos de Update/Draw

Fim !!!! Um Sistema de entidades não é mais do que isto !

Aspects

Aspectos são utilizados no construtor dos sistemas de entidades para definir quais componentes que estes sistemas estarão interessados em processar.

No momento temos três tipos de aspectos:

  • Aspect.All(params Type[] types) – Na maior parte do tempo esta opção vai ser utilizada, assim o sistema processa as entidades que possuem todos os componentes do aspecto.
  • Aspect.One(params Type[] types) – O sistema vai processar entidades que possuem pelo menos um dos componentes do aspecto.
  • Aspect.Exclude(params Type[] types) – O sistema não vai processar entidades que possuívem ao menos dos componentes do aspecto.

É possivel compor os aspectos como mostrado abaixo:

public LogEnemySystem() : 
    base(Aspect.All(typeof(Health))
        .One(
            typeof(Koopa), typeof(Goomba), typeof(Magikoopa))
        .exclude(typeof(Ghost))) 
{
}

Este sistema irá processar todas as entidades que conterem o componente Health e também conterem um ou mais dos componentes a seguir:( Koopa, Goomba, MagiKoopa) POREM não pode conter o componente Ghost.

È possível construir regras incrivelmente complexas com estas três simples primitivas.

Devido as técnicas utilizadas, independente do tamanho/complexidade da regra o custo em termos de performance é praticamente o mesmo.

Templates

É possível gerar templates a fim de evitar a criação manual de entidades, segue um exemplo de um template:

[Artemis.Attributes.ArtemisEntityTemplate("BulletExplosion")]
public EnemyTemplate : Artemis.IEntityTemplate 
{
    public Entity BuildEntity(Entity e) 
    {
        e.AddComponent(new Transform(200,400));
        e.AddComponent(new Velocity(2.4f,0.9f));
    }
}

Basta estender da classe IEntityTemplate e usar o atributo ArtemisEntityTemplate (caso o usuário prefira, é possível utilizar o método SetEntityTemplate do EntityWorld para cadastrar templates ao invés do DataAttribute acima).

Apenas para reforçar, quando o método InitializeAll do EntityWorld é chamado, todas as classes que contem os atributos são automaticamente registradas. Caso o usuário não queira usar os atributos, é sempre possível adicionar manualmente cada um dos objetos (System/Templates/ ComponentPool) no entityWorld.

Para usar uma template, basta seguir o exemplo a seguir:

var enemy = world.CreateEntityFromTemplate("BulletExplosion");

/// always call refresh when adding or removing entities
enemy.Refresh(); 

Blackboard

È possível compartilhar objetos entre sistemas usando a BlackBoard (padrão bastante conhecido na área de IA).

Segue um exemplo mostrando como compartilhar alguns objetos específicos do XNA: (Durante a inicialização do jogo):

EntitySystem.BlackBoard.SetEntry("ContentManager", Content);

EntitySystem.BlackBoard.SetEntry("GraphicsDevice", GraphicsDevice);

EntitySystem.BlackBoard.SetEntry("SpriteBatch", spriteBatch);

Dentro dos sistemas, é possível recuperar estes objetos usando:

this.device = EntitySystem.BlackBoard.GetEntry("GraphicsDevice");

this.spriteBatch = EntitySystem.BlackBoard.GetEntry("SpriteBatch");

this.contentManager = EntitySystem.BlackBoard.GetEntry("ContentManager");

BlackBoards também podem ser utilizados para notificar sistemas/entidades. Pode-se colocar um valor dentro do BlackBoard e cadastrar uma trigger que notificará o usuário quando ele for para um estado predeterminado.

FAQ

Artemis é uma Engine ?

Não, esta API não cuida de absolutamente nada relacionado a renderização, Input, IA, etc.

Ela apenas ajuda o usuário a controlar e organizar a lógica de seu jogo.

Então como que eu Faço um jogo usando Artemis ???

Existe uma mudança de paradigma razoável, todas “funcionalidades” de antes como renderização, Input, IA e etc, devem ser ajustadas.

Ex: Para desenhar uma entidade basta criar um componente Renderable e um Sistema que processa este componente. (usando a tecnologia que quiser (xna, directx, opengl ...))

Para uma entidade ser desenhada, basta adicionar este componente a ela !

Deem uma olhada no joguinho Demo que ilustra estas situações muito bem!

Porque usar um Entity System ?

Difícil de responder em poucas linhas !!!

Basicamente um entity system força a utilização do princípio “Composition over Inheritance” (composição ao invés de herança), e o benefício imediato disto é que o código ganha uma capacidade gigantesca de se adaptar a novos requisitos (algo comum em jogos).

Ex: um personagem X hoje consegue voar e atacar com uma espada, se eventualmente alguém quiser que ele passe a nadar, basta adicionar um componente “Nadar” a ele (e um sistema que processa o “Nadar”), se um personagem Z eventualmente também precisar “aprender” a nadar, basta adicionar este componente a ele também ....

Não é necessário construir infinitas cadeias de herança como fazemos quando modelamos usando a Orientação a Objetos.

Para entender com mais profundidade estes conceitos, sugiro a leitura dos artigos recomentados no início.

Qual a relação entre Orientação a Objetos e EntitySystem ?

Novamente uma pergunta difícil de responder em poucas linhas !!!

Ambos são maneiras diferente de modelar um sistema/jogo. Enquanto o primeiro tem como principal foco o uso de herança, encapsulamento, etc, o segundo foca-se na trinca de entidades/componentes/sistemas.

Novamente, para entender com mais profundidade estes conceitos, sugiro a leitura dos artigos recomentados no início.

DEMO

Entity System é fácil de entender porém não é tão simples de usar/modelar um sistema usando os seus princípios.

Para facilitar a migração e a melhor compreensão dos conceitos, fizemos um jogo simples (famoso jogo da navinha que fica atirando na vertical), que pode ser encontrado aqui: https://github.com/thelinuxlich/starwarrior_CSharp (usamos XNA)

Uma outra maneira de entender um pouco mais do funcionamento da API é olhar o próprio código fonte e os testes unitários que se encontram no nosso repositório.

Download da API

Esta API foi desenvolvida por Thiago Dias Pastor (eu) e Alisson Cavalcante Agiani


Comentários (2)
  • Anônimo
    avatar

    Excelente post!!

  • Emir
    avatar

    Não sabia que eram brasileiros por trás do Artemis .Net! Parabéns pela iniciativa.

    Me tirem duas dúvidas, por favor:

    1) Como vocês fazem para gerenciar as entidades ao passar de uma tela para outra? Por exemplo: em um jogo de luta tem a tela de título, a tela de menu, de seleção de personagens, a de jogo propriamente dita etc.

    Cada tela dessas tem entidades específicas que precisam estar ativas e visíveis apenas em suas determinadas telas. Como é a melhor forma que a Artemis lida com essa questão?

    2) Como estruturar objetos compostos? Por exemplo uma barra de life. Ela tem um contorno (esse contorno deve ficar piscando quando o life está abaixo de determinado valor), tem uma moldura estática e tem lá a barra interna com uma máscara que vai diminuindo de acordo com o life do personagem. Cada elemento destes possui posições e comportamentos próprios, mas todos fazem parte do objeto Barra de life. Seria uma entidade com uma lista de entidades filhas?

Escrever um comentário
Your Contact Details:
Gravatar enabled
Comentário:
[b] [i] [u] [url] [quote] [code] [img]   
:angry::0:confused::cheer:B):evil::silly::dry::lol::kiss::D:pinch::(:shock:
:X:side::):P:unsure::woohoo::huh::whistle:;):S:!::?::idea::arrow:
Security
Por favor coloque o código anti-spam que você lê na imagem.
LAST_UPDATED2  

Busca

Linguagens

Twitter