Como estruturar o código de muitas armas / feitiços / poderes exclusivos

22

Sou um programador inexperiente que cria um jogo "tipo roguelike" na veia da FTL , usando Python (nenhum PyGame até o momento, pois ainda estou preocupado apenas com o texto).

Meu jogo conterá um grande número de armas (cerca de 50 para iniciantes) que produzem habilidades únicas. Estou lutando para entender como estruturar o código do objeto de uma maneira que seja poderosa (em termos de permitir que armas tenham efeitos radicalmente diferentes) e extensível (para que eu possa adicionar mais armas facilmente mais tarde, por exemplo, soltando-as em uma pasta )

Meu primeiro instinto foi ter uma classe BasicWeapon e armas diferentes herdadas dessa classe. No entanto, isso me parece problemático: ou eu tenho que tornar a classe BasicWeapon tão básica que é basicamente inútil (os únicos recursos que todas as armas têm em comum são nome e tipo (pistola, machado, etc.)) ou preciso prever todos os efeito único que eu vou criar e codificar isso no BasicWeapon.

O último é claramente impossível, mas o primeiro ainda pode ser trabalhado. No entanto, isso me deixa com a pergunta: onde coloco o código para armas individuais?

Crio plasmarifle.py, rocketlauncher.py, swarmofbees.py, etc etc, e os solto em uma pasta de onde o jogo pode importá-los?

Ou existe uma maneira de ter um arquivo no estilo de banco de dados (talvez algo tão simples quanto uma planilha do Excel) que de alguma forma contenha código único para cada arma - sem a necessidade de recorrer a eval / exec?

Em termos da última solução (banco de dados), acho que a questão fundamental com a qual estou lutando é que, embora compreenda que é desejável manter a separação entre código e dados, sinto que as armas desfocam a linha entre "código" e "dados" um pouco; eles representam a grande variedade de coisas semelhantes que podem ser encontradas no jogo; nesse sentido, são como dados, mas a maioria deles exigirá pelo menos algum código exclusivo não compartilhado com qualquer outro item; nesse sentido, eles são, naturalmente, código.

Uma solução parcial que encontrei em outras partes deste site sugere dar à classe BasicWeapon vários métodos vazios - on_round_start (), on_attack (), on_move () etc - e, em seguida, substituir esses métodos para cada arma. Na fase relevante do ciclo de combate, o jogo chamará o método apropriado para a arma de cada personagem, e apenas aqueles que possuem métodos definidos farão alguma coisa. Isso ajuda, mas ainda não me diz onde devo colocar o código e / ou os dados de cada arma.

Existe uma linguagem ou ferramenta diferente por aí que eu possa usar como uma espécie de quimera de meio dado e meio código? Estou massacrando completamente as boas práticas de programação?

Meu entendimento sobre OOP é superficial, na melhor das hipóteses, então eu apreciaria respostas que não são muito científicas em computação.

Edição: Vaughan Hilts deixou claro em seu post abaixo que o que eu estou falando essencialmente é sobre programação orientada a dados. A essência da minha pergunta é a seguinte: como posso implementar um design orientado a dados de forma que os dados possam conter scripts, permitindo que novas armas façam coisas novas sem alterar o código principal do programa?

henrebotha
fonte
3
Relacionados: gamedev.stackexchange.com/questions/17276/...
Michaelhouse
@ Byte56 Relacionado; mas acho que é isso que o OP está tentando evitar. Eu acho que eles estão tentando encontrar uma abordagem mais orientada a dados. Corrija-me se eu estiver errado.
Vaughan Hilts
Concordo que eles estão tentando encontrar uma abordagem mais orientada a dados. Especificamente, eu gosto da resposta de Josh para essa pergunta: gamedev.stackexchange.com/a/17286/7191
MichaelHouse
Ah, desculpe por isso. :) Eu tenho o péssimo hábito de ler a "resposta aceita".
Vaughan Hilts

Respostas:

17

Você deseja uma abordagem orientada a dados quase certamente, a menos que seu jogo seja completamente inesperado e / ou processual gerado para o núcleo.

Essencialmente, isso envolve armazenar informações sobre suas armas em uma linguagem de marcação ou formato de arquivo de sua escolha. XML e JSON são boas opções legíveis que podem ser usadas para tornar a edição bastante simples, sem a necessidade de editores complicados, se você está apenas tentando iniciar rapidamente. ( E o Python também pode analisar o XML com bastante facilidade! ) Você pode definir atributos como 'poder', 'defesa', 'custo' e 'estatísticas' relevantes. A maneira como você estrutura seus dados depende de você.

Se uma arma precisar adicionar um efeito de status, forneça um nó de efeito Status e especifique os efeitos de um efeito de status por meio de outro objeto controlado por dados. Isso tornará seu código menos dependente do jogo específico e tornará a edição e o teste triviais do jogo. Não ter que recompilar o tempo todo também é um bônus.

A leitura suplementar está disponível abaixo:

