Come evitare la codifica rigida nei motori di gioco


22

La mia domanda non è una domanda di codifica; si applica a tutto il design del motore di gioco in generale.

Come evitare la codifica hard?

Questa domanda è molto più profonda di quanto sembri. Ad esempio, se si desidera eseguire un gioco che carica i file necessari per il funzionamento, come si evita di dire qualcosa come load specificfile.wadnel codice del motore? Inoltre, quando il file viene caricato, come evitare di direload aspecificmap in specificfile.wad ?

Questa domanda si applica praticamente a tutto il design del motore e il meno possibile del motore dovrebbe essere codificato. Qual è il modo migliore per raggiungere questo obiettivo?

Risposte:


42

Codifica basata sui dati

Ogni cosa che menzioni è qualcosa che può essere specificato nei dati. Perché stai caricando aspecificmap? Perché la configurazione del gioco dice che è di primo livello quando un giocatore inizia una nuova partita, o perché quello è il nome del punto di salvataggio corrente nel file di salvataggio del giocatore che ha appena caricato, ecc.

Come lo trovi aspecificmap? Perché è in un file di dati che elenca gli ID delle mappe e le loro risorse su disco.

È necessario solo un insieme particolarmente ristretto di risorse "core" che sono legittimamente difficili o impossibili da evitare. Con un po 'di lavoro, questo può essere limitato a un singolo nome di asset predefinito codificato comemain.wad o simile. Questo file può potenzialmente essere modificato in fase di esecuzione passando un argomento della riga di comando al gioco, aka game.exe -wad mymain.wad.

La scrittura di codice basato sui dati si basa su alcuni altri principi. Ad esempio, si può evitare che sistemi o moduli richiedano una particolare risorsa e invertire invece tali dipendenze. Cioè, non DebugDrawercaricare debug.fontnel suo codice di inizializzazione; invece, è necessario DebugDrawerprendere un handle di risorse nel suo codice di inizializzazione. Tale handle potrebbe essere caricato dal file di configurazione del gioco principale.

Come esempi concreti della nostra base di codice, abbiamo un oggetto "dati globali" che viene caricato dal database delle risorse (che di per sé è la ./resourcescartella ma può essere sovraccaricato con un argomento della riga di comando). L'ID del database di risorse di questi dati globali è l'unico nome di risorsa hardcoded necessario nella base di codice (ne abbiamo altri perché a volte i programmatori diventano pigri, ma alla fine finiamo per risolverli / rimuoverli). Questo oggetto dati globale è pieno di componenti il ​​cui unico scopo è fornire dati di configurazione. Uno dei componenti è il componente Dati globali dell'interfaccia utente che contiene gli handle di risorse per tutte le principali risorse dell'interfaccia utente (caratteri, file Flash, icone, dati di localizzazione, ecc.) Tra una serie di altri elementi di configurazione. Quando uno sviluppatore dell'interfaccia utente decide di rinominare l'asset dell'interfaccia utente principale da /ui/mainmenu.swfa/ui/lobby.swfaggiornano semplicemente quel riferimento globale di dati; nessun codice motore deve cambiare affatto.

Utilizziamo questi dati globali per tutto. Tutti i personaggi giocabili, tutti i livelli, l'interfaccia utente, l'audio, le risorse principali, la configurazione di rete, tutto. (beh, non tutto , ma quelle altre cose sono bug da correggere.)

Questo approccio ha molti altri vantaggi. Per uno, rende l'imballaggio e il raggruppamento delle risorse parte integrante dell'intero processo. I percorsi di codifica rigida nel motore tendono anche a significare che quegli stessi percorsi devono essere codificati in modo rigido in qualsiasi script o strumento impacchetta le risorse di gioco e tali percorsi possono quindi non essere sincronizzati. Basandoci invece su un singolo asset principale e catene di riferimento da lì, possiamo costruire un bundle di asset con un singolo comando come bundle.exe -root config.data -out main.wade sapere che includerà tutti gli asset di cui abbiamo bisogno. Inoltre, poiché il bundler seguirà solo i riferimenti alle risorse, sappiamo che includerà solo le risorse di cui abbiamo bisogno e salterà tutta la lanugine residua che inevitabilmente si accumula durante la vita di un progetto (inoltre possiamo generare automaticamente elenchi di ciò lanugine per potatura).

