Wie man Code für viele einzigartige Waffen / Zauber / Kräfte strukturiert

22

Ich bin ein unerfahrener Programmierer , der mit Python ein "roguelartiges" Spiel im Stil von FTL erstellt (noch kein PyGame, da ich mich immer noch nur mit Text befasse).

Mein Spiel wird eine große Anzahl von Waffen enthalten (ungefähr 50 für Anfänger), die einzigartige Fähigkeiten ergeben. Ich habe Mühe zu verstehen, wie man den Objektcode so strukturiert, dass er sowohl leistungsfähig (so dass Waffen völlig andere Effekte haben) als auch erweiterbar ist (so dass ich später problemlos weitere Waffen hinzufügen kann, indem ich sie z. B. in einen Ordner lege ).

Mein erster Instinkt war, eine BasicWeapon-Klasse zu haben und verschiedene Waffen von dieser Klasse zu erben. Dies scheint mir jedoch problematisch zu sein: Entweder muss ich die BasicWeapon-Klasse so machen, dass sie im Grunde genommen unbrauchbar ist (die einzigen Merkmale, die alle Waffen gemeinsam haben, sind Name und Typ (Pistole, Axt usw.)), oder ich muss jeden voraussagen Ein einzigartiger Effekt, den ich mir jemals ausgedacht und in BasicWeapon codiert habe.

Letzteres ist eindeutig unmöglich, Ersteres kann jedoch weiter bearbeitet werden. Das lässt mich jedoch bei der Frage zurück: Wo lege ich den Code für einzelne Waffen ab?

Erstelle ich plasmarifle.py, rocketlauncher.py, swarmofbees.py usw. usw. und lege sie alle in einem Ordner ab, aus dem das Spiel sie importieren kann?

Oder gibt es eine Möglichkeit, eine Datei im Stil einer Datenbank (möglicherweise so einfach wie eine Excel-Tabelle) zu haben, die irgendwie einen eindeutigen Code für jede Waffe enthält - ohne auf eval / exec zurückgreifen zu müssen?

In Bezug auf die letztere Lösung (Datenbank) denke ich, dass das grundlegende Problem, mit dem ich zu kämpfen habe, darin besteht, dass ich zwar verstehe, dass es wünschenswert ist, die Trennung zwischen Code und Daten beizubehalten, ich aber das Gefühl habe, dass die Waffen die Grenze zwischen "Code" verwischen. und "Daten" ein wenig; Sie repräsentieren die große Vielfalt ähnlicher Dinge, die im Spiel zu finden sind. In diesem Sinne sind sie wie Daten, aber für die meisten von ihnen ist mindestens ein eindeutiger Code erforderlich, der mit keinem anderen Element geteilt wird. In diesem Sinne sind sie natürlich Code.

Eine Teillösung, die ich an anderer Stelle auf dieser Site gefunden habe, schlägt vor, der BasicWeapon-Klasse eine Reihe leerer Methoden zuzuweisen - on_round_start (), on_attack (), on_move () usw. - und diese Methoden dann für jede Waffe zu überschreiben. In der relevanten Phase des Kampfzyklus ruft das Spiel die geeignete Methode für die Waffe jedes Charakters auf, und nur diejenigen, für die Methoden definiert wurden, tun tatsächlich etwas. Dies hilft, aber es sagt mir immer noch nicht, wo ich den Code und / oder die Daten für jede Waffe ablegen muss.

Gibt es eine andere Sprache oder ein anderes Tool, das ich als Halbdaten-, Halbcode-Chimäre verwenden kann? Mache ich mich an die gute Programmierpraxis?

Mein Verständnis von OOP ist bestenfalls lückenhaft, daher würde ich Antworten begrüßen, die nicht zu Informatik sind.

EDIT: Vaughan Hilts hat in seinem Beitrag unten klargestellt, dass ich im Wesentlichen von datengesteuerter Programmierung spreche. Die Essenz meiner Frage lautet: Wie kann ich ein datengesteuertes Design so implementieren, dass die Daten Skripte enthalten können, sodass neue Waffen neue Dinge tun können, ohne den Hauptprogrammcode zu ändern?

Henrebotha
quelle
@ Byte56 Related; Aber ich denke, das ist es, was das OP zu vermeiden versucht. Ich denke, sie versuchen, einen datenorientierteren Ansatz zu finden. Korrigiere mich, wenn ich falsch liege.
Vaughan Hilts
Ich bin damit einverstanden, dass sie versuchen, einen datenorientierteren Ansatz zu finden. Insbesondere gefällt mir Joshs Antwort auf diese Frage: gamedev.stackexchange.com/a/17286/7191
MichaelHouse
Ah, tut mir leid. :) Ich habe die schlechte Angewohnheit, die "akzeptierte Antwort" zu lesen.
Vaughan Hilts

