Quali sono i contro dell'utilizzo di DrawableGameComponent per ogni istanza di un oggetto di gioco?


25

Ho letto in molti posti che DrawableGameComponents dovrebbe essere salvato per cose come "livelli" o qualche tipo di manager invece di usarli, ad esempio, per personaggi o tessere (come dice questo ragazzo qui ). Ma non capisco perché sia ​​così. Ho letto questo post e ha avuto molto senso per me, ma questi sono la minoranza.

Di solito non presterei troppa attenzione a cose come queste, ma in questo caso vorrei sapere perché la maggioranza apparente ritiene che questa non sia la strada da percorrere. Forse mi manca qualcosa.


Sono curioso di sapere perché, due anni dopo, hai rimosso il segno "accettato" dalla mia risposta ? Normalmente non me ne importerebbe davvero, ma in questo caso provoca la falsa rappresentazione esasperante di VeraShackle della mia risposta a bolle in alto :(
Andrew Russell

@AndrewRussell Ho letto questa discussione non molto tempo fa (a causa di alcune risposte e voti) e ho pensato che non ero qualificato a dare un'opinione sul fatto che la tua risposta sia corretta o meno. Come dici tu, però, non penso che la risposta di VeraShackle dovrebbe essere quella con il maggior numero di voti, quindi segnerò di nuovo la tua risposta come corretta.
Kenji Kina

1
Posso condensare la mia risposta qui come segue: "L' utilizzo DrawableGameComponentti blocca in un unico set di firme di metodi e in uno specifico modello di dati conservati. Queste sono inizialmente una scelta errata e il lock-in ostacola in modo significativo l'evoluzione del codice in futuro. "Ma lascia che ti dica cosa sta succedendo qui ...
Andrew Russell,

1
DrawableGameComponentfa parte dell'API XNA di base (di nuovo: principalmente per motivi storici). I programmatori inesperti lo usano perché è "lì" e "funziona" e non conoscono meglio. Vedono la mia risposta dicendo loro che sono " sbagliati " e - a causa della natura della psicologia umana - lo respingono e scelgono l'altro "lato". Che sembra essere la non risposta polemica di VeraShackle; che è successo a prendere slancio perché non l'ho chiamato immediatamente (vedi quei commenti). Sento che la mia eventuale confutazione rimane sufficiente.
Andrew Russell,

1
Se qualcuno è interessato a mettere in discussione la legittimità della mia risposta sulla base di voti positivi: mentre è vero che (GDSE è in difficoltà con i suddetti novizi) la risposta di VeraShackle è riuscita ad acquisire un paio di voti in più rispetto alla mia negli ultimi anni, non dimenticare che ho altre migliaia di voti a livello di rete, in gran parte su XNA. Ho implementato l'API XNA su più piattaforme. E sono uno sviluppatore di giochi professionale che utilizza XNA. Il pedigree della mia risposta è impeccabile.
Andrew Russell,

Risposte:


22

I "componenti di gioco" erano una parte molto antica del progetto XNA 1.0. Avrebbero dovuto funzionare come controlli per WinForms. L'idea era che saresti stato in grado di trascinarli nel tuo gioco usando una GUI (un po 'come il bit per aggiungere controlli non visivi, come timer, ecc.) E impostarne le proprietà.

Quindi potresti avere componenti come forse una fotocamera o un ... contatore FPS? ... o ...?

Vedi? Sfortunatamente questa idea non funziona davvero per i giochi del mondo reale. Il tipo di componenti che desideri per un gioco non è realmente riutilizzabile. Potresti avere un componente "giocatore", ma sarà molto diverso dal componente "giocatore" di ogni altro gioco.

Immagino sia stato per questo motivo, e per una semplice mancanza di tempo, che la GUI è stata estratta prima di XNA 1.0.

Ma l'API è ancora utilizzabile ... Ecco perché non dovresti usarlo:

Fondamentalmente si riduce a ciò che è più facile da scrivere, leggere, eseguire il debug e mantenere come sviluppatore di giochi. Fare qualcosa del genere nel codice di caricamento:

player.DrawOrder = 100;
enemy.DrawOrder = 200;

È molto meno chiaro del semplice fare questo:

virtual void Draw(GameTime gameTime)
{
    player.Draw();
    enemy.Draw();
}

Soprattutto quando, in un gioco del mondo reale, potresti finire con qualcosa del genere:

