Comment structurer le code de nombreuses armes / sorts / pouvoirs uniques

22

Je suis un programmeur inexpérimenté qui crée un jeu "roguelike-like" dans la veine de FTL , en utilisant Python (pas encore PyGame pour l'instant car je ne m'intéresse qu'au texte).

Mon jeu contiendra un grand nombre d'armes (environ 50 pour les débutants) qui donnent des capacités uniques. J'ai du mal à comprendre comment structurer le code objet d'une manière à la fois puissante (en termes de permettre aux armes d'avoir des effets radicalement différents) et extensible (afin que je puisse facilement ajouter plus d'armes plus tard, par exemple en les déposant dans un dossier ).

Mon premier réflexe a été d'avoir une classe BasicWeapon et d'avoir différentes armes héritées de cette classe. Cependant, cela me semble problématique: soit je dois rendre la classe BasicWeapon si simple qu'elle est fondamentalement inutile (les seules caractéristiques que toutes les armes ont en commun sont le nom et le type (pistolet, hache, etc.)), ou je dois prédire chaque effet unique que je trouverai et coderai dans BasicWeapon.

Ce dernier est clairement impossible, mais le premier peut encore être travaillé. Cependant, cela me laisse avec la question: où dois-je mettre le code pour les armes individuelles?

Dois-je créer plasmarifle.py, rocketlauncher.py, swarmofbees.py, etc etc, et les déposer tous dans un dossier d'où le jeu peut les importer?

Ou existe-t-il un moyen d'avoir un fichier de style base de données (peut-être quelque chose d'aussi simple qu'une feuille de calcul Excel) qui contient en quelque sorte un code unique pour chaque arme - sans avoir besoin de recourir à eval / exec?

En ce qui concerne cette dernière solution (base de données), je pense que le problème fondamental avec lequel je me bats est que même si je comprends qu'il est souhaitable de maintenir la séparation entre le code et les données, j'ai l'impression que les armes brouillent la ligne entre le "code" et "données" un peu; ils représentent la grande variété de choses similaires qui peuvent être trouvées dans le jeu, dans ce sens, elles sont comme des données, mais la plupart d'entre elles nécessiteront au moins un code unique qui ne sera partagé avec aucun autre élément, dans ce sens, elles sont, naturellement, code.

Une solution partielle que j'ai trouvée ailleurs sur ce site suggère de donner à la classe BasicWeapon un tas de méthodes vides - on_round_start (), on_attack (), on_move () etc. - puis de remplacer ces méthodes pour chaque arme. À la phase pertinente du cycle de combat, le jeu appellera la méthode appropriée pour l'arme de chaque personnage, et seules celles qui ont des méthodes définies feront réellement quelque chose. Cela aide, mais cela ne me dit toujours pas où je dois mettre le code et / ou les données pour chaque arme.

Existe-t-il un langage ou un outil différent que je peux utiliser comme une sorte de chimère mi-données, mi-code? Suis-je en train d'abattre complètement les bonnes pratiques de programmation?

Ma compréhension de la POO est au mieux sommaire, donc j'apprécierais des réponses qui ne sont pas trop informatiques.

EDIT: Vaughan Hilts a clairement indiqué dans son article ci-dessous que ce dont je parle essentiellement est une programmation basée sur les données. L'essence de ma question est la suivante: comment puis-je implémenter une conception basée sur les données de telle manière que les données puissent contenir des scripts, permettant à de nouvelles armes de faire de nouvelles choses sans changer le code du programme principal?

henrebotha
la source
@ Byte56 Related; mais je pense que c'est ce que le PO essaie d'éviter. Je pense qu'ils essaient de trouver une approche davantage axée sur les données. Corrige moi si je me trompe.
Vaughan Hilts
Je suis d'accord qu'ils essaient de trouver une approche plus orientée vers les données. Plus précisément, j'aime la réponse de Josh à cette question: gamedev.stackexchange.com/a/17286/7191
MichaelHouse
Ah, désolé pour ça. :) J'ai une mauvaise habitude de lire la "réponse acceptée".
Vaughan Hilts

