Quali sono alcuni modi per separare la logica di gioco dalle animazioni e dal loop di disegno?


9

Ho realizzato solo giochi flash in precedenza, usando MovieClips e simili per separare le mie animazioni dalla mia logica di gioco. Ora mi sto cimentando nella realizzazione di un gioco per Android, ma la teoria della programmazione del gioco sulla separazione di queste cose mi confonde ancora. Vengo da un background di sviluppo di applicazioni web non di gioco, quindi sono esperto in più modelli MVC e sono bloccato in quella mentalità mentre mi avvicino alla programmazione di gioco.

Voglio fare cose come astrarre il mio gioco avendo, ad esempio, una classe di gioco che contiene i dati per una griglia di tessere con istanze di una classe di tessere che contengono ciascuna proprietà. Posso dare accesso al mio ciclo di pesca a questo e farlo disegnare il tabellone in base alle proprietà di ogni tessera sul tabellone, ma non capisco dove dovrebbe esattamente andare l'animazione. Per quanto ne so, l'animazione si colloca tra la logica di gioco astratta (modello) e il loop di disegno (vista). Con la mia mentalità MVC, è frustrante cercare di decidere dove dovrebbe effettivamente andare l'animazione. Avrebbe un bel po 'di dati associati ad esso come un modello, ma apparentemente deve essere strettamente associato al ciclo di disegno per avere cose come l'animazione indipendente dal frame.

Come posso uscire da questa mentalità e iniziare a pensare a schemi che hanno più senso per i giochi?

Risposte:


6

L'animazione può ancora essere perfettamente divisa tra logica e rendering. Lo stato astratto dei dati di animazione sarebbe l'informazione necessaria affinché l'API grafica esegua il rendering dell'animazione.

Nei giochi 2D, ad esempio, quella potrebbe essere un'area rettangolare che segna l'area che mostra la parte corrente del tuo foglio sprite che deve essere disegnata (quando hai un foglio composto da diciamo 30 disegni 80x80 contenenti le varie fasi del tuo personaggio saltare, sedersi, muoversi ecc.). Può anche essere qualsiasi tipo di dati che non è necessario per il rendering, ma forse per gestire gli stati di animazione stessi, come il tempo rimanente fino alla scadenza del passaggio di animazione corrente o il nome dell'animazione ("walking", "standing" ecc.) Tutto ciò può essere rappresentato nel modo desiderato. Questa è la parte della logica.

Nella parte di rendering, lo fai come al solito, ottieni quel rettangolo dal tuo modello e usa il tuo renderer per fare effettivamente le chiamate all'API grafica.

Nel codice (usando la sintassi C ++ qui):

class Sprite //Model
{
    private:
       Rectangle subrect;
       Vector2f position;
       //etc.

    public:
       Rectangle GetSubrect() 
       {
           return subrect;
       }
       //etc.
};

class AnimatedSprite : public Sprite, public Updatable //arbitrary interface for classes that need to change their state on a regular basis
{
    AnimationController animation_controller;
    //etc.
    public:
        void Update()
        {
            animation_controller.Update(); //Good OOP design ;) It will take control of changing animations in time etc. for you
            this.SetSubrect(animation_controller.GetCurrentAnimation().GetRect());
        }
        //etc.
};

Questi sono i dati. Il tuo renderer prenderà quei dati e li disegnerà. Poiché sia ​​gli Sprite normali che quelli animati sono disegnati allo stesso modo, puoi usare la polimorfia qui!

class Renderer
{
    //etc.
    public:
       void Draw(const Sprite &spr)
       {
           graphics_api_pointer->Draw(spr.GetAllTheDataThatINeed());
       }
};

TMV:

Ho trovato un altro esempio. Supponi di avere un gioco di ruolo. Il tuo modello che rappresenta la mappa del mondo, ad esempio, probabilmente avrebbe bisogno di memorizzare la posizione del personaggio nel mondo come coordinate della tessera sulla mappa. Tuttavia, quando sposti il ​​personaggio, camminano di alcuni pixel alla volta fino al quadrato successivo. Memorizzi questa posizione "tra riquadri" in un oggetto animazione? Come si aggiorna il modello quando il personaggio è finalmente "arrivato" alla coordinata successiva della tessera sulla mappa?

