Separare Game Engine dal codice di gioco in giochi simili, con controllo delle versioni


15

Ho un gioco finito, che voglio rifiutare in altre versioni. Questi sarebbero giochi simili, con più o meno lo stesso tipo di design, ma non sempre, in sostanza le cose potrebbero cambiare, a volte piccole, a volte grandi.

Vorrei che il codice principale fosse sottoposto a versione separatamente dal gioco, in modo che se dico che correggo un bug trovato nel gioco A, la correzione sarà presente nel gioco B.

Sto cercando di trovare il modo migliore per gestirlo. Le mie idee iniziali sono queste:

  • Crea un enginemodulo / cartella / qualunque cosa, che contenga tutto ciò che può essere generalizzato ed è indipendente al 100% dal resto del gioco. Ciò include un po 'di codice, ma anche risorse generiche condivise tra i giochi.
  • Metti questo motore nel suo gitrepository, che sarà incluso nei giochi comegit submodule

La parte con cui sto lottando è come gestire il resto del codice. Supponiamo che tu abbia la tua scena di menu, questo codice è specifico del gioco, ma anche la maggior parte tende ad essere generica e potrebbe essere riutilizzata in altri giochi. Non riesco a inserirloengine , ma ricodificarlo per ogni gioco sarebbe inefficace.

Forse usare una sorta di variazione dei rami git potrebbe essere efficace per gestirlo, ma non credo che questo sia il modo migliore di procedere.

Qualcuno ha qualche idea, esperienza da condividere o qualcosa al riguardo?


In che lingua è il tuo motore? Alcune lingue hanno gestori di pacchetti dedicati che potrebbero avere più senso rispetto all'utilizzo dei sottomoduli git. Ad esempio, NodeJS ha npm (che può indirizzare i repository Git come sorgenti).
Dan Pantry,

La tua domanda su come gestire al meglio il codice generico o su come configurare il codice "semi-generico" o su come progettare il codice, come progettare il codice o cosa?
Dunk,

Questo può variare in ogni ambiente del linguaggio di programmazione, ma potresti considerare non solo il software della versione di controllo, ma anche sapere come iniziare a dividere il motore di gioco dal codice di gioco (come pacchetti, cartelle e API) e versioni successive , applica la versione di controllo.
Umlcat,

Come avere una cronologia pulita di una cartella in un ramo: Rifattorizza il tuo motore in modo che i repository separati (futuri) siano in cartelle separate, questo è il tuo ultimo commit. Quindi crea un nuovo ramo, elimina tutto al di fuori di quella cartella e esegui il commit. Quindi vai al primo commit del repository e fondilo con il tuo nuovo ramo. Ora hai un ramo con solo quella cartella: estrailo in altri progetti e / o fondilo con il tuo progetto esistente. Questo mi ha aiutato molto nel separare i motori nei rami, se il tuo codice è già separato. Non ho bisogno di moduli git.
Barry Staes,

Risposte:


13

Crea un modulo motore / cartella / qualunque cosa, che contenga tutto ciò che può essere generalizzato ed è indipendente al 100% dal resto del gioco. Ciò include un po 'di codice, ma anche risorse generiche condivise tra i giochi.

Inserisci questo motore nel suo repository git, che verrà incluso nei giochi come sottomodulo git

È esattamente quello che faccio e funziona molto bene. Ho un framework applicativo e una libreria di rendering, e ognuno di questi sono trattati come sottomoduli dei miei progetti. Trovo SourceTree sia utile quando si tratta di sottomoduli, poiché li gestisce bene e non ti farà dimenticare nulla, ad esempio se hai aggiornato il sottomodulo del motore nel progetto A, ti avviserà di eseguire le modifiche nel progetto B.

Con l'esperienza arriva la conoscenza di quale codice dovrebbe essere nel motore rispetto a ciò che dovrebbe essere per progetto. Suggerisco che se non si è ancora abbastanza sicuri, per ora lo si tiene in ogni progetto. Col passare del tempo, vedrai tra i tuoi vari progetti ciò che rimane lo stesso e quindi puoi gradualmente tenerlo presente nel tuo codice motore. In altre parole: duplicare il codice fino a quando si è vicini al 100% certi che non cambi discretamente per progetto, quindi generalizzarlo.

Nota sul controllo del codice sorgente e binari

Ricorda solo che se ti aspetti che le tue risorse binarie cambino spesso, potresti non volerle mettere nel controllo del codice sorgente basato su testo come git. Dico solo ... ci sono soluzioni migliori per i binari. La cosa più semplice che puoi fare per ora per aiutare a mantenere pulito e performante il tuo repository "engine-source" è avere un repository "engine-binaries" separato che contenga solo binari, che includi anche come sottomodulo nel tuo progetto. In questo modo si mitigano i danni alle prestazioni causati al repository "engine-source", che cambia continuamente e su cui sono quindi necessarie iterazioni veloci: commit, push, pull ecc. I sistemi di gestione del controllo del codice sorgente come git operano su delta di testo e non appena si introducono i binari, si introducono enormi delta da una prospettiva testuale, che alla fine ti costa tempo di sviluppo.Allegato GitLab . Google è tuo amico.