GraphicsDevice.Viewport = cameraOne.Viewport;
player.Draw(cameraOne, spriteBatch);
enemy.Draw(cameraOne, spriteBatch);

GraphicsDevice.Viewport = cameraTwo.Viewport;
player.Draw(cameraTwo, spriteBatch);
enemy.Draw(cameraTwo, spriteBatch);

(Certo, potresti condividere la videocamera e spriteBatch usando Servicesun'architettura simile . Ma questa è anche una cattiva idea per lo stesso motivo.)

Questa architettura - o la sua mancanza - è molto più leggera e flessibile!

Posso facilmente cambiare l'ordine di disegno o aggiungere più finestre o aggiungere un effetto target di rendering, semplicemente cambiando alcune righe. Per farlo nell'altro sistema, mi viene da pensare a cosa stanno facendo tutti quei componenti, distribuiti su più file, e in quale ordine.

È un po 'come la differenza tra la programmazione dichiarativa e quella imperativa. Si scopre che, per lo sviluppo del gioco, la programmazione imperativa è molto più preferibile.


2
Avevo l'impressione che fossero per la programmazione basata su componenti , che apparentemente è ampiamente usata per la programmazione di giochi.
BlueRaja - Danny Pflughoeft

3
Le classi dei componenti del gioco XNA non sono realmente direttamente applicabili a quel modello. Questo modello consiste nel comporre oggetti da più componenti. Le classi XNA sono orientate a un componente per oggetto. Se si adattano perfettamente al tuo design, allora usali sicuramente. Ma si dovrebbe non costruire il vostro disegno intorno a loro! Vedi anche la mia risposta qui - cerca dove menziono DrawableGameComponent.
Andrew Russell,

1
Concordo sul fatto che l'architettura GameComponent / Service non è poi così utile e sembra che concetti simili a Office o applicazioni Web vengano forzati in un framework di gioco. Ma preferirei di gran lunga usare il player.DrawOrder = 100;metodo su quello imperativo per l'ordine di disegno. Sto finendo un gioco Flixel in questo momento, che disegna gli oggetti nell'ordine in cui li aggiungi alla scena. Il sistema FlxGroup allevia un po 'di questo, ma avere una sorta di ordinamento Z-index integrato sarebbe stato bello.
michael.bartnett,

@ michael.bartnett Nota che Flixel è un'API in modalità mantenuta, mentre XNA (tranne, ovviamente, il sistema dei componenti di gioco) è un'API in modalità immediata. Se si utilizza un'API in modalità mantenuta o si implementa la propria , la possibilità di modificare al volo l'ordine di disegno è sicuramente importante. In effetti, la necessità di cambiare l'ordine di disegno al volo è una buona ragione per creare qualcosa in modalità mantenuta (viene in mente l'esempio di un sistema a finestre). Ma se puoi scrivere qualcosa in modalità immediata, se l'API della piattaforma (come XNA) è costruita in questo modo, allora IMO dovresti.
Andrew Russell,


26

Hmmm. Ho sfidato questo per amore di un po 'di equilibrio qui, temo.

Sembra che ci siano 5 accuse nella risposta di Andrew, quindi proverò a gestirle a turno:

1) I componenti sono un design obsoleto e abbandonato.

L'idea del modello di progettazione dei componenti esisteva molto prima di XNA - in sostanza è semplicemente una decisione di prendere oggetti, piuttosto che l'oggetto di gioco sovrastante, la casa del proprio comportamento di aggiornamento e disegno. Per gli oggetti nella sua raccolta di componenti, il gioco chiamerà questi metodi (e un paio di altri) automaticamente al momento opportuno - fondamentalmente l'oggetto di gioco non deve "conoscere" proprio questo codice. Se desideri riutilizzare le tue classi di gioco in diversi giochi (e quindi in diversi oggetti di gioco), questo disaccoppiamento di oggetti è comunque una buona idea. Una rapida occhiata alle ultime demo (prodotte professionalmente) di XNA4.0 dall'AppHub conferma la mia impressione che Microsoft sia ancora (giustamente a mio avviso) saldamente dietro a questa.

2) Andrew non riesce a pensare a più di 2 classi di gioco riutilizzabili.

