Progettare un gioco basato su componenti


16

Sto scrivendo uno sparatutto (come il 1942, la classica grafica 2D) e mi piacerebbe usare un approccio basato sui componenti. Finora ho pensato al seguente design:

  1. Ogni elemento di gioco (dirigibile, proiettile, potenziamento, nemico) è un'entità

  2. Ogni entità è un insieme di componenti che possono essere aggiunti o rimossi in fase di esecuzione. Esempi sono Position, Sprite, Health, IA, Damage, BoundingBox ecc.

L'idea è che Airship, Projectile, Enemy, Powerup NON siano classi di gioco. Un'entità è definita solo dai componenti che possiede (e che possono cambiare nel tempo). Quindi il Dirigibile del giocatore inizia con i componenti Sprite, Posizione, Salute e Input. Un powerup ha Sprite, Position, BoundingBox. E così via.

Il ciclo principale gestisce la "fisica" del gioco, ovvero il modo in cui i componenti interagiscono tra loro:

foreach(entity (let it be entity1) with a Damage component)
    foreach(entity (let it be entity2) with a Health component)
    if(the entity1.BoundingBox collides with entity2.BoundingBox)
    {
        entity2.Health.decrease(entity1.Damage.amount());
    }

foreach(entity with a IA component)
    entity.IA.update(); 

foreach(entity with a Sprite component)
    draw(entity.Sprite.surface()); 

...

I componenti sono codificati nell'applicazione C ++ principale. Le entità possono essere definite in un file XML (la parte IA in un file lua o python).

Il ciclo principale non si preoccupa molto delle entità: gestisce solo i componenti. La progettazione del software dovrebbe consentire di:

  1. Dato un componente, ottieni l'entità a cui appartiene

  2. Data un'entità, ottieni il componente di tipo "tipo"

  3. Per tutte le entità, fai qualcosa

  4. Per tutti i componenti dell'entità, fai qualcosa (ad es. Serializzare)

Stavo pensando a quanto segue:

class Entity;
class Component { Entity* entity; ... virtual void serialize(filestream, op) = 0; ...}
class Sprite : public Component {...};
class Position : public Component {...};
class IA : public Component {... virtual void update() = 0; };

// I don't remember exactly the boost::fusion map syntax right now, sorry.
class Entity
{
   int id; // entity id
   boost::fusion::map< pair<Sprite, Sprite*>, pair<Position, Position*> > components;
   template <class C> bool has_component() { return components.at<C>() != 0; }
   template <class C> C* get_component() { return components.at<C>(); }
   template <class C> void add_component(C* c) { components.at<C>() = c; }
   template <class C> void remove_component(C* c) { components.at<C>() = 0; }
   void serialize(filestream, op) { /* Serialize all componets*/ }
...
};

std::list<Entity*> entity_list;

Con questo design posso ottenere # 1, # 2, # 3 (grazie agli algoritmi boost :: fusion :: map) e # 4. Inoltre tutto è O (1) (ok, non esattamente, ma è ancora molto veloce).

Esiste anche un approccio più "comune":

class Entity;
class Component { Entity* entity; ... virtual void serialize(filestream, op) = 0; ...}
class Sprite : public Component { static const int type_id = 0; };
class Position : public Component { static const int type_id = 1; };

class Entity
{
   int id; // entity id
   std::vector<Component*> components;
   bool has_component() { return components[i] != 0; }
   template <class C> C* get_component() { return dynamic_cast<C> components[C::id](); } // It's actually quite safe
...
};

Un altro approccio è quello di sbarazzarsi della classe Entity: ogni tipo di componente vive nel proprio elenco. Quindi c'è una lista Sprite, una lista di salute, una lista di danni ecc. So che appartengono alla stessa entità logica a causa dell'ID entità. Questo è più semplice, ma più lento: i componenti IA hanno bisogno di accedere sostanzialmente a tutti i componenti di altre entità e ciò richiederebbe la ricerca dell'elenco di ogni altro componente ad ogni passaggio.

