Componenti di gioco, gestori di giochi e proprietà degli oggetti


15

Sto cercando di orientarmi nella progettazione di entità basata su componenti.

Il mio primo passo è stato quello di creare vari componenti che potevano essere aggiunti a un oggetto. Per ogni tipo di componente avevo un manager, che chiamava la funzione di aggiornamento di ogni componente, passando cose come lo stato della tastiera ecc. Come richiesto.

La prossima cosa che ho fatto è stato rimuovere l'oggetto e avere solo ogni componente con un ID. Quindi un oggetto è definito da componenti con gli stessi ID.

Ora, sto pensando che non ho bisogno di un manager per tutti i miei componenti, ad esempio ho un SizeComponent, che ha solo una Sizeproprietà). Di conseguenza il SizeComponentnon ha un metodo di aggiornamento e il metodo di aggiornamento del gestore non fa nulla.

Il mio primo pensiero era di avere una ObjectPropertyclasse su cui i componenti potevano interrogare, invece di averli come proprietà dei componenti. Quindi un oggetto avrebbe un numero di ObjectPropertyeObjectComponent . I componenti avrebbero una logica di aggiornamento che richiede l'oggetto per le proprietà. Il gestore riuscirà a chiamare il metodo di aggiornamento del componente.

Questo mi sembra troppo ingegneristico, ma non credo di poter eliminare i componenti, perché ho bisogno di un modo per i gestori di sapere quali oggetti necessitano di quale logica del componente eseguire (altrimenti rimuoverei semplicemente il componente completamente e inserire la sua logica di aggiornamento nel gestore).

  1. È questo (avendo ObjectProperty, ObjectComponente ComponentManagerclassi) over-engineering?
  2. Quale sarebbe una buona alternativa?

1
Hai l'idea giusta cercando di imparare il modello di componente, ma devi capire meglio cosa deve fare - e l'unico modo per farlo è [principalmente] completare un gioco senza usarlo. Penso che creare un SizeComponentsia eccessivo - puoi presumere che la maggior parte degli oggetti abbia una dimensione - sono cose come il rendering, l'intelligenza artificiale e la fisica in cui viene utilizzato il modello componente; Le dimensioni si comporteranno sempre allo stesso modo, quindi puoi condividere quel codice.
Jonathan Dickinson,


@JonathanDickinson, @Den: Penso, quindi il mio problema è dove immagazzino le proprietà comuni. Ad esempio un oggetto come posizione, che viene utilizzato da a RenderingComponente a PhysicsComponent. Sto pensando troppo a dove mettere la proprietà? Devo solo attaccarlo in entrambi, quindi l'altra query deve un oggetto per il componente che ha la proprietà necessaria?
George Duckett,

Il mio commento precedente e il processo di pensiero alla base è ciò che mi sta spingendo ad avere una classe separata per una proprietà (o forse un gruppo di proprietà correlate) su cui i componenti possono interrogare.
George Duckett,

1
Mi piace davvero quell'idea - potrebbe valere la pena provarla; ma avere un oggetto per descrivere ogni singola proprietà è davvero costoso. Potresti provare PhysicalStateInstance(uno per oggetto) insieme a GravityPhysicsShared(uno per gioco); tuttavia sono tentato di dire che questo si sta avventurando nei regni dell'euforia degli architetti, non progettare te stesso in un buco (esattamente quello che ho fatto con il mio primo sistema componente). BACIO.
Jonathan Dickinson,

Risposte:


6

La semplice risposta alla tua prima domanda è Sì, stai progettando troppo. Il "Fino a che punto si rompono le cose?" la domanda è molto comune quando viene eseguito il passaggio successivo e l'oggetto centrale (di solito chiamato Entità) viene rimosso.

Quando si scompongono gli oggetti a un livello così dettagliato da avere dimensioni proprie, il design è andato troppo oltre. Un valore di dati da solo non è un componente. È un tipo di dati integrato e spesso può essere chiamato esattamente come hai iniziato a chiamarli, una proprietà. Una proprietà non è un componente, ma un componente contiene proprietà.

