Implementare il comportamento in un semplice gioco di avventura


11

Ultimamente mi sono divertito programmando un semplice gioco di avventura basato su testo e sono bloccato su quello che sembra un problema di design molto semplice.

Per dare una breve panoramica: il gioco è suddiviso in Roomoggetti. Ognuno Roomha un elenco di Entityoggetti che si trovano in quella stanza. Ognuno Entityha uno stato di evento, che è una semplice mappa booleana stringa> e un elenco di azioni, che è una mappa stringa> funzione.

L'input dell'utente assume la forma [action] [entity]. Il Roomutilizza il nome dell'entità di restituire l'appropriato Entityoggetto, che poi utilizza il nome dell'azione per trovare il corretto funzionamento, e lo esegue.

Per generare la descrizione della stanza, ogni Roomoggetto visualizza la propria stringa di descrizione, quindi aggiunge le stringhe di descrizione di ciascuna Entity. La Entitydescrizione può cambiare in base al suo stato ("La porta è aperta", "La porta è chiusa", "La porta è chiusa", ecc.).

Ecco il problema: usando questo metodo, il numero di descrizioni e funzioni di azione che devo implementare rapidamente sfugge di mano. La mia stanza di partenza da sola ha circa 20 funzioni tra 5 entità.

Posso combinare tutte le azioni in un'unica funzione e if-else / passare da una all'altra, ma sono comunque due funzioni per entità. Posso anche creare Entitysottoclassi specifiche per oggetti comuni / generici come porte e chiavi, ma questo mi porta solo così lontano.

EDIT 1: Come richiesto, esempi di pseudo-codice di queste funzioni di azione.

string outsideDungeonBushesSearch(currentRoom, thisEntity, player)
    if thisEntity["is_searched"] then
        return "There was nothing more in the bushes."
    else
        thisEntity["is_searched"] := true
        currentRoom.setEntity("dungeonDoorKey")
        return "You found a key in the bushes."
    end if

string dungeonDoorKeyUse(currentRoom, thisEntity, player)
    if getEntity("outsideDungeonDoor")["is_locked"] then
        getEntity("outsideDungeonDoor")["is_locked"] := false
        return "You unlocked the door."
    else
        return "The door is already unlocked."
    end if

Le funzioni di descrizione agiscono praticamente allo stesso modo, controllando lo stato e restituendo la stringa appropriata.

EDIT 2: rivisto il mio testo delle domande. Supponiamo che ci possa essere un numero significativo di oggetti in-game che non condividono comportamenti comuni (risposte basate sullo stato ad azioni specifiche) con altri oggetti. Esiste un modo per definire questi comportamenti unici in un modo più pulito e più gestibile che scrivere una funzione personalizzata per ogni azione specifica dell'entità?


1
Penso che tu debba spiegare cosa fanno queste "funzioni d'azione" e magari pubblicare un po 'di codice, perché non sono sicuro di cosa tu stia parlando lì.
scherzare il

Aggiunto il codice.
Eric

Risposte:


5

Invece di creare una funzione separata per ogni combinazione di nomi e verbi, dovresti impostare un'architettura in cui esiste un'interfaccia comune che tutti gli oggetti nel gioco implementano.

Un approccio al di sopra della mia testa sarebbe quello di definire un oggetto Entità che estenda tutti gli oggetti specifici nel tuo gioco. Ogni Entità avrà una tabella (qualunque sia la struttura di dati utilizzata dalla tua lingua per gli array associativi) che associa azioni diverse a risultati diversi. Le azioni nella tabella saranno probabilmente Stringhe (ad es. "Aperto") mentre il risultato associato potrebbe persino essere una funzione privata nell'oggetto se la tua lingua supporta funzioni di prima classe.

Allo stesso modo, lo stato dell'oggetto è memorizzato in vari campi dell'oggetto. Quindi, ad esempio, puoi avere un array di cose in un Bush, e quindi la funzione associata alla "ricerca" agirà su quell'array, restituendo l'oggetto trovato o la stringa "Non c'era più niente tra i cespugli".

Nel frattempo, uno dei metodi pubblici è qualcosa come Entity.actOn (azione String) Quindi in quel metodo confronta l'azione passata con la tabella delle azioni per quell'oggetto; se tale azione è nella tabella, restituisce il risultato.

