Per molti sviluppatori indie di partenza, l'ottimizzazione del codice diventa quasi un secondo pensiero. Viene passato in favore di motori o strutture, o può essere considerato una tecnica più "avanzata" fuori dalla loro portata. Tuttavia, esistono metodi di ottimizzazione che possono essere utilizzati in modi più basilari, consentendo al tuo codice di operare in modo più efficiente e su più sistemi. Diamo un'occhiata all'ottimizzazione di base del codice per iniziare.
Non è raro che gli sviluppatori indie emulino i metodi di ottimizzazione di aziende più grandi. Non è necessariamente una brutta cosa, ma il tentativo di ottimizzare il tuo gioco oltre il punto di utili utili è un buon modo per impazzire. Una tattica intelligente per tenere traccia dell'efficienza delle tue ottimizzazioni è quella di segmentare la tua fascia demografica target e esaminare i tipi di specifiche delle loro macchine. Benchmarking del tuo gioco contro i computer e le console che i tuoi potenziali giocatori stanno usando contribuirà a mantenere un equilibrio tra ottimizzazione e sanità mentale.
A parte questo, ci sono alcune ottimizzazioni che possono quasi sempre essere utilizzate per migliorare il rendimento del gioco. Molti di questi sono agnostici di sistema (e alcuni motori e framework li tengono già in considerazione), quindi includerò alcuni esempi di pseudocodice per farti partire con il piede giusto. Diamo un'occhiata.
Spesso gestita dai motori e, a volte, persino dalle GPU stesse, è estremamente importante ridurre al minimo la quantità di potenza di elaborazione in oggetti fuori dallo schermo. Per le tue creazioni, è una buona idea iniziare a separare i tuoi oggetti in due "livelli" essenzialmente: il primo è la sua rappresentazione grafica, e il secondo sono i suoi dati e le sue funzioni (come la sua posizione). Quando un oggetto è fuori schermo, non è più necessario spendere le risorse per renderlo, e invece dovremmo optare per il tracciamento. Tracciare le cose, come la posizione e lo stato, con le variabili riduce le risorse necessarie a solo una frazione del costo iniziale.
Per i giochi con un numero elevato di oggetti o oggetti che pesano molto, potrebbe anche essere utile fare un ulteriore passo avanti creando routine di aggiornamento separate, impostandone una da aggiornare mentre l'oggetto è sullo schermo e l'altro da fuori schermo . L'impostazione di una separazione più avanzata in questo modo può impedire al sistema di dover eseguire un numero di animazioni, algoritmi e altri aggiornamenti che potrebbero non essere necessari quando l'oggetto è nascosto.
Ecco un esempio pseudocodice di una classe di oggetti che utilizza flag e vincoli di posizione:
Oggetto NPC Int locationX, locationY; // posizione corrente dell'oggetto su un piano 2d Funzione drawObject () // una funzione per disegnare l'oggetto da chiamare nella funzione di aggiornamento dello schermo // che controlla se l'oggetto si trova nella porta di visualizzazione corrente Funzione pollObjectDraw (array currentViewport [minX, minY, maxX, maxY]) // se si trova all'interno della finestra, restituisce che può essere disegnato If (this.within (currentViewport)) Restituisce true; Altro Restituisce falso;
Sebbene questo esempio sia semplificato, ci consente di verificare se l'oggetto verrà visualizzato prima di disegnarlo, permettendoci di eseguire una funzione abbastanza semplificata invece di una chiamata completa. Per separare le funzioni oltre alle chiamate grafiche, potrebbe essere necessario utilizzare un buffer aggiuntivo, ad esempio una funzione che includa tutto ciò che un lettore può essere in grado di vedere a breve, piuttosto che ciò che è attualmente in grado di vedere.
A seconda del motore o del framework che si sta utilizzando, è possibile che gli oggetti vengano aggiornati su ogni frame o "tick". In questo modo è possibile tassare un processore abbastanza rapidamente, quindi per alleggerire tale carico, vorremmo ridurre le chiamate su ogni frame quando possibile.
La prima cosa che vorremmo separare è il rendering della funzione. Queste chiamate sono in genere ad uso intensivo di risorse, quindi l'integrazione di una chiamata che può dirci quando le proprietà visive di un oggetto sono cambiate può ridurre drasticamente il rendering.
Per fare un ulteriore passo avanti, possiamo utilizzare uno schermo temporaneo per i nostri oggetti. Avendo gli oggetti disegnati direttamente in questo contenitore temporaneo, possiamo assicurarci che vengano disegnati solo quando necessario.
Simile alla prima ottimizzazione menzionata sopra, l'iterazione iniziale del nostro codice introduce un semplice polling:
NPC oggetto booleano hasChanged; // imposta questo flag su true ogni volta che viene apportata una modifica alla funzione // dell'oggetto che restituisce se Function pollObjectChanged (return hasChanged ();
In ogni fotogramma ora, invece di eseguire una serie di funzioni, possiamo vedere se è addirittura necessario. Anche se questa implementazione è semplice, può già iniziare a mostrare enormi miglioramenti nell'efficienza del tuo gioco, specialmente quando si tratta di oggetti statici e oggetti a lento aggiornamento come un HUD.
Per fare ulteriormente questo nel tuo gioco, spezzare la bandiera in più componenti più piccoli può essere utile per la funzionalità di segmentazione. Ad esempio, potresti avere dei flag per una modifica dei dati e una modifica grafica avviene separatamente.
Si tratta di un'ottimizzazione che è stata utilizzata sin dai primi giorni dei sistemi di gioco. Determinare i trade-off tra calcoli in tempo reale e ricerche di valore può aiutare a ridurre drasticamente i tempi di elaborazione. Un noto utilizzo nella storia del gioco sta memorizzando i valori delle funzioni di trigonometria nelle tabelle poiché, nella maggior parte dei casi, è stato più efficiente memorizzare una tabella di grandi dimensioni e recuperarla da essa anziché eseguire i calcoli in tempo reale e sottoporre a ulteriore pressione sulla CPU.
Nel computing moderno, raramente è necessario scegliere tra la memorizzazione dei risultati e l'esecuzione di un algoritmo. Tuttavia, ci sono ancora situazioni in cui ciò può ridurre le risorse utilizzate, consentendo l'inclusione di altre funzionalità senza sovraccaricare un sistema.
Un modo semplice per iniziare a implementare questo è identificare i calcoli che si verificano comunemente, o parti di calcoli, all'interno del gioco: più grande è il calcolo, meglio è. L'esecuzione di bit di algoritmi ricorrenti in una sola volta e la relativa memorizzazione può spesso ridurre considerevoli quantità di potenza di elaborazione. Anche isolare queste parti in specifici loop di gioco può aiutare a ottimizzare le prestazioni.
Ad esempio, in molti sparatutto top-down ci sono spesso grandi gruppi di nemici che eseguono gli stessi comportamenti. Se ci sono 20 nemici, ciascuno che si muove lungo un arco, invece di calcolare singolarmente ciascun movimento, è più efficiente memorizzare i risultati dell'algoritmo. Ciò consente di modificarlo in base alla posizione di partenza di ciascun nemico.
Per determinare se questo metodo è utile per il tuo gioco, prova a utilizzare il benchmarking per confrontare la differenza di risorse utilizzate tra il calcolo in tempo reale e l'archiviazione dei dati.
Mentre questo gioca maggiormente sull'utilizzo di risorse dormienti, con un'attenta riflessione per i tuoi oggetti e algoritmi, possiamo impilare le attività in un modo che spinge l'efficienza del nostro codice.
Per iniziare ad usare la sensibilità all'ozio nel proprio software, per prima cosa dovrai separare quali attività all'interno del tuo gioco non sono cruciali per il tempo o possono essere calcolate prima che siano necessarie. La prima area in cui cercare il codice che rientra in questa categoria è la funzionalità strettamente correlata all'atmosfera del gioco. I sistemi meteorologici che non interagiscono con la geografia, gli effetti visivi di sfondo e l'audio di sottofondo possono facilmente adattarsi al calcolo inattivo.
Al di là degli oggetti che sono strettamente atmosferici, i calcoli garantiti sono un altro tipo di calcolo che può essere collocato in spazi inattivi. I calcoli di intelligenza artificiale che si verificheranno indipendentemente dall'interazione del giocatore (sia perché non prendono in considerazione il giocatore, o difficilmente richiederanno l'interazione del giocatore fino a ora) possono essere resi più efficienti, così come i movimenti calcolati, come eventi scriptati.
La creazione di un sistema che utilizza l'ozio fa anche di più che consentire una maggiore efficienza, può essere usato per ridimensionare gli "occhi". Ad esempio, su un rig di fascia bassa, forse un giocatore prova semplicemente una versione vaniglia del gameplay. Se il nostro sistema rileva fotogrammi inattivi, tuttavia, possiamo usarlo per aggiungere particelle aggiuntive, eventi grafici e altri ritocchi atmosferici per dare al gioco un po 'più di brio.
Per implementare ciò, utilizzare la funzionalità disponibile nel motore, nel framework o nella lingua preferiti per misurare la quantità di CPU utilizzata. Imposta i flag all'interno del tuo codice che semplificano la verifica della quantità di potenza di elaborazione "extra" disponibile, quindi imposta i tuoi sottosistemi per osservare questo flag e comportarsi di conseguenza.
Combinando questi metodi, è possibile rendere il codice significativamente più efficiente. Con questa efficienza arriva la possibilità di aggiungere più funzionalità, eseguire più sistemi e garantire un'esperienza più solida per i giocatori.
Avete implementazioni di codice facili da implementare che utilizzate regolarmente? Fammi sapere di loro!