Codificare stati diversi in Giochi di avventura


12

Sto pianificando un gioco di avventura e non riesco a capire quale sia il modo giusto per attuare il comportamento di un livello in base allo stato di avanzamento della storia.

Il mio gioco per giocatore singolo presenta un enorme mondo in cui il giocatore deve interagire con le persone in una città in vari punti del gioco. Tuttavia, a seconda della progressione della storia, al giocatore verranno presentate diverse cose, ad esempio il leader della gilda cambierà le posizioni dalla piazza della città a varie località della città; Le porte si aprivano solo in determinati momenti della giornata dopo aver terminato una particolare routine; Diversi eventi cut-screen / trigger si verificano solo dopo il raggiungimento di un determinato traguardo.

Ho ingenuamente pensato di utilizzare inizialmente un'istruzione switch {} per decidere cosa dire l'NPC o su cui si potesse trovare e rendere interattivi gli obiettivi della missione solo dopo aver verificato le condizioni di una variabile game_state globale. Ma mi sono reso conto che mi sarei imbattuto rapidamente in molti stati di gioco e casi diversi per cambiare il comportamento di un oggetto. Anche questa istruzione switch sarebbe molto difficile da eseguire il debug e immagino che potrebbe essere difficile da usare in un editor di livelli.

Quindi ho pensato, invece di avere un singolo oggetto con più stati, forse avrei dovuto avere più istanze dello stesso oggetto, con un solo stato. In questo modo, se uso qualcosa come un editor di livelli, posso mettere un'istanza dell'NPC in tutte le diverse posizioni in cui potrebbe mai apparire, e anche un'istanza per ogni stato di conversazione che ha. Ciò significa che ci saranno molti oggetti di gioco inattivi e invisibili che fluttuano intorno al livello, il che potrebbe essere un problema per la memoria o semplicemente difficile da vedere in un editor di livelli, non lo so.

O semplicemente, crea un livello identico, ma separato per ogni stato del gioco. Questo sembra il modo più pulito e privo di bug per fare le cose, ma sembra un enorme lavoro manuale assicurandosi che ogni versione del livello sia davvero identica l'una all'altra.

Tutti i miei metodi sembrano così inefficienti, quindi per ricapitolare la mia domanda, esiste un modo migliore o standardizzato per implementare il comportamento di un livello a seconda dello stato di avanzamento della storia?

PS: Non ho ancora un editor di livelli, sto pensando di usare qualcosa come JME SDK o crearne uno mio.

Risposte:


9

