Sono sulla strada giusta con questa architettura componente?


9

Di recente ho deciso di rinnovare la mia architettura di gioco per sbarazzarmi di gerarchie di classe profonde e sostituirle con componenti configurabili. La prima gerarchia che sto sostituendo è la gerarchia degli articoli e vorrei alcuni consigli per sapere se sono sulla buona strada.

In precedenza, avevo una gerarchia simile a questa:

Item -> Equipment -> Weapon
                  -> Armor
                  -> Accessory
     -> SyntehsisItem
     -> BattleUseItem -> HealingItem
                      -> ThrowingItem -> ThrowsAsAttackItem

Inutile dire che stava iniziando a diventare disordinato e queste non erano una soluzione facile per gli oggetti che dovevano essere di più tipi (cioè alcune attrezzature vengono utilizzate nella sintesi degli oggetti, alcune sono gettabili, ecc.)

Ho quindi tentato di refactoring e posizionare funzionalità nella classe di articoli di base. Ma poi ho notato che l'articolo aveva molti dati inutilizzati / superflui. Ora sto provando a fare un componente come l'architettura, almeno per i miei oggetti prima di provare a farlo con le mie altre classi di gioco.

Ecco cosa sto pensando attualmente per l'installazione del componente:

Ho una classe di articoli di base che ha slot per vari componenti (ad es. Uno per componenti di equipaggiamento, uno per componenti di guarigione, ecc. Così come una mappa per componenti arbitrari) quindi qualcosa del genere:

class Item
{
    //Basic item properties (name, ID, etc.) excluded
    EquipmentComponent* equipmentComponent;
    HealingComponent* healingComponent;
    SynthesisComponent* synthesisComponent;
    ThrowComponent* throwComponent;
    boost::unordered_map<std::string, std::pair<bool, ItemComponent*> > AdditionalComponents;
} 

Tutti i componenti dell'articolo erediterebbero da una classe ItemComponent di base e ogni tipo di componente è responsabile di dire al motore come implementare quella funzionalità. vale a dire, il HealingComponent dice ai meccanici di battaglia come consumare l'oggetto come un oggetto di guarigione, mentre il ThrowComponent dice al motore di battaglia come trattare l'oggetto come un oggetto da lanciare.

La mappa viene utilizzata per archiviare componenti arbitrari che non sono componenti di base. Lo sto associando a un bool per indicare se il contenitore degli articoli deve gestire ItemComponent o se è gestito da una fonte esterna.

La mia idea qui era quella di definire i componenti principali utilizzati dal mio motore di gioco in anticipo, e la mia fabbrica di oggetti avrebbe assegnato i componenti che l'oggetto effettivamente ha, altrimenti sarebbero nulli. La mappa conterrebbe componenti arbitrari che sarebbero generalmente aggiunti / consumati dai file di script.

La mia domanda è: è un buon design? In caso contrario, come può essere migliorato? Ho considerato il raggruppamento di tutti i componenti nella mappa, ma l'utilizzo dell'indicizzazione delle stringhe sembrava non necessario per i componenti degli elementi principali

Risposte:


8

Sembra un primo passo molto ragionevole.

Stai optando per una combinazione di generalità (la mappa dei "componenti aggiuntivi") e prestazioni di ricerca (i membri hardcoded), che potrebbe essere un po 'una pre-ottimizzazione - il tuo punto sull'inefficienza generale della stringa la ricerca è ben fatta, ma puoi alleviarla scegliendo di indicizzare i componenti con qualcosa di più veloce per l'hash. Un approccio potrebbe essere quello di assegnare a ciascun tipo di componente un ID di tipo univoco (essenzialmente stai implementando RTTI personalizzati leggeri ) e un indice basato su quello.

Indipendentemente da ciò, ti avvertirei di esporre un'API pubblica per l'oggetto Item che ti consente di richiedere qualsiasi componente - quelli hardcoded e quelli aggiuntivi - in modo uniforme. Ciò renderebbe più semplice modificare la rappresentazione o l'equilibrio sottostante dei componenti hardcoded / non hardcoded senza dover riformattare tutti i client dei componenti dell'articolo.

