Как структурировать код для множества уникальных видов оружия / заклинаний / сил

22

Я неопытный программист, создающий "похожую на roguelike" игру в духе FTL с использованием Python (пока PyGame нет, так как я до сих пор занимаюсь только текстом).

Моя игра будет содержать большое количество оружия (около 50 для начинающих), дающего уникальные способности. Я изо всех сил пытаюсь понять, как структурировать объектный код таким образом, чтобы он был мощным (с точки зрения предоставления оружию радикально отличающихся эффектов) и расширяемым (чтобы я мог легко добавить больше оружия позже, например, поместив его в папку ).

Моим первым инстинктом было иметь класс BasicWeapon и иметь другое оружие, унаследованное от этого класса. Тем не менее, это кажется мне проблематичным: либо я должен сделать класс BasicWeapon настолько скромным, что он в принципе бесполезен (единственными функциями, которые имеют все оружие, являются имя и тип (пистолет, топор и т. Д.)), Либо я должен предсказать каждый уникальный эффект, который я когда-либо придумаю и закодирую в BasicWeapon.

Последнее явно невозможно, но с первым все еще можно работать. Однако, это оставляет меня с вопросом: куда я могу поместить код для индивидуального оружия?

Должен ли я создать plasmarifle.py, rocketlauncher.py, swarmofbees.py и т. Д. И т. Д. И поместить их в папку, откуда игра может их импортировать?

Или есть способ иметь файл в стиле базы данных (может быть, такой простой, как электронная таблица Excel), который каким-то образом содержит уникальный код для каждого оружия - без необходимости прибегать к eval / exec?

Что касается последнего решения (базы данных), я думаю, что основная проблема, с которой я борюсь, заключается в том, что, хотя я понимаю, что желательно поддерживать разделение между кодом и данными, я чувствую, что оружие стирает грань между «кодом» и немного "данных"; они представляют большое разнообразие похожих вещей, которые можно найти в игре, в этом смысле они похожи на данные, но большинству из них потребуется, по крайней мере, какой-то уникальный код, не предоставленный другим элементам, в каком смысле они, естественно, код.

Частичное решение, которое я нашел в другом месте на этом сайте, предлагает дать классу BasicWeapon набор пустых методов - on_round_start (), on_attack (), on_move () и т. Д., А затем переопределить эти методы для каждого оружия. На соответствующей фазе боевого цикла игра будет вызывать соответствующий метод для оружия каждого персонажа, и только те, у которых есть определенные методы, будут действительно что-то делать. Это помогает, но это все еще не говорит мне, где я должен поместить код и / или данные для каждого оружия.

Есть ли другой язык или инструмент, который я мог бы использовать как своего рода половину данных, половину кода? Я полностью оправдываю хорошую практику программирования?

Мое понимание ООП в лучшем случае схематично, поэтому я был бы признателен за ответы, которые не слишком информатичны.

РЕДАКТИРОВАТЬ: Vaughan Hilts ясно дал понять в своем посте ниже, что я по сути говорю о программировании на основе данных. Суть моего вопроса заключается в следующем: как я могу реализовать управляемый данными дизайн таким образом, чтобы данные могли содержать сценарии, позволяющие новому оружию делать новые вещи, не меняя основной программный код?

henrebotha
источник
3
Связанный: gamedev.stackexchange.com/questions/17276/…
MichaelHouse
@ Byte56 Похожие; но я думаю, что именно этого пытается избежать ФП. Я думаю, что они пытаются найти более управляемый данными подход. Поправьте меня если я ошибаюсь.
Вон Хилтс
Я согласен, что они пытаются найти более ориентированный на данные подход. В частности, мне нравится ответ Джоша на этот вопрос: gamedev.stackexchange.com/a/17286/7191
MichaelHouse
Ах, извините за это. :) У меня плохая привычка читать «принятый ответ».
Вон Хилтс

Ответы:

17

Вы почти наверняка нуждаетесь в подходе, основанном на данных, если ваша игра не будет совершенно неожиданной и / или процедурной, сгенерированной до глубины души.

По сути, это включает в себя хранение информации о вашем оружии на языке разметки или формате файла по вашему выбору. XML и JSON являются хорошими читаемыми вариантами, которые можно использовать для упрощения редактирования без необходимости в сложных редакторах, если вы просто пытаетесь получить быстрый старт. ( И Python также может анализировать XML довольно легко! ) Вы бы установили такие атрибуты, как «сила», «защита», «стоимость» и «статистика», которые все имеют отношение. То, как вы структурируете свои данные, будет зависеть от вас.

Если оружию нужно добавить эффект статуса, назначьте ему узел эффекта статуса, а затем укажите эффекты эффекта статуса через другой объект, управляемый данными. Это сделает ваш код менее зависимым от конкретной игры и сделает редактирование и тестирование вашей игры тривиальным. Не нужно перекомпилировать все время, это тоже бонус.

Дополнительное чтение доступно ниже:

