Entity System e rendering


11

Okey, quello che so finora; L'entità contiene un componente (archiviazione dei dati) che contiene informazioni simili; - Texture / sprite - Shader - ecc

E poi ho un sistema di rendering che disegna tutto questo. Ma quello che non capisco è come dovrebbe essere progettato il renderer. Dovrei avere un componente per ogni "tipo visivo". Un componente senza shader, uno con shader, ecc.?

Ho solo bisogno di input su qual è il "modo corretto" per farlo. Suggerimenti e insidie ​​a cui prestare attenzione.


2
Cerca di non rendere le cose troppo generiche. Sembrerebbe strano avere un'entità con un componente Shader e non un componente Sprite, quindi forse lo Shader dovrebbe far parte del componente Sprite. Naturalmente avrai quindi bisogno di un solo sistema di rendering.
Jonathan Connell,

Risposte:


8

Questa è una domanda difficile a cui rispondere perché ognuno ha la propria idea su come dovrebbe essere strutturato un sistema di componenti di entità. Il meglio che posso fare è condividere con te alcune delle cose che ho trovato più utili per me.

Entità

Adotto l'approccio di classe grassa all'ECS, probabilmente perché trovo i metodi estremi di programmazione altamente inefficienti (in termini di produttività umana). A tal fine, un'entità per me è una classe astratta che deve essere ereditata da classi più specializzate. L'entità ha un numero di proprietà virtuali e un semplice flag che mi dice se questa entità dovrebbe esistere o meno. Quindi rispetto alla tua domanda su un sistema di rendering, ecco Entitycome appare:

public abstract class Entity {
    public bool IsAlive = true;
    public virtual SpatialComponent   Spatial   { get; set; }
    public virtual ImageComponent     Image     { get; set; }
    public virtual AnimationComponent Animation { get; set; }
    public virtual InputComponent     Input     { get; set; }
}

componenti

I componenti sono "stupidi" in quanto non fanno o non sanno nulla. Non hanno riferimenti ad altri componenti e in genere non hanno funzioni (lavoro in C #, quindi utilizzo le proprietà per gestire getter / setter - se hanno funzioni, si basano sul recupero dei dati in loro possesso).

sistemi

I sistemi sono meno "stupidi", ma sono ancora automi stupidi. Non hanno contesto dell'intero sistema, non hanno riferimenti ad altri sistemi e non contengono dati se non per alcuni buffer di cui potrebbero aver bisogno per eseguire la loro elaborazione individuale. A seconda del sistema, può disporre di un metodo specializzato Updateo Draw, in alcuni casi, di entrambi.

interfacce

Le interfacce sono una struttura chiave nel mio sistema. Sono usati per definire ciò che un Systemprocesso può e di cosa Entityè capace. Le interfacce rilevanti per il rendering sono: IRenderablee IAnimatable.

Le interfacce indicano semplicemente al sistema quali componenti sono disponibili. Ad esempio, il sistema di rendering deve conoscere il rettangolo di selezione dell'entità e l'immagine da disegnare. Nel mio caso, quello sarebbe il SpatialComponente il ImageComponent. Quindi sembra così:

public interface IRenderable {
    SpatialComponent Component { get; }
    ImageComponent   Image     { get; }
}

Il sistema di rendering

Quindi, come fa il sistema di rendering a disegnare un'entità? In realtà è abbastanza semplice, quindi ti mostrerò la lezione ridotta per darti un'idea:

public class RenderSystem {
    private SpriteBatch batch;
    public RenderSystem(SpriteBatch batch) {
        this.batch = batch;
    }
    public void Draw(List<IRenderable> list) {
        foreach(IRenderable obj in list) {
            this.batch.draw(
                obj.Image.Texture,
                obj.Spatial.Position,
                obj.Image.Source,
                Color.White);
        }
    }
}

Guardando la classe, il sistema di rendering non sa nemmeno cosa Entitysia. Tutto quello che sa è IRenderablee viene semplicemente dato un elenco di loro da disegnare.

Come funziona tutto

Può aiutare a capire anche come creo nuovi oggetti di gioco e come li alimento ai sistemi.

Creazione di entità

Tutti gli oggetti di gioco ereditano da Entità e tutte le interfacce applicabili che descrivono cosa può fare quell'oggetto di gioco. Quasi tutto ciò che è animato sullo schermo è simile al seguente:

public class MyAnimatedWidget : Entity, IRenderable, IAnimatable {}

Nutrire i sistemi

Tengo un elenco di tutte le entità esistenti nel mondo di gioco in un unico elenco chiamato List<Entity> gameObjects. Ogni fotogramma, quindi setaccio l'elenco e copio i riferimenti agli oggetti in più elenchi in base al tipo di interfaccia, ad esempio List<IRenderable> renderableObjects, e List<IAnimatable> animatableObjects. In questo modo, se diversi sistemi devono elaborare la stessa entità, possono farlo. Quindi consegno semplicemente quegli elenchi a ciascuno dei sistemi Updateo Drawmetodi e lascio che i sistemi facciano il loro lavoro.

Animazione

Potresti essere curioso di sapere come funziona il sistema di animazione. Nel mio caso potresti voler vedere l'interfaccia IAnimatable:

public interface IAnimatable {
    public AnimationComponent Animation { get; }
    public ImageComponent Image         { get; set; }
}

La cosa fondamentale da notare qui è che l' ImageComponentaspetto IAnimatabledell'interfaccia non è di sola lettura; ha un setter .

Come avrai intuito, il componente animazione contiene solo dati sull'animazione; un elenco di fotogrammi (che sono componenti dell'immagine), il fotogramma corrente, il numero di fotogrammi al secondo da disegnare, il tempo trascorso dall'ultimo incremento del fotogramma e altre opzioni.