Potresti anche considerare di fornire versioni non fittizie "fittizie" di ciascuno dei componenti hardcoded e assicurarti che siano sempre assegnati: puoi quindi utilizzare i membri di riferimento anziché i puntatori e non dovrai mai cercare un puntatore NULL prima di interagire con una delle classi di componenti hardcoded. Sosterrai comunque il costo dell'invio dinamico per interagire con i membri di quel componente, ma ciò si verificherebbe anche con i membri puntatore. Questo è più un problema di pulizia del codice perché l'impatto sulle prestazioni sarà trascurabile con ogni probabilità.

Non penso sia una grande idea avere due diversi tipi di ambiti di vita (in altre parole, non penso che il bool che hai nella mappa aggiuntiva dei componenti sia un'ottima idea). Ciò complica il sistema e implica che la distruzione e il rilascio di risorse non saranno terribilmente deterministici. L'API per i tuoi componenti sarebbe molto più chiara se avessi optato per una strategia di gestione della durata o l'altra - o l'entità gestisce la durata del componente, o il sottosistema che realizza i componenti lo fa (preferisco quest'ultimo perché si accoppia meglio con il componente esterno approccio, di cui parlerò in seguito).

Il grande svantaggio che vedo nel tuo approccio è che stai raggruppando tutti i componenti nell'oggetto "entità", che in realtà non è sempre il miglior design. Dalla mia risposta correlata a un'altra domanda basata sui componenti:

Il tuo approccio all'utilizzo di una grande mappa di componenti e una chiamata update () nell'oggetto di gioco è piuttosto subottimale (e una trappola comune per coloro che costruiscono prima questo tipo di sistemi). Garantisce una scarsa coerenza della cache durante l'aggiornamento e non consente di sfruttare la concorrenza e la tendenza verso il processo in stile SIMD di grandi quantità di dati o comportamenti contemporaneamente. Spesso è meglio usare un design in cui l'oggetto di gioco non aggiorna i suoi componenti, ma piuttosto il sottosistema responsabile del componente stesso li aggiorna tutti in una volta.

In sostanza stai adottando lo stesso approccio memorizzando i componenti nell'entità articolo (che è, di nuovo, un primo passo del tutto accettabile). Quello che potresti scoprire è che la maggior parte dell'accesso ai componenti di cui sei preoccupato per le prestazioni è solo quello di aggiornarli e se scegli di utilizzare un approccio più esterno all'organizzazione dei componenti, in cui i componenti sono mantenuti in una cache coerente , struttura dei dati efficiente (per il loro dominio) da parte di un sottosistema che comprende di più le loro esigenze, è possibile ottenere prestazioni di aggiornamento molto migliori e più parallelizzabili.

Ma lo sottolineo solo come qualcosa da considerare come una direzione futura - non vorrai certo esagerare con questo ingegno; puoi fare una transizione graduale attraverso il refactoring costante o potresti scoprire che la tua attuale implementazione soddisfa perfettamente le tue esigenze e non è necessario iterare su di essa.


1
+1 per aver suggerito di eliminare l'oggetto Oggetto. Mentre sarà più lavoro in anticipo, finirà per produrre un sistema di componenti migliore.
James,

Ho avuto qualche altra domanda, non sono sicuro se dovrei iniziare un nuovo topiuc, quindi proverò prima qui sotto: per la mia classe di articoli, non ci sono metodi che chiamerò evert frame (o anche close). Per i miei sottosistemi grafici, prenderò il tuo consiglio e terrò tutti gli oggetti da aggiornare sotto il sistema. L'altra domanda che ho avuto è come gestire i controlli dei componenti? Come ad esempio, voglio vedere se posso usare un oggetto come X, quindi naturalmente vorrei verificare se l'articolo ha il componente necessario per eseguire X. È questo il modo giusto per farlo? Grazie ancora per la risposta
user127817
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.