Quale approccio pensi sia migliore? la mappa boost :: fusion è adatta per essere utilizzata in quel modo?


2
perché un downvote? Cosa c'è di sbagliato in questa domanda?
Emiliano,

Risposte:


6

Ho scoperto che la progettazione basata su componenti e la progettazione orientata ai dati vanno di pari passo. Dici che avere elenchi omogenei di componenti ed eliminare l'oggetto entità di prima classe (optando invece per un ID entità sui componenti stessi) sarà "più lento", ma non è né qui né lì poiché non hai effettivamente profilato alcun codice reale che implementa entrambi gli approcci per arrivare a tale conclusione. In effetti, posso quasi garantirvi che omogeneizzare i componenti ed evitare la tradizionale virtualizzazione pesante sarà più veloce a causa dei vari vantaggi della progettazione orientata ai dati: parallelizzazione più semplice, utilizzo della cache, modularità, ecc.

Non sto dicendo che questo approccio sia l'ideale per tutto, ma i sistemi componenti che sono fondamentalmente raccolte di dati che richiedono le stesse trasformazioni eseguite su di loro in ogni frame, semplicemente urlano per essere orientati ai dati. Ci saranno momenti in cui i componenti devono comunicare con altri componenti di tipi diversi, ma questo sarà un male necessario in entrambi i casi. Non dovrebbe guidare il design, tuttavia, poiché ci sono modi per risolvere questo problema anche nel caso estremo in cui tutti i componenti vengano elaborati in parallelo come code di messaggi e future .

Sicuramente Google in giro per la progettazione orientata ai dati in quanto si riferisce ai sistemi basati su componenti, perché questo argomento emerge molto e c'è un bel po 'di discussione e dati aneddotici là fuori.


cosa intendi per "orientato ai dati"?
Emiliano,

Ci sono molte informazioni su Google, ma qui è apparso un articolo decente che dovrebbe fornire una panoramica di alto livello, seguito da una discussione in relazione ai sistemi componenti: gamesfromwithin.com/data-oriented-design , gamedev. net / topic /…
Skyler York,

non posso essere d'accordo con tutto ciò che riguarda DOD, dal momento che penso che non possa essere completo da solo, intendo solo DOD può suggerire un ottimo approccio per la memorizzazione dei dati ma per chiamare funzioni e procedure è necessario utilizzare procedurale o Approvazione OOP, intendo il problema è come combinare questi due metodi per trarre il massimo beneficio sia per le prestazioni che per la facilità di codifica, ad es. nella struttura suggerisco che ci saranno problemi di prestazioni quando tutte le entità non condividono alcuni componenti ma possono essere facilmente risolte usando DOD, devi solo creare array diversi per diversi tipi di entità.
Ali1S232,

Questo non risponde direttamente alla mia domanda ma è molto istruttivo. Mi sono ricordato qualcosa sui flussi di dati ai miei tempi all'università. È la risposta migliore finora e "vince".
Emiliano,

-1

se dovessi scrivere un codice del genere preferirei non usare questo approccio (e non sto usando alcun boost se è importante per te), dal momento che può fare tutto ciò che vuoi, ma il problema è quando ci sono troppe entità che non condividono alcuni componnet, trovando quelli che lo hanno consumeranno un po 'di tempo. a parte questo, non c'è altro problema che posso fare:

// declare components here------------------------------
class component
{
};

class health:public component
{
public:
    int value;
};

class boundingbox:public component
{
public :
    int left,right,top,bottom;
    bool collision(boundingbox& other)
    {
        if (left < other.right || right > other.left)
            if (top < other.bottom || bottom > other.top)
                return true;
        return false;
    }
};

class damage : public component
{
public:
    int value;
};

// declare enteties here------------------------------

class entity
{
    virtual int id() = 0;
    virtual int size() = 0;
};

class aircraft :public entity, public health,public boundingbox
{
    virtual int id(){return 1;}
    virtual int size() {return sizeof(*this);};
};

