Domanda

Per un gioco 2D che sto realizzando (per Android) sto usando un sistema basato sui componenti in cui un GameObject contiene diversi oggetti GameComponent. I gamecomponenti possono essere cose come componenti di input, componenti di rendering, componenti di emissione di proiettili e così via. Attualmente, GameComponents ha un riferimento all'oggetto che li possiede e può modificarlo, ma il GameObject stesso ha solo un elenco di componenti e non importa quali siano i componenti finché possono essere aggiornati quando l'oggetto viene aggiornato.

A volte un componente ha alcune informazioni che il GameObject deve sapere. Ad esempio, per il rilevamento delle collisioni un GameObject si registra con il sottosistema di rilevamento delle collisioni da avvisare quando si scontra con un altro oggetto. Il sottosistema di rilevamento delle collisioni deve conoscere la casella di delimitazione dell'oggetto. Storedo direttamente nell'oggetto X e Y (perché è usato da diversi componenti), ma la larghezza e l'altezza sono note solo al componente di rendering che contiene la bitmap dell'oggetto. Vorrei avere un metodo getBoundingBox o getwidth nel GameObject che ottiene tali informazioni. O in generale, voglio inviare alcune informazioni da un componente all'oggetto. Tuttavia, nel mio attuale design il GameObject non sa quali componenti specifici ha nell'elenco.

Posso pensare a diversi modi per risolvere questo problema:

  1. Invece di avere un elenco completamente generico di componenti, posso lasciare che GameObject abbia un campo specifico per alcuni dei componenti importanti. Ad esempio, può avere una variabile membro chiamata RenderingComponent; Ogni volta che ho bisogno di ottenere la larghezza dell'oggetto che uso solo renderingComponent.getWidth(). Questa soluzione consente ancora un elenco generico di componenti, ma tratta alcuni di essi in modo diverso e temo che finirò per avere diversi campi eccezionali poiché devono essere interrogati più componenti. Alcuni oggetti non hanno nemmeno i componenti di rendering.

  2. Avere le informazioni richieste come membri del GameObject ma consenti ai componenti di aggiornarlo. Quindi un oggetto ha una larghezza e un'altezza che sono 0 o -1 per impostazione predefinita, ma un componente di rendering può impostarli sui valori corretti nel suo ciclo di aggiornamento. Sembra un hack e potrei finire per spingere molte cose nella classe GameObject per comodità anche se non tutti gli oggetti ne hanno bisogno.

  3. Chiedi ai componenti di implementare un'interfaccia che indica il tipo di informazioni per cui possono essere interrogati. Ad esempio, un componente di rendering implementerebbe l'interfaccia Hassize che include metodi come GetWidth e Getheight. Quando il GameObject necessita della larghezza, si avvicina al controllo dei suoi componenti se implementano l'interfaccia Hassize (usando il instanceof parola chiave in java, o is in c#). Sembra una soluzione più generica, uno svantaggio è che la ricerca del componente potrebbe richiedere del tempo (ma poi la maggior parte degli oggetti ha solo 3 o 4 componenti).

Questa domanda non riguarda un problema specifico. Viene spesso fuori nel mio design e mi chiedevo qual è il modo migliore per gestirlo. Le prestazioni sono in qualche modo importanti poiché questo è un gioco, ma il numero di componenti per oggetto è generalmente piccolo (il massimo è 8).

La versione breve

In un sistema basato sui componenti per un gioco, qual è il modo migliore per passare le informazioni dai componenti all'oggetto mantenendo il design generico?

È stato utile?

Soluzione

Riceviamo variazioni su questa domanda tre o quattro volte a settimana su Gamedev.net (in cui il GameObject è in genere chiamato "entità") e finora non c'è consenso sull'approccio migliore. Diversi approcci diversi hanno dimostrato di essere realizzabili, quindi non me ne preoccuperei troppo.

Tuttavia, di solito i problemi considerano la comunicazione tra i componenti. Raramente le persone si preoccupano di ottenere informazioni da un componente all'entità - se un'entità sa di quali informazioni hanno bisogno, presumibilmente sa esattamente a quale tipo di componente deve accedere e quale proprietà o metodo deve chiamare su tale componente per ottenere i dati. Se devi essere reattivo piuttosto che attivo, allora registra i callback o fai impostare un modello di osservatore con i componenti per far sapere all'entità quando qualcosa nel componente è cambiato e leggere il valore a quel punto.

I componenti completamente generici sono in gran parte inutili: devono fornire una sorta di interfaccia nota, altrimenti c'è poco punto che esistono. Altrimenti potresti anche avere solo una vasta gamma associativa di valori non tipiti e essere fatto con esso. In Java, Python, C#e altre lingue di livello leggermente più alto rispetto a C ++ puoi usare la riflessione per darti un modo più generico di utilizzare sottoclassi specifici senza dover codificare le informazioni di tipo e interfaccia nei componenti stessi.

Per quanto riguarda la comunicazione:

Alcune persone stanno facendo ipotesi sul fatto che un'entità conterrà sempre un insieme noto di tipi di componenti (in cui ogni istanza è una delle numerose sottoclassi possibili) e quindi può semplicemente prendere un riferimento diretto all'altro componente e leggere/scrivere tramite la sua interfaccia pubblica.

Alcune persone utilizzano pubblicazione/iscrizione, segnali/slot, ecc. Per creare connessioni arbitrarie tra componenti. Questo sembra un po 'più flessibile, ma alla fine hai ancora bisogno di qualcosa con la conoscenza di queste dipendenze implicite. (E se questo è noto al momento della compilazione, perché non usare semplicemente l'approccio precedente?)

Oppure, puoi mettere tutti i dati condivisi nell'entità stessa e utilizzarli come area di comunicazione condivisa (tenuemente correlata al Sistema di lavagna In AI) a cui ciascuno dei componenti può leggere e scrivere. Questo di solito richiede un po 'di robustezza di fronte a determinate proprietà non esistenti quando ci si aspettava. Inoltre non si presta al parallelismo, anche se dubito che sia una grande preoccupazione per un piccolo sistema incorporato ...?

Infine, alcune persone hanno sistemi in cui l'entità non esiste affatto. I componenti vivono all'interno dei loro sottosistemi e l'unica nozione di entità è un valore ID in determinati componenti: se un componente di rendering (all'interno del sistema di rendering) e un componente del giocatore (all'interno del sistema dei giocatori) hanno lo stesso ID, puoi assumere Il primo gestisce il disegno del secondo. Ma non esiste un singolo oggetto che aggrega nessuno di questi componenti.

Altri suggerimenti

Come altri hanno detto, non c'è sempre una risposta giusta qui. Giochi diversi si presteranno verso diverse soluzioni. Se stai costruendo un grande gioco complesso con molti tipi diversi di entità, un'architettura generica più disaccoppiata con una sorta di messaggistica astratta tra i componenti può valere la pena per la manutenibilità che ottieni. Per un gioco più semplice con entità simili, può avere più senso spingere tutto quello stato in GameObject.

Per il tuo scenario specifico in cui è necessario conservare la scatola di delimitazione da qualche parte e solo il componente di collisione si preoccupa, lo farei:

  1. Conservalo nel componente di collisione stesso.
  2. Far funzionare direttamente il codice di rilevamento delle collisioni con i componenti.

Quindi, invece di avere il motore di collisione iterare attraverso una raccolta di gameobjects per risolvere l'interazione, mettilo in iterazione direttamente attraverso una raccolta di componenti collision. Una volta che si è verificata una collisione, spetterà al componente spingerlo al suo gameobject genitore.

Questo ti dà un paio di vantaggi:

  1. Lascia lo stato specifico per la collisione dal game object.
  2. Ti risparmia dall'iterare i gameobjects che non hanno componenti di collisione. (Se hai molti oggetti non interattivi come gli effetti visivi e la decorazione, questo può salvare un numero decente di cicli.)
  3. Ti risparmia dai cicli brucianti che camminano tra l'oggetto e il suo componente. Se iteri attraverso gli oggetti, allora fai getCollisionComponent() Su ognuno, quel seguire il puntatore può causare un mancato cache. Farlo per ogni frame per ogni oggetto può bruciare molto CPU.

Se sei interessato ne ho di più su questo schema qui, anche se sembra che tu capisca già la maggior parte di ciò che è in quel capitolo.

Usa un "Autobus per eventi". (Nota che probabilmente non puoi usare il codice così com'è, ma dovrebbe darti l'idea di base).

Fondamentalmente, crea una risorsa centrale in cui ogni oggetto può registrarsi come ascoltatore e dire "Se X accade, voglio sapere". Quando succede qualcosa nel gioco, l'oggetto responsabile può semplicemente inviare un evento X al bus evento e tutte le parti interessanti noteranno.

Modifica] Per una discussione più dettagliata, vedi passaggio di messaggio (grazie a snk_kid per averlo sottolineato).

Un approccio è inizializzare un contenitore di componenti. Ogni componente può fornire un servizio e può anche richiedere servizi da altri componenti. A seconda del linguaggio di programmazione e dell'ambiente devi trovare un metodo per fornire queste informazioni.

Nella sua forma più semplice hai connessioni one-to-one tra i componenti, ma avrai anche bisogno di connessioni one-to-many. Ad esempio il CollectionDetector avrà un elenco di componenti che implementano IBoundingBox.

Durante l'inizializzazione il contenitore collegherà i collegamenti tra i componenti e durante il tempo di esecuzione non ci saranno costi aggiuntivi.

Questo è vicino alla tua soluzione 3), aspettatevi che le connessioni tra i componenti siano cablate una sola volta e non sono controllate ad ogni iterazione del ciclo di gioco.

Il Framework di estensibilità gestito per .NET è una bella soluzione a questo problema. Mi rendo conto che intendi sviluppare su Android, ma potresti comunque avere un po 'di ispirazione da questo quadro.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top