Non cambiano molto spesso, ma mi interessa comunque. Non so nulla del versioning binario. Quali soluzioni ci sono?
Malharhak,

@Malharhak Modificato per rispondere al tuo commento.
Ingegnere,

@Malharak Ecco un bel po 'di informazioni su questo argomento.
Ingegnere,

1
+1 per mantenere le cose nel progetto il più a lungo possibile. Il codice comune garantisce una maggiore complessità. Dovrebbe essere evitato fino a quando non sarà assolutamente necessario.
Gusdor,

1
@Malharhak No, in particolare perché il tuo obiettivo è solo quello di conservare "copie" fino a quando noti che tale codice è immutabile e può essere considerato come comune. Gusdor lo ha ripetuto - attenzione - si può facilmente perdere un sacco di tempo prendendo in considerazione le cose troppo presto, quindi cercando di trovare modi per mantenere quel codice abbastanza generale da rimanere comune, ma abbastanza adattabile per adattarsi a vari progetti ... tutta una serie di parametri e le opzioni e si trasforma in un brutto pasticcio che ancora non è quello che serve perché si finisce per cambiarla per nuovo progetto in ogni caso . Non pensare troppo presto . Abbi pazienza.
Ingegnere,

6

Ad un certo punto un motore DEVE specializzarsi e conoscere cose sul gioco. Vado su una tangente qui.

Prendi risorse in un RTS. Un gioco può avere Creditse Crystalun altro MetalePotatoes

Dovresti usare i concetti OO correttamente e andare per max. code-riutilizzo. È chiaro che Resourcequi esiste un concetto di .

Quindi decidiamo che le risorse hanno il seguente:

  1. Un hook nel loop principale per incrementare / decrementare se stessi
  2. Un modo per ottenere l'importo corrente (restituisce un int)
  3. Un modo per sottrarre / aggiungere arbitrariamente (giocatori che trasferiscono risorse, acquisti ....)

Nota che questa nozione di a Resourcepotrebbe rappresentare uccisioni o punti in una partita! Non è molto potente.

Ora pensiamo a un gioco. Possiamo in qualche modo avere valuta trattando in penny e aggiungendo un punto decimale all'output. Ciò che non possiamo fare sono risorse "istantanee". Come dire "generazione della rete elettrica"

Diciamo che aggiungi una InstantResourceclasse con metodi simili. Ora stai (iniziando a) inquinare il tuo motore con risorse.


Il problema

Facciamo di nuovo l'esempio RTS. Supponiamo che qualunque cosa doni qualcuno Crystalad un altro giocatore. Vuoi fare qualcosa del tipo:

if(transfer.target == engine.getPlayerId()) {
    engine.hud.addIncoming("You got "+transfer.quantity+" of "+
        engine.resourceDictionary.getNameOf(transfer.resourceId)+
        " from "+engine.getPlayer(transfer.source).name);
}
engine.getPlayer(transfer.target).getResourceById(transfer.resourceId).add(transfer.quantity)
engine.getPlayer(transfer.source).getResourceById(transfer.resourceId).add(-transfer.quantity)

Tuttavia, questo è davvero piuttosto disordinato. È uno scopo generale, ma disordinato. Già se impone un resourceDictionaryche significa che ora le tue risorse devono avere nomi! E lo è per giocatore, quindi non puoi più avere risorse di squadra.

Questa è un'astrazione "troppo" (non un brillante esempio, lo ammetto) invece dovresti colpire un punto in cui accetti che il tuo gioco ha giocatori e cristalli, quindi puoi semplicemente avere (per esempio)

engine.getPlayer(transfer.target).crystal().receiveDonation(transfer)
engine.getPlayer(transfer.source).crystal().sendDonation(transfer)

Con una classe Playere una classe in CurrentPlayercui CurrentPlayerl' crystaloggetto mostrerà automaticamente le cose sull'HUD per il trasferimento / invio di donazioni.

Questo inquina il motore con il cristallo, la donazione del cristallo, i messaggi sull'HUD per i giocatori attuali e tutto il resto. È più veloce e più facile da leggere / scrivere / mantenere (che è più importante, in quanto non è significativamente più veloce)


Osservazioni finali

