Come strutturare il codice per molte armi / incantesimi / poteri unici


22

Sono un programmatore inesperto che crea un gioco "simile a un roguelike" sulla scia di FTL , usando Python (ancora nessun PyGame, dato che mi occupo solo di testo).

Il mio gioco conterrà un gran numero di armi (circa 50 per i principianti) che producono abilità uniche. Sto lottando per capire come strutturare il codice oggetto in un modo che sia sia potente (in termini di consentire alle armi di avere effetti radicalmente diversi) sia estensibile (in modo da poter aggiungere facilmente altre armi in seguito, ad esempio lasciandole cadere in una cartella ).

Il mio primo istinto era quello di avere una classe di armi di base e avere diverse armi ereditate da quella classe. Tuttavia, questo mi sembra problematico: o devo rendere la classe BasicWeapon così barebone che è sostanzialmente inutile (le uniche caratteristiche che tutte le armi hanno in comune sono il nome e il tipo (pistola, ascia, ecc.)), Oppure devo prevedere ogni effetto unico che mi inventerò e codificherò in BasicWeapon.

Il secondo è chiaramente impossibile, ma il primo può ancora essere lavorato. Tuttavia, ciò mi lascia con la domanda: dove inserisco il codice per le singole armi?

Creo plasmarifle.py, rocketlauncher.py, swarmofbees.py, ecc. Ecc. E li trascino in una cartella da cui il gioco può importarli?

O c'è un modo per avere un file in stile database (forse qualcosa di semplice come un foglio di calcolo Excel) che in qualche modo contiene un codice univoco per ogni arma - senza dover ricorrere a eval / exec?

In termini di quest'ultima soluzione (database), penso che il problema fondamentale con cui sto lottando è che mentre capisco che è desiderabile mantenere la separazione tra codice e dati, mi sento come se le armi offuscassero il confine tra "codice" e "dati" un po '; rappresentano la grande varietà di cose simili che si possono trovare nel gioco, nel senso che sono come dati, ma la maggior parte richiederà almeno un codice univoco non condiviso con nessun altro oggetto, nel senso che sono, naturalmente, codice.

Una soluzione parziale che ho trovato altrove in questo sito suggerisce di dare alla classe BasicWeapon un mucchio di metodi vuoti - on_round_start (), on_attack (), on_move () ecc. - e quindi scavalcare quei metodi per ogni arma. Nella fase pertinente del ciclo di combattimento, il gioco chiamerà il metodo appropriato per l'arma di ogni personaggio e solo quelli che hanno dei metodi definiti faranno effettivamente qualcosa. Questo aiuta, ma non mi dice ancora dove devo inserire il codice e / o i dati per ogni arma.

Esiste una lingua o uno strumento diverso là fuori che posso usare come una sorta di chimera di mezzo dati e mezzo codice? Sto macellando completamente le buone pratiche di programmazione?

La mia comprensione di OOP è al massimo imprecisa, quindi apprezzerei le risposte che non sono troppo informatiche.

EDIT: Vaughan Hilts ha chiarito nel suo post qui sotto che ciò di cui sto essenzialmente parlando è la programmazione basata sui dati. L'essenza della mia domanda è questa: come posso implementare una progettazione basata sui dati in modo tale che i dati possano contenere script, consentendo alle nuove armi di fare cose nuove senza cambiare il codice del programma principale?



@ Byte56 correlati; ma penso che questo sia ciò che l'OP sta cercando di evitare. Penso che stiano cercando di trovare un approccio più guidato dai dati. Correggimi se sbaglio.
Vaughan Hilts,

Sono d'accordo che stanno cercando di trovare un approccio più orientato ai dati. In particolare, mi piace la risposta di Josh a questa domanda: gamedev.stackexchange.com/a/17286/7191
MichaelHouse

Ah, scusa per quello. :) Ho la brutta abitudine di leggere la "risposta accettata".
Vaughan Hilts,

Risposte:


17

