Tattica per spostare la logica di rendering fuori dalla classe GameObject


10

Quando si creano giochi, si crea spesso il seguente oggetto di gioco da cui ereditano tutte le entità:

public class GameObject{
    abstract void Update(...);
    abstract void Draw(...);
}

Quindi, nel ciclo di aggiornamento, esegui l'iterazione su tutti gli oggetti del gioco e dai loro la possibilità di cambiare stato, quindi nel ciclo di estrazione successivo esegui nuovamente l'iterazione su tutti gli oggetti del gioco e dai loro la possibilità di disegnare se stessi.

Anche se funziona abbastanza bene in un gioco semplice con un semplice renderer in avanti, spesso porta ad alcuni oggetti di gioco giganteschi che devono memorizzare i loro modelli, trame multiple e, soprattutto, un metodo di disegno grasso che crea un accoppiamento stretto tra l'oggetto di gioco, la strategia di rendering corrente e tutte le classi correlate al rendering.

Se dovessi cambiare la strategia di rendering da avanti a differita, dovrei aggiornare molti oggetti di gioco. E gli oggetti di gioco che realizzo non sono riutilizzabili come potrebbero essere. Naturalmente l'ereditarietà e / o la composizione possono aiutarmi a combattere la duplicazione del codice e rendere un po 'più facile cambiare l'implementazione, ma mi sembra ancora carente.

Un modo migliore, forse, sarebbe rimuovere del tutto il metodo Draw dalla classe GameObject e creare una classe Renderer. GameObject dovrebbe comunque contenere alcuni dati relativi ai suoi elementi visivi, ad esempio con quale modello rappresentarlo e con quali trame dovrebbe essere dipinta sul modello, ma come sarà fatto al renderer. Tuttavia, ci sono spesso molti casi di confine nel rendering, quindi anche se questo rimuoverà l'accoppiamento stretto dal GameObject al Renderer, il Renderer dovrebbe comunque essere tutti a conoscenza di tutti gli oggetti di gioco che lo renderebbero grasso, sapendo e strettamente accoppiati. Ciò violerebbe alcune buone pratiche. Forse Design orientato ai dati potrebbe fare il trucco. Gli oggetti di gioco sarebbero certamente dati, ma come sarebbe guidato dal renderer? Non ne sono sicuro.

Quindi sono in perdita e non riesco a pensare a una buona soluzione. Ho provato ad usare i principi di MVC e in passato avevo alcune idee su come usarlo nei giochi, ma recentemente non sembra applicabile come pensavo. Mi piacerebbe sapere come affrontate tutti questo problema.

Ricapitoliamo comunque, sono interessato a come possono essere raggiunti i seguenti obiettivi di progettazione.

  • Nessuna logica di rendering nell'oggetto di gioco
  • Accoppiamento libero tra oggetti di gioco e motore di rendering
  • Nessun renderer che conosce tutti
  • Preferibilmente passaggio di runtime tra i motori di rendering

L'impostazione del progetto ideale sarebbe una "logica di gioco" separata e rendere un progetto di logica che non debba fare riferimento a vicenda.

Questo treno di pensiero è iniziato quando ho sentito John Carmack dire su Twitter che ha un sistema così flessibile da poter sostituire i motori di rendering in fase di esecuzione e persino dire al suo sistema di utilizzare entrambi i renderer (un renderer software e un renderer con accelerazione hardware) allo stesso tempo in modo da poter controllare le differenze. I sistemi che ho programmato finora non sono nemmeno così flessibili

Risposte:


7

Un primo passo veloce per disaccoppiare:

Gli oggetti di gioco fanno riferimento a un identificatore di ciò che sono le loro immagini ma non i dati, diciamo qualcosa di semplice come una stringa. Esempio: "human_male"

Renderer è responsabile del caricamento e della gestione dei riferimenti "human_male" e del ritorno agli oggetti di un handle da utilizzare.

Esempio in pseudocodice orribile:

GameObject( initialization parameters )
  me.render_handle = Renderer_Create( parameters.render_string )

- elsewhere
Renderer_Create( string )

  new data handle = Resources_Load( string );
  return new data handle

- some time later
GameObject( something happens to me parameters )
  me.state = something.what_happens
  Renderer_ApplyState( me.render_handle, me.state.effect_type )

- some time later
Renderer_Render()
  for each renderable thing
    for each rendering back end
        setup graphics for thing.effect
        render it