Antworten:

17

Sie möchten mit ziemlicher Sicherheit einen datengetriebenen Ansatz, es sei denn, Ihr Spiel wird völlig unerwartet und / oder prozedural bis in den Kern generiert.

Dies umfasst im Wesentlichen das Speichern von Informationen zu Ihren Waffen in einer Auszeichnungssprache oder einem Dateiformat Ihrer Wahl. XML und JSON sind beide gute, lesbare Optionen, mit denen sich das Bearbeiten relativ einfach gestalten lässt, ohne dass komplizierte Editoren erforderlich sind, wenn Sie nur einen schnellen Einstieg benötigen. ( Und Python kann XML auch ziemlich einfach analysieren! ) Sie würden Attribute wie "Macht", "Verteidigung", "Kosten" und "Statistiken" festlegen, die alle relevant sind. Die Art und Weise, wie Sie Ihre Daten strukturieren, liegt bei Ihnen.

Wenn eine Waffe einen Statuseffekt hinzufügen muss, weisen Sie ihr einen Statuseffektknoten zu und geben Sie dann die Auswirkungen eines Statuseffekts über ein anderes datengesteuertes Objekt an. Dies macht Ihren Code weniger abhängig von dem jeweiligen Spiel und macht das Bearbeiten und Testen Ihres Spiels trivial. Es ist auch ein Bonus, nicht ständig neu kompilieren zu müssen.

Eine ergänzende Lektüre finden Sie unten:

Vaughan Hilts
quelle
2
Eine Art komponentenbasiertes System, bei dem die Komponenten über Skripte eingelesen werden. Wie folgt
MichaelHouse
2
Und wenn Sie schon dabei sind, machen Sie ein Skript zu einem Teil dieser Daten, damit neue Waffen neue Dinge tun können, ohne dass sich der Hauptcode ändert.
Patrick Hughes
@Vaughan Hilts: Danke, datengesteuert scheint genau das zu sein, was ich intuitiv verstanden habe. Ich lasse die Frage noch eine Weile offen, da ich noch Antworten benötige, werde diese aber wahrscheinlich als beste Antwort auswählen.
Henrebotha
@Patrick Hughes: genau das will ich haben! Wie mache ich das? Können Sie mir ein einfaches Beispiel oder Tutorial zeigen?
Henrebotha
1
Zuerst brauchst du eine Skript-Engine in deiner Engine, viele Leute entscheiden sich für LUA, das auf Gameplay-Systeme wie Effekte und Statistiken zugreift. Da Sie dann Ihre Objekte bereits anhand einer Datenbeschreibung neu erstellen, können Sie das Skript einbetten, das Ihre Engine bei jeder Aktivierung Ihres neuen Objekts aufruft. In den alten Tagen der MUDs wurde dies als "proc" (Abkürzung für Process) bezeichnet. Der schwierige Teil besteht darin, Ihre Gameplay-Funktionen in der Engine so flexibel zu gestalten, dass sie von außen aufgerufen werden können und über genügend Funktionen verfügen.
Patrick Hughes
6

(Es tut mir leid, die Antwort anstelle eines Kommentars zu übermitteln, aber ich habe noch keinen Repräsentanten.)

Vaughans Antwort ist großartig, aber ich möchte meine zwei Cent hinzufügen.

Einer der Hauptgründe, warum Sie XML oder JSON verwenden und zur Laufzeit analysieren möchten, ist das Ändern und Experimentieren mit neuen Werten, ohne den Code neu kompilieren zu müssen. Da Python interpretiert wird und meiner Meinung nach ziemlich lesbar ist, könnten Sie die Rohdaten in einer Datei mit einem Wörterbuch und allem, was organisiert ist, haben:

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

Auf diese Weise importieren Sie einfach die Datei / das Modul und verwenden es als normales Wörterbuch.

Wenn Sie Skripte hinzufügen möchten, können Sie die dynamische Natur von Python- und 1st-Class-Funktionen nutzen. Sie könnten so etwas tun:

def special_shot():
    ...

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

Obwohl ich glaube, dass das gegen datengetriebenes Design wäre. Um 100% DDD zu sein, hätten Sie Informationen (Daten), die spezifizieren, welche Funktionen und welcher Code für eine bestimmte Waffe verwendet würden. Auf diese Weise wird DDD nicht beschädigt, da Daten nicht mit Funktionen gemischt werden.

