Come si possono implementare moduli C ++ sostituibili a caldo?


39

I tempi di iterazione rapidi sono fondamentali per lo sviluppo di giochi, molto più che fantasiosi grafici e motori con camion carichi di funzionalità secondo me. Non sorprende che molti piccoli sviluppatori scelgano i linguaggi di scripting.

Il modo Unity 3D di essere in grado di mettere in pausa un gioco e modificare risorse E codice, quindi continuare e rendere effettive le modifiche immediatamente è assolutamente fantastico per questo. La mia domanda è: qualcuno ha implementato un sistema simile sui motori di gioco C ++?

Ho sentito che alcuni motori davvero di fascia alta lo fanno, ma sono più interessato a scoprire se c'è un modo per farlo in un motore o in un gioco cresciuto in casa.

Ovviamente ci sarebbero dei compromessi e non riesco a immaginare che puoi semplicemente ricompilare il tuo codice mentre il gioco è in pausa, ricaricarlo e funzionare in ogni circostanza o piattaforma.

Ma forse è possibile per la programmazione AI e moduli logici di livello semplice. Non è come se volessi farlo in qualsiasi progetto a breve (o lungo termine), ma ero curioso.

Risposte:


26

È sicuramente possibile farlo, anche se non con il tipico codice C ++; dovrai creare una libreria in stile C che puoi collegare e ricaricare dinamicamente in fase di esecuzione. Per rendere ciò possibile, la libreria deve contenere tutto lo stato all'interno di un puntatore opaco che può essere fornito alla libreria al momento del ricaricamento.

Timothy Farrar discute il suo approccio:

