Quali progetti esistono per un sistema di entità basato su componenti che sia facile da usare ma comunque flessibile?


23

Sono stato interessato al sistema di entità basato su componenti per un po 'e ho letto innumerevoli articoli su di esso (I giochi Insomiac , il piuttosto standard Evolve Your Hierarchy , la T-Machine , Chronoclast ... solo per citarne alcuni).

Sembrano tutti avere una struttura all'esterno di qualcosa come:

Entity e = Entity.Create();
e.AddComponent(RenderComponent, ...);
//do lots of stuff
e.GetComponent<PositionComponent>(...).SetPos(4, 5, 6);

E se porti l'idea dei dati condivisi (questo è il miglior design che ho visto finora, in termini di non duplicare i dati ovunque)

e.GetProperty<string>("Name").Value = "blah";

Sì, questo è molto efficiente. Tuttavia, non è esattamente il più facile da leggere o scrivere; sembra molto goffo e lavora contro di te.

Personalmente vorrei fare qualcosa del tipo:

e.SetPosition(4, 5, 6);
e.Name = "Blah";

Anche se ovviamente l'unico modo per arrivare a quel tipo di design è tornato in Entity-> NPC-> Enemy-> FlyingEnemy-> FlyingEnemyWithAHatUn tipo di gerarchia che questo design cerca di evitare.

Qualcuno ha visto un design per questo tipo di sistema di componenti che è ancora flessibile, ma mantiene un livello di facilità d'uso? E del resto, riesce a aggirare l'archiviazione dei dati (probabilmente il problema più difficile) in modo positivo?

Quali progetti esistono per un sistema di entità basato su componenti che sia facile da usare ma comunque flessibile?

Risposte:


13

Una delle cose che fa Unity è fornire alcuni helper accessor sull'oggetto di gioco principale per fornire un accesso più intuitivo ai componenti comuni.

Ad esempio, potresti avere la tua posizione memorizzata in un componente Trasforma. Usando il tuo esempio dovresti scrivere qualcosa del genere

e.GetComponent<Transform>().position = new Vector3( whatever );

Ma in Unity, questo viene semplificato

e.transform.position = ....;

Dov'è transformletteralmente solo un semplice metodo di supporto nella GameObjectclasse base ( Entityclasse nel tuo caso) che lo fa

Transform transform
{ 
    get { return this.GetComponent<Transform>(); }
}

Unity fa anche alcune altre cose, come impostare una proprietà "Nome" sull'oggetto di gioco stesso anziché nei suoi componenti figlio.

Personalmente non mi piace l'idea del tuo progetto di dati condivisi per nome. Accesso alle proprietà per nome invece che da una variabile e con l'utente anche necessario sapere che tipo si tratta solo sembra davvero soggetto a errori per me. Quello che fa Unity è che usano metodi simili alla GameObject transformproprietà all'interno delle Componentclassi per accedere ai componenti di pari livello. Quindi qualsiasi componente arbitrario che scrivi può semplicemente fare questo:

var myPos = this.transform.position;

Per accedere alla posizione. Dove quella transformproprietà fa qualcosa del genere

Transform transform
{
    get { return this.gameObject.GetComponent<Transform>(); }
}

Certo, è un po ' più prolisso del semplice dire e.position = whatever, ma ci si abitua e non sembra così cattivo come le proprietà generiche. E sì, dovresti farlo alla rotonda per i componenti del tuo client, ma l'idea è che tutti i tuoi componenti comuni "motore" (renderer, collider, sorgenti audio, trasformazioni, ecc.) Abbiano gli accessi facili.


Posso capire perché il sistema di dati condivisi per nome può essere un problema, ma in quale altro modo posso evitare il problema di più componenti che necessitano di dati (ovvero in quale archivio conservo qualcosa)? Stai solo molto attento in fase di progettazione?
Il comunista Duck

Inoltre, in termini di "avere l'utente deve anche sapere di che tipo è", non potrei semplicemente lanciarlo su property.GetType () nel metodo get ()?
L'anatra comunista

