Cómo estructurar el código para muchas armas / hechizos / poderes únicos

22

Soy un programador inexperto que crea un juego "parecido a un roguelike" en la línea de FTL , usando Python (no PyGame hasta ahora, ya que todavía solo me preocupa el texto).

Mi juego contendrá una gran cantidad de armas (alrededor de 50 para empezar) que producen habilidades únicas. Me cuesta entender cómo estructurar el código objeto de una manera que sea poderosa (en términos de permitir que las armas tengan efectos radicalmente diferentes) y extensible (para poder agregar más armas fácilmente más tarde, por ejemplo, colocándolas en una carpeta )

Mi primer instinto fue tener una clase BasicWeapon y tener diferentes armas heredadas de esa clase. Sin embargo, esto me parece problemático: o tengo que hacer que la clase BasicWeapon sea tan básica que es básicamente inútil (las únicas características que todas las armas tienen en común son el nombre y el tipo (pistola, hacha, etc.), o tengo que predecir cada efecto único que se me ocurrirá y codificaré en BasicWeapon.

Lo último es claramente imposible, pero lo primero todavía se puede trabajar. Sin embargo, eso me deja con la pregunta: ¿dónde pongo el código para armas individuales?

¿Creo plasmarifle.py, rocketlauncher.py, swarmofbees.py, etc., etc. y los dejo en una carpeta desde donde el juego puede importarlos?

¿O hay una manera de tener un archivo de estilo de base de datos (tal vez algo tan simple como una hoja de cálculo de Excel) que de alguna manera contiene un código único para cada arma, sin necesidad de recurrir a eval / exec?

En términos de la última solución (base de datos), creo que el problema fundamental con el que estoy luchando es que, si bien entiendo que es deseable mantener la separación entre el código y los datos, siento que las armas desdibujan la línea entre el "código" y "datos" un poco; representan la gran variedad de cosas similares que se pueden encontrar en el juego, en cuyo sentido son como datos, pero la mayoría de ellos requerirán al menos algún código único que no se comparta con ningún otro elemento, en ese sentido, naturalmente, código.

Una solución parcial que he encontrado en otra parte de este sitio sugiere dar a la clase BasicWeapon un montón de métodos vacíos: on_round_start (), on_attack (), on_move (), etc., y luego anular esos métodos para cada arma. En la fase relevante del ciclo de combate, el juego llamará al método apropiado para el arma de cada personaje, y solo aquellos que tengan métodos definidos realmente harán algo. Esto ayuda, pero aún no me dice dónde debo poner el código y / o los datos para cada arma.

¿Existe algún idioma o herramienta diferente que pueda usar como una especie de quimera de medio código y medio código? ¿Estoy matando por completo las buenas prácticas de programación?

Mi comprensión de OOP es incompleta en el mejor de los casos, por lo que agradecería las respuestas que no son demasiado informáticas.

EDITAR: Vaughan Hilts ha dejado claro en su publicación a continuación que de lo que estoy hablando esencialmente es de la programación basada en datos. La esencia de mi pregunta es esta: ¿cómo puedo implementar un diseño basado en datos de tal manera que los datos puedan contener secuencias de comandos, permitiendo que nuevas armas hagan cosas nuevas sin cambiar el código del programa principal?

henrebotha
fuente
3
Relacionado: gamedev.stackexchange.com/questions/17276/…
MichaelHouse
@ Byte56 relacionados; pero creo que esto es lo que el OP está tratando de evitar. Creo que están tratando de encontrar un enfoque más basado en datos. Corrígeme si me equivoco.
Vaughan Hilts
Estoy de acuerdo en que están tratando de encontrar un enfoque más orientado a los datos. Específicamente, me gusta la respuesta de Josh a esa pregunta: gamedev.stackexchange.com/a/17286/7191
MichaelHouse
Ah, perdón por eso. :) Tengo la mala costumbre de leer la "respuesta aceptada".
Vaughan Hilts

Respuestas:

17

Desea un enfoque basado en datos casi con seguridad, a menos que su juego sea completamente inesperado y / o genere procedimientos para el núcleo.

Esencialmente, esto implica almacenar información sobre sus armas en un lenguaje de marcado o formato de archivo de su elección. XML y JSON son opciones buenas y legibles que se pueden usar para hacer que la edición sea bastante simple sin la necesidad de editores complicados si solo está tratando de comenzar rápidamente. (¡ Y Python también puede analizar XML bastante fácil! ) Establecería atributos como 'poder', 'defensa', 'costo' y 'estadísticas' que son todos relevantes. La forma en que estructura sus datos dependerá de usted.

Si un arma necesita agregar un efecto de estado, dele un nodo de efecto de estado y luego especifique los efectos de un efecto de estado a través de otro objeto controlado por datos. Esto hará que tu código dependa menos del juego específico y que la edición y prueba de tu juego sean triviales. No tener que recompilar todo el tiempo también es una ventaja.

La lectura suplementaria está disponible a continuación:

Vaughan Hilts
fuente
2
Algo así como un sistema basado en componentes, donde los componentes se leen a través de scripts. Así: gamedev.stackexchange.com/questions/33453/…
MichaelHouse
2
Y mientras lo hace, haga que un script forme parte de esos datos para que las nuevas armas puedan hacer cosas nuevas sin cambios en el código principal.
Patrick Hughes
@Vaughan Hilts: gracias, la información basada en datos parece ser exactamente lo que intuitivamente entendí que necesitaba. Dejo la pregunta abierta por un tiempo más, ya que todavía necesito respuestas, pero probablemente elegiré esta como la mejor respuesta.
henrebotha
@Patrick Hughes: ¡eso es exactamente lo que quiero! ¿Cómo puedo hacer eso? ¿Me puede mostrar un ejemplo simple o tutorial?
henrebotha
1
Primero necesitas un motor de script en tu motor, muchas personas eligen LUA, que accede a sistemas de juego como efectos y estadísticas. Luego, como ya está recreando sus objetos a partir de una descripción de datos, puede incrustar el script que su motor llama cada vez que se activa su nuevo objeto. En los viejos tiempos de los MUD, esto se llamaba "proc" (abreviatura de Proceso). La parte difícil es hacer que sus características de juego en el motor sean lo suficientemente flexibles como para ser llamadas desde el exterior y con suficientes características.
Patrick Hughes
6

(Lamento enviar la respuesta en lugar de un comentario, pero aún no tengo representante).

La respuesta de Vaughan es genial, pero me gustaría agregar mis dos centavos.

Una de las razones principales por las que desearía usar XML o JSON y analizarlo en tiempo de ejecución es cambiar y experimentar con nuevos valores sin tener que volver a compilar el código. Como Python se interpreta y, en mi opinión, es bastante legible, podría tener los datos en bruto en un archivo con un diccionario y todo organizado:

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

De esta manera, solo importa el archivo / módulo y lo usa como un diccionario normal.

Si desea agregar scripts, puede utilizar la naturaleza dinámica de Python y las funciones de primera clase. Podrías hacer algo como esto:

def special_shot():
    ...

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

Aunque creo que eso estaría en contra del diseño basado en datos. Para ser 100% DDD, tendría información (datos) que especificaría cuáles serían las funciones y el código que usaría un arma específica. De esta manera, no rompes DDD, ya que no mezclas datos con funcionalidad.

Vasco Correia
fuente
Gracias. Solo ver un ejemplo de código simple lo ayudó a hacer clic.
henrebotha
1
+1 por la buena respuesta y por tener suficiente representante para comentar. ;) Bienvenido.
ver
4

Diseño basado en datos

Envié algo como esta pregunta a la revisión de código recientemente.

Después de algunas sugerencias y mejoras, el resultado fue un código simple que permitiría cierta flexibilidad relativa en la creación de armas basada en un diccionario (o JSON). Los datos se interpretan en tiempo de ejecución y la Weaponclase misma realiza verificaciones simples , sin la necesidad de contar con un intérprete de script completo.

El diseño basado en datos, a pesar de que Python es un lenguaje interpretado (tanto los archivos de origen como los de datos se pueden editar sin la necesidad de volver a compilarlos), parece ser lo correcto en casos como el que usted presentó. Esta pregunta entra en más detalles sobre el concepto, sus pros y sus contras. También hay una buena presentación sobre la Universidad de Cornell al respecto.

En comparación con otros lenguajes, como C ++, que probablemente usarían un lenguaje de secuencias de comandos (como LUA) para manejar la interacción de datos x el motor y las secuencias de comandos en general, y un cierto formato de datos (como XML) para almacenar los datos, Python realmente puede hacer todo por sí mismo (considerando el estándar dictpero también weakref, este último específicamente para la carga de recursos y el almacenamiento en caché).

Sin embargo, un desarrollador independiente no puede llevar el enfoque basado en datos al extremo como se sugiere en este artículo :

¿Qué tanto sobre el diseño basado en datos soy yo? No creo que un motor de juego deba contener una sola línea de código específico del juego. Ni uno. No hay tipos de armas codificadas. No hay diseño HUD codificado. Sin unidad codificada AI. Nada Cremallera. Zilch

Tal vez, con Python, uno podría beneficiarse de lo mejor del enfoque orientado a objetos y basado en datos, con el objetivo de la productividad y la extensibilidad.

Procesamiento simple de muestras

En el caso específico discutido en la revisión de código, un diccionario almacenaría tanto los "atributos estáticos" como la lógica a interpretar, en caso de que el arma tenga un comportamiento condicional.

En el ejemplo a continuación, una espada debe tener algunas habilidades y estadísticas en manos de los personajes de la clase 'antipaladin', y ningún efecto, con estadísticas más bajas cuando son utilizadas por otros personajes):

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

Para fines de prueba, creé simples Playery Weaponclases: el primero en sostener / equipar el arma (llamando así a su configuración condicional on_equip) y el último como una sola clase que recuperaría los datos del diccionario, en función del nombre del elemento pasado como un argumento durante la Weaponinicialización. No reflejan el diseño adecuado de las clases de juego, pero aún pueden ser útiles para probar los datos:

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 alguna mejora futura, espero que esto me permita tener un sistema de fabricación dinámico algún día, procesando componentes de armas en lugar de armas enteras ...

Prueba

  1. El personaje A escoge un arma, la equipa (imprimimos sus estadísticas) y luego la suelta;
  2. El personaje B elige la misma arma, la equipa (e imprimimos sus estadísticas nuevamente para mostrar en qué se diferencian).

Me gusta esto:

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

Debería imprimir:

Para un bardo

Mejora: 2, Efectos de golpe: [], Otros efectos: []

Para un antipaladin

Mejora: 5, Efectos de golpe: ['impío'], Otros efectos: ['aurea impía']

Lucas Siqueira
fuente