Vaughan Hilts
fonte
2
Como um sistema baseado em componentes, onde os componentes são lidos através de scripts. Como esta: gamedev.stackexchange.com/questions/33453/...
Michaelhouse
2
E enquanto você está nisso, faça um script parte desses dados para que novas armas possam fazer coisas novas sem alterações no código principal.
Patrick Hughes
@ Vaughan Hilts: obrigado, orientado a dados parece ser exatamente o que eu intuitivamente entendi que precisava. Estou deixando a pergunta em aberto por mais algum tempo, pois ainda preciso de respostas, mas provavelmente vou escolher essa como a melhor resposta.
henrebotha
@ Patrick Hughes: é exatamente isso que eu quero! Como faço isso? Você pode me mostrar um exemplo ou tutorial simples?
henrebotha
1
Primeiro, você precisa de um mecanismo de script em seu mecanismo, muitas pessoas escolhem LUA, que acessa sistemas de jogabilidade como efeitos e estatísticas. Então, como você já está recriando seus objetos a partir de uma descrição dos dados, pode incorporar o script que seu mecanismo chama sempre que seu novo objeto é ativado. Nos velhos tempos dos MUDs, isso era chamado de "proc" (abreviação de Process). O difícil é tornar os recursos de jogabilidade no mecanismo flexíveis o suficiente para serem chamados de fora e com recursos suficientes.
Patrick Hughes
6

(Desculpe enviar a resposta em vez de um comentário, mas ainda não tenho representante.)

A resposta de Vaughan é ótima, mas eu gostaria de adicionar meus dois centavos.

Um dos principais motivos pelos quais você deseja usar XML ou JSON e analisá-lo em tempo de execução é alterar e experimentar novos valores sem precisar recompilar o código. Como o Python é interpretado e, na minha opinião, bastante legível, você pode ter os dados brutos em um arquivo com um dicionário e tudo organizado:

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

Dessa forma, basta importar o arquivo / módulo e usá-lo como um dicionário normal.

Se você deseja adicionar scripts, pode usar a natureza dinâmica das funções Python e de 1ª classe. Você poderia fazer algo assim:

def special_shot():
    ...

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

Embora eu acredite que isso seria contra o design orientado a dados. Para ser 100% DDD, você teria informações (dados) especificando quais seriam as funções e o código que essa arma específica usaria. Dessa forma, você não quebra o DDD, pois não mistura dados com funcionalidade.

Vasco Correia
fonte
Obrigado. Apenas ver um exemplo de código simples ajudou a clicar.
precisa saber é o seguinte
1
+1 para a resposta legal e para você ter representante suficiente para comentar. ;) Bem vinda.
ver
4

Design orientado a dados

Enviei algo como esta pergunta à revisão de código recentemente.

Após algumas sugestões e melhorias, o resultado foi um código simples que permitiria certa flexibilidade na criação de armas com base em um dicionário (ou JSON). Os dados são interpretados em tempo de execução e verificações simples são feitas pela Weaponprópria classe, sem a necessidade de contar com um interpretador de script inteiro.

O Design Orientado a Dados, apesar de o Python ser uma linguagem interpretada (os arquivos de origem e de dados podem ser editados sem a necessidade de recompilá-los), parece a coisa certa a fazer em casos como o que você apresentou. Esta pergunta entra em mais detalhes sobre o conceito, seus prós e contras. Há também uma boa apresentação na Universidade de Cornell sobre isso.

Comparado com outras linguagens, como C ++, que provavelmente usariam uma linguagem de script (como LUA) para lidar com interação e script de dados x mecanismos em geral, e com um determinado formato de dados (como XML) para armazenar os dados, o Python pode realmente fazer tudo por conta própria (considerando o padrão, dictmas também weakref, o último especificamente para carregamento e armazenamento de recursos).

Um desenvolvedor independente, no entanto, não pode levar ao extremo a abordagem orientada a dados, conforme sugerido neste artigo :

Quanto de design orientado a dados eu sou? Não acho que um mecanismo de jogo deva conter uma única linha de código específico do jogo. Nenhum. Nenhum tipo de arma codificado. Nenhum layout de HUD codificado. Nenhuma unidade codificada AI. Nada. Fecho eclair. Zilch.

Talvez, com o Python, alguém possa se beneficiar do melhor da abordagem orientada a objetos e orientada a dados, visando produtividade e extensibilidade.

Processamento de amostra simples

No caso específico discutido na revisão de código, um dicionário armazenaria os "atributos estáticos" e a lógica a ser interpretada - caso a arma tenha algum comportamento condicional.

No exemplo abaixo, uma espada deve ter algumas habilidades e estatísticas nas mãos de personagens da classe 'antipaladin', e sem efeitos, com estatísticas mais baixas quando usadas por outros personagens):

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 fins de teste, criei classes Playere simples Weapon: a primeira a segurar / equipar a arma (chamando sua configuração de on_equip condicional) e a segunda como uma classe única que recuperaria os dados do dicionário, com base no nome do item passado como argumento durante a Weaponinicialização. Eles não refletem o design adequado das classes de jogos, mas ainda podem ser úteis para testar os dados:

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)

Com algumas melhorias futuras, espero que isso me permita ter um sistema de criação dinâmico algum dia, processando componentes de armas em vez de armas inteiras ...

Teste

  1. O personagem A pega uma arma, equipa-a (imprimimos suas estatísticas) e depois a solta;
  2. O personagem B escolhe a mesma arma, equipa-a (e imprimimos suas estatísticas novamente para mostrar como elas são diferentes).

Como isso:

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

Deve imprimir:

Para um bardo

Aprimoramento: 2, Efeitos de hits: [], Outros efeitos: []

Para um antipaladin

Aprimoramento: 5, Efeitos de hit: ['profano'], Outros efeitos: ['aurea profana']

Lucas Siqueira
fonte