Desideri un approccio basato sui dati quasi certamente a meno che il tuo gioco non sia completamente imprevisto e / o generato proceduralmente al centro.

In sostanza, ciò comporta la memorizzazione di informazioni sulle tue armi in un linguaggio di markup o in un formato di file a tua scelta. XML e JSON sono entrambe buone scelte leggibili che possono essere utilizzate per rendere l'editing abbastanza semplice senza la necessità di editor complicati se stai solo cercando di iniziare rapidamente. ( E Python può anche analizzare XML abbastanza facilmente! ) Impostare attributi come "potenza", "difesa", "costo" e "statistiche" che sono tutti rilevanti. Il modo in cui strutturi i tuoi dati dipenderà da te.

Se un'arma deve aggiungere un effetto di stato, assegnagli un nodo Effetto stato, quindi specifica gli effetti di un effetto di stato attraverso un altro oggetto guidato dai dati. Ciò renderà il tuo codice meno dipendente dal gioco specifico e renderà banale la modifica e il test del gioco. Non dover ricompilare continuamente è anche un bonus.

Di seguito è disponibile una lettura supplementare:


2
Un po 'come un sistema basato su componenti, in cui i componenti vengono letti tramite script. In questo modo: gamedev.stackexchange.com/questions/33453/…
MichaelHouse

2
E mentre ci sei, fai uno script parte di quei dati in modo che le nuove armi possano fare cose nuove senza modifiche al codice principale.
Patrick Hughes,

@Vaughan Hilts: grazie, i dati guidati sembrano essere esattamente ciò che ho capito intuitivamente di cui avevo bisogno. Lascio la domanda aperta per un po 'di più poiché ho ancora bisogno di risposte, ma probabilmente sceglierò questa come la migliore risposta.
henrebotha,

@Patrick Hughes: è esattamente quello che voglio! Come lo faccio? Puoi mostrarmi un semplice esempio o tutorial?
henrebotha,

1
Per prima cosa hai bisogno di un motore di script nel tuo motore, molte persone scelgono LUA, che accede a sistemi di gioco come effetti e statistiche. Quindi, poiché stai già ricreando i tuoi oggetti da una descrizione dei dati, puoi incorporare lo script che il tuo motore chiama ogni volta che il tuo nuovo oggetto viene attivato. Ai vecchi tempi dei MUD questo era chiamato "proc" (abbreviazione di Process). La parte difficile è rendere le tue funzionalità di gioco nel motore abbastanza flessibili da poter essere chiamate dall'esterno e con abbastanza funzioni.
Patrick Hughes,

6

(Mi dispiace inviare la risposta invece di un commento, ma non ho ancora un rappresentante.)

La risposta di Vaughan è ottima, ma vorrei aggiungere i miei due centesimi.

Uno dei motivi principali per cui si desidera utilizzare XML o JSON e analizzarlo in runtime è modificare e sperimentare nuovi valori senza dover ricompilare il codice. Dato che Python viene interpretato e, a mio avviso, abbastanza leggibile, potresti avere i dati grezzi in un file con un dizionario e tutto organizzato:

weapons = {
           'megaLazer' : {
                          'name' : "Mega Lazer XPTO"
                          'damage' : 100
                       },
           'ultraCannon' : {
                          'name' : "Ultra Awesome Cannon",
                          'damage' : 200
                       }
          }

In questo modo basta importare il file / modulo e usarlo come un normale dizionario.

Se si desidera aggiungere script, è possibile utilizzare la natura dinamica di Python e le funzioni di 1a classe. Potresti fare qualcosa del genere:

def special_shot():
    ...

weapons = { 'megalazer' : { ......
                            shoot_gun = special_shot
                          }
          }

Anche se credo che sarebbe contro la progettazione guidata dai dati. Per essere DDD al 100% avresti informazioni (dati) che specificano quali sarebbero le funzioni e il codice che l'arma specifica avrebbe usato. In questo modo non si rompe DDD, in quanto non si mescolano i dati con la funzionalità.


Grazie. Basta vedere un semplice esempio di codice per aiutarlo a fare clic.
henrebotha,