Ora tutte le diverse funzioni necessarie per ciascun oggetto saranno contenute all'interno dell'oggetto, facilitando la ripetizione di tale oggetto in altre stanze (es. Istanziare l'oggetto Porta in ogni stanza che ha una porta)

Infine, definisci tutte le stanze in XML o JSON o altro in modo da poter avere molte stanze uniche senza la necessità di scrivere un codice separato per ogni stanza. Carica questo file di dati all'avvio del gioco e analizza i dati per creare un'istanza degli oggetti che popolano il gioco. Qualcosa di simile a:

<rooms>
  <room id="room1">
    <description>Outside the dungeon you see some bushes and a heavy door over the entrance.</description>
    <entities>
      <bush>
        <description>The bushes are thick and leafy.</description>
        <contains>
          <key />
        </contains>
      </bush>
      <door connection="room2" isLocked="true">
        <description>It's an oak door with stout iron clasps.</description>
      </door>
    </entities>
  </room>

  <room id="room2">
    etc.

AGGIUNTA: aha, ho appena letto la risposta di FxIII e questo verso la fine mi è balzato addosso:

(no things like <item triggerFlamesOnPicking="true"> that you will use just once)

Anche se non sono d'accordo sul fatto che una trappola di fiamma che viene innescata sia qualcosa che potrebbe accadere solo una volta (potrei vedere che questa trappola viene riutilizzata per molti oggetti diversi), penso di aver finalmente capito cosa intendevi sulle entità che reagiscono in modo univoco all'input dell'utente. Probabilmente affronterei cose come fare in modo che una porta della tua prigione abbia una trappola per palle di fuoco costruendo tutte le mie entità con un'architettura componente (spiegata in dettaglio altrove).

In questo modo ogni entità Door è costruita come un fascio di componenti e posso combinare e abbinare in modo flessibile componenti tra entità diverse. Ad esempio, la maggior parte delle porte avrebbe configurazioni simili

<entity name="door">
  <description>It's an oak door with stout iron clasps.</description>
  <components>
    <lock isLocked="true" />
    <portal connection="room2" />
  </components>
</entity>

ma l'unica porta con una trappola per palle di fuoco sarebbe

<entity name="door">
  <description>There are strange runes etched into the wood.</description>
  <components>
    <lock isLocked="true" />
    <portal connection="room7" />
    <fireballTrap />
  </components>
</entity>

e quindi l'unico codice univoco che dovrei scrivere per quella porta è il componente FireballTrap. Userebbe gli stessi componenti Lock e Portal di tutte le altre porte, e se in seguito decidessi di usare FireballTrap su una cassa del tesoro o qualcosa di così semplice come aggiungere il componente FireballTrap a quella cassa.

Il fatto che tu definisca o meno tutti i componenti nel codice compilato o in un linguaggio di scripting separato non è una grande distinzione nella mia mente (in entrambi i casi scriverai il codice da qualche parte ) ma l'importante è che puoi ridurre significativamente il quantità di codice univoco che devi scrivere. Diamine, se non sei preoccupato per la flessibilità per i designer / modder di livello (dopo tutto stai scrivendo questo gioco da solo) potresti persino creare tutte le entità ereditate da Entità e aggiungere componenti nel costruttore piuttosto che un file di configurazione o uno script o qualunque cosa:

Door extends Entity {
  public Door() {
    addComponent(new LockComponent());
    addComponent(new PortalComponent());
  }
}

TrappedDoor extends Entity {
  public TrappedDoor() {
    addComponent(new LockComponent());
    addComponent(new PortalComponent());
    addComponent(new FireballTrap());
  }
}

1
Funziona con oggetti comuni e ripetibili. Ma che dire delle entità che rispondono in modo univoco all'input dell'utente? La creazione di una sottoclasse di Entitysolo per un singolo oggetto raggruppa il codice ma non riduce la quantità di codice che devo scrivere. O è inevitabile una trappola in questo senso?
Eric

1
Ho affrontato gli esempi che hai dato. Non riesco a leggere la tua mente; quali oggetti e input vuoi avere?
scherzare il

Ho modificato il mio post per spiegare meglio le mie intenzioni. Se capisco correttamente il tuo esempio, sembra che ogni tag entità corrisponda ad alcune sottoclassi di Entitye gli attributi ne definiscano lo stato iniziale. Suppongo che i tag figlio dell'entità fungano da parametri per qualsiasi azione a cui è associato quel tag, giusto?
Eric

sì, questa è l'idea.
jhocking

Avrei dovuto immaginare che i componenti avrebbero fatto parte della soluzione. Grazie per l'aiuto.
Eric

1

Il problema dimensionale che affronti è abbastanza normale e quasi inevitabile. Vuoi trovare un modo per esprimere le tue entità che sia al contempo coincidente e flessibile .

Un "contenitore" (il cespuglio nella risposta jhocking) è un modo coincidente ma vedi che non è abbastanza flessibile .

Non ti suggerisco di provare a trovare un'interfaccia generica e quindi utilizzare i file di configurazione per specificare i comportamenti, perché avrete sempre la sgradevole sensazione di essere tra l'incudine (entità standard e noioso, facile da descrivere) e il martello ( entità fantastiche uniche ma troppo lunghe da implementare).

Il mio consiglio è di usare un linguaggio interpretato per codificare i comportamenti.

Pensa all'esempio della boscaglia: è un contenitore ma la nostra boscaglia deve contenere degli articoli specifici; l'oggetto contenitore può avere:

  • un metodo per lo storyteller per aggiungere un oggetto,
  • un metodo per mostrare al motore l'elemento che contiene,
  • un metodo per il giocatore di scegliere un oggetto.

Uno di questi oggetti ha una corda che innesca un aggeggio che a sua volta spara una fiamma che brucia il cespuglio ... (vedi, posso leggere la tua mente in modo da sapere le cose che ti piacciono).

Puoi usare uno script per descrivere questo cespuglio invece di un file di configurazione mettendo il codice aggiuntivo pertinente in un gancio che esegui dal tuo programma principale ogni volta che qualcuno preleva un oggetto da un contenitore.

Ora hai molte scelte di architettura: puoi definire strumenti comportamentali come classi di base usando il tuo linguaggio di codice o il linguaggio di scripting (cose come contenitori, simili a porte e così via). Lo scopo di queste cose è di permetterti di descrivere facilmente le entità aggregando comportamenti semplici e configurandoli usando le associazioni su un linguaggio di scripting .

Tutte le entità dovrebbero essere accessibili allo script: è possibile associare un identificatore a ciascuna entità e metterle in un contenitore che viene esportato nell'estensione dello script del linguaggio di scripting.

L'utilizzo di strategie di scripting ti consente di mantenere semplice la tua configurazione (niente di simile <item triggerFlamesOnPicking="true">che userai una volta sola) mentre ti consente di esprimere strani segnali (quelli divertenti) aggiungendo una riga di codice

In poche parole: script come file di configurazione in grado di eseguire codice.

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.