Вон Хилтс
источник
2
Вроде как компонентная система, где компоненты читаются через скрипты. Вот так
MichaelHouse
2
И пока вы работаете над этим, сделайте сценарий частью этих данных, чтобы новое оружие могло делать новые вещи без изменений основного кода.
Патрик Хьюз
@ Vaughan Hilts: спасибо, управление данными, кажется, именно то, что я интуитивно понял, что мне нужно. Я оставляю вопрос открытым некоторое время, так как мне все еще нужны ответы, но, вероятно, выберу это как лучший ответ.
henrebotha
@ Патрик Хьюз: это именно то , что я хочу! Как я могу это сделать? Можете ли вы показать мне простой пример или учебник?
henrebotha
1
Сначала вам нужен движок скриптов в вашем движке, многие выбирают LUA, который обращается к таким игровым системам, как эффекты и статистика. Тогда, так как вы уже воссоздаете свои объекты из описания данных, вы можете встроить скрипт, который ваш движок вызывает каждый раз, когда ваш новый объект активируется. В старые времена MUD это называлось «proc» (сокращение от Process). Сложнее всего сделать игровой процесс в движке достаточно гибким, чтобы вызывать его извне и с достаточным количеством функций.
Патрик Хьюз
6

(Извините, что отправляю ответ вместо комментария, но у меня пока нет представителя.)

Ответ Вогана великолепен, но я бы хотел добавить свои два цента.

Одна из основных причин, по которой вы хотите использовать XML или JSON и анализировать их во время выполнения, - это изменение и экспериментирование с новыми значениями без перекомпиляции кода. Поскольку Python интерпретируется и, на мой взгляд, довольно читабелен, вы можете хранить необработанные данные в файле со словарем и всем организованным:

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

Таким образом, вы просто импортируете файл / модуль и используете его как обычный словарь.

Если вы хотите добавить сценарии, вы можете использовать динамическую природу функций Python и 1-го класса. Вы могли бы сделать что-то вроде этого:

def special_shot():
    ...

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

Хотя я считаю, что это будет против дизайна, управляемого данными. Чтобы быть на 100% DDD, у вас будет информация (данные), определяющая, какие функции и код будет использоваться конкретным оружием. Таким образом, вы не нарушаете DDD, поскольку не смешиваете данные с функциональностью.

Васко Коррея
источник
Спасибо. Просто просмотр простого примера кода помог ему нажать.
henrebotha
1
+1 за хороший ответ и для вас достаточно репутации, чтобы комментировать. ;) Добро пожаловать.
вер
4

Управляемый данными дизайн

Я отправил что-то вроде этого вопроса в обзор кода недавно.

После некоторых предложений и улучшений, результатом стал простой код, который позволял бы относительную гибкость при создании оружия на основе словаря (или JSON). Данные интерпретируются во время выполнения, и Weaponсам класс выполняет простые проверки без необходимости полагаться на весь интерпретатор сценариев.

Проектирование на основе данных, несмотря на то, что Python является интерпретируемым языком (исходные файлы и файлы данных могут редактироваться без необходимости их перекомпиляции), звучит как правильное решение в таких случаях, как тот, который вы представили. Этот вопрос более подробно раскрывает концепцию, ее плюсы и минусы. Theres также хорошая презентация в Корнельском университете об этом.

По сравнению с другими языками, такими как C ++, которые, вероятно, будут использовать язык сценариев (например, LUA) для обработки взаимодействия с механизмом данных и сценариями в целом, а также определенный формат данных (например, XML) для хранения данных, Python действительно может сделать все самостоятельно его ( с учетом стандарта , dictно и weakref, в частности , последний для загрузки ресурсов и кэширования).

Независимый разработчик, однако, не может использовать управляемый данными подход до предела, как предлагается в этой статье :

Как насчет дизайна, управляемого данными? Я не думаю, что игровой движок должен содержать одну строку кода, специфичного для игры. Не один. Нет жестко закодированных типов оружия. Нет жесткого кода HUD. Нет жестко закодированного блока AI. Нада. Zip. Шиш.

Возможно, с Python можно было бы извлечь выгоду из лучшего как объектно-ориентированного, так и управляемого данными подхода, нацеленного как на производительность, так и на расширяемость.

Простая обработка образцов

В конкретном случае, обсуждаемом при проверке кода, словарь будет хранить как «статические атрибуты», так и логику, которую нужно интерпретировать - если оружие будет иметь какое-либо условное поведение.

В приведенном ниже примере меч должен иметь некоторые способности и характеристики в руках персонажей класса «антипаладин», и никаких эффектов, с более низкими характеристиками при использовании другими персонажами):

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

Для целей тестирования я создал простые Playerи Weaponклассы: первый для хранения / оснащения оружия (таким образом вызывая его условную настройку on_equip), а второй - как отдельный класс, который будет извлекать данные из словаря на основе имени элемента, переданного как аргумент во время Weaponинициализации. Они не отражают правильное оформление игровых классов, но все же могут быть полезны для проверки данных:

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)

С некоторыми будущими улучшениями я надеюсь, что это позволит мне когда-нибудь иметь динамическую систему крафта, обрабатывая компоненты оружия вместо целого оружия ...

Тест

  1. Персонаж A подбирает оружие, вооружает его (мы печатаем его статистику), затем бросаем его;
  2. Персонаж B выбирает то же оружие, экипирует его (и мы снова печатаем его статистику, чтобы показать, как они различаются).

Так:

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

Следует напечатать:

Для барда

Улучшение: 2, Эффекты попадания: [], Другие эффекты: []

Для антипаладина

Улучшение: 5, Эффекты попадания: ['Нечестивый'], Другие эффекты: ['Нечестивый Ореол']

Лукас Сикейра
источник