La mappa del mondo non conosce direttamente la posizione dei giocatori (non ha un Vector2f o qualcosa del genere che memorizza direttamente la posizione dei giocatori =, invece ha un riferimento diretto all'oggetto giocatore stesso, che a sua volta deriva da AnimatedSprite così puoi passarlo facilmente al renderer e ottenere tutti i dati necessari da esso.

In generale, tuttavia, la tua tilemap non dovrebbe essere in grado di fare proprio tutto: avrei una classe "TileMap" che si occupa della gestione di tutte le tessere, e forse fa anche il rilevamento delle collisioni tra oggetti che gli passo e le tessere sulla mappa. Quindi, avrei un'altra classe "RPGMap", o comunque ti piacerebbe chiamarla, che ha sia un riferimento alla tua piastrella che il riferimento al giocatore ed effettua le effettive chiamate Update () al tuo giocatore e al tuo tilemap.

Il modo in cui vuoi aggiornare il modello quando il giocatore si muove dipende da cosa vuoi fare.

Il tuo giocatore può muoversi tra le tessere in modo indipendente (stile Zelda)? Gestisci semplicemente l'input e sposta il giocatore di conseguenza in ogni frame. Oppure vuoi che il giocatore prema "a destra" e il tuo personaggio sposta automaticamente una tessera a destra? Lascia che la tua classe RPGMap interpoli la posizione dei giocatori fino a quando non arriva a destinazione e nel frattempo blocca tutte le operazioni di immissione dei tasti di movimento.

In entrambi i casi, se vuoi semplificarti, tutti i tuoi modelli avranno metodi Update () se in realtà hanno bisogno di un po 'di logica per aggiornarsi (invece di cambiare solo i valori delle variabili) - Non dai via il controller nel modello MVC in quel modo, basta spostare il codice da "un passo sopra" (il controller) verso il basso al modello, e tutto ciò che il controller fa è chiamare questo metodo Update () del modello (Il controller nel nostro caso sarebbe la RPGMap). Puoi ancora scambiare facilmente il codice della logica: puoi semplicemente modificare direttamente il codice della classe o se hai bisogno di un comportamento completamente diverso, puoi semplicemente derivare dalla tua classe del modello e sovrascrivere solo il metodo Update ().

Questo approccio riduce molto le chiamate ai metodi e cose del genere - che erano uno dei principali svantaggi del modello MVC puro (finisci per chiamare GetThis () GetThat () molto molto spesso) - rende il codice più lungo e un un po 'più difficile da leggere e anche più lento - anche se potrebbe essere curato dal tuo compilatore che ottimizza molte cose del genere.


Manterresti i dati di animazione nella classe contenente la logica di gioco, la classe contenente il loop di gioco o separati da entrambi? Inoltre, dipende interamente dal ciclo o dalla classe che contiene il ciclo per capire come tradurre i dati di animazione nel disegno dello schermo, giusto? Spesso non sarebbe semplice come ottenere un retto che rappresenta una sezione del foglio sprite e usarlo per tagliare un disegno bitmap dal foglio sprite.
TMV,

Ho trovato un altro esempio. Supponi di avere un gioco di ruolo. Il tuo modello che rappresenta la mappa del mondo, ad esempio, dovrebbe probabilmente memorizzare la posizione del personaggio nel mondo come coordinate della tessera sulla mappa. Tuttavia, quando sposti il ​​personaggio, camminano di alcuni pixel alla volta fino al quadrato successivo. Memorizzi questa posizione "tra riquadri" in un oggetto animazione? Come si aggiorna il modello quando il personaggio è finalmente "arrivato" alla coordinata successiva della tessera sulla mappa?
TMV,

Ho modificato la risposta alla tua domanda, poiché i commenti non consentono abbastanza caratteri per quello.
TravisG,

Se capisco tutto correttamente:
TMV

Potresti avere un'istanza di una classe "Animator" nella tua Vista e avrebbe un metodo pubblico di "aggiornamento" che viene chiamato ogni frame dalla vista. Il metodo di aggiornamento chiama i metodi di "aggiornamento" di istanze di vari tipi di singoli oggetti di animazione al suo interno. L'animatore e le animazioni al suo interno hanno un riferimento al Modello (tramandato attraverso i loro costruttori) in modo che possano aggiornare i dati del modello se un'animazione lo cambierebbe. Quindi, nel ciclo di disegno, ottieni i dati dalle animazioni all'interno dell'animatore in un modo che può essere compreso dalla vista e disegnato.
TMV,

2

Posso svilupparlo se lo desideri, ma ho un renderer centrale che mi viene detto di disegnare in loop. Piuttosto che

handle input

for every entity:
    update entity

for every entity:
    draw entity

Ho un sistema più simile

handle input (well, update the state. Mine is event driven so this is null)

for every entity:
    update entity //still got game logic here

renderer.draw();

La classe renderer contiene semplicemente un elenco di riferimenti a componenti disegnabili di oggetti. Questi sono assegnati nei costruttori per semplicità.

Per il tuo esempio, avrei una classe GameBoard con un numero di tessere. Ogni tessera ovviamente conosce la sua posizione e presumo una sorta di animazione. Inseriscilo in una sorta di classe di animazione che possiede la tessera e fai passare un riferimento di se stesso a una classe di rendering. Lì, tutti separati. Quando aggiorni Tile, chiama Aggiorna sull'animazione ... o lo aggiorna da solo. Quando Renderer.Draw()viene chiamato, disegna l'animazione.

L'animazione indipendente dal frame non dovrebbe avere troppo a che fare con il loop di disegno.


0

Ultimamente ho imparato da me i paradigmi, quindi se questa risposta è incompleta, sono sicuro che qualcuno ci aggiungerà.

La metodologia che sembra avere più senso per la progettazione del gioco è quella di separare la logica dall'output dello schermo.

Nella maggior parte dei casi vorresti usare un approccio multi-thread, se non hai familiarità con quell'argomento, è una domanda tutta sua, ecco il primer di wiki . Essenzialmente vuoi che la tua logica di gioco venga eseguita in un thread, bloccando variabili a cui deve accedere per garantire l'integrità dei dati. Se il tuo loop logico è incredibilmente veloce (Super Mega 3d animato?) Puoi provare a fissare la frequenza che esegue il loop dormendo il thread per piccole durate (120 hz è stato suggerito in questo forum per i loop di fisica del gioco). Allo stesso tempo, l'altro thread sta ridisegnando lo schermo (60 hz è stato suggerito in altri argomenti) con le variabili aggiornate, chiedendo di nuovo un blocco delle variabili prima di accedervi.

In tal caso, animazioni o transizioni, ecc., Vanno nel thread del disegno ma devi segnalare attraverso una bandiera di qualche tipo (forse una variabile di stato globale) che il thread della logica di gioco non deve fare nulla (o fare qualcosa di diverso ... imposta il nuovi parametri della mappa forse).

Una volta che hai la testa intorno alla concorrenza, il resto è abbastanza comprensibile. Se non hai esperienza con la concorrenza ti consiglio vivamente di scrivere alcuni semplici programmi di test in modo da poter capire come avviene il flusso.

Spero che sia di aiuto :)

[modifica] Sui sistemi che non supportano il multithreading, l'animazione può ancora andare nel ciclo di disegno, ma si desidera impostare lo stato in modo tale da segnalare alla logica che si sta verificando qualcosa di diverso e non continuare a elaborare il livello attuale / mappa / ecc ...


1
Non sono d'accordo qui. Nella maggior parte dei casi non si desidera eseguire il multi-thread, soprattutto se si tratta di un gioco piccolo.
Il comunista Duck il

@TheCommunistDuck Abbastanza giusto, il sovraccarico e la complessità per il multithreading possono sicuramente renderlo eccessivo, inoltre se il gioco è piccolo, dovrebbe essere in grado di aggiornarsi rapidamente.
Stephen,
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.