class bullet :public entity, public damage, public boundingbox
{
    virtual int id(){return 2;}
    virtual int size() {return sizeof(*this);};
};

int main()
{
    entity* gameobjects[3];
    gameobjects[0] = new aircraft;
    gameobjects[1] = new bullet;
    gameobjects[2] = new bullet;
    for (int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            if (dynamic_cast<boundingbox*>(gameobjects[i]) && dynamic_cast<boundingbox*>(gameobjects[j]) &&
                dynamic_cast<boundingbox*>(gameobjects[i])->collision(*dynamic_cast<boundingbox*>(gameobjects[j])))
                if (dynamic_cast<health*>(gameobjects[i]) && dynamic_cast<damage*>(gameobjects[j]))
                    dynamic_cast<health*>(gameobjects[i])->value -= dynamic_cast<damage*>(gameobjects[j])->value;
}

in questo approch ogni componente è una base per un'entità, quindi dato il componente il puntatore è anche un'entità! la seconda cosa che chiedi è avere un accesso diretto ai componenti di alcune entità, ad es. quando ho bisogno di accedere al danno in una delle mie entità che uso dynamic_cast<damage*>(entity)->value, quindi se entityha un componente di danno restituirà il valore. se non si è sicuri se entitypresenta danni ai componenti o no, è possibile verificare facilmente che il if (dynamic_cast<damage*> (entity))valore restituito dynamic_castsia sempre NULL se il cast non è valido e lo stesso puntatore ma con il tipo richiesto se è valido. quindi per fare qualcosa con tutto ciò entitiesche ne ha alcuni componentpuoi farlo come di seguito

for (int i=0;i<enteties.size();i++)
    if (dynamic_cast<component*>(enteties[i]))
        //do somthing here

se ci sono altre domande, sarò felice di rispondere.


perché ho ottenuto il voto negativo? cosa c'era di sbagliato nella mia soluzione?
Ali1S232,

3
La tua soluzione non è in realtà una soluzione basata sui componenti poiché i componenti non sono separati dalle tue classi di gioco. Le tue istanze si basano tutte sulla relazione IS A (eredità) anziché su una relazione HAS A (composizione). Farlo nel modo della composizione (le entità indirizzano diversi componenti) offre molti vantaggi rispetto a un modello di ereditarietà (che di solito è il motivo per cui si utilizzano i componenti). La tua soluzione non offre nessuno dei vantaggi di una soluzione basata su componenti e introduce alcune stranezze (eredità multipla ecc.). Nessuna localizzazione dei dati, nessun aggiornamento del componente separato. Nessuna modifica di runtime dei componenti.
Vuoto

prima di tutto, la domanda richiede la struttura secondo cui ogni istanza del componente è correlata solo a un'entità e puoi attivare e disattivare i componenti aggiungendo solo una bool isActiveclasse commponent di base. c'è ancora bisogno di introdurre componenti utilizzabili quando si definiscono entità ma non lo considero un problema, eppure si hanno aggiornamenti componenti separati (ricordate qualcosa del genere dynamic_cast<componnet*>(entity)->update().
Ali1S232,

e sono d'accordo che ci sarà ancora un problema quando vorrà avere un componente che può condividere dati ma considerando ciò che ha chiesto immagino che non ci sarà un problema per questo, e di nuovo ci sono anche alcuni trucchi per quel problema che se tu voglio che posso spiegare.
Ali1S232,

Mentre sono d'accordo che è possibile implementarlo in questo modo, non credo sia una buona idea. I progettisti non possono comporre gli oggetti da soli, a meno che non si disponga di una classe über che erediti tutti i componenti possibili. E mentre puoi chiamare l'aggiornamento su un solo componente, non avrà un buon layout in memoria, in un modello composto tutte le istanze del componente dello stesso tipo possono essere tenute vicine in memoria e ripetute senza perdere cache. Ti affidi anche a RTTI, che di solito è disattivato nei giochi per motivi di prestazioni. Un buon layout di oggetti ordinati risolve questo per lo più.
Vuoto
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.