- finally
GameObject_Destroy()
  Renderer_Destroy( me.render_handle )

Ci scusiamo per quel casino, comunque le tue condizioni sono soddisfatte da quel semplice cambiamento lontano dal puro OOP basato sul guardare cose come oggetti del mondo reale e in OOP basato sulle responsabilità.

  • Nessuna logica di rendering nell'oggetto di gioco (fatto, tutto l'oggetto sa è una maniglia in modo che possa applicare effetti a se stesso)
  • Accoppiamento lento tra oggetti di gioco e motore di rendering (fatto, tutti i contatti avvengono tramite una maniglia astratta, gli stati che possono essere applicati e non cosa fare con quegli stati)
  • Nessun render render tutto conosciuto (fatto, sa solo di se stesso)
  • Preferibilmente il runtime cambia tra i motori di rendering (ciò avviene nella fase Renderer_Render (), ma devi scrivere entrambi i back-end)

Le parole chiave su cui si può cercare per andare oltre un semplice refactoring di classi sarebbero "sistema entità / componente" e "iniezione di dipendenza" e potenzialmente modelli di interfaccia grafica "MVC" solo per far girare i vecchi ingranaggi del cervello.


Questo è estremamente diverso da quello che ho fatto prima, sembra che abbia un bel po 'di potenziale. Fortunatamente non sono vincolato da alcun motore esistente, quindi posso solo armeggiare. Cercherò anche i termini che hai menzionato, anche se l'iniezione di dipendenza mi fa sempre male al cervello: P.
Roy T.,

2

Quello che ho fatto per il mio motore è di raggruppare tutto in moduli. Quindi ho la mia GameObjectclasse e ha una maniglia per:

  • ModuleSprite - disegno di sprite
  • ModuleWeapon - sparare pistole
  • ModuleScriptingBase - scripting
  • ModuleParticles - effetti particellari
  • ModuleCollision: rilevamento e risposta delle collisioni

Quindi ho una Playerclasse e una Bulletlezione. Entrambi derivano da GameObjecte vengono aggiunti a Scene. Ma Playerha i seguenti moduli:

  • ModuleSprite
  • ModuleWeapon
  • ModuleParticles
  • ModuleCollision

E Bulletha questi moduli:

  • ModuleSprite
  • ModuleCollision

Questo modo di organizzare le cose evita il "Diamante della Morte" dove hai un Vehicle, un VehicleLande un VehicleWatere ora vuoi un VehicleAmphibious. Invece hai un Vehiclee può avere un ModuleWatere un ModuleLand.

Bonus aggiunto: puoi creare oggetti usando un set di proprietà. Tutto quello che devi sapere è il tipo di base (Player, Enemy, Bullet, ecc.) E quindi creare handle per i moduli necessari per quel tipo.

Nella mia scena, faccio quanto segue:

  • Chiama Updateper tutte le GameObjectmaniglie.
  • Effettuare il controllo delle collisioni e la risposta alle collisioni per coloro che hanno una ModuleCollisionmaniglia.
  • Chiama il UpdatePostper tutte le GameObjectmaniglie per far conoscere la loro posizione finale dopo la fisica.
  • Distruggi gli oggetti con la loro bandiera impostata.
  • Aggiungi nuovi oggetti m_ObjectsCreateddall'elenco m_Objectsall'elenco.

E potrei organizzarlo ulteriormente: per moduli anziché per oggetto. Quindi vorrei rendere un elenco di ModuleSprite, aggiornare un gruppo di ModuleScriptingBasee fare collisioni con un elenco di ModuleCollision.


Sembra la composizione al massimo! Molto bella. Tuttavia, non vedo molti suggerimenti specifici sul rendering qui. Come lo gestisci, semplicemente aggiungendo moduli diversi?
Roy T.,

Oh si. Questo è il rovescio della medaglia di questo sistema: se hai un requisito specifico per un GameObject(ad esempio un modo per rendere un "serpente" di Sprites) dovrai o creare un figlio ModuleSpriteper quella specifica funzionalità ( ModuleSpriteSnake) o aggiungere del tutto un nuovo modulo ( ModuleSnake). Fortunatamente sono solo dei puntatori, ma ho visto del codice in cui GameObjectfaceva letteralmente tutto ciò che un oggetto poteva fare.
knight666,
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.