Il caso delle risorse non è eccezionale. Spero che tu possa ancora vedere il punto però. Semmai ho dimostrato che "le risorse non appartengono al motore" in quanto ciò di cui un gioco specifico ha bisogno e ciò che è applicabile a tutte le nozioni di risorse sono cose MOLTO diverse. Quello che troverai di solito sono 3 (o 4) "livelli"

  1. Il "Core" - questa è la definizione da manuale del motore, è un grafico di scena con hook di eventi, si occupa di shader e pacchetti di rete e una nozione astratta di giocatori
  2. Il "GameCore" - Questo è piuttosto generico per il tipo di gioco, ma non per tutti i giochi, ad esempio risorse in RTS o munizioni in FPS. La logica del gioco inizia a filtrare qui. Questo è dove la nostra precedente nozione di risorse sarebbe. Abbiamo aggiunto queste cose che hanno senso per la maggior parte delle risorse RTS.
  3. "GameLogic" MOLTO specifico per il gioco reale in corso di realizzazione. Troverai variabili con nomi come creatureo shipo squad. Usando l' ereditarietà otterrai classi che si estendono su tutti e 3 i livelli (ad esempio Crystal è un Resource che è un GameLoopEventListener dire)
  4. Le "risorse" sono inutili per qualsiasi altro gioco. Prendiamo ad esempio gli script AI combinati in Half Life 2, non verranno utilizzati in un RTS con lo stesso motore.

Realizzare un nuovo gioco da un vecchio motore

Questo è MOLTO comune. La fase 1 è quella di strappare i livelli 3 e 4 (e 2 se il gioco è di tipo TOTALMENTE diverso) Supponiamo di fare un RTS da un vecchio RTS. Abbiamo ancora risorse, non solo cristallo e roba del genere - quindi le classi di base nei livelli 2 e 1 hanno ancora senso, tutto quel cristallo a cui si fa riferimento in 3 e 4 può essere scartato. Quindi lo facciamo. Possiamo comunque verificarlo come riferimento per ciò che vogliamo fare.


Inquinamento nel livello 1

Questo può succedere. Astrazione e prestazioni sono nemici. UE4 ad esempio fornisce molti casi ottimizzati di composizione (quindi se vuoi X e Y qualcuno ha scritto codice che fa X e Y insieme molto velocemente - sa che sta facendo entrambi) e di conseguenza è DAVVERO piuttosto grande. Questo non è male ma richiede tempo. Il livello 1 deciderà cose come "come si passano i dati agli shader" e come si animano le cose. Farlo nel modo migliore per il tuo progetto è SEMPRE buono. Prova a pianificare il futuro, riutilizzare il codice è tuo amico, ereditare dove ha senso.


Livelli di classificazione

ULTIMAMENTE (lo prometto) non aver paura dei livelli. Engine è un termine arcaico dei vecchi tempi delle pipeline a funzione fissa in cui i motori funzionavano praticamente allo stesso modo graficamente (e di conseguenza aveva molto in comune) la pipeline programmabile ha ribaltato questo concetto e come tale "strato 1" è stato inquinato con qualunque effetto gli sviluppatori volessero ottenere. L'intelligenza artificiale era la caratteristica distintiva (a causa della miriade di approcci) dei motori, ora è AI e grafica.

Il tuo codice non dovrebbe essere archiviato in questi livelli. Anche il famoso motore Unreal ha MOLTE versioni diverse ognuna specifica per un gioco diverso. Esistono pochi file (diversi dalle strutture dati simili forse) che sarebbero rimasti invariati. Questo va bene! Se vuoi creare un nuovo gioco da un altro, ci vorranno più di 30 minuti. La chiave è pianificare, sapere quali bit copiare e incollare e cosa lasciarsi alle spalle.


1

Il mio suggerimento personale su come gestire il contenuto che è un mix di generico e specifico è di renderlo dinamico. Prenderò la tua schermata del menu come esempio. Se ho frainteso quello che stavi chiedendo, fammi sapere cosa volevi sapere e adatterò la mia risposta.

Ci sono 3 cose che sono (quasi) sempre presenti in una scena di menu: lo sfondo, il logo del gioco e il menu stesso. Queste cose sono generalmente diverse in base al gioco. Quello che puoi fare per questo contenuto è creare un MenuScreenGenerator nel tuo motore, che accetta 3 parametri oggetto: BackGround, Logo e Menu. Anche la struttura di base di queste 3 parti fa parte del tuo motore, ma il tuo motore in realtà non dice come vengono generate queste parti, ma solo quali parametri dovresti dare loro.

Quindi nel tuo attuale codice di gioco, crei oggetti per un BackGround, un Logo e un Menu e lo passi al tuo MenuScreenGenerator. Ancora una volta, il gioco stesso non gestisce la modalità di generazione del menu, vale a dire per il motore. Il tuo gioco deve solo dire al motore come dovrebbe essere e dove dovrebbe essere.

In sostanza, il tuo motore dovrebbe essere un'API che il gioco dice cosa visualizzare. Se fatto correttamente, il tuo motore dovrebbe fare il duro lavoro e il gioco stesso dovrebbe solo dire al motore quali risorse usare, quali azioni intraprendere e che aspetto ha il mondo.

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.