1
+1 per la bella risposta e per avere abbastanza rappresentante per commentare. ;) Benvenuto.
ver

4

Progettazione basata sui dati

Ho inviato qualcosa come questa domanda per la revisione del codice di recente.

Dopo alcuni suggerimenti e miglioramenti, il risultato è stato un semplice codice che avrebbe consentito una certa flessibilità relativa alla creazione dell'arma basata su un dizionario (o JSON). I dati vengono interpretati in fase di esecuzione e semplici verifiche vengono eseguite dalla Weaponclasse stessa, senza la necessità di fare affidamento su un intero interprete di script.

Data-Driven Design, nonostante Python sia un linguaggio interpretato (sia i file di origine che quelli di dati possono essere modificati senza la necessità di ricompilarli), suona come la cosa giusta da fare in casi come quello che hai presentato. Questa domanda approfondisce il concetto, i suoi pro e contro. C'è anche una bella presentazione sulla Cornell University a riguardo.

Rispetto ad altri linguaggi, come C ++, che probabilmente utilizzerebbe un linguaggio di scripting (come LUA) per gestire l'interazione dei dati x engine e gli script in generale, e un determinato formato di dati (come XML) per archiviare i dati, Python può effettivamente fare tutto da solo (considerando lo standard dictma anche weakrefquest'ultimo, in particolare per il caricamento delle risorse e la memorizzazione nella cache).

Uno sviluppatore indipendente, tuttavia, potrebbe non portare l'approccio basato sui dati all'estremo, come suggerito in questo articolo :

Quanto costa la progettazione basata sui dati? Non credo che un motore di gioco debba contenere una singola riga di codice specifico del gioco. Non uno. Nessun tipo di arma codificata. Nessun layout HUD codificato. Nessuna unità AI codificata. Nada. Cerniera lampo. Zilch.

Forse, con Python, si potrebbe beneficiare del meglio dell'approccio orientato agli oggetti e basato sui dati, mirando sia alla produttività che all'estensibilità.

Semplice elaborazione del campione

Nel caso specifico discusso sulla revisione del codice, un dizionario memorizzerebbe sia gli "attributi statici" che la logica da interpretare - se l'arma avesse un comportamento condizionale.

Nell'esempio seguente una spada dovrebbe avere alcune abilità e statistiche nelle mani dei personaggi della classe "antipaladino" e nessun effetto, con statistiche più basse se usate da altri personaggi):

WEAPONS = {
    "bastard's sting": {
        # magic enhancement, weight, value, dmg, and other attributes would go here.
        "magic": 2,

        # Those lists would contain the name of effects the weapon provides by default.
        # They are empty because, in this example, the effects are only available in a
        # specific condition.    
        "on_turn_actions": [],
        "on_hit_actions": [],
        "on_equip": [
            {
                "type": "check",
                "condition": {
                    'object': 'owner',
                    'attribute': 'char_class',
                    'value': "antipaladin"
                },
                True: [
                    {
                        "type": "action",
                        "action": "add_to",
                        "args": {
                            "category": "on_hit",
                            "actions": ["unholy"]
                        }
                    },
                    {
                        "type": "action",
                        "action": "add_to",
                        "args": {
                            "category": "on_turn",
                            "actions": ["unholy aurea"]
                        }
                    },
                    {
                        "type": "action",
                        "action": "set_attribute",
                        "args": {
                            "field": "magic",
                            "value": 5
                        }
                    }
                ],
                False: [
                    {
                        "type": "action",
                        "action": "set_attribute",
                        "args": {
                            "field": "magic",
                            "value": 2
                        }
                    }
                ]
            }
        ],
        "on_unequip": [
            {
                "type": "action",
                "action": "remove_from",
                "args": {
                    "category": "on_hit",
                    "actions": ["unholy"]
                },
            },
            {
                "type": "action",
                "action": "remove_from",
                "args": {
                    "category": "on_turn",
                    "actions": ["unholy aurea"]
                },
            },
            {
                "type": "action",
                "action": "set_attribute",
                "args": ["magic", 2]
            }
        ]
    }
}