Io posso. In effetti posso pensare a un sacco! Ho appena controllato la mia attuale libreria condivisa e comprende oltre 80 componenti e servizi riutilizzabili . Per darti un sapore, potresti avere:

  • Gestori di stato / schermo del gioco
  • ParticleEmitters
  • Primitivi disegnabili
  • Controller AI
  • Controller di input
  • Controller di animazione
  • cartelloni
  • Gestori di fisica e collisione
  • Macchine fotografiche

Ogni particolare idea di gioco avrà bisogno di almeno 5-10 cose da questo set - garantito - e possono essere completamente funzionali in un gioco completamente nuovo con una riga di codice.

3) Il controllo dell'ordine di sorteggio in base all'ordine delle chiamate di sorteggio esplicite è preferibile all'uso della proprietà DrawOrder del componente.

Bene, sono sostanzialmente d'accordo con Andrew su questo. MA, se lasci tutte le proprietà DrawOrder del componente come predefinite, puoi comunque controllare esplicitamente il loro ordine di disegno dall'ordine in cui le aggiungi alla collezione. Questo è veramente identico in termini di "visibilità" all'ordinamento esplicito delle loro chiamate di estrazione manuali.

4) Chiamare un carico di metodi Draw di un oggetto da soli è più "leggero" che farli chiamare con un metodo framework esistente.

Perché? Inoltre, in tutti i progetti, tranne il più banale, la tua logica di aggiornamento e il codice Draw oscureranno totalmente il tuo metodo di chiamata in termini di prestazioni in ogni caso.

5) Posizionare il codice in un file è preferibile, durante il debug, piuttosto che averlo nel file del singolo oggetto.

Bene, in primo luogo, quando eseguo il debug in Visual Studio, la posizione di un punto di interruzione in termini di file su disco è quasi invisibile in questi giorni: l'IDE si occupa della posizione senza alcun dramma. Ma considera anche per un momento che hai un problema con il tuo disegno Skybox. Il metodo Draw di Skybox è davvero più oneroso rispetto alla localizzazione del codice di disegno skybox nel metodo (presumibilmente molto lungo) di master draw nell'oggetto Game? No, non proprio - in effetti direi che è esattamente il contrario.

Quindi, in sintesi, ti preghiamo di dare un'occhiata alle argomentazioni di Andrew qui. Non credo che forniscano fondamentalmente motivi per scrivere codice di gioco impigliato. I lati positivi del modello di componente disaccoppiato (usato con giudizio) sono enormi.


