Як структурувати код для багатьох унікальних озброєнь / заклинань / повноважень


22

Я недосвідчений програміст, який створює гру "схожий на шахрайство" у вені FTL , використовуючи Python (жодного PyGame поки що не стосується тексту).

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

Першим моїм інстинктом було мати клас BasicWeapon і мати успадковану зброю від цього класу. Однак це здається мені проблематичним: або я повинен зробити клас BasicWeapon настільки босоніжним, що він в основному марний (єдині риси, які мають у всіх зброя, - це ім'я та тип (пістолет, сокира тощо)), або мені потрібно передбачити кожен Унікальний ефект, який я коли-небудь придумаю і зашифрую його в BasicWeapon.

Останнє явно неможливо, але над першим все ж можна працювати. Однак це залишає у мене питання: куди я кладу код для окремої зброї?

Чи потрібно створити plasmarifle.py, rocketlauncher.py, swarmofbees.py тощо, і передати їх у папку, звідки гра може імпортувати їх?

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

Щодо останнього рішення (бази даних), я думаю, що основним питанням, з яким я борюся, є те, що, хоча я розумію, що бажано підтримувати розділення між кодом і даними, я відчуваю, що зброя розмиває межу між "кодом" і "дані" трохи; вони представляють велику різноманітність подібних речей, які можна знайти в грі, і в цьому сенсі вони схожі на дані, але для більшості з них знадобиться принаймні якийсь унікальний код, який не поділяється ні з одним іншим елементом, в якому сенсі вони, природно, код.

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

Чи є інша мова чи інструмент там, який я можу використовувати як своєрідну химерну половину даних? Чи я повністю розрізний хорошої практики програмування?

Моє розуміння ООП в кращому випадку є схематичним, тому я би вдячний відповідям, які не надто інформатики.

EDIT: Вон Хілтс в своєму дописі нижче дав зрозуміти, що я, по суті, говорю про це програмування, кероване даними. Суть мого питання полягає в наступному: як я можу реалізувати керовану даними конструкцію таким чином, щоб дані могли містити сценарії, дозволяючи новій зброї робити нові речі без зміни основного програмного коду?



@ Byte56 Пов'язані; але я думаю, що цього намагається уникнути ОП. Я думаю, що вони намагаються знайти більш підхід, керований даними. Виправте мене, якщо я помиляюся.
Вуан Хілтс

Я згоден, вони намагаються знайти більш орієнтований на даних підхід. В Зокрема, я , як відповідь Джоша на це питання: gamedev.stackexchange.com/a/17286/7191
MichaelHouse

Ах, вибачте з цього приводу. :) У мене погана звичка читати "прийняту відповідь".
Вуан Хілтс

Відповіді:


17

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

По суті, це включає зберігання інформації про вашу зброю на мові розмітки або файловому форматі на ваш вибір. XML і JSON - це хороший, читабельний вибір, який можна використовувати для того, щоб зробити редагування досить простим, не потребуючи складних редакторів, якщо ви просто намагаєтесь швидко почати. ( І Python також може легко проаналізувати XML! ) Ви встановите такі атрибути, як "влада", "захист", "вартість" та "статистика", які є релевантними. Те, як ви структуруєте свої дані, залежить від вас.

Якщо зброї потрібно додати ефект статусу, надайте їй вузол ефекту статусу, а потім вкажіть ефекти ефекту статусу через інший об'єкт, керований даними. Це зробить ваш код менш залежним від конкретної гри та зробить редагування та тестування вашої гри дрібницею. Бонус також не потребувати перекомпіляції постійно - це також бонус.

Додаткове читання доступне нижче:


2
Наче подібна система, що базується на компонентах, де компоненти читаються через сценарії. Як це: gamedev.stackexchange.com/questions/33453 / ...
MichaelHouse

2
І поки ви на ньому, зробіть частину скрипту з цих даних, щоб нова зброя могла робити нові речі без зміни основного коду.
Патрік Х'юз

@Vaughan Hilts: спасибі, керується даними, здається, саме те, що я інтуїтивно зрозумів, що мені потрібно. Я залишаю питання відкритим на деякий час довше, оскільки мені ще потрібні відповіді, але, мабуть, виберу це як найкращу відповідь.
henrebotha

@Patrick Hughes: саме цього я хочу! Як це зробити? Чи можете ви показати мені простий приклад чи підручник?
henrebotha

1
По-перше, у вашому двигуні потрібен сценарій, багато людей обирають LUA, який має доступ до ігрових систем, таких як ефекти та статистика. Тоді, оскільки ви вже відтворюєте об'єкти з опису даних, ви можете вставляти сценарій, який викликає ваш двигун, коли активується ваш новий об’єкт. У старі часи MUDs це називали "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 є інтерпретованою мовою (і вихідні, і файли даних можна редагувати без необхідності перекомпілювати їх), здається правильним робити у таких випадках, як представлений вами. Це питання детальніше описує концепцію, її плюси та мінуси. Є також приємна презентація про Корнельський університет про це.

У порівнянні з іншими мовами, такими як C ++, яка, ймовірно, використовує мову сценаріїв (наприклад, LUA) для обробки взаємодії двигуна даних x та сценаріїв загалом, і певний формат даних (наприклад, 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. Персонаж А підбирає зброю, оснащуємо її (друкуємо її статистику), а потім скидаємо її;
  2. Персонаж Б підбирає ту саму зброю, озброює її (і ми знову друкуємо її статистику, щоб показати, чим вони відрізняються).

Подобається це:

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, Ефекти ударів: ['нечесні'], Інші ефекти: ['нечесна аурея']

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.