Quindi, ecco alcune linee guida che cerco di seguire quando sviluppo in un sistema a componenti:

  • Non c'è il cucchiaio.
    • Questo è il passo che hai già fatto per sbarazzarti dell'oggetto centrale. Questo rimuove l'intero dibattito su ciò che entra nell'oggetto Entità e ciò che entra in un componente poiché ora tutto ciò che hai sono i componenti.
  • I componenti non sono strutture
    • Se analizzi qualcosa in cui contiene solo dati, non è più un componente, è solo una struttura di dati.
    • Un componente deve contenere tutte le funzionalità necessarie per eseguire un'attività molto specifica in un modo specifico.
    • L'interfaccia IRenderable fornisce la soluzione generica per visualizzare visivamente qualsiasi cosa nel gioco. CRenderableSprite e CRenderableModel è un'implementazione componente di quell'interfaccia che fornisce le specifiche per il rendering rispettivamente in 2D e 3D.
    • IUseable è l'interfaccia per qualcosa con cui un giocatore è in grado di interagire. CUseableItem sarebbe il componente che spara la pistola attiva o beve la pozione selezionata mentre CUseableTrigger potrebbe essere il punto in cui un giocatore salta su una torretta o lancia una leva per far cadere il ponte levatoio.

Quindi, con la linea guida dei componenti che non sono strutture, SizeComponent è stato suddiviso troppo. Contiene solo dati e ciò che definisce la dimensione di qualcosa può variare. Ad esempio, in un componente di rendering potrebbe essere uno scalare 1d o un vettore 2 / 3d. In un componente fisico potrebbe essere il volume limite dell'oggetto. In un articolo di inventario potrebbe essere lo spazio che occupa su una griglia 2D.

Cerca di tracciare una buona linea tra teoria e praticità.

Spero che sia di aiuto.


Non dimentichiamo che su alcune piattaforme, chiamare una funzione da un'interfaccia è più lungo di chiamarne una da una classe genitore (poiché la tua risposta includeva menzioni di interfacce e classi)
ADB

Buon punto da ricordare, ma stavo cercando di rimanere agnostico nel linguaggio e di usarli solo nei termini generali del design.
James,

"Se si suddivide qualcosa nel punto in cui contiene solo dati, allora non è più un componente, è solo una struttura di dati." -- Perché? "Componente" è una parola così generica che può significare anche la struttura dei dati.
Paul Manta,

@PaulManta Sì, è un termine generico, ma l'intero punto di questa domanda e risposta è dove tracciare la linea. La mia risposta, come hai citato, è solo il mio suggerimento per una regola empirica di fare proprio questo. Come sempre, sostengo che la teoria o le considerazioni progettuali non siano mai ciò che guida lo sviluppo, ma che intende aiutarlo.
James

1
@James È stata una discussione interessante. :) chat.stackexchange.com/rooms/2175 La mia più grande lamentela se la tua implementazione è che i componenti sanno troppo di ciò a cui sono interessati altri componenti. Vorrei continuare la discussione in futuro.
Paul Manta,

14

Hai già accettato una risposta, ma ecco la mia pugnalata alla CBS. Ho scoperto che una Componentclasse generica ha alcune limitazioni, quindi ho seguito un progetto descritto da Radical Entertainment al GDC 2009, che ha suggerito di separare i componenti in Attributese Behaviors. (" Teoria e pratica dell'architettura dei componenti dell'oggetto di gioco ", Marcin Chady)

Spiego le mie decisioni di progettazione in un documento di due pagine. Pubblicherò semplicemente il link poiché è troppo lungo per incollarlo tutto qui. Attualmente copre solo i componenti logici (non anche i componenti di rendering e fisica), ma dovrebbe darti un'idea di ciò che ho provato a fare:

http://www.pdf-archive.com/2012/01/08/entity-component-system/preview/page/1

Ecco un estratto dal documento:

Attributi e comportamenti in breve

Attributesgestire una categoria di dati e ogni logica che hanno è di portata limitata. Ad esempio, Healthpotrebbe assicurarsi che il suo valore corrente non sia mai maggiore del suo valore massimo e può persino notificare altri componenti quando il valore corrente scende al di sotto di un certo livello critico, ma non contiene alcuna logica più complessa. Attributesnon dipendono da nessun altro Attributeso Behaviors.

