Progettazione basata su componenti: gestione dell'interazione degli oggetti


9

Non sono sicuro di come esattamente gli oggetti facciano cose ad altri oggetti in una progettazione basata su componenti.

Di 'che ho una Objlezione. Lo voglio:

Obj obj;
obj.add(new Position());
obj.add(new Physics());

Come potrei quindi avere un altro oggetto non solo muovere la palla ma avere quella fisica applicata. Non sto cercando dettagli di implementazione ma piuttosto astrattamente come comunicano gli oggetti. In una progettazione basata su entità, potresti avere solo:

obj1.emitForceOn(obj2,5.0,0.0,0.0);

Qualsiasi articolo o spiegazione per ottenere una migliore comprensione di una progettazione basata su componenti e su come eseguire le operazioni di base sarebbe davvero utile.

Risposte:


10

Questo di solito viene fatto usando i messaggi. Puoi trovare molti dettagli in altre domande su questo sito, come qui o .

Per rispondere al tuo esempio specifico, una strada da percorrere è quella di definire una piccola Messageclasse che i tuoi oggetti possano elaborare, ad esempio:

struct Message
{
    Message(const Objt& sender, const std::string& msg)
        : m_sender(&sender)
        , m_msg(msg) {}
    const Obj* m_sender;
    std::string m_msg;
};

void Obj::Process(const Message& msg)
{
    for (int i=0; i<m_components.size(); ++i)
    {
        // let components do some stuff with msg
        m_components[i].Process(msg);
    }
}

In questo modo non ti "inquina" Obj interfaccia della classe con metodi relativi ai componenti. Alcuni componenti possono scegliere di elaborare il messaggio, altri potrebbero semplicemente ignorarlo.

Puoi iniziare chiamando questo metodo direttamente da un altro oggetto:

Message msg(obj1, "EmitForce(5.0,0.0,0.0)");
obj2.ProcessMessage(msg);

In questo caso, obj2's Physicssceglierà il messaggio, e fare tutto ciò di trasformazione che deve fare. Al termine, sarà:

  • Invia un messaggio "SetPosition" a se stesso, che il Position componente sceglierà;
  • O accedi direttamente al Positioncomponente per le modifiche (piuttosto errato per una progettazione basata su componenti puri, poiché non puoi presumere che ogni oggetto abbia un Positioncomponente, ma il Positioncomponente potrebbe essere un requisito di Physics).

È generalmente una buona idea ritardare l'effettiva elaborazione del messaggio alla successiva all'aggiornamento del componente . Elaborarlo immediatamente potrebbe significare inviare messaggi ad altri componenti di altri oggetti, quindi inviare un solo messaggio potrebbe significare rapidamente uno stack di spaghetti inestricabile.

Probabilmente dovrai scegliere un sistema più avanzato in seguito: code di messaggi asincrone, invio di messaggi a un gruppo di oggetti, registrazione / annullamento della registrazione per componente dai messaggi, ecc.

La Messageclasse può essere un contenitore generico per una stringa semplice come mostrato sopra, ma l'elaborazione delle stringhe in fase di runtime non è davvero efficiente. Puoi cercare un contenitore di valori generici: stringhe, numeri interi, float ... Con un nome o, meglio ancora, un ID, per distinguere diversi tipi di messaggi. Oppure puoi anche derivare una classe base per soddisfare esigenze specifiche. Nel tuo caso, potresti immaginare un EmitForceMessageche deriva Messagee aggiunge il vettore di forza desiderato, ma fai attenzione al costo di runtime di RTTI se lo fai.


3
Non mi preoccuperei della "non purezza" dell'accesso diretto ai componenti. I componenti vengono utilizzati per soddisfare le esigenze funzionali e di progettazione, non per il mondo accademico. Si desidera verificare l'esistenza di un componente (ad es., Verificare che il valore restituito non sia nullo per la chiamata get component).
Sean Middleditch,