Réponses:

17

Vous voulez une approche basée sur les données presque certainement, à moins que votre jeu ne soit complètement inattendu et / ou généré de manière procédurale.

Essentiellement, cela implique de stocker des informations sur vos armes dans un langage de balisage ou un format de fichier de votre choix. XML et JSON sont tous deux de bons choix lisibles qui peuvent être utilisés pour rendre l'édition assez simple sans avoir besoin d'éditeurs compliqués si vous essayez simplement de démarrer rapidement. ( Et Python peut également analyser XML assez facilement! ) Vous définiriez des attributs tels que «puissance», «défense», «coût» et «statistiques» qui sont tous pertinents. La façon dont vous structurez vos données dépendra de vous.

Si une arme doit ajouter un effet d'état, attribuez-lui un nœud d'effet d'état, puis spécifiez les effets d'un effet d'état via un autre objet piloté par les données. Cela rendra votre code moins dépendant du jeu spécifique et rendra l'édition et le test de votre jeu triviaux. Ne pas avoir à recompiler tout le temps est également un bonus.

Une lecture supplémentaire est disponible ci-dessous:

Vaughan Hilts
la source
2
Un peu comme un système basé sur des composants, où les composants sont lus via des scripts. Comme ceci: gamedev.stackexchange.com/questions/33453/…
MichaelHouse
2
Et pendant que vous y êtes, intégrez un script à ces données afin que les nouvelles armes puissent faire de nouvelles choses sans modifier le code principal.
Patrick Hughes
@Vaughan Hilts: merci, les données semblent être exactement ce dont j'avais intuitivement besoin. Je laisse la question ouverte pendant un certain temps car j'ai encore besoin de réponses, mais je choisirai probablement cela comme la meilleure réponse.
henrebotha
@Patrick Hughes: c'est exactement ce que je veux! Comment je fais ça? Pouvez-vous me montrer un exemple simple ou un tutoriel?
henrebotha
1
Tout d'abord, vous avez besoin d'un moteur de script dans votre moteur, beaucoup de gens choisissent LUA, qui accède à des systèmes de jeu comme les effets et les statistiques. Puis, puisque vous recréez déjà vos objets à partir d'une description de données, vous pouvez incorporer le script que votre moteur appelle chaque fois que votre nouvel objet est activé. Dans l'ancien temps des MUD, cela s'appelait un "proc" (abréviation de Process). La partie difficile est de rendre vos fonctionnalités de jeu dans le moteur suffisamment flexibles pour être appelées de l'extérieur et avec suffisamment de fonctionnalités.
Patrick Hughes
6

(Je suis désolé de soumettre la réponse au lieu d'un commentaire, mais je n'ai pas encore de représentant.)

La réponse de Vaughan est excellente, mais j'aimerais ajouter mes deux cents.

L'une des principales raisons pour lesquelles vous souhaitez utiliser XML ou JSON et l'analyser lors de l'exécution est de modifier et d'expérimenter de nouvelles valeurs sans avoir à recompiler le code. Comme Python est interprété et, à mon avis, assez lisible, vous pourriez avoir les données brutes dans un fichier avec un dictionnaire et tout organisé:

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

De cette façon, il vous suffit d'importer le fichier / module et de l'utiliser comme un dictionnaire normal.

Si vous souhaitez ajouter des scripts, vous pouvez utiliser la nature dynamique de Python et des fonctions de 1ère classe. Vous pouvez faire quelque chose comme ça:

def special_shot():
    ...

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

Bien que je pense que ce serait contraire à la conception basée sur les données. Pour être 100% DDD, vous auriez des informations (données) spécifiant quelles seraient les fonctions et le code qu'une arme spécifique utiliserait. De cette façon, vous ne cassez pas DDD, car vous ne mélangez pas les données avec les fonctionnalités.