Penso che ciò di cui hai bisogno in questo caso sia lo State Design Pattern . Invece di avere più istanze di ciascun oggetto di gioco, crea una singola istanza, ma incapsula il suo comportamento in una classe separata. Crea più classi, una per ogni possibile comportamento e assegna a tutte le classi la stessa interfaccia. Associa uno al tuo oggetto di gioco (lo stato iniziale) e, quando cambiano le condizioni (viene raggiunta una pietra miliare, passa l'ora del giorno, ecc.) Cambi lo stato di quell'oggetto (cioè associalo a un oggetto diverso a seconda della logica del tuo gioco) e aggiorna le sue proprietà se applicabile.

Un esempio di come apparirebbe un'interfaccia di stato (completamente inventata - solo per illustrare il livello di controllo che questo schema offre):

interface NPCState {
    Scene whereAmI(NPC o);
    String saySomething(NPC o);
}

E due classi di attuazione:

class Busy implements NPCState {
    Scene whereAmI(NPC o) {
        return o.getWorkScene();
    }
    String saySomething(NPC o) {
        return "Can't talk now, I'm busy!";
    }
}

class Available implements NPCState {
    Scene whereAmI(NPC o) {
        return TAVERN;
    }
    String saySomething(NPC o) {
        String[] choices = o.getRandomChat();
        return choices[RANDOM.getInt(choices.length)];
    }
}

E stati di commutazione:

// The time of day passed from "afternoon" to "evening"
NPCState available = new Available();
for ( NPC o : list ) {
    Scene oldScene = o.state.whereAmI(o);
    o.state = available;
    Scene newScene = o.state.whereAmI(o);
    moveGameObject(o, oldScene, newScene);
    ...

Gli NPC importanti possono avere i loro stati personalizzati, la logica di scelta dello stato può essere più personalizzabile e puoi avere stati diversi per diverse sfaccettature del gioco (in questo esempio, ho usato una singola classe per dire sia la posizione che la chat, ma potresti separare li e fare molte combinazioni).

Funziona bene anche con gli editor di livelli: puoi avere una semplice casella combinata per cambiare lo stato "globale" di un livello, quindi aggiungere e riposizionare gli oggetti di gioco come desideri che appaiano in quello stato. Il motore di gioco sarebbe responsabile di "aggiungere" effettivamente l'oggetto alla scena solo quando ha lo stato corretto, ma i suoi parametri sarebbero comunque modificabili in modo intuitivo.

(Dichiarazione di non responsabilità: ho poca esperienza nel mondo reale con i redattori di giochi, quindi posso dire con sicurezza su come funzionano i redattori professionisti; ma il mio punto sul modello di stato vale ancora, organizzare il codice in questo modo dovrebbe essere pulito, gestibile e non sprecare il sistema risorse).


sai, potresti combinare questo modello di progettazione statale con l'array associativo che ho descritto. È possibile codificare gli oggetti di stato come descritto qui, quindi scegliere tra diversi oggetti di stato utilizzando una matrice associativa come ho suggerito.
jhocking

Sono d'accordo, è anche bello separare il gioco dal suo motore e la logica di gioco hardcoding rafforza l'accoppiamento tra loro (riducendo la possibilità di riutilizzo). C'è un compromesso, tuttavia, poiché a seconda della complessità del comportamento previsto, il tentativo di "codificare" tutto può causare ingombri inutili . In questo caso, potrebbe essere auspicabile un approccio misto (ovvero avere una logica di transizione di stato "generica", ma che consenta di incorporare anche il codice personalizzato)
mgibsonbr

Quindi, a quanto ho capito, esiste una mappatura individuale tra NPCState e GameState. Quindi inserisco gli NPC in un array e lo ripasso, assegnando il nuovo NPCState quando si osserva un cambio di stato del gioco. L'NPCState deve essere in grado di sapere come gestire ogni NPC diff che gli viene inviato, quindi essenzialmente l'NPCState contiene il comportamento di tutti gli NPC per un dato stato? Mi piace il fatto che tutti i comportamenti vengano archiviati in modo pulito in un singolo NPCState, che si associ perfettamente all'implementazione dell'editor di giochi, ma rende l'NPCState piuttosto grande.
Cardin,

Oh, penso di aver frainteso la tua risposta. L'ho cambiato un po 'per includere gli osservatori. Quindi è un NPCStato diff per ogni NPC diff, tranne quelli super generici come Crowd NPC che possono condividere lo stato. Per ogni stato di gioco, l'NPC registrerà se stesso e il suo stato NPC con un osservatore. Quindi l'Osservatore saprà esattamente quale NPC è registrato per cambiare il comportamento in quale stato del gioco e semplicemente scorrerà attraverso di essi. E dal lato dell'editor di gioco, l'editor di giochi deve solo trasmettere un segnale all'Osservatore per cambiare lo stato dell'intero livello.
Cardin,

1
Sì, questa è l'idea! Gli NPC importanti avranno molti stati e la transizione dello stato dipenderà principalmente dalle pietre miliari completate. Gli NPC generici potrebbero anche reagire alle pietre miliari a volte e persino lo stato scelto dipende da una proprietà interna (diciamo che tutti gli NPC hanno uno stato iniziale predefinito e quando parli con uno di essi per la prima volta che si presenta, quindi inserisci il normale ciclo di commutazione dello stato).
mgibsonbr,

2

Le scelte che prenderei in considerazione sono o far sì che i singoli oggetti rispondano a gamestati diversi o servire livelli diversi in gamestati diversi. La scelta tra questi due dipenderebbe da cosa sto esattamente cercando di fare nel gioco (quali sono i diversi stati? Come sarà la transizione del gioco tra stati? Ecc.)

Ad ogni modo, tuttavia, non lo farei codificando gli stati nel codice di gioco. Piuttosto che una massiccia istruzione switch in oggetti NPC, vorrei invece che i comportamenti NPC caricati in un array associativo da un file di dati e quindi utilizzare tale array associativo per eseguire comportamenti diversi per i loro stati associati, qualcosa del genere:

if (state in behaviors) {
  behaviors[state]();
}

Quel file di dati sarebbe una specie di linguaggio di scripting? Penso che un file di dati in testo semplice potrebbe non essere sufficiente per descrivere il comportamento. Ad ogni modo, hai ragione che dovrebbe essere caricato dinamicamente. Non riesco proprio a pensare di usare un editor di giochi per generare un codice Java valido, deve sicuramente essere analizzato in qualche modo.
Cardin,

1
Bene, quello era il mio pensiero iniziale, ma dopo aver visto la risposta di mgibsonbr ho capito che potevi codificare i vari bevavi come classi separate e poi nel file di dati dire semplicemente quali classi di comportamento vanno con quale stato. Carica quei dati in un array associativo in fase di runtime.
jhocking

Oh .. Sì, è decisamente più semplice! : D Rispetto allo scenario di incorporare qualcosa come Lua haha ​​..
Cardin

2

Che dire dell'utilizzo di un modello di osservatore per cercare cambiamenti fondamentali? Se si verifica una modifica, alcune classi lo riconoscono e gestiscono ad esempio una modifica che deve essere eseguita su un npc.

Invece del menzionato modello di progettazione dello stato, userei un modello di strategia.

Se un npc non ha modi di interagire con il personaggio e le posizioni m dove potrebbe essere, c'è un massimo di (m * n) +1 classi che devi progettare. Usando il modello di strategia finiresti con n + m + 1 classi ma queste strategie potrebbero essere usate anche da altri npc.

Quindi potrebbe esserci una classe che gestisce le pietre miliari e le classi che osservano questa classe e gestiscono npc o nemici o qualunque cosa debba essere cambiata. Se gli osservatori vengono aggiornati, deciderebbero se devono modificare qualcosa nelle istanze che governano. La classe NPC, ad esempio, dovrebbe, nel costruttore, informare l'NPC-Manager quando deve essere aggiornato e cosa deve essere aggiornato ...


Il modello Observer sembra interessante. Penso che potrebbe lasciare chiaramente tutte le responsabilità all'NPC per registrarsi all'osservatore di stato. Il modello di strategia è molto simile a Trigger di Unity Engine e comportamenti di intelligenza artificiale, che viene utilizzato per condividere il comportamento tra oggetti di gioco diff (penso). Sembra fattibile. Non sono sicuro di quali siano i pro / contro in questo momento, ma che anche Unity usi lo stesso metodo è un po 'rassicurante haha ​​..
Cardin

Ho appena usato questi due schemi alcune volte, quindi non sono in grado di parlarti dei contro: - / Ma penso che sia bello in caso di singola responsabilità e la disponibilità a testare ogni strategia :) Potrebbe diventare confuso se il tuo usando una strategia in molte classi diverse e vuoi trovare ogni classe che la utilizza.
TOAOGG,

0

Tutti gli approcci forniti sono validi. Dipende dalla situazione in cui ti trovi in ​​un dato momento. Molte avventure o MMO usano una combinazione di questi.

Ad esempio, se un evento fondamentale modifica gran parte del livello (ad esempio, un esattore ripulisce il tuo appartamento e tutti vengono arrestati), di solito è più facile sostituire l'intera stanza con una seconda stanza che assomiglia.

OTOH, se i personaggi camminano sulla mappa e fanno cose diverse in luoghi diversi, spesso hai un solo attore che ruota attraverso diversi oggetti comportamentali (ad es. Cammina dritto / nessuna conversazione vs. stare qui / conversazione sulla morte di Mitch), che potrebbe includere "nascosto" se il loro scopo è stato raggiunto.

Detto questo, di solito avere duplicati di un oggetto che crei manualmente non dovrebbe causare problemi. Quanti oggetti puoi creare? Se riesci a creare più oggetti di quanti il ​​tuo gioco riesca a girare, guarda la loro proprietà "nascosta" e salta, il tuo motore è troppo lento. Quindi non me ne preoccuperei troppo. Molti giochi online lo fanno davvero. Alcuni personaggi o oggetti sono sempre lì, ma non vengono mostrati ai personaggi che non hanno la missione corrispondente.

Puoi anche combinare gli approcci: hai due porte nel tuo condominio. Uno conduce all'appartamento "prima dell'esattore", l'altro all'appartamento dopo. Quando entri nel corridoio, viene mostrato solo quello che si applica alla tua progressione nella storia. In questo modo, puoi semplicemente avere un meccanismo generico per "l'oggetto è visibile nel punto corrente della storia" e una porta con una sola destinazione. In alternativa, potresti creare porte più complicate che possono avere comportamenti che possono essere scambiati, e uno di questi è "vai all'appartamento completo", l'altro "vai all'appartamento vuoto". Questo può sembrare insensato se cambia davvero solo la destinazione della porta, ma se cambia anche il suo aspetto (ad esempio una grande serratura davanti alla porta che devi prima rompere),

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.