Come posso accedere correttamente ai componenti nel mio C ++ Entity-Component-Systems?


18

(Quello che sto descrivendo si basa su questo disegno: cos'è un sistema di entità? Scorri verso il basso e lo troverai)

Sto riscontrando alcuni problemi durante la creazione di un sistema con componenti entità in C ++. Ho la mia classe Component:

class Component { /* ... */ };

Che è in realtà un'interfaccia, per la creazione di altri componenti. Quindi, per creare un componente personalizzato, devo solo implementare l'interfaccia e aggiungere i dati che verranno utilizzati nel gioco:

class SampleComponent : public Component { int foo, float bar ... };

Questi componenti sono archiviati all'interno di una classe Entity, che fornisce a ciascuna istanza di Entity un ID univoco:

class Entity {
     int ID;
     std::unordered_map<string, Component*> components;
     string getName();
     /* ... */
};

I componenti vengono aggiunti all'entità tramite l'hashing del nome del componente (questa probabilmente non è una grande idea). Quando aggiungo un componente personalizzato, viene archiviato come tipo Componente (classe base).

Ora, invece, ho un'interfaccia di sistema, che utilizza un'interfaccia di nodo all'interno. La classe Node viene utilizzata per archiviare alcuni dei componenti di una singola entità (poiché il Sistema non è interessato all'utilizzo di tutti i componenti dell'entità). Quando è necessario update(), il sistema deve solo scorrere i nodi archiviati creati da entità diverse. Così:

/* System and Node implementations: (not the interfaces!) */

class SampleSystem : public System {
        std::list<SampleNode> nodes; //uses SampleNode, not Node
        void update();
        /* ... */
};

class SampleNode : public Node {
        /* Here I define which components SampleNode (and SampleSystem) "needs" */
        SampleComponent* sc;
        PhysicsComponent* pc;
        /* ... more components could go here */
};

Ora il problema: diciamo che costruisco i SampleNodes passando un'entità al SampleSystem. Il SampleNode quindi "controlla" se l'entità ha i componenti richiesti per essere utilizzati dal SampleSystem. Il problema si presenta quando ho bisogno di accedere al componente desiderato all'interno dell'Entità: il componente è archiviato in una Componentraccolta (classe base), quindi non posso accedere al componente e copiarlo sul nuovo nodo. Ho temporaneamente risolto il problema lanciando il Componentdown a un tipo derivato, ma volevo sapere se esiste un modo migliore per farlo. Capisco se questo significherebbe riprogettare ciò che già ho. Grazie.

Risposte:


23

Se hai intenzione di archiviare tutti Componenti messaggi di posta in una raccolta, devi usare una classe base comune come tipo archiviato nella raccolta, e quindi devi lanciare il tipo corretto quando provi ad accedere ai messaggi di posta Componentelettronica nella raccolta. I problemi nel tentativo di eseguire il cast nella classe derivata errata possono essere eliminati mediante un uso intelligente dei modelli e della typeidfunzione, tuttavia:

Con una mappa dichiarata così:

std::unordered_map<const std::type_info* , Component *> components;

una funzione addComponent come:

components[&typeid(*component)] = component;

e un getComponent:

template <typename T>
T* getComponent()
{
    if(components.count(&typeid(T)) != 0)
    {
        return static_cast<T*>(components[&typeid(T)]);
    }
    else 
    {
        return NullComponent;
    }
}

Non otterrai un errore. Questo perché typeidrestituirà un puntatore alle informazioni sul tipo del tipo di runtime (il tipo più derivato) del componente. Poiché il componente è memorizzato con quel tipo di informazioni in quanto chiave, il cast non può causare problemi a causa di tipi non corrispondenti. Si ottiene anche il controllo del tipo di tempo di compilazione sul tipo di modello in quanto deve essere un tipo derivato da Componente o static_cast<T*>avrà tipi non corrispondenti con unordered_map.

Tuttavia, non è necessario archiviare i componenti di tipi diversi nella raccolta comune. Se si abbandona l'idea di un s Entitycontenente Component, e invece si dispone di un Componentarchivio ogni Entity(in realtà, probabilmente sarà solo un ID intero), è possibile archiviare ciascun tipo di componente derivato nella propria raccolta del tipo derivato anziché come tipo di base comune e trova la Component"appartenenza a" Entityattraverso un ID.

Questa seconda implementazione è un po 'più poco intuitiva da pensare rispetto alla prima, ma probabilmente potrebbe essere nascosta come dettagli di implementazione dietro un'interfaccia in modo che gli utenti del sistema non debbano preoccuparsene. Non commenterò quale sia il migliore in quanto non ho usato veramente il secondo, ma non vedo l'uso di static_cast come un problema con una garanzia sui tipi così forte come prevede la prima implementazione. Si noti che richiede RTTI che può essere o meno un problema a seconda della piattaforma e / o delle convinzioni filosofiche.


3
Uso C ++ da quasi 6 anni, eppure ogni settimana imparo qualche nuovo trucco.
knight666,

Grazie per aver risposto. Proverò prima ad usare il primo metodo e, se dopo, forse, penserò a un modo di usare l'altro. Ma il addComponent()metodo non dovrebbe essere anche un modello? Se definisco un addComponent(Component* c), qualsiasi sottocomponente che aggiungo verrà memorizzato in un Componentpuntatore e typeidfarà sempre riferimento alla Componentclasse base.
Federico

2
Typeid ti fornirà il tipo reale dell'oggetto puntato anche se il puntatore è di una classe base
Chewy Gumball

Mi è piaciuta molto la risposta gommosa, quindi ho provato a implementarla su mingw32. Ho riscontrato il problema menzionato da fede rico in cui addComponent () memorizza tutto come componente perché typeid restituisce il componente come tipo per tutto. Qualcuno qui ha menzionato che typeid dovrebbe fornire il tipo effettivo dell'oggetto a cui viene puntato anche se il puntatore si trova su una classe base, ma penso che possa variare in base al compilatore, ecc. Qualcun altro può confermarlo? Stavo usando g ++ std = c ++ 11 mingw32 su Windows 7. Ho finito per modificare getComponent () in modo che diventasse un modello, quindi ho salvato il tipo da quello in th
shwoseph

Questo non è specifico del compilatore. Probabilmente non hai avuto la giusta espressione come argomento per la funzione typeid.
Chewy Gumball,

17

Chewy ha ragione, ma se stai usando C ++ 11 hai alcuni nuovi tipi che puoi usare.

Invece di usare const std::type_info*come chiave nella tua mappa, puoi usare std::type_index( vedi cppreference.com ), che è un wrapper attorno a std::type_info. Perché dovresti usarlo? In std::type_indexrealtà memorizza la relazione con std::type_infocome un puntatore, ma questo è un puntatore in meno di cui ti devi preoccupare.

Se stai effettivamente utilizzando C ++ 11, consiglierei di memorizzare i Componentriferimenti all'interno dei puntatori intelligenti. Quindi la mappa potrebbe essere qualcosa del tipo:

std::map<std::type_index, std::shared_ptr<Component> > components

L'aggiunta di una nuova voce potrebbe essere effettuata in questo modo:

components[std::type_index(typeid(*component))] = component

dov'è componentdi tipo std::shared_ptr<Component>. Il recupero di un riferimento a un determinato tipo di Componentpotrebbe apparire come:

template <typename T>
std::shared_ptr<T> getComponent()
{
    std::type_index index(typeid(T));
    if(components.count(std::type_index(typeid(T)) != 0)
    {
        return static_pointer_cast<T>(components[index]);
    }
    else
    {
        return NullComponent
    }
}

Nota anche l'uso di static_pointer_castinvece di static_cast.


1
In realtà sto usando questo tipo di approccio nel mio progetto.
venerdì

Questo in realtà è abbastanza conveniente, poiché ho imparato il C ++ usando lo standard C ++ 11 come riferimento. Una cosa che ho notato, però, è che tutti i sistemi a componenti di entità che ho trovato in giro per il web usano una sorta di cast. Sto iniziando a pensare che sarebbe impossibile implementare questo, o un progetto di sistema simile senza cast.
Federico,

@Fede La memorizzazione dei Componentpuntatori in un singolo contenitore richiede necessariamente il loro casting nel tipo derivato. Ma, come ha sottolineato Chewy, hai altre opzioni a tua disposizione, che non richiedono casting. Io stesso non vedo nulla di "brutto" nell'avere questo tipo di cast nel design, in quanto sono relativamente sicuri.
vijoc,

@vijoc A volte sono considerati cattivi a causa del problema di coerenza della memoria che possono introdurre.
Akaltar,
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.