Ci ho sempre pensato come hai detto l'ultima volta, usando RTTI, ma così tante persone hanno detto così tante cose cattive su RTTI
jmasterx,

@SeanMiddleditch Certo, lo farei in questo modo, citando solo quello per chiarire che dovresti sempre ricontrollare cosa stai facendo quando accedi ad altri componenti della stessa entità.
Laurent Couvidou,

@Milo Il RTTI implementato dal compilatore e il suo dynamic_cast può diventare un collo di bottiglia, ma per ora non me ne preoccupo. Puoi ancora ottimizzarlo in seguito se diventa un problema. Gli identificatori di classe basati su CRC funzionano come un fascino.
Laurent Couvidou,

´template <typename T> uint32_t class_id () {static uint32_t v; return (uint32_t) & v; } ´ - nessun RTTI necessario.
arul

3

Quello che ho fatto per risolvere un problema simile a quello che mostri, è aggiungere alcuni gestori di componenti specifici e aggiungere un qualche tipo di sistema di risoluzione degli eventi.

Quindi, nel caso del tuo oggetto "Fisica", quando viene inizializzato si aggiungerebbe a un responsabile centrale degli oggetti Fisica. Nel ciclo di gioco, questo tipo di manager ha il proprio passo di aggiornamento, quindi quando questo PhysicsManager viene aggiornato, calcola tutte le interazioni fisiche e le aggiunge in una coda di eventi.

Dopo aver prodotto tutti i tuoi eventi, puoi risolvere la coda degli eventi semplicemente controllando cosa è successo e intraprendendo azioni di conseguenza, nel tuo caso, dovrebbe esserci un evento che dice che gli oggetti A e B hanno interagito in qualche modo, quindi chiami il tuo metodo emitForceOn.

Pro di questo metodo:

  • Concettualmente, è davvero semplice da seguire.
  • Ti dà spazio per ottimizzazioni specifiche come l'utilizzo di quadtress o qualsiasi altra cosa di cui avresti bisogno.
  • Finisce per essere davvero "plug and play". Gli oggetti con fisica non interagiscono con oggetti non fisici perché non esistono per il manager.

Contro:

  • Si finisce con molti riferimenti che si spostano, quindi può diventare un po 'disordinato pulire correttamente tutto se non si presta attenzione (dal componente al proprietario del componente, dal gestore al componente, dall'evento ai partecipanti, ecc. ).
  • Devi pensare in modo speciale all'ordine in cui risolvi tutto. Immagino che non sia il tuo caso, ma ho affrontato più di un loop infinito in cui un evento ha creato un altro evento e lo stavo semplicemente aggiungendo direttamente alla coda degli eventi.

Spero che questo possa essere d'aiuto.

PS: Se qualcuno ha un modo più pulito / migliore per risolverlo, mi piacerebbe davvero ascoltarlo.


1
obj->Message( "Physics.EmitForce 0.0 1.1 2.2" );
// and some variations such as...
obj->Message( "Physics.EmitForce", "0.0 1.1 2.2" );
obj->Message( "Physics", "EmitForce", "0.0 1.1 2.2" );

Alcune cose da notare su questo design:

  • Il nome del componente è il primo parametro - questo per evitare che troppi codici funzionino sul messaggio - non possiamo sapere quali componenti qualsiasi messaggio potrebbe innescare - e non vogliamo che tutti masticino un messaggio con un fallimento del 90% tasso che si converte in molti rami e strcmp non necessari .
  • Il nome del messaggio è il secondo parametro.
  • Il primo punto (in # 1 e # 2) non è necessario, è solo per facilitare la lettura (per le persone, non per i computer).
  • È compatibile con sscanf, iostream, tu lo chiami. Nessuno zucchero sintattico che non fa nulla per semplificare l'elaborazione del messaggio.
  • Un parametro stringa: il passaggio dei tipi nativi non è più economico in termini di requisiti di memoria perché è necessario supportare un numero sconosciuto di parametri di tipo relativamente sconosciuto.
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.