2
Ho appena notato questo post e devo emettere una forte confutazione: non ho problemi con le classi riutilizzabili! (Che possono avere metodi di disegno e aggiornamento e così via.) Sto riscontrando un problema specifico con l'utilizzo di ( Drawable) GameComponentper fornire la tua architettura di gioco, a causa della perdita di controllo esplicito sull'ordine di disegno / aggiornamento (che sei d'accordo con # 3) sulla rigidità delle loro interfacce (di cui non ti occupi).
Andrew Russell,

1
Il tuo punto 4 usa il termine "leggero" per indicare prestazioni, dove in realtà intendo chiaramente flessibilità architettonica. I punti rimanenti n. 1, n. 2 e in particolare il n. 5 sembrano erigere l' assurdo uomo di paglia che penso che la maggior parte / tutto il codice dovrebbe essere inserito nella tua classe di gioco principale! Leggi la mia risposta qui per alcuni dei miei pensieri più dettagliati sull'architettura.
Andrew Russell,

5
Infine: se hai intenzione di pubblicare una risposta alla mia risposta, dicendo che mi sbaglio, sarebbe stato cortese aggiungere anche una risposta alla mia risposta in modo da vedere una notifica al riguardo, piuttosto che incappare in che due mesi più tardi.
Andrew Russell,

Potrei aver frainteso, ma come risponde alla domanda? Dovrei usare un GameComponent per i miei sprite o no?
Superbo


4

Non sono d'accordo con Andrew, ma penso anche che ci sia un tempo e un posto per tutto. Non userei a Drawable(GameComponent)per qualcosa di così semplice come uno Sprite o un nemico. Tuttavia, lo userei per un SpriteManagero EnemyManager.

Ritornando a ciò che Andrew ha detto riguardo a disegnare e aggiornare l'ordine, penso che Sprite.DrawOrdersarebbe molto confuso per molti sprite. Inoltre, è relativamente facile impostare un DrawOrdere UpdateOrderper un numero limitato (o ragionevole) di manager che lo sono (Drawable)GameComponents.

Penso che Microsoft avrebbe eliminato con loro se fossero stati davvero rigidi e nessuno li avrebbe usati. Ma non lo fanno perché funzionano perfettamente bene, se il ruolo è giusto. Io stesso li uso per sviluppare Game.Servicesche vengono aggiornati in background.


2

Stavo pensando anche a questo, e mi è venuto in mente: per esempio, per rubare l'esempio nel tuo link, in un gioco RTS potresti rendere ogni unità un'istanza del componente di gioco. Va bene.

Ma potresti anche creare un componente UnitManager, che mantiene un elenco di unità, le disegna e le aggiorna se necessario. Immagino che il motivo per cui vuoi trasformare le tue unità in componenti di gioco sia innanzitutto quello di compartimentare il codice, quindi questa soluzione raggiungerebbe adeguatamente quell'obiettivo?


2

Nei commenti, ho pubblicato una versione ridotta della mia risposta vecchia risposta:

L'utilizzo DrawableGameComponentti blocca in un unico set di firme dei metodi e in uno specifico modello di dati conservati. Inizialmente si tratta in genere di una scelta errata e il lock-in ostacola in modo significativo l'evoluzione del codice in futuro.

Qui cercherò di illustrare perché questo è il caso inviando un po 'di codice dal mio progetto attuale.


L'oggetto root che mantiene lo stato del mondo di gioco ha un Updatemetodo simile al seguente:

void Update(UpdateContext updateContext, MultiInputState currentInput)

Esistono diversi punti di accesso Update, a seconda che il gioco funzioni o meno sulla rete. È possibile, ad esempio, che lo stato del gioco venga ripristinato a un punto temporale precedente e quindi nuovamente simulato.

Esistono anche altri metodi simili agli aggiornamenti, come:

void PlayerJoin(int playerIndex, string playerName, UpdateContext updateContext)

All'interno dello stato del gioco c'è un elenco di attori da aggiornare. Ma questa è più di una semplice Updatechiamata. Ci sono passaggi aggiuntivi prima e dopo per gestire la fisica. Ci sono anche alcune cose fantasiose per la generazione / distruzione differita di attori, oltre alle transizioni di livello.

Al di fuori dello stato del gioco, esiste un sistema di interfaccia utente che deve essere gestito in modo indipendente. Tuttavia, alcune schermate dell'interfaccia utente devono essere in grado di funzionare anche in un contesto di rete.


Per disegnare, a causa della natura del gioco, esiste un sistema di ordinamento e abbattimento personalizzato che accetta input da questi metodi:

virtual void RegisterToDraw(SortedDrawList sortedDrawList, Definitions definitions)
virtual void RegisterShadow(ShadowCasterList shadowCasterList, Definitions definitions)

E poi, una volta capito cosa disegnare, chiama automaticamente:

virtual void Draw(DrawContext drawContext, int tag)

Il tagparametro è obbligatorio perché alcuni attori possono aggrapparsi ad altri attori e quindi devono essere in grado di disegnare sia sopra che sotto l'attore detenuto. Il sistema di smistamento garantisce che ciò avvenga nell'ordine corretto.

C'è anche il sistema di menu da disegnare. E un sistema gerarchico per disegnare l'interfaccia utente, attorno all'HUD, attorno al gioco.


Ora confronta questi requisiti con ciò che DrawableGameComponentprevede:

public class DrawableGameComponent
{
    public virtual void Update(GameTime gameTime);
    public virtual void Draw(GameTime gameTime);
    public int UpdateOrder { get; set; }
    public int DrawOrder { get; set; }
}

Non esiste un modo ragionevole per passare da un sistema DrawableGameComponental sistema sopra descritto. (E quelli non sono requisiti insoliti per un gioco di complessità anche modesta).

Inoltre, è molto importante notare che tali requisiti non sono semplicemente emersi all'inizio del progetto. Si sono evoluti in molti mesi di lavoro di sviluppo.


Ciò che le persone fanno in genere, se iniziano lungo il DrawableGameComponentpercorso, iniziano a costruire sugli hack:

  • Invece di aggiungere metodi, fanno un brutto down-casting al proprio tipo di base (perché non usarlo direttamente?).

  • Invece di sostituire il codice per l'ordinamento e il richiamo Draw/ Update, fanno alcune cose molto lente per cambiare i numeri di ordinazione al volo.

  • E la cosa peggiore di tutte: invece di aggiungere parametri ai metodi, iniziano a "passare" "parametri" in posizioni di memoria che non sono lo stack (l'uso di Servicesper questo è popolare). Questo è contorto, soggetto a errori, lento, fragile, raramente sicuro, e probabilmente diventerà un problema se si aggiungono reti, si creano strumenti esterni e così via.

    Rende anche più difficile il riutilizzo del codice , perché ora le tue dipendenze sono nascoste all'interno del "componente", anziché essere pubblicate al limite dell'API.

  • Oppure vedono quasi quanto sia folle tutto questo e iniziano a fare "manager" DrawableGameComponentper aggirare questi problemi. Tranne che hanno ancora questi problemi ai confini del "manager".

    Invariabilmente questi "gestori" potrebbero essere facilmente sostituiti con una serie di chiamate di metodo nell'ordine desiderato. Qualcos'altro è troppo complicato.

Naturalmente, una volta che inizi a utilizzare quegli hack, ci vuole un vero lavoro per annullare quegli hack una volta che ti rendi conto che hai bisogno di più di quello che DrawableGameComponentfornisce. Diventi " bloccato ". E la tentazione è spesso quella di continuare ad accumulare hack - che in pratica ti impantaneranno e limiteranno i tipi di giochi che puoi realizzare a giochi relativamente semplicistici.


Molte persone sembrano arrivare a questo punto e hanno una reazione viscerale, probabilmente perché usano DrawableGameComponente non vogliono accettare di essere "sbagliate". Quindi si aggrappano al fatto che è almeno possibile farlo "funzionare".

Naturalmente può essere fatto di "lavorare"! Siamo programmatori: far funzionare le cose con restrizioni insolite è praticamente il nostro lavoro. Ma questa limitazione non è necessaria, utile o razionale ...


Ciò che è particolarmente sbalorditivo è che è sorprendentemente banale affiancare l'intera questione. Qui, ti darò anche il codice:

public class Actor
{
    public virtual void Update(GameTime gameTime);
    public virtual void Draw(GameTime gameTime);
}

List<Actor> actors = new List<Actor>();

foreach(var actor in actors)
    actor.Update(gameTime);

foreach(var actor in actors)
    actor.Draw(gameTime);

(È semplice aggiungere l'ordinamento come da DrawOrdere UpdateOrder. Un modo semplice è quello di mantenere due elenchi e utilizzare List<T>.Sort(). È anche banale gestire Load/ UnloadContent.)

Non è importante che cosa sia effettivamente quel codice . Ciò che è importante è che posso modificarlo banalmente .

Quando mi rendo conto che ho bisogno di un diverso sistema di temporizzazione gameTime, o ho bisogno di più parametri (o di un intero UpdateContextoggetto con circa 15 cose al suo interno), o ho bisogno di un ordine di operazioni completamente diverso per il disegno, o ho bisogno di alcuni metodi extra per diversi passaggi di aggiornamento, posso solo andare avanti e farlo .

E posso farlo immediatamente. Non ho bisogno di pensare a come scegliere DrawableGameComponentil mio codice. O peggio: hackeralo in qualche modo che renderà ancora più difficile la prossima volta che tale necessità si presenta.


Esiste davvero solo uno scenario in cui è una buona scelta da usare DrawableGameComponent: vale a dire se stai letteralmente creando e pubblicando middleware in stile componente drag-and-drop per XNA. Qual era il suo scopo originale. Quale - aggiungerò - nessuno lo sta facendo!

(Allo stesso modo, ha davvero senso usare soloServices quando si creano tipi di contenuto personalizzati ContentManager.)


Sebbene io sia d'accordo con i tuoi punti, non vedo nulla di particolarmente sbagliato nell'usare cose che ereditano da DrawableGameComponentalcuni componenti, specialmente per cose come schermate di menu (menu, molti pulsanti, opzioni e cose) o sovrapposizioni di FPS / debug hai nominato. In questi casi di solito non è necessario un controllo accurato sull'ordine di disegno e si finisce con meno codice che è necessario scrivere manualmente (il che è una buona cosa, a meno che non sia necessario scrivere quel codice). Ma immagino che sia praticamente lo scenario drag-and-drop di cui hai parlato.
Groo,
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.