1
Che tipo di dati globali (nell'ambito dell'entità) stai spingendo verso questi repository di dati condivisi? Qualsiasi tipo di oggetto di gioco dovrebbe essere facilmente accessibile attraverso le classi del "motore" (trasformazione, collider, renderer, ecc.). Se stai apportando modifiche alla logica di gioco in base a questi "dati condivisi", è molto più sicuro possedere una classe proprietaria e in realtà assicurarti che il componente si trovi su quell'oggetto di gioco piuttosto che chiedere ciecamente di vedere se esiste una variabile denominata. Quindi sì, dovresti gestirlo in fase di progettazione. Puoi sempre creare un componente che memorizza i dati se ne hai davvero bisogno.
Tetrad,

@Tetrad - Puoi approfondire brevemente come funzionerebbe il tuo metodo GetComponent <Transform> proposto? Riesce a scorrere tutti i componenti di GameObject e restituisce il primo componente di tipo T? O sta succedendo qualcos'altro?
Michael

@Michael che avrebbe funzionato. Potresti semplicemente affidare al chiamante la responsabilità di memorizzarlo nella cache locale come miglioramento delle prestazioni.
Tetrado

3

Suggerirei che un qualche tipo di classe Interface per i tuoi oggetti Entity sarebbe carino. Potrebbe fare la gestione degli errori con il controllo per assicurarsi che un'entità contenga anche un componente del valore appropriato in una posizione, quindi non dovresti farlo ovunque tu acceda ai valori. In alternativa, la maggior parte dei progetti che io abbia mai fatto con un sistema basato su componenti, mi occupo direttamente dei componenti, richiedendo, ad esempio, il loro componente posizionale e quindi accedendo / aggiornando direttamente le proprietà di quel componente.

Di base a un livello elevato, la classe comprende l'entità in questione e fornisce un'interfaccia di facile utilizzo alle parti componenti sottostanti come segue:

public class EntityInterface
{
    private Entity entity { get; set };
    public EntityInterface(Entity e)
    {
        entity = e;
    }

    public Vector3 Position
    {
        get
        {
            return entity.GetProperty<Vector3>("Position");
        }
        set
        {
            entity.GetProperty<Vector3>("Position") = value;
        }
    }
}

Se suppongo che dovrei gestire NoPositionComponentException o qualcosa del genere, qual è il vantaggio di questo oltre a mettere tutto ciò nella classe Entity?
L'anatra comunista

Non essendo sicuro di cosa contenga effettivamente la tua classe di entità, non posso dire che ci sia un vantaggio o meno. Sostengo personalmente i sistemi di componenti in cui non esiste un'entità di base per dire semplicemente per evitare la domanda "Bene, cosa appartiene ad essa". Tuttavia, considererei una classe di interfaccia come questa una buona argomentazione perché esiste.
James,

Posso vedere come è più semplice (non devo interrogare la posizione tramite GetProperty), ma non vedo il vantaggio di questo modo invece di inserirlo nella classe Entity stessa.
L'anatra comunista

2

In Python puoi intercettare la parte 'setPosition' e.SetPosition(4,5,6)tramite la dichiarazione di una __getattr__funzione su Entity. Questa funzione può scorrere i componenti e trovare il metodo o la proprietà appropriati e restituirli, in modo che la chiamata o l'assegnazione della funzione vadano nel posto giusto. Forse C # ha un sistema simile, ma potrebbe non essere possibile a causa della sua e.setPositionscrittura statica - presumibilmente non può essere compilato a meno che e abbia impostatoPosizione nell'interfaccia da qualche parte.

Potresti anche fare in modo che tutte le tue entità implementino le interfacce pertinenti per tutti i componenti che potresti mai aggiungere a loro, con metodi stub che generano un'eccezione. Quindi quando si chiama addComponent, si reindirizza semplicemente le funzioni dell'entità per l'interfaccia di quel componente al componente. Un po 'complicato però.

Ma forse più semplice sarebbe quella di sovraccaricare l'operatore [] sulla tua classe Entity per cercare i componenti collegati per la presenza di una proprietà valida e restituire che, in modo che possa essere assegnato a questo modo: e["Name"] = "blah". Ciascun componente dovrà implementare il proprio GetPropertyByName e l'Entity chiama ciascuno a turno finché non trova il componente responsabile della proprietà in questione.


Avevo pensato di usare gli indicizzatori..questo è davvero bello. Aspetterò un po 'per vedere cos'altro appare, ma sto andando verso un mix di questo, il solito GetComponent () e alcune proprietà come @James suggerite per i componenti comuni.
L'anatra comunista