Il sistema di animazione sfrutta il sistema di rendering e la relazione tra i componenti dell'immagine. Cambia semplicemente il componente dell'immagine dell'entità mentre aumenta la cornice dell'animazione. In questo modo, l'animazione viene renderizzata indirettamente dal sistema di rendering.


Dovrei probabilmente notare che non so davvero se questo è anche vicino a ciò che le persone chiamano un sistema a componenti di entità . Nel mio tentativo di implementare un design basato sulla composizione, mi sono trovato a cadere in questo schema.
Cypher

Interessante! Non sono troppo appassionato della classe astratta per la tua Entità, ma l'interfaccia IRenderable è una buona idea!
Jonathan Connell,

5

Vedi questa risposta per vedere il tipo di sistema di cui sto parlando.

Il componente deve contenere i dettagli su cosa disegnare e come disegnarlo. Il sistema di rendering prenderà quei dettagli e disegnerà l'entità nel modo specificato dal componente. Solo se dovessi usare tecnologie di disegno significativamente diverse avresti componenti separati per stili separati.


3

Il motivo principale per separare la logica in componenti è creare un insieme di dati che, se combinati in un'entità, producono un comportamento utile e riutilizzabile. Ad esempio, separare un'entità in un componente PhysicsComponent e RenderComponent ha senso in quanto è probabile che non tutte le entità dispongano di fisica e alcune entità potrebbero non avere Sprite.

Per rispondere alla tua domanda devi guardare la tua architettura e porsi due domande:

  1. Ha senso avere uno shader senza trama
  2. La separazione di Shader da Texture mi consentirà di evitare la duplicazione del codice?

Quando si divide un componente è importante porre questa domanda, se la risposta a 1. è sì, allora probabilmente si ha un buon candidato per la creazione di due componenti separati, uno con uno Shader e uno con Texture. La risposta a 2. è in genere sì per componenti come Posizione in cui più componenti possono utilizzare la posizione.

Ad esempio, sia la fisica che l'audio possono usare la stessa posizione, invece che entrambi i componenti che memorizzano posizioni duplicate li refattano in un PositionComponent e richiedono che anche le entità che usano PhysicsComponent / AudioComponent dispongano di un PositionComponent.

In base alle informazioni che ci hai fornito, sembra che il tuo RenderComponent non sia un buon candidato per suddividere in un componente Texture e componente Shader poiché gli shader sono totalmente dipendenti da Texture e nient'altro.

Supponendo che tu stia usando qualcosa di simile a T-Machine: Entity Systems, un'implementazione di esempio di un RenderComponent & RenderSystem in C ++ sarebbe simile a questa:

struct RenderComponent {
    Texture* textureData;
    Shader* shaderData;
};

class RenderSystem {
    public:
        RenderSystem(EntityManager& manager) :
            m_manager(manager) {
            // Initialize Window, rendering context, etc...
        }