Vasco Correia
la source
Merci. Le simple fait de voir un exemple de code simple l'a aidé à cliquer.
henrebotha
1
+1 pour la bonne réponse et pour que vous ayez suffisamment de représentants pour commenter. ;) Bienvenue.
ver
4

Conception basée sur les données

J'ai récemment soumis quelque chose comme cette question à la révision du code .

Après quelques suggestions et améliorations, le résultat a été un code simple qui permettrait une certaine flexibilité relative sur la création d'armes basée sur un dictionnaire (ou JSON). Les données sont interprétées au moment de l'exécution et de simples vérifications sont effectuées par la Weaponclasse elle-même, sans avoir besoin de s'appuyer sur un interpréteur de script complet.

La conception pilotée par les données, bien que Python soit un langage interprété (les fichiers source et de données peuvent être modifiés sans avoir besoin de les recompiler), semble être la bonne chose à faire dans des cas tels que celui que vous avez présenté. Cette question va plus en détail sur le concept, ses avantages et ses inconvénients. Il y a aussi une belle présentation sur Cornell University à ce sujet.

Par rapport à d'autres langages, tels que C ++, qui utiliseraient probablement un langage de script (tel que LUA) pour gérer l'interaction data x engine et les scripts en général, et un certain format de données (comme XML) pour stocker les données, Python peut réellement faire le tout seul (compte tenu de la norme dictmais aussi weakref, cette dernière spécifiquement pour le chargement et la mise en cache des ressources).

Un développeur indépendant, cependant, peut ne pas pousser à l'extrême l'approche basée sur les données comme suggéré dans cet article :

Combien suis-je sur la conception basée sur les données? Je ne pense pas qu'un moteur de jeu devrait contenir une seule ligne de code spécifique au jeu. Pas une. Aucun type d'arme codé en dur. Pas de mise en page HUD codée en dur. Pas d'unité AI codée en dur. Nada. Zip *: français. Rien.

Peut-être qu'avec Python, on pourrait bénéficier du meilleur de l'approche orientée objet et orientée données, visant à la fois la productivité et l'extensibilité.

Traitement simple des échantillons

Dans le cas spécifique discuté lors de la révision du code, un dictionnaire stockerait à la fois les "attributs statiques" et la logique à interpréter - si l'arme avait un comportement conditionnel.

Dans l'exemple ci-dessous, une épée devrait avoir des capacités et des statistiques entre les mains des personnages de la classe `` antipaladin '', et aucun effet, avec des statistiques inférieures lorsqu'elles sont utilisées par d'autres personnages):

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

À des fins de test, j'ai créé des classes simples Playeret Weapon: la première pour tenir / équiper l'arme (appelant ainsi son paramètre on_equip conditionnel) et la seconde en tant que classe unique qui récupérerait les données du dictionnaire, en fonction du nom de l'élément passé en tant que lors de l' Weaponinitialisation. Ils ne reflètent pas la conception appropriée des classes de jeu, mais peuvent néanmoins être utiles pour tester les données:

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)

Avec quelques améliorations futures, j'espère que cela me permettra même d'avoir un jour un système d'artisanat dynamique, traitant des composants d'armes au lieu d'armes entières ...

Tester

  1. Le personnage A choisit une arme, l'équiper (nous imprimons ses statistiques), puis la laisser tomber;
  2. Le personnage B choisit la même arme, l'équiper (et nous imprimons à nouveau ses statistiques pour montrer comment elles sont différentes).

Comme ça:

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

Il devrait imprimer:

Pour un barde

Amélioration: 2, Effets de touche: [], Autres effets: []

Pour un antipaladin

Amélioration: 5, Effets de touche: [«impie»], Autres effets: [«aurée impie»]

Lucas Siqueira
la source