Un caso complicato di questa faccenda è negli script. Rendere il motore basato sui dati è concettualmente semplice, ma ho visto moltissimi progetti (passatempo per AAA) in cui gli script sono considerati dati e quindi "autorizzati" a utilizzare i percorsi delle risorse in modo indiscriminato. Non farlo. Se un file Lua ha bisogno di una risorsa e chiama solo una funzione come textures.lua("/path/to/texture.png")quella, la pipeline delle risorse avrà molti problemi sapendo che lo script richiede /path/to/texture.pngper funzionare correttamente e potrebbe considerare quella trama inutilizzata e non necessaria. Gli script devono essere trattati come qualsiasi altro codice: tutti i dati di cui hanno bisogno, comprese le risorse o le tabelle, devono essere specificati in una voce di configurazione che il motore e la pipeline delle risorse possono verificare le dipendenze. I dati che dicono "carica script foo.lua" invece dovrebbero dire "foo.luae assegnagli questi parametri "laddove i parametri includano qualsiasi risorsa necessaria. Se uno script genera casualmente nemici, ad esempio, passa l'elenco di possibili nemici nello script da quel file di configurazione. Il motore può quindi precaricare i nemici con il livello ( poiché conosce l'elenco completo di possibili spawn) e la pipeline di risorse sa raggruppare tutti i nemici con il gioco (poiché sono definitivamente referenziati dai dati di configurazione). Se gli script generano stringhe di nomi di percorso e chiamano semplicemente una loadfunzione, allora nessuno dei due il motore né la pipeline di risorse hanno modo di conoscere in modo specifico quali risorse potrebbe essere caricata dallo script.


Buona risposta, molto pratica, e spiega anche le insidie ​​e gli errori che le persone commettono durante l'implementazione! +1
quando

+1. Aggiungo che seguire il modello di puntamento a risorse che contengono dati di configurazione è molto utile se si desidera abilitare il modding. È davvero molto più difficile e rischioso modificare i giochi che richiedono di modificare i file di dati originali anziché crearne uno proprio e indicarli. Ancora meglio se puoi puntare su più file con un ordine di priorità definito.
Jeutnarg,

12

Allo stesso modo si evita l'hardcoding nelle funzioni generali.

Passi i parametri e conservi le tue informazioni nei file di configurazione.

In quella situazione, non c'è assolutamente alcuna differenza nell'ingegneria del software tra scrivere un motore e scrivere una classe.

MgrAssets
public:
  errorCode loadAssetFromDisk( filePath )
  errorCode getMap( mapName, map& )

private:
  maps[name, map]

Quindi il codice client legge un file di configurazione "master" ( questo è hard coded o passato come argomento della riga di comando) che contiene le informazioni che indicano dove si trovano i file delle risorse e quale mappa contengono.

Da lì, tutto è guidato dal file di configurazione "master".


1
Sì, questo oltre a una sorta di meccanismo per portare la logica personalizzata. Potrebbe essere incorporando un linguaggio come C #, Python ecc. Al fine di estendere le funzionalità principali del motore con funzionalità definite dall'utente
qCring

3

Mi piacciono le altre risposte, quindi sarò un po 'contrario. ;)

Non puoi evitare la conoscenza del codice dei tuoi dati nel tuo motore. Ovunque provengano le informazioni, il motore deve sapere per cercarle. Tuttavia, puoi evitare di codificare le informazioni stesse nel tuo motore.