Forse mi manca ciò che viene detto qui, ma questo sembra più un sostituto di e.GetProperty <type> ("NameOfProperty"). Qualunque cosa con e ["NameOfProperty"]. Qualunque cosa ??
James,

@James È un involucro (molto simile al tuo design) .. tranne che è più dinamico - lo fa automaticamente e in modo più pulito del GetPropertymetodo .. L'unico aspetto negativo è la manipolazione e il confronto delle stringhe. Ma questo è un punto controverso.
L'anatra comunista

Ah, il mio malinteso lì. Supponevo che GetProperty facesse parte dell'entità (e quindi farebbe quanto descritto sopra) e non il metodo C #.
James,

2

Per espandere la risposta di Kylotan, se stai usando C # 4.0, puoi digitare staticamente una variabile per essere dinamico .

Se si eredita da System.Dynamic.DynamicObject, è possibile ignorare TryGetMember e TrySetMember (tra i molti metodi virtuali) per intercettare i nomi dei componenti e restituire il componente richiesto.

dynamic entity = EntityRegistry.Entities[1000];
entity.MyComponent.MyValue = 100;

Ho scritto un po 'sui sistemi di entità nel corso degli anni, ma presumo di non poter competere con i giganti. Alcune note ES (in qualche modo obsolete e non riflettono necessariamente la mia attuale comprensione dei sistemi di entità, ma vale la pena leggere, imho).


Grazie per questo, sarà d'aiuto :) Il modo dinamico non avrebbe lo stesso tipo di effetto del semplice casting su property.GetType (), comunque?
L'anatra comunista

Ci sarà un cast ad un certo punto, poiché TryGetMember ha un objectparametro out - ma tutto questo verrà risolto in fase di esecuzione. Penso che sia un modo glorificato per mantenere un'interfaccia fluida su ie entity. TryGetComponent (compName, componente fuori). Non sono sicuro di aver capito la domanda, comunque :)
Raine,

La domanda è che potrei usare tipi dinamici ovunque o (AFAICS) return (property.GetType ()) proprietà.
L'anatra comunista

1

Vedo molto interesse qui in ES su C #. Dai un'occhiata al mio port dell'eccellente implementazione ES Artemis:

https://github.com/thelinuxlich/artemis_CSharp

Inoltre, un gioco di esempio per iniziare su come funziona (usando XNA 4): https://github.com/thelinuxlich/starwarrior_CSharp

I suggerimenti sono benvenuti!


Sicuramente sembra carino. Personalmente non sono un fan dell'idea di tanti EntityProcessingSystems - nel codice, come funziona in termini di utilizzo?
Il comunista Duck il

Ogni sistema è un aspetto che elabora i suoi componenti dipendenti. Guarda questo per avere l'idea: github.com/thelinuxlich/starwarrior_CSharp/tree/master/…
thelinuxlich

0

Ho deciso di utilizzare l'eccellente sistema di riflessione di C #. L'ho basato sul sistema di componenti generali, oltre a una combinazione di risposte qui.

I componenti vengono aggiunti molto facilmente:

Entity e = new Entity(); //No templates/archetypes yet.
e.AddComponent(new HealthComponent(100));

E può essere interrogato per nome, tipo o (se comuni come salute e posizione) dalle proprietà:

e["Health"] //This, unfortunately, is a bit slower since it requires a dict search
e.GetComponent<HealthComponent>()
e.Health

E rimosso:

e.RemoveComponent<HealthComponent>();

I dati sono di nuovo accessibili comunemente dalle proprietà:

e.MaxHP

Quale è immagazzinato dal lato componente; meno spese generali se non ha HP:

public int? MaxHP
{
get
{

    return this.Health.MaxHP; //Error handling returns null if health isn't here. Excluded for clarity
}
set
{
this.Health.MaxHP = value;
}

Intellisense in VS supporta la grande quantità di digitazione, ma per lo più c'è poca digitazione - la cosa che stavo cercando.

Dietro le quinte, sto memorizzando un Dictionary<Type, Component>e ho Componentsscavalcato il ToStringmetodo per il controllo del nome. Il mio design originale utilizzava principalmente stringhe per nomi e cose, ma è diventato un maiale con cui lavorare (oh guarda, devo lanciare ovunque anche la mancanza di proprietà di facile accesso).

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.