A scopo di test, ho creato semplici Playere Weaponclassi: il primo a contenere / equipaggiare l'arma (chiamando così la sua impostazione on_equip condizionale) e il secondo come una singola classe che avrebbe recuperato i dati dal dizionario, in base al nome dell'oggetto passato come argomento durante l' Weaponinizializzazione. Non riflettono il corretto design delle classi di gioco, ma possono comunque essere utili per testare i dati:

class Player:
    """Represent the player character."""

    inventory = []

    def __init__(self, char_class):
        """For this example, we just store the class on the instance."""
        self.char_class = char_class

    def pick_up(self, item):
        """Pick an object, put in inventory, set its owner."""
        self.inventory.append(item)
        item.owner = self


class Weapon:
    """A type of item that can be equipped/used to attack."""

    equipped = False
    action_lists = {
        "on_hit": "on_hit_actions",
        "on_turn": "on_turn_actions",
    }

    def __init__(self, template):
        """Set the parameters based on a template."""
        self.__dict__.update(WEAPONS[template])

    def toggle_equip(self):
        """Set item status and call its equip/unequip functions."""
        if self.equipped:
            self.equipped = False
            actions = self.on_unequip
        else:
            self.equipped = True
            actions = self.on_equip

        for action in actions:
            if action['type'] == "check":
                self.check(action)
            elif action['type'] == "action":
                self.action(action)

    def check(self, dic):
        """Check a condition and call an action according to it."""
        obj = getattr(self, dic['condition']['object'])
        compared_att = getattr(obj, dic['condition']['attribute'])
        value = dic['condition']['value']
        result = compared_att == value

        self.action(*dic[result])

    def action(self, *dicts):
        """Perform action with args, both specified on dicts."""
        for dic in dicts:
            act = getattr(self, dic['action'])
            args = dic['args']
            if isinstance(args, list):
                act(*args)
            elif isinstance(args, dict):
                act(**args)

    def set_attribute(self, field, value):
        """Set the specified field with the given value."""
        setattr(self, field, value)

    def add_to(self, category, actions):
        """Add one or more actions to the category's list."""
        action_list = getattr(self, self.action_lists[category])

        for action in actions:
            if action not in action_list:
                action_list.append(action)

    def remove_from(self, category, actions):
        """Remove one or more actions from the category's list."""
        action_list = getattr(self, self.action_lists[category])

        for action in actions:
            if action in action_list:
                action_list.remove(action)

Con qualche miglioramento futuro spero che questo mi permetta persino di avere un sistema di crafting dinamico un giorno, che elabora componenti di armi anziché intere armi ...

Test

  1. Il personaggio A prende l'arma, la equipaggia (stampiamo le sue statistiche), quindi la rilascia;
  2. Il personaggio B prende la stessa arma, la equipaggia (e stampiamo di nuovo le sue statistiche per mostrare come sono diverse).

Come questo:

def test():
    """A simple test.

    Item features should be printed differently for each player.
    """
    weapon = Weapon("bastard's sting")
    player1 = Player("bard")
    player1.pick_up(weapon)
    weapon.toggle_equip()
    print("Enhancement: {}, Hit effects: {}, Other effects: {}".format(
        weapon.magic, weapon.on_hit_actions, weapon.on_turn_actions))
    weapon.toggle_equip()

    player2 = Player("antipaladin")
    player2.pick_up(weapon)
    weapon.toggle_equip()
    print("Enhancement: {}, Hit effects: {}, Other effects: {}".format(
        weapon.magic, weapon.on_hit_actions, weapon.on_turn_actions))

if __name__ == '__main__':
    test()

Dovrebbe stampare:

Per un bardo

Miglioramento: 2, Effetti hit: [], Altri effetti: []

Per un antipaladino

Miglioramento: 5, Effetti hit: ['unholy'], Altri effetti: ['unholy aurea']

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.