Un approccio "puro" basato sui dati ti consentirebbe di avviare l'eseguibile con i parametri della riga di comando necessari per caricare la configurazione iniziale, ma il motore dovrà essere codificato per sapere come interpretare tali informazioni. Ad esempio, se i file di configurazione sono JSON, è necessario codificare le variabili che si cercano, ad esempio il motore dovrà sapere per cercare"intro_movies" e "level_list"e così via.

Tuttavia, un motore "ben costruito" può funzionare per molti giochi diversi semplicemente scambiando i dati di configurazione e i dati a cui fa riferimento.

Quindi il mantra non è tanto da evitare la codifica, quanto da garantire che sia possibile apportare modifiche con il minor sforzo possibile.

Per contrastare con l'approccio dei file di dati (che io appoggio con tutto il cuore), è possibile che tu stia bene compilando i dati nel tuo motore. Se il "costo" di farlo è inferiore, non vi è alcun danno reale; se sei l'unico che ci sta lavorando, puoi rimandare la gestione dei file per una data successiva e non necessariamente fregarti. Nei miei primi progetti di gioco c'erano grandi tabelle di dati codificati nel gioco stesso, ad esempio un elenco di armi e i loro dati assortiti:

struct Weapon
{
    enum IconID icon;
    enum ModelID model;
    int damage;
    int rateOfFire;
    // etc...
};

const struct Weapon g_weapons[] =
{
    { ICON_PISTOL, MODEL_PISTOL, 5, 6 },
    { ICON_RIFLE, MODEL_RIFLE, 10, 20 },
    // etc...
};

Quindi metti questi dati in un posto facile da consultare ed è facile da modificare se necessario. L'ideale sarebbe mettere questa roba in un file di configurazione di qualche tipo, ma poi è necessario eseguire l'analisi e la traduzione e tutto quel jazz, inoltre agganciare i riferimenti tra le strutture potrebbe diventare un ulteriore dolore che davvero non si desidera avere a che fare con.


Non è terribilmente difficile analizzare JSON. L'unico "costo" coinvolto è l' apprendimento. (In particolare, imparare ad usare il modulo o la libreria appropriati. Go ha un buon supporto json, per esempio.)
Wildcard

Non è tremendamente difficile, ma richiede di farlo oltre il semplice apprendimento. Ad esempio, so come analizzare tecnicamente JSON, ho scritto parser per molti altri formati di file, ma avrei bisogno di trovare e installare una soluzione di terze parti (e capire le dipendenze e come costruirla) o crearne una mia. Richiede più tempo che non farlo.
dash-tom-bang,

4
Tutto richiede più tempo rispetto al non farlo. Ma gli strumenti di cui hai bisogno sono già stati scritti. Proprio come non devi progettare un compilatore per scrivere un gioco o armeggiare con il codice macchina, ma devi imparare una lingua per la piattaforma con cui stai lavorando. Quindi, impara anche a usare un parser json.
Wildcard il

Non sono sicuro di quale sia la tua tesi. In questa risposta sto sostenendo YAGNI; se non hai bisogno di perdere / perdere tempo a fare qualcosa che non ti aiuterà, allora non farlo. Se vuoi trascorrere del tempo, allora fantastico. Forse dovrai passare il tempo più tardi, forse no, ma farlo in anticipo ti distrae solo dal compito di realizzare effettivamente il gioco. Lo sviluppo del gioco è banale; ogni singolo compito che viene svolto nel creare un gioco è semplice. È solo che la maggior parte dei giochi ha un milione di compiti semplici e uno sviluppatore responsabile sceglie quelli che raggiungono l'obiettivo più velocemente.
dash-tom-bang,

2
In realtà, ho votato a favore della tua risposta; nessun vero argomento in quanto tale. Volevo solo notare che JSON non è difficile da analizzare. Rileggendo, suppongo che stavo principalmente rispondendo allo snippet "ma poi devi fare analisi e traduzione e tutto quel jazz". Ma sono d'accordo che per i giochi di progetti personali e simili, YAGNI. :)
Wildcard l'
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.