Vasco Correia
quelle
Vielen Dank. Schon ein einfaches Codebeispiel hat das Klicken erleichtert.
Henrebotha
1
+1 für die nette Antwort und damit du genug Repräsentanten hast, um Kommentare abzugeben. ;) Herzlich willkommen.
Ver
4

Datengetriebenes Design

Ich habe so etwas wie diese Frage zur Codeüberprüfung eingereicht kürzlich .

Nach einigen Vorschlägen und Verbesserungen war das Ergebnis ein einfacher Code, der eine relative Flexibilität bei der Waffenerstellung basierend auf einem Wörterbuch (oder JSON) ermöglichen würde. Die Daten werden zur Laufzeit interpretiert und einfache Überprüfungen werden von der WeaponKlasse selbst durchgeführt, ohne dass ein vollständiger Skriptinterpreter erforderlich ist.

Datengesteuertes Design, obwohl Python eine interpretierte Sprache ist (sowohl Quell- als auch Datendateien können bearbeitet werden, ohne dass sie neu kompiliert werden müssen), scheint in Fällen wie dem von Ihnen vorgestellten die richtige Vorgehensweise zu sein. Diese Frage geht näher auf das Konzept, seine Vor- und Nachteile ein. Es gibt auch eine schöne Präsentation über die Cornell University .

Im Vergleich zu anderen Sprachen, wie C ++, die wahrscheinlich eine Skriptsprache (wie LUA) verwenden würden, um die Interaktion mit der Daten-Engine und Skripts im Allgemeinen zu verarbeiten, und ein bestimmtes Datenformat (wie XML), um die Daten zu speichern, kann Python dies tatsächlich tun alles für sich allein (unter Berücksichtigung des Standards, dictaber auch weakrefletzterer speziell für das Laden und Zwischenspeichern von Ressourcen).

Ein unabhängiger Entwickler kann den datengetriebenen Ansatz jedoch möglicherweise nicht bis zum Äußersten verfolgen, wie in diesem Artikel vorgeschlagen :

Wie sehr geht es mir um datengetriebenes Design? Ich denke nicht, dass eine Game Engine eine einzelne Zeile spielspezifischen Codes enthalten sollte. Nicht eins. Keine fest codierten Waffentypen. Kein fest codiertes HUD-Layout. Keine fest codierte Einheit AI. Nada. Postleitzahl. Zilch.

Vielleicht könnte man mit Python vom besten objektorientierten und datengetriebenen Ansatz profitieren, der sowohl auf Produktivität als auch auf Erweiterbarkeit abzielt.

Einfache Probenaufbereitung

In dem speziellen Fall, der bei der Codeüberprüfung erörtert wurde, werden in einem Wörterbuch sowohl die "statischen Attribute" als auch die zu interpretierende Logik gespeichert, falls die Waffe ein bedingtes Verhalten aufweist.

In dem folgenden Beispiel sollte ein Schwert einige Fähigkeiten und Werte in den Händen von Charakteren der Klasse 'Antipaladin' und keine Effekte haben, mit niedrigeren Werten, wenn sie von anderen Charakteren verwendet werden.

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]
            }
        ]
    }
}

Zu Testzwecken habe ich einfache Playerund WeaponKlassen erstellt: die erste, die die Waffe hält / ausrüstet (und damit die bedingte Einstellung on_equip aufruft) und die letztere als einzelne Klasse, die die Daten aus dem Wörterbuch basierend auf dem als übergebenen Objektnamen abruft Argument während der WeaponInitialisierung. Sie spiegeln nicht das richtige Design der Spielklassen wider, können aber dennoch nützlich sein, um die Daten zu testen:

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)

Mit einigen zukünftigen Verbesserungen hoffe ich, dass ich eines Tages sogar ein dynamisches Handwerkssystem haben kann, das Waffenkomponenten statt ganzer Waffen verarbeitet ...

Prüfung

  1. Charakter A wählt eine Waffe, rüstet sie aus (wir drucken ihre Statistiken aus) und lässt sie dann fallen.
  2. Charakter B wählt die gleiche Waffe aus, rüstet sie aus (und wir drucken die Statistiken erneut, um zu zeigen, wie unterschiedlich sie sind).

So was:

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()

Es sollte drucken:

Für einen Barden

Verbesserung: 2, Treffereffekte: [], Andere Effekte: []

Für ein Antipaladin

Verbesserung: 5, Treffereffekte: ['unheilig'], Andere Effekte: ['unheilige Aura']

Lucas Siqueira
quelle