Consigli sull'architettura di gioco / modelli di progettazione


16

Ho lavorato su un gioco di ruolo 2d per un po 'di tempo, e ho capito che ho preso alcune cattive decisioni di progettazione. Ci sono alcune cose in particolare che mi stanno causando problemi, quindi mi chiedevo che tipo di progetti altri usavano per superarli o che avrebbero usato.

Per un po 'di storia, ho iniziato a lavorarci nel mio tempo libero l'estate scorsa. Inizialmente stavo realizzando il gioco in C #, ma circa 3 mesi fa ho deciso di passare al C ++. Volevo avere una buona padronanza del C ++ poiché è passato un po 'di tempo da quando l'ho usato pesantemente e ho pensato che un progetto interessante come questo sarebbe stato un buon motivatore. Ho usato ampiamente la libreria boost e ho usato SFML per la grafica e FMOD per l'audio.

Ho scritto un bel po 'di codice, ma sto pensando di scartarlo e ricominciare da capo.

Ecco le principali aree di interesse che ho e ho voluto ottenere alcune opinioni sul modo corretto in cui gli altri le hanno risolte o le avrebbero risolte.

1. Dipendenze cicliche Quando stavo facendo il gioco in C #, non dovevo preoccuparmene poiché non è un problema. Passando al C ++, questo è diventato un problema piuttosto grave e mi ha fatto pensare che avrei potuto progettare le cose in modo errato. Non riesco davvero a immaginare come separare le mie lezioni e farle comunque fare quello che voglio. Ecco alcuni esempi di una catena di dipendenze:

Ho una classe di effetti di stato. La classe ha una serie di metodi (Applica / Non applica, Spunta, ecc.) Per applicare i suoi effetti a un personaggio. Per esempio,

virtual void TickCharacter(Character::BaseCharacter* character, Battles::BattleField *field, int ticks = 1);

Questa funzione viene chiamata ogni volta che il personaggio inflitto dall'effetto status fa un turno. Sarebbe utile per implementare effetti come Regen, Poison, ecc. Tuttavia, introduce anche dipendenze dalla classe BaseCharacter e dalla classe BattleField. Naturalmente, la classe BaseCharacter deve tenere traccia di quali effetti di stato sono attualmente attivi su di essi, quindi questa è una dipendenza ciclica. Il campo di battaglia deve tenere traccia delle parti in lotta e la classe del partito ha un elenco di Personaggi Base che introduce un'altra dipendenza ciclica.

2 - Eventi

In C # ho fatto ampio uso di delegati per agganciare eventi su personaggi, campi di battaglia ecc. (Ad esempio, c'era un delegato per quando cambiava la salute del personaggio, quando cambiava una statistica, quando veniva aggiunto / rimosso un effetto di stato, ecc. .) e il campo di battaglia / componenti grafici si aggancerebbero a quei delegati per far valere i loro effetti. In C ++, ho fatto qualcosa di simile. Ovviamente non esiste un equivalente diretto per i delegati C #, quindi invece ho creato qualcosa del genere:

typedef boost::function<void(BaseCharacter*, int oldvalue, int newvalue)> StatChangeFunction;

e nella mia classe di personaggi

std::map<std::string, StatChangeFunction> StatChangeEventHandlers;

ogni volta che cambiava la statistica del personaggio, ripetevo e chiamavo ogni StatChangeFunction sulla mappa. Mentre funziona, sono preoccupato che questo sia un cattivo approccio nel fare le cose.

3 - Grafica

Questa è la cosa più grande. Non è correlato alla libreria grafica che sto usando, ma è più una cosa concettuale. In C #, ho unito la grafica a molte delle mie lezioni che so essere un'idea terribile. Volendo farlo disaccoppiato questa volta ho provato un approccio diverso.

Al fine di implementare la mia grafica, immaginavo tutto ciò che riguardava la grafica nel gioco come una serie di schermi. Cioè c'è una schermata del titolo, una schermata di stato del personaggio, una schermata della mappa, una schermata di inventario, una schermata di battaglia, una schermata della GUI di battaglia, e fondamentalmente potrei impilare queste schermate l'una sopra l'altra, se necessario, per creare la grafica del gioco. Qualunque sia la schermata attiva, possiede l'input di gioco.

Ho progettato un gestore dello schermo che avrebbe spinto e pop gli schermi in base all'input dell'utente.

Ad esempio, se ti trovassi su una schermata della mappa (un gestore di input / visualizzatore per una mappa a tessere) e premi il pulsante di avvio, invierebbe una chiamata al gestore dello schermo per spingere una schermata del menu principale sulla schermata della mappa e contrassegnare la mappa schermo da non disegnare / aggiornare. Il giocatore dovrebbe spostarsi nel menu, che invierà più comandi al gestore dello schermo, a seconda dei casi, per inserire nuovi schermi nella pila di schermate, quindi farli apparire quando l'utente cambia schermate / annulla. Alla fine, quando il giocatore esce dal menu principale, lo apro e torno alla schermata della mappa, osservo che deve essere disegnato / aggiornato e andare da lì.