"Per lo sviluppo il codice viene compilato come libreria, un programma di caricamento di piccole dimensioni crea una copia della libreria e carica la copia della libreria per eseguire il programma. Il programma alloca tutti i dati all'avvio e utilizza un singolo puntatore per fare riferimento a tutti i dati. Non uso nulla di globale tranne i dati di sola lettura. Mentre il programma è in esecuzione, è possibile ricompilare la libreria originale. Quindi, premendo un tasto, il programma torna al caricatore e restituisce quel singolo puntatore di dati. Il caricatore crea una copia della nuova libreria, carica la copia e passa il puntatore ai dati al nuovo codice. Quindi il motore continua da dove era stato interrotto. " ( fonte originale , ora disponibile tramite l' archivio web )


Preferisco questo alla risposta di Blair, dato che in realtà rispondi alla domanda.
Jonathan Dickinson,

15

Lo scambio a caldo è un problema molto, molto difficile da risolvere con il codice binario. Anche se il codice viene inserito in librerie a collegamento dinamico separate, il codice deve garantire che non vi siano riferimenti diretti alle funzioni in memoria (poiché l'indirizzo delle funzioni può cambiare con una ricompilazione). Ciò significa essenzialmente non utilizzare le funzioni virtuali e tutto ciò che utilizza i puntatori a funzioni lo fa tramite una sorta di tabella di invio.

Roba pelosa e vincolante. È meglio usare un linguaggio di scripting per il codice che necessita di una rapida iterazione.

Al lavoro, la nostra attuale base di codice è un mix di C ++ e Lua. Anche Lua non è una piccola parte del nostro progetto: è quasi una divisione del 50/50. Abbiamo implementato la ricarica al volo di Lua, in modo da poter cambiare una riga di codice, ricaricare e andare avanti. In effetti, puoi farlo per correggere i bug che si verificano nel codice Lua, senza riavviare il gioco!


3
Un altro punto importante è che anche se non si intende mantenere un sistema nello script, se si prevede di ripetere molto presto, potrebbe essere comunque del tutto ragionevole iniziare da Lua, quindi passare a C ++ lungo la strada quando è necessario il le prestazioni extra e il tasso di iterazione sono rallentati. L'ovvio svantaggio qui è che finisci per eseguire il porting del codice, ma molte volte ottenere la logica giusta è la parte difficile - il codice quasi si scrive da solo una volta che lo hai capito.
Logan Kincaid,

15

(Potresti voler conoscere il termine "rattoppare le scimmie" o "pugni d'anatra" se non altro per l'immagine mentale umoristica.)

A parte questo: se il tuo obiettivo è ridurre il tempo di iterazione per i cambiamenti di "comportamento", prova alcuni approcci che ti portano la maggior parte del percorso lì, e combinali bene per consentire più di questo in futuro.

(Questo uscirà un po 'tangente, ma prometto che tornerà!)

  • Inizia con i dati e inizia in piccolo: ricarica ai limiti ("livelli" o simili), quindi procedi fino all'utilizzo della funzionalità del sistema operativo per ricevere notifiche di modifica dei file o semplicemente sondare regolarmente.
  • (Per i punti bonus e i tempi di caricamento inferiori (di nuovo, diminuendo il tempo di iterazione) esaminare la cottura dei dati .)
  • Gli script sono dati e consentono di iterare il comportamento. Se usi un linguaggio di scripting, ora hai le notifiche / capacità di ricaricare quegli script, interpretati o compilati. Puoi anche collegare il tuo interprete a una console di gioco, a un socket di rete o simili per una maggiore flessibilità di runtime.
  • Anche il codice può essere un dato : il tuo compilatore può supportare overlay , librerie condivise, DLL o simili. Quindi ora puoi scegliere un momento "sicuro" per scaricare e ricaricare un overlay o DLL, sia manuale che automatico. Le altre risposte vanno nel dettaglio qui. Si noti che alcune varianti di questo possono interferire con la vecificazione della firma crittografica, il bit NX (no-execute) o meccanismi di sicurezza simili.
  • Prendi in considerazione un sistema di salvataggio / caricamento approfondito e con versione . Se riesci a salvare e ripristinare il tuo stato in modo robusto anche di fronte alle modifiche del codice, puoi arrestare il gioco e riavviarlo con una nuova logica esattamente nello stesso punto. Più facile a dirsi che a farsi, ma è fattibile, ed è notevolmente più facile e portatile della ricerca della memoria per cambiare le istruzioni.
  • A seconda della struttura e del determinismo del tuo gioco potresti essere in grado di effettuare registrazioni e riproduzioni . Se quella registrazione è appena finita "comandi di gioco" (ad esempio un gioco di carte), puoi modificare tutto il codice di rendering che desideri e riprodurre la registrazione per vedere le tue modifiche. Per alcuni giochi questo è "facile" come la registrazione di alcuni parametri iniziali (ad esempio un seme casuale) e quindi le azioni dell'utente. Per alcuni è molto più complicato.
  • Fare sforzi per ridurre i tempi di compilazione . In combinazione con i summenzionati sistemi di salvataggio / caricamento o registrazione / riproduzione, o anche con sovrapposizioni o DLL, ciò può ridurre i tempi di risposta più di ogni altra cosa.

Molti di questi punti sono utili anche se non riesci a ricaricare dati o codice.

Aneddoti di supporto:

Su un PC RTS di grandi dimensioni (un team di circa 120 persone, principalmente C ++), esisteva un sistema di salvataggio dello stato incredibilmente profondo, utilizzato per almeno tre scopi:

  • Un salvataggio "superficiale" non è stato inviato al disco ma a un motore CRC per garantire che i giochi multiplayer rimanessero in simulazione blocco-passo un CRC ogni 10-30 fotogrammi; questo ha assicurato che nessuno stava imbrogliando e ha catturato bug desincronici pochi fotogrammi dopo
  • Se e quando si verificava un bug di desincronizzazione multiplayer, veniva eseguito un salvataggio molto approfondito in ogni fotogramma e di nuovo inviato al motore CRC, ma questa volta il motore CRC generava molti CRC, ciascuno per piccoli lotti di byte. In questo modo, potrebbe dirti esattamente quale porzione di stato aveva iniziato a divergere nell'ultimo fotogramma. Abbiamo riscontrato una brutta differenza di "modalità predefinita in virgola mobile" tra i processori AMD e Intel.
  • Un normale salvataggio in profondità potrebbe non salvare, ad esempio, il fotogramma esatto dell'animazione riprodotta dalla tua unità, ma otterrebbe la posizione, la salute, ecc. Di tutte le tue unità, consentendoti di salvare e riprendere in qualsiasi momento durante il gioco.

Da allora ho usato la registrazione / riproduzione deterministica su un gioco di carte C ++ e Lua per il DS. Abbiamo collegato l'API progettata per l'IA (lato C ++) e registrato tutte le azioni dell'utente e dell'IA. Abbiamo utilizzato questa funzionalità nel gioco (per fornire un replay al giocatore), ma anche per diagnosticare i problemi: quando si verificava un arresto anomalo o un comportamento strano, tutto ciò che dovevamo fare era recuperare il file di salvataggio e riprodurlo in una build di debug.

Da allora ho anche usato overlay più di un paio di volte e l'abbiamo combinato con il nostro sistema "spider automatico di questa directory e carica nuovi contenuti sul palmare". Tutto ciò che dovremmo fare è lasciare il filmato / livello / qualunque cosa e rientrare, e non solo i nuovi dati (sprite, layout di livello, ecc.) Verrebbero caricati, ma anche qualsiasi nuovo codice nella sovrapposizione. Sfortunatamente, i palmari più recenti stanno diventando molto più difficili a causa della protezione dalla copia e dei meccanismi anti-hacking che trattano il codice in modo speciale. Lo facciamo ancora per gli script lua.

Ultimo ma non meno importante: puoi (e io, in varie circostanze specifiche molto piccole) fare un po 'di punzonatura con le anatre eseguendo direttamente l'applicazione di patch sui codici operativi. Funziona meglio se sei su una piattaforma e un compilatore fissi, e poiché è quasi impossibile da mantenere, molto soggetto a bug e limitato in ciò che puoi realizzare rapidamente, lo uso principalmente solo per reindirizzare il codice durante il debug. Si fa insegnare un inferno di molto sulla tua set di istruzioni in fretta, però.


6

Puoi fare qualcosa come i moduli hot-swap in fase di esecuzione implementando i moduli come librerie a collegamento dinamico (o librerie condivise su UNIX) e usando dlopen () e dlsym () per caricare dinamicamente le funzioni dalla libreria.

Per Windows, gli equivalenti sono LoadLibrary e GetProcAddress.

Questo è un metodo C e presenta alcune insidie ​​usandolo in C ++, puoi leggere qui .


quella guida è la chiave per creare librerie sostituibili a caldo, grazie
JqueryToAddNumbers

4

Se si utilizza Visual Studio C ++, è possibile infatti mettere in pausa e ricompilare il codice in determinate circostanze. Visual Studio supporta Modifica e continua . Collegalo al tuo gioco nel debugger, fermalo in corrispondenza di un punto di interruzione, quindi modifica il codice dopo il punto di interruzione. Se si desidera salvare e continuare, Visual Studio tenterà di ricompilare il codice, reinserirlo nell'eseguibile in esecuzione e continuare. Se tutto va bene, le modifiche apportate verranno applicate al gioco in esecuzione senza dover eseguire un intero ciclo di compilazione-build-test. Tuttavia, ciò che segue smetterà di funzionare:

  1. La modifica delle funzioni di richiamata non funzionerà correttamente. E&C funziona aggiungendo un nuovo blocco di codice e modificando la tabella delle chiamate in modo che punti al nuovo blocco. Per i callback e tutto ciò che utilizza i puntatori a funzione, verranno comunque eseguite le chiamate precedenti non modificate. Per risolvere questo problema puoi avere una funzione di callback wrapper che chiama una funzione statica
  2. La modifica dei file di intestazione non funzionerà quasi mai. Questo è progettato per modificare le chiamate di funzione effettive.
  3. Vari costrutti linguistici lo faranno misteriosamente fallire. Dalla mia esperienza personale pre-dichiarando cose come enum spesso lo farà.

Ho usato Modifica e Continua per migliorare notevolmente la velocità di cose come l'iterazione dell'interfaccia utente. Supponiamo che tu abbia un'interfaccia utente funzionante interamente integrata nel codice, tranne per il fatto che hai accidentalmente scambiato l'ordine di disegno di due caselle in modo da non poter vedere nulla. Modificando 2 righe di codice in tempo reale è possibile salvare un ciclo di compilazione / compilazione / test di 20 minuti per verificare una banale correzione dell'interfaccia utente.

Questa NON è una soluzione completa per un ambiente di produzione e ho trovato la soluzione migliore che ci sia per spostare quanta più logica possibile nei file di dati e quindi ricaricare quei file di dati.


quale ciclo di compilazione richiede 20 minuti?!? preferirei spararmi
JqueryToAddNumbers il

3

Come altri hanno già detto, è un problema difficile, che collega dinamicamente C ++. Ma è un problema risolto: potresti aver sentito parlare di COM o di uno dei nomi di marketing che gli sono stati applicati nel corso degli anni: ActiveX.

COM ha un nome un po 'brutto dal punto di vista dello sviluppatore perché può essere molto difficile implementare componenti C ++ che espongono la loro funzionalità utilizzandolo (sebbene ciò sia reso più semplice con ATL - la libreria di modelli ActiveX). Dal punto di vista del consumatore ha un brutto nome perché le applicazioni che lo utilizzano, ad esempio per incorporare un foglio di calcolo Excel in un documento Word o un diagramma Visio in un foglio di calcolo Excel, tendevano a bloccarsi un po 'indietro nel corso della giornata. E questo dipende dagli stessi problemi - anche con tutte le indicazioni che Microsoft offre, COM / ActiveX / OLE era / è difficile da ottenere.

Sottolineerò che la tecnologia della COM non è intrinsecamente cattiva. Prima di tutto, DirectX utilizza le interfacce COM per esporre la sua funzionalità e che funziona abbastanza bene, così come una moltitudine di applicazioni che incorporano Internet Explorer usando il suo controllo ActiveX. In secondo luogo, è uno dei modi più semplici per collegare dinamicamente il codice C ++: un'interfaccia COM è essenzialmente una pura classe virtuale. Anche se ha un IDL come CORBA, non sei obbligato a usarlo, specialmente se le interfacce che definisci sono utilizzate solo all'interno del tuo progetto.

Se non stai scrivendo per Windows, non pensare che non valga la pena considerare COM. Mozilla lo ha reimplementato nel suo codebase (utilizzato nel browser Firefox) perché aveva bisogno di un modo per compartimentare il proprio codice C ++.


2

C'è un'implementazione del C ++ compilato in runtime per il codice di gioco qui . Inoltre, so che esiste almeno un motore di gioco proprietario che fa lo stesso. È difficile, ma fattibile.

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.