        void update() {
            // Get all the entities with RenderComponent
            std::vector<RenderComponent>& components = m_manager.getComponents<RenderComponent>();

            for(auto component = components.begin(); entity != components.end(); ++components) {
                // Do something with the texture
                doSomethingWithTexture(component->textureData);

                // Do something with the shader if it's not null
                if(component->shaderData != nullptr) {
                    doSomethingWithShader(component->shaderData);
                }
            }
        }
    private:
        EntityManager& m_manager;
}

È completamente sbagliato. L'intero punto dei componenti è separarli dalle entità, non fare in modo che i sistemi di rendering cerchino le entità per trovarle. I sistemi di rendering dovrebbero controllare completamente i propri dati. PS Non mettere std :: vector (specialmente con i dati di istanza) nei loop, è terribile (lento) C ++.
snake5,

@ snake5 hai ragione su entrambi i fronti. Ho digitato il codice dalla parte superiore della mia testa e ci sono stati alcuni problemi, grazie per averli indicati. Ho corretto il codice interessato in modo che fosse meno lento e che usasse correttamente i modi di dire del sistema di entità.
Jake Woods,

2
@ snake5 Non stai ricalcolando i dati ogni frame, getComponents restituisce un vettore di proprietà di m_manager che è già noto e cambia solo quando aggiungi / rimuovi componenti. È un vantaggio quando si dispone di un sistema che desidera utilizzare più componenti della stessa entità, ad esempio un PhysicsSystem che desidera utilizzare PositionComponent e PhysicsComponent. Altri sistemi probabilmente vorranno la posizione e avendo un PositionComponent non hai dati duplicati. Principalmente risolve il problema di come comunicano i componenti.
Jake Woods,

5
@ snake5 La domanda non riguarda il modo in cui il sistema EC dovrebbe essere strutturato o le sue prestazioni. La domanda riguarda l'installazione del sistema di rendering. Esistono diversi modi per strutturare un sistema EC, non lasciarti sorprendere dai problemi di prestazioni l'uno rispetto all'altro. L'OP sta probabilmente utilizzando una struttura CE completamente diversa rispetto a una delle tue risposte. Il codice fornito in questa risposta vuole solo mostrare meglio l'esempio, non essere criticato per le sue prestazioni. Se la domanda riguardasse le prestazioni di quanto forse questo renderebbe la risposta "non utile", ma non lo è.
MichaelHouse

2
Preferisco di gran lunga il design esposto in questa risposta rispetto a Cyphers. È molto simile a quello che uso. I componenti più piccoli sono meglio imo, anche se hanno solo una o due variabili. Dovrebbero definire un aspetto di un'entità, come se il mio componente "Damagable" avesse 2, forse 4 variabili (massimo e corrente per ogni salute e armatura). Questi commenti stanno diventando lunghi, passiamo alla chat se vuoi discutere di più.
John McDonald,

2

Pitfall n. 1: codice sovrascritto. Pensa se hai davvero bisogno di tutto ciò che implementi perché dovrai conviverci per un po 'di tempo.

Pitfall # 2: troppi oggetti. Non userei un sistema con troppi oggetti (uno per ogni tipo, sottotipo e quant'altro) perché rende più difficile l'elaborazione automatizzata. A mio avviso, è molto più bello avere ogni oggetto controllare un determinato set di funzionalità (al contrario di una funzione). Ad esempio, la creazione di componenti per ogni bit di dati inclusi nel rendering (componente texture, componente shader) è troppo divisa - di solito dovresti avere tutti questi componenti insieme, non sei d'accordo?

Pitfall # 3: controllo esterno troppo rigoroso. Preferisci cambiare i nomi in oggetti shader / texture perché gli oggetti possono cambiare con renderer / tipo di texture / formato shader / qualunque cosa. I nomi sono semplici identificatori: spetta al renderer decidere cosa ricavarne. Un giorno potresti voler avere materiali anziché semplici shader (aggiungi shader, trame e metodi di fusione dai dati, ad esempio). Con un'interfaccia testuale, è molto più semplice implementarlo.

Per quanto riguarda il renderer, può essere una semplice interfaccia che crea / distrugge / mantiene / rende gli oggetti creati dai componenti. La sua rappresentazione più primitiva potrebbe essere qualcosa del genere:

class Renderer {
    function Draw() { ... }
    function AddSprite( ... ) { ... return sprite; }
    function RemoveSprite( sprite ) { ... }
    ...
};

Ciò ti consentirebbe di gestire questi oggetti dai tuoi componenti e di mantenerli abbastanza lontani da permetterti di renderli nel modo che preferisci.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.