Gli schermi di battaglia sarebbero più complessi. Avrei uno schermo che fungerà da sfondo, uno schermo per visualizzare ogni parte in battaglia e uno schermo per visualizzare l'interfaccia utente per la battaglia. L'interfaccia utente si aggancerebbe agli eventi dei personaggi e li userebbe per determinare quando aggiornare / ridisegnare i componenti dell'interfaccia utente. Infine, ogni attacco che ha uno script di animazione disponibile chiamerebbe un livello aggiuntivo per animarsi prima di saltar fuori dallo stack dello schermo. In questo caso, ogni livello è costantemente contrassegnato come disegnabile e aggiornabile e ottengo una pila di schermate che gestiscono la mia grafica di battaglia.

Anche se non sono ancora riuscito a far funzionare perfettamente lo screen manager, penso che potrò farlo con un po 'di tempo. La mia domanda al riguardo è: è un approccio assolutamente utile? Se è un cattivo design che voglio sapere ora prima di investire troppo tempo a realizzare tutti gli schermi di cui avrò bisogno. Come costruisci la grafica per il tuo gioco?

Risposte:


15

Nel complesso, non direi che qualsiasi cosa tu abbia elencato dovrebbe farti cancellare il sistema e ricominciare da capo. Questo è qualcosa che ogni programmatore vuole fare circa il 50-75% durante qualsiasi progetto a cui sta lavorando, ma porta a un ciclo di sviluppo senza fine e a non finire mai nulla. Quindi, a tal fine, alcuni feedback su ogni sezione.

  1. Questo può essere un problema, ma di solito è più un fastidio che altro. Stai usando #pragma una volta o #ifndef MY_HEADER_FILE_H #define MY_HEADER_FILE_H ... #endif nella parte superiore o intorno ai tuoi file .h rispettivamente? In questo modo il file .h esiste solo una volta all'interno di ciascun ambito? In tal caso, la mia raccomandazione diventa quindi la rimozione di tutte le istruzioni e la compilazione #include, aggiungendo quelle necessarie per compilare nuovamente il gioco.

  2. Sono un fan di questi tipi di sistemi e non vedo nulla di sbagliato in questo. Ciò che è un evento in C # viene comunemente sostituito con un sistema di eventi o un sistema di messaggistica (è possibile cercare le domande qui per trovare tali informazioni per ulteriori informazioni). La chiave qui è quella di mantenerli al minimo quando le cose devono accadere, cosa che sembra già che tu stia facendo, quindi dovrebbe essere no a preoccupazioni minime qui.

  3. Questo mi sembra anche sulla strada giusta ed è ciò che faccio per i miei motori, sia a livello personale che professionale. Questo trasforma il sistema dei menu in un sistema statale che ha il menu principale (prima dell'inizio del gioco) o l'HUD del giocatore come schermata 'radice' visualizzata, a seconda di come lo hai impostato.

Quindi, per riassumere, non vedo nulla di nuovo degno di ciò in cui ti imbatti. Potresti desiderare una sostituzione del sistema di eventi più formale lungo la strada, ma arriverà in tempo. Cyclic include è un ostacolo a cui tutti i programmatori C / C ++ devono costantemente saltare e, lavorando per disaccoppiare la grafica, sembrano logici "passi successivi".

Spero che sia di aiuto!


#ifdef non aiuta i problemi di inclusione circolare.
L'anatra comunista

Stavo solo coprendo le mie basi aspettandomi che fosse lì prima di rintracciare le inclusioni cicliche. Può essere un intero altro bollitore di pesce quando hai più simboli definiti rispetto a un file che deve includere un file che include se stesso. (anche se da quello che ha descritto se le inclusioni sono nei file .CPP e non nei file .H, dovrebbe essere a posto con due oggetti base che si conoscono a vicenda)
James,

Grazie per il consiglio :) Sono contento di sapere che sono sulla buona strada
user127817

4

Le tue dipendenze cicliche non dovrebbero essere un problema fintanto che stai dichiarando in avanti le classi dove puoi nei file di intestazione e in realtà #includendole nei file .cpp (o qualunque altra cosa).

Per il sistema di eventi, due suggerimenti:

1) Se vuoi mantenere il modello che stai usando ora, considera di passare a una boost :: unordered_map invece di std :: map. La mappatura con stringhe come chiavi è lenta, soprattutto perché .NET fa alcune cose carine per velocizzare le cose. L'uso di unordered_map esegue l'hashing delle stringhe, quindi i confronti sono generalmente più veloci.

2) Valuta di passare a qualcosa di più potente come boost :: segnali. Se lo fai, puoi fare cose carine come rendere tracciabili i tuoi oggetti di gioco derivando da boost :: signal :: trackable e lasciare che il distruttore si occupi di ripulire tutto invece di dover annullare la registrazione manualmente dal sistema degli eventi. Puoi anche avere più segnali che puntano a ogni slot (o viceversa, non ricordo l'esatta nomenclatura) quindi è molto simile a fare +=in un delegateC #. Il problema più grande con boost :: segnali è che deve essere compilato, non è solo intestazioni, quindi a seconda della piattaforma potrebbe essere una seccatura alzarsi e funzionare.

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.