Behaviorscontrollare il modo in cui l'entità reagisce agli eventi di gioco, prendere decisioni e modificare i valori Attributessecondo necessità. Behaviorsdipendono da alcuni dei Attributes, ma non possono interagire direttamente tra loro - reagiscono solo al modo in cui i Attributes’valori vengono modificati dall'altro Behaviorse agli eventi che vengono inviati.


Modifica: ecco un diagramma delle relazioni che mostra come i componenti comunicano tra loro:

Communication diagram between Attributes and Behaviors

Un dettaglio di implementazione: il livello entità EventManagerviene creato solo se utilizzato. La Entityclasse memorizza solo un puntatore a un EventManagerinizializzato solo se alcuni componenti lo richiedono.


Modifica: su un'altra domanda ho dato una risposta simile a questa. Puoi trovarlo qui per una, forse, migliore spiegazione del sistema:
/gamedev//a/23759/6188


2

Dipende molto dalle proprietà di cui hai bisogno e da dove le hai bisogno. La quantità di memoria che avrai e la potenza / tipo di elaborazione che utilizzerai. Ho visto e provo a fare quanto segue:

  • Le proprietà utilizzate da più componenti ma modificate solo da una sono memorizzate in quel componente. La forma è un buon esempio in un gioco in cui il sistema di intelligenza artificiale, il sistema fisico e il sistema di rendering hanno bisogno di accedere alla forma di base, è una proprietà pesante e dovrebbe rimanere solo in un posto, se possibile.
  • Proprietà come la posizione a volte devono essere duplicate. Ad esempio, se si eseguono più sistemi in parallelo, si desidera evitare di sbirciare tra i sistemi e si sincronizzerà piuttosto la posizione (copia dal componente master o sincronizzazione tramite delta o con un passaggio di collisione se necessario).
  • Le proprietà originate da controlli o "intenzioni" AI possono essere archiviate in un sistema dedicato in quanto possono essere applicate ad altri sistemi senza essere visibili dall'esterno.
  • Le proprietà semplici possono diventare complicate. A volte la tua posizione richiederà un sistema dedicato se devi condividere molti dati (posizione, orientamento, delta del frame, movimento totale delta corrente, movimento delta per il frame corrente e per il frame precedente, rotazione ...). In tal caso dovrai andare con il sistema e accedere agli ultimi dati dal componente dedicato e potresti doverlo cambiare tramite accumulatori (delta).
  • A volte le tue proprietà possono essere archiviate in una matrice grezza (doppia *) e i tuoi componenti avranno semplicemente dei puntatori alle matrici che contengono le diverse proprietà. L'esempio più ovvio è quando hai bisogno di enormi calcoli paralleli (CUDA, OpenCL). Quindi avere un sistema per gestire correttamente i puntatori potrebbe rivelarsi utile.

Questi principi hanno i loro limiti. Ovviamente dovrai spingere la geometria sul renderer ma probabilmente non vorrai recuperarla da lì. La geometria master verrà memorizzata nel motore fisico nel caso in cui si verifichino deformazioni e sincronizzata con il renderer (di volta in volta a seconda della distanza degli oggetti). Quindi in un certo senso lo duplicherai comunque.

Non ci sono sistemi perfetti. E alcuni giochi andranno meglio con un sistema più semplice mentre altri richiedono sincronizzazioni più complesse tra i sistemi.

Innanzitutto assicurati che tutte le proprietà siano accessibili in modo semplice dai tuoi componenti in modo da poter cambiare il modo in cui memorizzi le proprietà in modo trasparente una volta che inizi a mettere a punto i tuoi sistemi.

Non c'è da vergognarsi nel copiare alcune proprietà. Se alcuni componenti devono contenere una copia locale, a volte è più efficiente copiare e sincronizzare anziché accedere a un valore "esterno"

