Il seguente post del blog, se non diversamente specificato, è stato scritto da un membro della comunità di Gamasutras.
I pensieri e le opinioni espresse sono quelle dello scrittore e non di Gamasutra o della sua società madre.
Parte 1 – Messaggistica
Parte 2 – Memoria
Parte 3 – Dati & Cache
Parte 4 – Librerie grafiche
Viviamo in un grande momento per essere sviluppatori. Con una tale quantità di grandi motori di livello AAA disponibili per tutti, fare semplici giochi può essere facile come il drag-and-drop. Sembra che non ci sia più alcun motivo per scrivere un motore in questi giorni. E con il sentimento comune, “Scrivi giochi, non motori”, perché dovresti farlo?
Questo articolo è rivolto principalmente a sviluppatori solitari e piccoli team. Presumo una certa familiarità con la programmazione orientata agli oggetti.
Voglio darvi qualche idea su come approcciare lo sviluppo di un motore e userò un semplice motore fittizio per illustrarlo.
Perché scrivere un motore?
La risposta breve è: Non fatelo, se potete evitarlo.
La vita è troppo breve per scrivere un motore per ogni gioco (Tratto dal libro 3D Graphics Programming di Sergei Savchenko)
L’attuale selezione di eccellenti motori come Unity, Unreal o CryEngine sono flessibili come si potrebbe sperare e possono essere usati per fare praticamente qualsiasi gioco. Per compiti più specializzati, ci sono naturalmente soluzioni più specializzate come Adventure Game Studio o RPG Maker, solo per citarne alcuni. Nemmeno il costo dei motori commerciali è più un argomento.
Sono rimaste solo poche ragioni di nicchia per scrivere il proprio motore:
- Vuoi imparare come funziona un motore
- Hai bisogno di certe funzionalità che non sono disponibili o le soluzioni disponibili sono instabili
- Credi di poterlo fare meglio / più velocemente
- Vuoi avere il controllo dello sviluppo
Tutti questi sono motivi perfettamente validi e se stai leggendo questo, probabilmente appartieni a uno di questi campi. Il mio obiettivo non è quello di entrare in un lungo dibattito “Quale motore dovrei usare?” o “Dovrei scrivere un motore? Quindi, cominciamo.
Come non scrivere un Engine
Aspettate. Prima vi dico di non scriverne uno, poi vi spiego come fallire? Grande introduzione…
Ad ogni modo, ci sono molte cose da considerare prima di scrivere una sola riga di codice. Il primo e più grande problema di chiunque inizi a scrivere un motore di gioco può essere riassunto in questo:
Voglio vedere del gameplay il più velocemente possibile!
Tuttavia, prima ci si rende conto che ci vorrà molto tempo prima di vedere effettivamente accadere qualcosa di interessante, meglio sarà scrivere il motore.
Forzare il vostro codice a mostrare una qualche forma di grafica o di gameplay il più velocemente possibile, solo per avere qualche conferma visiva di “progresso”, è il vostro più grande nemico a questo punto. Prendete. Il tuo. Tempo!
Non pensate nemmeno di iniziare con la grafica. Probabilmente avete letto molti tutorial e libri su OpenGL / DirectX e sapete come renderizzare un semplice triangolo o sprite. Potreste pensare che un breve frammento di codice per il rendering di una piccola mesh sullo schermo sia un buon punto di partenza. Non lo è.
Sì, i vostri progressi iniziali saranno sorprendenti. Diamine, potreste girare per un piccolo livello in prima persona in un solo giorno copiando frammenti di codice da vari tutorial e Stack Overflow. Ma vi garantisco che cancellerete ogni singola riga di quel codice 2 giorni dopo. Ancora peggio, potreste persino scoraggiarvi dallo scrivere un Engine, poiché non è motivante vedere sempre meno.
Il secondo grande problema che gli sviluppatori affrontano mentre scrivono Engine è il feature creep. Tutti vorrebbero scrivere il Santo Graal dei motori. Tutti vogliono il motore perfetto che può fare tutto. Sparatutto in prima persona, RPG tattici, e così via. Ma il semplice fatto rimane, non possiamo. Non ancora. Basta guardare i grandi nomi. Nemmeno Unity può davvero soddisfare perfettamente ogni genere di gioco.
Non pensate nemmeno di scrivere un motore che possa fare più di un genere al primo tentativo. Non fatelo!
Da dove cominciare quando si scrive un motore
Scrivere un motore è come progettare un vero motore per una macchina. I passi sono in realtà abbastanza ovvi, assumendo che tu sappia su quale gioco (o auto) stai lavorando. Eccoli:
- Puntate esattamente su ciò che il vostro motore deve essere in grado di fare E su ciò che il vostro motore non deve essere in grado di fare.
- Organizzate i bisogni in sistemi che il vostro motore richiederà.
- Progettate la vostra architettura perfetta che leghi tutti questi sistemi insieme.
- Ripeti i passi 1. – 3. il più spesso possibile.
- Codifica.
Se (= se e solo se) spendi abbastanza tempo e sforzo nei passi 1. – 4. e il design del gioco non cambia improvvisamente da un gioco horror a una slot machine (leggi: Silent Hill), codificare sarà un’impresa molto piacevole. La codifica sarà ancora tutt’altro che facile, ma perfettamente gestibile, anche da sviluppatori solitari.
Questa è la ragione per cui questo articolo è principalmente sui passi 1. – 4. Pensate al passo 5. come “riempire gli spazi vuoti. 50.000 LOC di spazi vuoti”.
La parte più cruciale di tutto questo è il Passo 3. Concentreremo la maggior parte dei nostri sforzi qui!
Step 1. Individuare i Bisogni e i Non Bisogni
Tutti questi Passi possono sembrare piuttosto banali all’inizio. Ma in realtà non lo sono. Si potrebbe pensare che il Passo 1 del processo di sviluppo di un motore per sparatutto in prima persona possa essere ridotto a questo:
Ho bisogno di caricare un livello, la pistola dei giocatori, alcuni nemici con l’IA. Fatto, passo alla fase 2.
Se solo fosse così facile. Il modo migliore per affrontare il Passo 1 è quello di passare attraverso l’intero gioco Click-by-Click, Azione-by-Azione dal Cliccare l’Icona sul vostro Desktop, al premere il tasto Exit dopo il lancio dei Crediti. Fate una lista, una grande lista di ciò che vi serve. Fai una lista di ciò che sicuramente non ti serve.
Questo probabilmente si svolgerà in questo modo:
Inizio il gioco e va direttamente al menu principale. Il menu userà un’immagine statica? Una scena tagliata? Come controllo il menu principale, mouse? Tastiera? Di che tipo di elementi dell’interfaccia grafica ho bisogno per il menu principale? Pulsanti, moduli, barre di scorrimento? E la musica?
E queste sono solo macroconsiderazioni. Entrate nel modo più dettagliato possibile. Decidere che hai bisogno di pulsanti è bello e buono, ma considera anche cosa può fare un pulsante.
Voglio che i pulsanti abbiano 4 stati, Su, Hover, Giù, Disabilitato. Avrò bisogno di suoni per i pulsanti? E gli effetti speciali? Sono animati nello stato di inattività?
Se la vostra lista dei bisogni e dei non bisogni contiene solo una decina di elementi alla fine del menu principale, avete fatto qualcosa di sbagliato.
A questo punto, quello che state facendo è simulare il motore nel vostro cervello e scrivere quello che deve essere fatto. Il passo 1 diventerà più chiaro ad ogni iterazione, non preoccuparti di perdere qualcosa la prima volta.
Passo 2. Organizzare i bisogni in sistemi
Allora, avete le vostre liste di cose che vi servono e non vi servono. È il momento di organizzarle. Ovviamente, le cose relative alla GUI come i pulsanti andranno in una sorta di sistema GUI. Gli elementi relativi al rendering vanno nel Graphics System / Engine.
Anche in questo caso, come per il Passo 1, decidere cosa va dove sarà più ovvio nella vostra seconda iterazione, dopo il Passo 3. Per il primo passaggio, raggruppateli logicamente come nell’esempio sopra.
La migliore referenza su “cosa va dove” e “cosa fa cosa” è senza dubbio il libro Game Engine Architecture di Jason Gregory.
Iniziate a raggruppare le funzionalità. Iniziate a pensare a come combinarle. Non avete bisogno di Camera->rotateYaw(float yaw)
e Camera->rotatePitch(float pitch)
se potete combinarle in Camera->rotate(float yaw, float pitch)
. Mantenete la semplicità. Troppe funzionalità (ricordate, feature creep) vi faranno male in seguito.
Pensate a quali funzionalità devono essere esposte pubblicamente e quali devono risiedere solo all’interno del sistema stesso. Per esempio, il vostro Renderer ha bisogno di ordinare tutti gli sprites trasparenti prima di disegnare. La funzione per ordinare questi sprites, tuttavia, non ha bisogno di essere esposta. Sapete che avete bisogno di ordinare gli sprites trasparenti prima di disegnare, non avete bisogno di un sistema esterno che vi dica questo.
Step 3. L’architettura (o l’articolo vero e proprio)
Potremmo anche aver iniziato l’articolo qui. Questa è la parte interessante e importante.
Una delle architetture più semplici che il vostro motore può avere è mettere ogni sistema in una classe e avere il ciclo di gioco principale che chiama le sue subroutine. Potrebbe essere qualcosa del genere:
while(isRunning)
{
Input->readInput();
isRunning = GameLogic->doLogic();
Camera->update();
World->update();
GUI->update();
AI->update();
Audio->play();
Render->draw();
}
Sembra perfettamente ragionevole all’inizio. Avete tutte le basi coperte, Input -> elaborazione Input -> Output.
E in effetti, questo sarà sufficiente per un semplice gioco. Ma sarà un dolore da mantenere. La ragione di questo dovrebbe essere ovvia: dipendenze.
Ogni sistema deve comunicare con altri sistemi in qualche modo. Non abbiamo alcun mezzo per farlo nel nostro Game Loop di cui sopra. Quindi l’esempio indica chiaramente che ogni sistema deve avere qualche riferimento agli altri sistemi per fare qualcosa di significativo. La nostra GUI e la logica di gioco devono sapere qualcosa del nostro input. Il nostro Renderer deve sapere qualcosa della nostra Logica di Gioco per poter visualizzare qualcosa di significativo.
Questo porterà a questa meraviglia architettonica:
Se puzza di Spaghetti, è Spaghetti. Sicuramente non è quello che vogliamo. Sì, è facile e veloce da codificare. Sì, avremo risultati accettabili. Ma non è mantenibile. Cambia un piccolo pezzo di codice da qualche parte e potrebbe avere effetti devastanti su tutti gli altri sistemi senza che noi lo sappiamo.
Inoltre, ci sarà sempre del codice a cui molti sistemi avranno bisogno di accedere. Sia la GUI che il Renderer hanno bisogno di fare chiamate di disegno o almeno avere accesso a qualche tipo di interfaccia per gestire questo per noi. Sì, potremmo semplicemente dare ad ogni sistema il potere di chiamare direttamente le funzioni OpenGL / DirectX, ma finiremo con un sacco di ridondanze.
Potremmo risolvere questo raccogliendo tutte le funzioni di disegno all’interno del sistema Renderer e chiamarle dal sistema GUI. Ma allora il sistema di rendering avrà funzioni specifiche per la GUI. Queste non hanno posto nel Renderer ed è quindi contrario ai passi 1 e 2. Decisioni, decisioni.
Quindi la prima cosa che dovremmo considerare è dividere il nostro Engine in Layers.
Engine Lasagne
La lasagna è meglio degli spaghetti. Almeno dal punto di vista della programmazione. Restando al nostro esempio di Renderer, quello che vogliamo è chiamare le funzioni OpenGL / DirectX senza chiamarle direttamente nel sistema. Questo ha l’odore di un Wrapper. E per la maggior parte lo è. Raccogliamo tutte le funzionalità di disegno all’interno di un’altra classe. Queste classi sono ancora più basilari dei nostri Sistemi. Chiamiamo queste nuove classi Framework.
L’idea dietro a questo è di astrarre molte delle chiamate API di basso livello e formarle in qualcosa su misura per il nostro gioco. Non vogliamo impostare il Vertex Buffer, impostare l’Index Buffer, impostare le Textures, abilitare questo, disabilitare quello solo per fare una semplice chiamata di disegno nel nostro Renderer System. Mettiamo tutta questa roba di basso livello nel nostro Framework. E chiamerò questa parte del Framework “Draw”. Perché? Beh, tutto ciò che fa è impostare tutto per il disegno e poi disegnarlo. Non si preoccupa di cosa disegna, dove disegna, perché disegna. Questo è lasciato al Renderer System.
Questa potrebbe sembrare una cosa strana, vogliamo velocità nel nostro motore, giusto? Più livelli di astrazione = meno velocità.
E avresti ragione, se fossimo negli anni ’90. Ma abbiamo bisogno della manutenibilità e possiamo vivere con una perdita di velocità appena percettibile per la maggior parte delle parti.
Come dovrebbe essere progettato il nostro Draw Framework? In parole povere, come la nostra piccola API. SFML è un grande esempio di questo.
Cose importanti da tenere a mente:
- Mantenetelo ben documentato. Quali funzioni abbiamo? Quando possono essere chiamate? Come vengono chiamate?
- Mantenete la semplicità. Funzioni semplici come drawMesh(Mesh* oMesh) o loadShader(String sPath) vi renderanno felici nel lungo periodo.
- Mantenetelo funzionale. Non siate troppo specifici. invece di
drawButtonSprite
, abbiate unadrawSprite
funzione e lasciate che il chiamante faccia il resto.
Cosa ci guadagniamo? Molto:
- Abbiamo solo bisogno di impostare il nostro Framework una volta e possiamo usarlo in ogni sistema di cui abbiamo bisogno (GUI, Renderer….)
- Possiamo facilmente cambiare le API sottostanti se vogliamo, senza riscrivere ogni sistema. Passare da OpenGL a DirectX? Nessun problema, basta riscrivere la classe del Framework.
- Mantiene il codice nei nostri sistemi pulito e stretto.
- Avere un’interfaccia ben documentata significa che una persona può lavorare sul Framework, mentre una persona lavora nel System Layer.
Finiamo probabilmente con qualcosa come questo:
La mia regola di cosa va nel Framework è piuttosto semplice. Se ho bisogno di chiamare una libreria esterna (OpenGL, OpenAL, SFML…) o ho strutture di dati / algoritmi di cui ogni sistema ha bisogno, dovrei farlo nel Framework.
Ora abbiamo il nostro primo strato di Lasagna fatto. Ma abbiamo ancora questa enorme palla di spaghetti sopra. Affrontiamo quello dopo.
Messaggio
Il grande problema comunque rimane. I nostri sistemi sono ancora tutti interconnessi. Noi non lo vogliamo. Ci sono una moltitudine di modi per affrontare questo problema. Eventi, messaggi, classi astratte con puntatori a funzioni (quanto è esoterico)…
Rimaniamo sui messaggi. Questo è un concetto semplice che è ancora molto popolare nella programmazione GUI. È anche adatto come facile esempio per il nostro motore.
Funziona come un servizio postale. L’azienda A invia un messaggio all’azienda B e chiede che venga fatto qualcosa. Queste aziende non hanno bisogno di una connessione fisica. L’azienda A presume semplicemente che l’azienda B lo farà ad un certo punto. Ma, per ora, all’azienda A non importa quando o come l’azienda B lo farà. Ha solo bisogno di farlo. Diamine, l’azienda B potrebbe anche decidere di reindirizzare il messaggio alle aziende C e D e lasciare che se ne occupino loro.
Possiamo fare un passo avanti, l’azienda A non ha nemmeno bisogno di inviarlo a qualcuno in particolare. L’azienda A pubblica semplicemente la lettera e chiunque si senta responsabile la elaborerà. In questo modo l’azienda C e D possono elaborare direttamente la richiesta.
Ovviamente, le aziende sono uguali ai nostri sistemi. Diamo un’occhiata ad un semplice esempio:
- Framework notifica al sistema di input che “A” è stato premuto
- Input traduce che la pressione del tasto “A” significa “Aprire Inventario” e invia un messaggio contenente “Aprire Inventario”
- GUI gestisce il messaggio e apre la finestra dell’inventario
- La logica di gioco gestisce il messaggio e mette in pausa il gioco
.
Input non si preoccupa nemmeno di cosa viene fatto al suo messaggio. Alla GUI non importa che anche la logica di gioco elabori lo stesso messaggio. Se fossero tutti accoppiati, Input avrebbe bisogno di chiamare una funzione nel sistema GUI e una funzione in Game Logic. Ma non ne ha più bisogno. Siamo stati in grado di disaccoppiare con successo questo usando Messages.
Come è fatto un Messaggio? Dovrebbe avere almeno qualche tipo. Per esempio, l’apertura dell’inventario potrebbe essere un qualche enum chiamato OPEN_INVENTORY
. Questo è sufficiente per messaggi semplici come questo. Messaggi più avanzati che hanno bisogno di includere dati avranno bisogno di un modo per memorizzare quei dati. Ci sono una moltitudine di modi per realizzare questo. Il più semplice da implementare è l’uso di una semplice struttura a mappa.
Ma come facciamo a inviare messaggi? Attraverso un Message Bus, naturalmente!
Non è bello? Niente più spaghetti, solo le buone vecchie lasagne. Ho deliberatamente messo la nostra Logica di Gioco dall’altra parte del Bus dei Messaggi. Come potete vedere, non ha alcuna connessione con il livello del Framework. Questo è importante per evitare qualsiasi tentazione di “chiamare solo quella funzione”. Fidatevi, prima o poi vorrete farlo, ma questo romperebbe il nostro progetto. Abbiamo già abbastanza sistemi che trattano con il Framework, non c’è bisogno di farlo nella nostra logica di gioco.
Il Message Bus è una semplice classe con riferimenti ad ogni sistema. Se ha un messaggio in coda, il Message Bus lo invia ad ogni sistema tramite una semplice chiamata handleMessage(Msg msg)
. In cambio, ogni sistema ha un riferimento al Bus dei messaggi per postare i messaggi. Questo può ovviamente essere memorizzato internamente o passato come argomento di una funzione.
Tutti i nostri Sistemi devono quindi ereditare o avere la seguente forma:
class System
{
public:
void handleMessage(Msg *msg);
{
switch(msg->type)
{
//// Example
//case Msg::OPEN_INVENTORY:
// break;
}
}
private:
MessageBus *msgBus;
//// Usage: msgBus->postMessage(msg);
}
(Sì, sì, puntatori grezzi…)
Improvvisamente, il nostro Game Loop cambia per lasciare semplicemente che il Message Bus invii messaggi. Avremo ancora bisogno di aggiornare periodicamente ogni sistema tramite qualche forma di chiamata update()
. Ma la comunicazione sarà gestita in modo diverso.
Tuttavia, come per i nostri Framework, l’uso dei messaggi crea un overhead. Questo rallenterà un po’ il motore, non prendiamoci in giro. Ma non ci interessa! Vogliamo un Design semplice e pulito. Un’architettura semplice e pulita!
E la parte più bella? Otteniamo cose incredibili gratuitamente!
La console
Ogni messaggio è praticamente una chiamata di funzione. E ogni messaggio viene inviato praticamente ovunque! E se avessimo un sistema che stampa semplicemente ogni messaggio che viene inviato in una finestra di output? E se questo sistema potesse anche inviare i messaggi che digitiamo in quella finestra?
Sì, abbiamo appena dato vita a una Console. E tutto ciò che ci è servito sono poche righe di codice. La mia mente è rimasta sconvolta la prima volta che l’ho vista in azione. Non è nemmeno legata a nulla, esiste e basta.
Una console è ovviamente molto utile durante lo sviluppo del gioco e possiamo semplicemente toglierla nel rilascio, se non vogliamo che il giocatore abbia quel tipo di accesso.
Cinematografia nel gioco, Replay & Debug
E se falsificassimo i messaggi? E se creassimo un nuovo sistema che semplicemente invia messaggi ad una certa ora? Immaginate di inviare qualcosa come MOVE_CAMERA
, seguito da ROTATE_OBJECT
.
E voilà, abbiamo la In-Game Cinematics.
E se semplicemente registriamo i messaggi di input che sono stati inviati durante il gioco e li salviamo in un file?
E voilà, abbiamo i Replay.
E se registrassimo semplicemente tutto ciò che fa il giocatore, e quando il gioco va in crash, gli facessimo inviare quei file di dati a noi?
E voilà, abbiamo una copia esatta delle azioni del giocatore che hanno portato al crash.
Multi-Threading
Multi-Threading? Sì, Multi-Threading. Abbiamo disaccoppiato tutti i nostri sistemi. Questo significa che possono elaborare i loro messaggi quando vogliono, come vogliono e, soprattutto, dove vogliono. Possiamo far decidere al nostro Bus dei messaggi su quale thread ogni sistema deve elaborare un messaggio -> Multi-Threading
Frame Rate Fixing
Abbiamo troppi messaggi per elaborare questo frame? Nessun problema, teniamoli nella coda del Message Bus e mandiamoli al prossimo Frame. Questo ci darà l’opportunità di assicurare che il nostro gioco funzioni a 60 FPS senza problemi. I giocatori non noteranno che l’IA impiega qualche frame in più per “pensare”. Noteranno comunque cali di Frame Rate.
I messaggi sono fighi.
È importante che documentiamo meticolosamente ogni messaggio e i suoi parametri. Trattatelo come un’API. Se lo fate bene, ogni sviluppatore può lavorare su diversi sistemi senza rompere nulla. Anche se un sistema dovesse essere offline o in costruzione, il gioco continuerà a funzionare e potrà essere testato. Nessun sistema audio? Va bene, abbiamo ancora le immagini. Niente Renderer, va bene, possiamo usare la Console…
Ma i messaggi non sono perfetti. Purtroppo.
A volte, vogliamo sapere il risultato di un messaggio. A volte abbiamo bisogno che vengano elaborati immediatamente. Abbiamo bisogno di trovare opzioni valide. Una soluzione a questo è avere uno Speedway. Oltre ad una semplice postMessage
funzione, possiamo implementare una postImmediateMessage
funzione che viene elaborata immediatamente. Gestire i messaggi di ritorno è molto più semplice. Questi vengono inviati alla nostra handleMessage
funzione prima o poi. Dobbiamo solo ricordare questo quando inviamo un messaggio.
I messaggi immediati ovviamente rompono il Multi-Threading e il Frame Rate Fixing se fatto in eccesso. È quindi vitale limitarsi a limitare il loro uso.
Ma il più grande problema di questo sistema è la latenza. Non è l’architettura più veloce. Se state lavorando su uno sparatutto in prima persona con tempi di risposta tipo twitch, questo potrebbe essere un problema.
Torniamo a progettare la nostra architettura
Abbiamo deciso di usare sistemi e un bus di messaggi. Sappiamo esattamente come vogliamo strutturare il nostro Engine.
È il momento del Passo 4 del nostro processo di Design. Iterazione. Alcune funzioni potrebbero non entrare in nessun Sistema, dobbiamo trovare una soluzione. Alcune funzioni hanno bisogno di essere chiamate estesamente e intaserebbero il Message Bus, dobbiamo trovare una soluzione.
Questo richiede tempo. Ma ne vale la pena a lungo termine.
E’ finalmente tempo di codificare!
Passo 4. Dove iniziare a codificare?
Prima di iniziare a codificare, leggete il libro/articolo Game Programming Patterns di Robert Nystrom.
Oltre a questo, ho abbozzato una piccola tabella di marcia che potete seguire. Non è di gran lunga il modo migliore, ma è produttivo.
- Se state andando con un motore di tipo Message Bus, considerate di codificare prima la Console e il Message Bus. Una volta che questi sono implementati, potete fingere l’esistenza di qualsiasi sistema che non è stato ancora codificato. Avrai un controllo costante sull’intero motore in ogni fase dello sviluppo.
- Considera di passare poi alla GUI, così come alla necessaria funzionalità Draw all’interno del Framework. Una solida GUI abbinata alla Console vi permetterà di falsificare tutti gli altri sistemi ancora più facilmente. I test saranno un gioco da ragazzi.
- Il prossimo dovrebbe essere il Framework, almeno la sua interfaccia. La funzionalità può seguire dopo.
- Finalmente, passate agli altri sistemi, incluso il Gameplay.
Vedrete che il rendering di qualsiasi cosa relativa al Gameplay potrebbe essere l’ultima cosa che fate. E questa è una buona cosa! Sarà molto più gratificante e ti terrà motivato a finire gli ultimi ritocchi del tuo Engine.
Il tuo Game Designer potrebbe spararti durante questo processo. Testare il gameplay attraverso i comandi della console è divertente quanto giocare a Counter Strike via IRC.
Conclusione
Prendi tempo per trovare una solida architettura e seguila! Questo è il consiglio che spero tu prenda da questo articolo. Se lo fate, sarete in grado di costruire un motore perfettamente buono e mantenibile alla fine della giornata. O secolo.
Personalmente, mi piace di più scrivere Engine che fare tutta quella roba di Gameplay. Se avete domande, sentitevi liberi di contattarmi via Twitter @Spellwrath. Attualmente sto finendo un altro Engine usando i metodi che ho descritto in questo articolo.
Puoi trovare la Parte 2 qui.