Inoltre, la sincronizzazione non deve avvenire in tutti i frame. Alcuni componenti possono essere sincronizzati meno frequentemente di altri. I componenti di rendering sono spesso un buon esempio. Quelli che non interagiscono con i giocatori possono essere sincronizzati meno frequentemente, proprio come quelli che sono lontani. Quelli lontani e al di fuori del campo della telecamera possono essere sincronizzati anche meno frequentemente.


Quando si tratta del componente della dimensione, probabilmente potrebbe essere raggruppato nel componente della posizione:

  • non tutte le entità con una dimensione hanno una componente fisica, ad esempio aree, quindi raggrupparla con la fisica non è nel tuo interesse.
  • la dimensione probabilmente non importerà senza una posizione
  • tutti gli oggetti con una posizione avranno probabilmente una dimensione (che può essere usata per script, fisica, intelligenza artificiale, rendering ...).
  • la dimensione probabilmente non viene aggiornata ad ogni ciclo ma la posizione potrebbe essere.

Invece di dimensioni potresti persino usare un modificatore di dimensioni, potrebbe essere più pratico.

Per quanto riguarda l'archiviazione di tutte le proprietà in un sistema di archiviazione di proprietà generico ... Non sono sicuro che tu stia andando nella giusta direzione ... Concentrati sulle proprietà che sono fondamentali per il tuo gioco e crea componenti che raggruppano il maggior numero possibile di proprietà correlate. Finché si astratta l'accesso a queste proprietà correttamente (ad esempio tramite getter nei componenti che ne hanno bisogno), si dovrebbe essere in grado di spostarli, copiarli e sincronizzarli in un secondo momento, senza interrompere troppo le logiche.


2
BTW +1 o -1 me perché il mio attuale rappresentante è 666 dal 9 novembre ... È inquietante.
Coyote,

1

Se i componenti possono essere aggiunti arbitrariamente alle entità, è necessario un modo per interrogare se un determinato componente esiste in un'entità e per ottenere un riferimento ad esso. Quindi è possibile scorrere su un elenco di oggetti derivati ​​da ObjectComponent fino a trovare quello desiderato e restituirlo. Ma restituiresti un oggetto del tipo corretto.

In C ++ o C # questo di solito significa che avresti un metodo modello sull'entità simile T GetComponent<T>(). E una volta che hai quel riferimento, sai esattamente quali dati dei membri contiene, quindi accedi direttamente.

In qualcosa come Lua o Python non hai necessariamente un tipo esplicito di quell'oggetto, e probabilmente non ti interessa neanche. Ma ancora una volta, puoi semplicemente accedere alla variabile membro e gestire qualsiasi eccezione derivante dal tentativo di accedere a qualcosa che non c'è.

La query per le proprietà degli oggetti suona esplicitamente come duplicare il lavoro che la lingua può fare per te, sia in fase di compilazione per linguaggi tipicamente statici o in fase di esecuzione per quelli tipizzati dinamicamente.


Comprendo come ottenere componenti fortemente tipizzati da un'entità (usando generici o simili), la mia domanda è più su dove dovrebbero andare quelle proprietà, in particolare dove una proprietà viene utilizzata da più componenti e non si può dire che un singolo componente ne sia il proprietario. Vedi il mio terzo e quarto commento sulla domanda.
George Duckett,

Basta sceglierne uno esistente se si adatta, o fattorizzare la proprietà in un nuovo componente in caso contrario. Ad esempio, Unity ha un componente 'Trasforma' che è solo la posizione, e se qualcos'altro ha bisogno di modificare la posizione dell'oggetto, lo fanno tramite quel componente.
Kylotan,

1

"Penso, quindi il mio problema è dove immagazzino le proprietà comuni. Ad esempio un oggetto come una posizione, che viene utilizzato da un RenderingComponent e un PhysicsComponent. Sto pensando troppo alla decisione su dove mettere la proprietà? Devo semplicemente attenermi? in entrambi i casi, quindi l'altra query ha un oggetto per il componente che ha la proprietà necessaria? "

Il fatto è che RenderingComponent utilizza la posizione, ma PhysicsComponent lo fornisce . Hai solo bisogno di un modo per dire a ciascun componente utente quale provider utilizzare. Idealmente in modo agnostico, altrimenti ci sarà una dipendenza.

"... la mia domanda è più su dove dovrebbero andare quelle proprietà, in particolare dove una proprietà viene utilizzata da più componenti e nessun singolo componente può dirsi proprietario. Vedi il mio terzo e quarto commento sulla domanda."

Non esiste una regola comune. Dipende dalla proprietà specifica (vedi sopra).

Crea un gioco con un'architettura brutta ma basata su componenti e poi riflattala.


Non credo di aver capito bene cosa PhysicsComponentdovrebbe fare allora. Lo vedo come gestire la simulazione dell'oggetto all'interno di un ambiente fisico, il che mi porta a questa confusione: non tutte le cose che devono essere rese dovranno essere simulate, quindi mi sembra sbagliato aggiungere PhysicsComponentquando aggiungo RenderingComponentperché contiene una posizione che RenderingComponentusa. Potevo facilmente vedermi finire con una rete di componenti interconnessi, il che significa che tutti / la maggior parte devono essere aggiunti a ciascuna entità.
George Duckett,

In realtà ho avuto una situazione simile :). Ho un PhysicsBodyComponent e un SimpleSpatialComponent. Entrambi forniscono posizione, angolo e dimensioni. Ma il primo partecipa alla simulazione della fisica e ha ulteriori proprietà pertinenti e il secondo contiene solo quei dati spaziali. Se hai il tuo motore fisico puoi persino ereditare il primo dal secondo.
Den,

"Potrei facilmente vedermi finire con una rete di componenti interconnessi, il che significa che tutti / la maggior parte devono essere aggiunti a ciascuna entità." Questo perché non hai un vero prototipo di gioco. Stiamo parlando di alcuni componenti di base qui. Non c'è da stupirsi che verranno utilizzati ovunque.
Den,

1

Il vostro intestino vi sta dicendo che avere la ThingProperty, ThingComponente ThingManagerper ogniThing tipo di componente un po 'eccessivo. Penso sia giusto.

Tuttavia, è necessario un modo per tenere traccia dei componenti correlati in termini di quali sistemi li utilizzano, a quale entità appartengono, ecc.

TransformPropertysarà abbastanza comune. Ma chi se ne occupa, il sistema di rendering? Il sistema fisico? Il sistema audio? Perché dovrebbe aTransform componente dovrebbe persino aggiornarsi?

La soluzione è rimuovere qualsiasi tipo di codice dalle proprietà al di fuori di getter, setter e inizializzatori. I componenti sono dati, che vengono utilizzati dai sistemi nel gioco per eseguire varie attività come rendering, AI, riproduzione audio, movimento, ecc.

Leggi di Artemis: http://piemaster.net/2011/07/entity-component-artemis/

Guarda il suo codice e vedrai che è basato su sistemi che dichiarano le loro dipendenze come elenchi di ComponentTypes . Scrivi ciascuna delle tue Systemclassi e nel metodo di costruzione / init dichiari da quali tipi dipende il sistema.

Durante l'installazione per livelli o quant'altro, si creano le entità e si aggiungono componenti ad esse. Successivamente dici a quell'entità di riferire ad Artemide, e Artemide quindi scopre in base alla composizione di quell'entità quali sistemi sarebbero interessati a conoscere quell'entità.

Quindi durante la fase di aggiornamento del tuo ciclo, i tuoi Systems ora hanno un elenco di quali entità aggiornare. Ora potete avere granularità dei componenti in modo da poter elaborare sistemi folli, entità costruire fuori di un ModelComponent, TransformComponent, FliesLikeSupermanComponent, eSocketInfoComponent , e fare qualcosa di strano, come fanno un disco volante che vola tra i client connessi a una partita multiplayer. Va bene, forse non quello, ma l'idea è che mantiene le cose disaccoppiate e flessibili.

Artemis non è perfetta, e gli esempi sul sito sono un po 'di base, ma la separazione di codice e dati è potente. Fa anche bene alla cache se lo fai bene. Artemis probabilmente non lo fa proprio su quel fronte, ma è bello imparare da.

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.