Как реализовать гибкую систему баффов / дебаффов?


66

Обзор:

Во многих играх с RPG-подобной статистикой предусмотрены «положительные эффекты» для персонажей, начиная от простого «Нанести 25% дополнительного урона» до более сложных вещей, таких как «Наносить 15 урона обратно атакующим при попадании».

Специфика каждого типа баффа на самом деле не актуальна. Я ищу (предположительно объектно-ориентированный) способ обработки произвольных эффектов.

Подробности:

В моем конкретном случае у меня есть несколько персонажей в пошаговом боевом окружении, поэтому я предполагал, что баффы будут привязаны к таким событиям, как «OnTurnStart», «OnReceiveDamage» и т. Д. Возможно, каждый бафф является подклассом основного абстрактного класса Buff, где перегружены только соответствующие события. Тогда у каждого персонажа может быть свой вектор положительных эффектов.

Имеет ли это решение смысл? Я, конечно, вижу, что нужны десятки типов событий, похоже, что создание нового подкласса для каждого баффа является излишним, и, похоже, он не допускает каких-либо "взаимодействий" баффов. То есть, если бы я хотел применить ограничение на усиление урона, чтобы даже если у вас было 10 различных баффов, каждый из которых дает 25% дополнительного урона, вы наносите только 100% дополнительного вместо 250% дополнительного.

И есть более сложные ситуации, которые в идеале я мог бы контролировать. Я уверен, что каждый может придумать примеры того, как более сложные любители могут потенциально взаимодействовать друг с другом таким образом, который я, как разработчик игры, может и не захотеть.

Как относительно неопытный программист C ++ (я обычно использовал C во встроенных системах), я чувствую, что мое решение упрощено и, вероятно, не в полной мере использует объектно-ориентированный язык.

Мысли? Кто-нибудь здесь разрабатывал довольно надежную систему баффов раньше?

Изменить: Относительно ответа (ов):

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

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

Для такой игры, как Diablo 3 (упоминается ниже), где почти любой бит экипировки может изменить силу баффа, баффы - это просто статистика персонажей, и система всегда кажется хорошей идеей, когда это возможно.

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

В любом случае, я все еще надеюсь, что кто-то придет с необычной магической пулей "ОО", которая позволит мне применить +2 к расстоянию перемещения за бафф хода , нанести 50% урона обратно баффу атакующего , и автоматически телепортироваться к соседней плитке при атаке из 3 или более плиток прочь любителя в одной системе , не поворачивая +5 силы бафф в свой собственный подкласс.

Я думаю, что самым близким является ответ, который я отметил, но слово все еще открыто. Спасибо всем за вклад.


Я не публикую это как ответ, так как я просто мозговой штурм, но как насчет списка баффов? Каждый бафф имеет константу и модификатор фактора. Константа будет +10 к урону, коэффициент будет 1.10 для + 10% к урону В своих расчетах урона вы перебираете все бафы, чтобы получить общий модификатор, а затем накладываете любые ограничения, которые хотите. Вы сделали бы это для любого модифицируемого атрибута. Вам понадобится особый метод для сложных вещей, хотя.
Уильям Маригер

Кстати, я уже реализовал нечто подобное для своего объекта Stats, когда создавал систему для вооружения и аксессуаров. Как вы сказали, это достаточно приличное решение для баффов, которые только изменяют существующие атрибуты, но, конечно, даже тогда я хочу, чтобы у некоторых баффов истекал после X ходов, у других истекал, когда эффект появлялся Y раз, и т. Д. Я не делал упомяните об этом в основном вопросе, потому что это уже очень долго.
gkimsey

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

Да, я ожидал, что каждый шаблон события для абстрактного класса Buff будет включать соответствующие параметры, подобные этому. Это, безусловно, сработает, но я колеблюсь, потому что такое чувство, что оно не будет хорошо масштабироваться Мне трудно представить, что MMORPG с несколькими сотнями разных баффов имеет отдельный класс, определенный для каждого бафа, выбирая из ста различных событий. Не то чтобы я делал так много баффов (вероятно, ближе к 30), но если есть более простая, более элегантная или более гибкая система, я бы хотел ее использовать. Более гибкая система = больше интересных баффов / способностей.
gkimsey

4
Это не очень хороший ответ на проблему взаимодействия, но мне кажется, что шаблон декоратора здесь применим; просто нанесите больше баффов (декораторов) друг на друга. Может быть, с системой, которая обрабатывает взаимодействие, «объединяя» баффы вместе (например, 10x 25% сливается в один 100% бафф).
ashes999

Ответы:


32

Это сложный вопрос, потому что вы говорите о нескольких разных вещах, которые (в наши дни) объединяются в «баффы»:

  • модификаторы атрибутов игрока
  • спецэффекты, которые происходят на определенных событиях
  • комбинации вышеперечисленного.

Я всегда реализую первый со списком активных эффектов для определенного персонажа. Удаление из списка, будь то на основе продолжительности или явно, довольно тривиально, поэтому я не буду здесь останавливаться на этом. Каждый эффект содержит список модификаторов атрибутов и может применять его к базовому значению путем простого умножения.

Затем я обертываю его функциями для доступа к измененным атрибутам. например.:

def get_current_attribute_value(attribute_id, criteria):
    val = character.raw_attribute_value[attribute_id]
    # Accumulate the modifiers
    for effect in character.all_effects:
        val = effect.apply_attribute_modifier(attribute_id, val, criteria)
    # Make sure it doesn't exceed game design boundaries
    val = apply_capping_to_final_value(val)
    return val

class Effect():
    def apply_attribute_modifier(attribute_id, val, criteria):
        if attribute_id in self.modifier_list:
            modifier = self.modifier_list[attribute_id]
            # Does the modifier apply at this time?
            if modifier.criteria == criteria:
                # Apply multiplicative modifier
                return val * modifier.amount
        else:
            return val

class Modifier():
    amount = 1.0 # default that has no effect
    criteria = None # applies all of the time

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

Значение критерия состоит в том, чтобы позволить вам реализовать «+ 20% против нежити» - установите значение UNDEAD для эффекта и передайте значение UNDEAD только get_current_attribute_value()при расчете броска урона против врага-нежити.

Между прочим, у меня не было бы соблазна попробовать написать систему, которая применяет и отменяет применение значений непосредственно к значению базового атрибута - конечный результат состоит в том, что ваши атрибуты с большой вероятностью отклонятся от предполагаемого значения из-за ошибки. (Например, если вы умножаете что-то на 2, но затем ограничиваете это, когда вы снова разделите это на 2, это будет ниже, чем началось.)

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

# This is a method on a Character, called during combat
def on_receive_damage(damage_info):
    for effect in character.all_effects:
        effect.on_receive_damage(character, damage_info)

class Effect():
    self.on_receive_damage_handler = DoNothing # a default function that does nothing
    def on_receive_damage(character, damage_info):
        self.on_receive_damage_handler(character, damage_info)

def reflect_damage(character, damage_info):
    damage_info.attacker.receive_damage(15)

reflect_damage_effect = new Effect()
reflect_damage_effect.on_receive_damage_handler = reflect_damage
my_character.all_effects.add(reflect_damage_effect)

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


2
+1 за отличную детализацию. Это самый близкий ответ к официальному ответу на мой вопрос, который я видел. Базовая настройка здесь, кажется, обеспечивает большую гибкость и небольшую абстракцию того, что в противном случае могло бы быть грязной игровой логикой. Как вы сказали, для более эффектных эффектов все равно потребуются свои собственные классы, но я думаю, что это удовлетворяет основную часть потребностей типичной "положительной" системы.
gkimsey

+1 за указание на концептуальные различия, скрытые здесь. Не все из них будут работать с одинаковой логикой обновления на основе событий. Смотрите ответ @ Росса для совершенно другого приложения. Оба должны будут существовать рядом друг с другом.
ctietze

22

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

Идея была проста, и хотя мы применили ее в Python, она была довольно эффективной.

В основном, вот как это было:

  • У пользователя был список примененных в настоящее время баффов и дебаффов (обратите внимание, что бафф и дебафф относительно одинаковы, просто эффект имеет другой результат)
  • Баффы имеют различные атрибуты, такие как длительность, имя и текст для отображения информации и время жизни. Важными являются время, продолжительность и ссылка на актера, к которому применяется этот бафф.
  • Для Баффа, когда он присоединяется к игроку через player.apply (buff / debuff), он вызывает метод start (), это применяет к игроку критические изменения, такие как увеличение скорости или замедление.
  • Затем мы перебираем каждый бафф в цикле обновления, и бафы обновляются, что увеличивает их время жизни. Подклассы будут реализовывать такие вещи, как отравление игрока, предоставление игроку HP со временем и т. Д.
  • Когда бафф был сделан для, то есть timeAlive> = duration, логика обновления удалит бафф и вызовет метод finish (), который будет варьироваться от снятия ограничений скорости у игрока до создания небольшого радиуса (например, эффект бомбы) после DoT)

Теперь о том, как на самом деле применять баффы из мира, - другая история. Вот моя пища для размышлений.


1
Это звучит как лучшее объяснение того, что я пытался описать выше. Это относительно просто, конечно, легко понять. По сути, вы упомянули три «события» (OnApply, OnTimeTick, OnExpired), чтобы связать их с моим мышлением. Как есть, он не поддерживает такие вещи, как возврат урона при попадании и т. Д., Но он лучше масштабируется для большого количества баффов. Я бы предпочел не ограничивать то, что могут делать мои баффы (что = ограничение количества событий, которые я придумываю, которые должны вызываться логикой основной игры), но масштабируемость баффов может быть более важной. Спасибо за ваш вклад!
gkimsey

Да, мы не реализовали ничего подобного. Звучит очень аккуратно и отличная концепция (вроде как шип Thinns).
Росс

@gkimsey Для таких вещей, как Thorns и другие пассивные баффы, я бы реализовал логику в вашем классе моба как пассивный стат, похожий на урон или здоровье, и увеличил бы этот стат при применении баффа. Это упрощает много случая , когда у вас есть несколько положительных эффекты шипов, а также держать интерфейс чистым (10 бафф покажут 1 обратного урон , а не 10) и позволяет система любителя остается простой.
3Doubloons

Это почти нелогичный подход, но я начал думать о себе, когда играл в Diablo 3. Я заметил, что кража жизни, жизнь при попадании, урон атакующим в ближнем бою и т. Д. - все это были их собственные характеристики в окне персонажа. Конечно, у D3 нет самой сложной системы полировки или взаимодействия в мире, но это вряд ли тривиально. Это имеет большой смысл. Тем не менее, есть потенциально 15 различных баффов с 12 различными эффектами, которые могут попасть в это. Кажется странным
добавление таблицы

11

Я не уверен, читаете ли вы это до сих пор, но я долго боролся с подобной проблемой.

Я разработал множество различных типов систем воздействия. Я кратко расскажу о них сейчас. Это все основано на моем опыте. Я не утверждаю, что знаю все ответы.


Статические модификаторы

Этот тип системы в основном полагается на простые целые числа для определения любых модификаций. Например, от +100 до Макс. HP, +10 к атаке и так далее. Эта система также может обрабатывать проценты. Вам просто нужно убедиться, что укладка не выходит из-под контроля.

Я никогда не кэшировал сгенерированные значения для системы такого типа. Например, если бы я хотел показать максимальное здоровье чего-либо, я бы сгенерировал значение на месте. Это предотвратило склонность к ошибкам и облегчило понимание для всех участников.

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

В целом, это работает очень хорошо с простыми статическими модификаторами Тем не менее, код должен существовать в надлежащих местах для используемых модификаторов: getAttack, getMaxHP, getMeleeDamage и так далее и так далее.

Там, где этот метод не работает (для меня) очень сложное взаимодействие между любителями. Нет реального простого способа взаимодействия, кроме как немного поднять его. У него есть несколько простых возможностей взаимодействия. Чтобы сделать это, вы должны внести изменения в способ хранения статических модификаторов. Вместо использования enum в качестве ключа, вы используете String. Эта строка будет именем Enum + дополнительная переменная. 9 раз из 10 дополнительная переменная не используется, поэтому вы по-прежнему сохраняете имя перечисления в качестве ключа.

Давайте сделаем быстрый пример: если вы хотите иметь возможность изменять урон против нежити, у вас может быть упорядоченная пара, подобная этой: (DAMAGE_Undead, 10) DAMAGE - это Enum, а Undead - дополнительная переменная. Поэтому во время боя вы можете сделать что-то вроде:

dam += attacker.getMod(Mod.DAMAGE + npc.getRaceFamily()); //in this case the race family would be undead

В любом случае, это работает довольно хорошо и быстро. Но он терпит неудачу при сложных взаимодействиях и наличии «специального» кода везде. Например, рассмотрим ситуацию с «25% шансом телепортироваться при смерти». Это «довольно» сложный вопрос. Вышеуказанная система может справиться с этим, но не легко, так как вам необходимо следующее:

  1. Определите, есть ли у игрока этот мод.
  2. Где-нибудь, есть код для выполнения телепортации, если все прошло успешно. Расположение этого кода само по себе является обсуждением!
  3. Получить правильные данные с карты модов. Что означает значение? Это комната, где они тоже телепортируются? Что, если у игрока есть два мода на телепорт? Не сложатся ли суммы вместе ?????? ПРОВАЛ!

Так что это подводит меня к следующему:


Ультимативная комплексная система баффов

Однажды я попытался написать 2D-MMORPG самостоятельно. Это была ужасная ошибка, но я многому научился!

Я переписал систему аффектов 3 раза. Первый использовал менее мощный вариант из вышеперечисленного. Вторым было то, о чем я собираюсь поговорить.

Эта система имела ряд классов для каждой модификации, поэтому такие вещи, как: ChangeHP, ChangeMaxHP, ChangeHPByPercent, ChangeMaxByPercent. У меня был миллион таких парней - даже такие вещи, как TeleportOnDeath.

У моих классов были вещи, которые делали следующее:

  • applyAffect
  • removeAffect
  • checkForInteraction <--- важно

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

Метод checkForInteraction был ужасно сложным фрагментом кода. В каждом из классов аффектов (т. Е. ChangeHP) он будет иметь код, чтобы определить, должен ли он быть изменен входным аффектом. Так, например, если у вас было что-то вроде ....

  • Buff 1: Наносит 10 ед. Урона от огня при атаке
  • Buff 2: Увеличивает весь урон от огня на 25%.
  • Buff 3: Увеличивает весь урон от огня на 15.

Метод checkForInteraction будет обрабатывать все эти эффекты. Для этого нужно было проверить каждое влияние на ВСЕХ игроков поблизости! Это потому, что тип аффектов, с которыми я сталкивался у нескольких игроков на протяжении области. Это означает, что в коде НИКОГДА не было каких-либо специальных утверждений, подобных приведенным выше - «если мы только что умерли, мы должны проверить телепорт при смерти». Эта система будет автоматически обрабатывать его правильно в нужное время.

Попытка написать эту систему заняла у меня около 2 месяцев и несколько раз заставила голову взорваться. ОДНАКО, он был ДЕЙСТВИТЕЛЬНО мощным и мог делать безумное количество вещей - особенно если учесть следующие два факта для способностей в моей игре: 1. У них были целевые диапазоны (то есть: одиночный, сам, только группа, PB AE self , PB AE target, целевой AE и т. Д.). 2. Способности могут иметь более 1 влияния на них.

Как я уже упоминал выше, это была 2-ая из 3-х аффектных систем для этой игры. Почему я отошел от этого?

У этой системы была худшая производительность, которую я когда-либо видел! Это было ужасно медленно, так как нужно было так много проверять каждую происходящую вещь. Я пытался улучшить его, но счел это неудачей.

Итак, мы подошли к моей третьей версии (и другому типу баффов):


Комплексный аффект-класс с обработчиками

Так что это в значительной степени комбинация первых двух: у нас могут быть статические переменные в классе Affect, который содержит множество функций и дополнительных данных. Затем просто вызовите обработчики (для меня, скорее, некоторые статические служебные методы вместо подклассов для конкретных действий. Но я уверен, что вы можете использовать подклассы для действий, если вы тоже этого хотите), когда мы хотим что-то сделать.

Класс Affect будет содержать все сочные полезные вещи, такие как целевые типы, продолжительность, количество использований, шанс выполнения и так далее, и так далее.

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

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

Таким образом, она имеет высокую производительность первой системы и все еще очень сложна, как вторая (но не так много). В Java, по крайней мере, вы можете сделать несколько хитрых вещей, чтобы получить производительность почти первой в большинстве случаев (например, наличие карты enum ( http://docs.oracle.com/javase/6/docs/api/java) /util/EnumMap.html ) с Enums в качестве ключей и ArrayList воздействий в качестве значений. Это позволяет увидеть, есть ли у вас быстрые эффекты [поскольку список будет равен 0, или на карте не будет перечисления], и не иметь постоянно перебирать списки аффектов игрока без всякой причины. Я не возражаю перебирать аффекты, если они нам нужны в данный момент. Я оптимизирую позже, если это станет проблемой).

В настоящее время я заново открываю (переписываю игру на Java вместо базы кода FastROM, в которой она была изначально), мой MUD, который закончился в 2005 году, и недавно я столкнулся с тем, как я хочу реализовать свою систему баффов? Я собираюсь использовать эту систему, потому что она хорошо работала в моей предыдущей неудачной игре.

Что ж, надеюсь, кто-нибудь где-нибудь найдет некоторые из этих идей полезными.


6

Различный класс (или адресуемая функция) для каждого баффа не является избыточным, если поведение этих баффов отличается друг от друга. Одно было бы иметь баффы + 10% или + 20% (что, конечно, было бы лучше представить в виде двух объектов одного класса), другое - реализовывать совершенно разные эффекты, которые в любом случае требовали бы пользовательского кода. Тем не менее, я считаю, что лучше иметь стандартные способы настройки игровой логики, а не позволять каждому баффу делать то, что ему нравится (и, возможно, мешать друг другу непредвиденными способами, нарушая баланс игры).

Я бы предложил разделить каждый «цикл атаки» на этапы, где каждый шаг имеет базовое значение, упорядоченный список модификаций, которые могут быть применены к этому значению (возможно, ограничен), и окончательный предел. Каждая модификация имеет преобразование идентичности по умолчанию и может зависеть от нуля или более баффов / дебаффов. Специфика каждой модификации будет зависеть от примененного шага. Как реализовать этот цикл, зависит от вас (включая опцию архитектуры, управляемой событиями, как вы уже обсуждали).

Одним из примеров цикла атаки может быть:

  • рассчитать атаку игрока (база + моды);
  • рассчитать защиту противника (база + моды);
  • сделать разницу (и применить моды) и определить базовый урон;
  • рассчитывать любые эффекты парирования / брони (моды на базовый урон) и наносить урон;
  • рассчитать любой эффект отдачи (моды на базовый урон) и применить к атакующему.

Важно отметить, что чем раньше в цикле применяется бафф, тем больший эффект он будет иметь в результате . Поэтому, если вы хотите более «тактический» бой (где умение игрока важнее уровня персонажа), создайте много баффов / дебаффов на базовых характеристиках. Если вы хотите более «сбалансированный» бой (где уровень важнее - важно в MMOG-играх ограничивать скорость прогресса), используйте баффы / дебаффы только в конце цикла.

Различие между «Модификациями» и «Баффами», о которых я упоминал ранее, имеет цель: решения о правилах и балансе могут быть применены к первым, поэтому любые изменения в них не должны отражаться в изменениях в каждом классе последних. OTOH, количество и виды положительных эффектов ограничены только вашим воображением, поскольку каждый из них может выражать желаемое поведение, не принимая во внимание любое возможное взаимодействие между ними и другими (или даже вообще существование других).

Итак, отвечая на вопрос: не создавайте класс для каждого Баффа, а по одному для каждой (типа) Модификации, и привязывайте Модификацию к циклу атаки, а не к персонажу. Баффы могут быть просто списком (модификация, ключ, значение) кортежей, и вы можете применить бафф к персонажу, просто добавив / удалив его в набор баффов персонажа. Это также уменьшает окно для ошибки, так как статистика персонажа вообще не должна изменяться при применении баффов (так что меньше риск восстановить статистику до неправильного значения после истечения баффа).


Это интересный подход, потому что он находится где-то посередине между двумя реализациями, которые я рассмотрел, то есть либо просто ограничивает положительные эффекты довольно простыми модификаторами урона от stat и result, либо создает очень надежную, но с большими накладными расходами систему, которая может справиться с любыми задачами. Это своего рода расширение первого, позволяющее «шипам» поддерживать простой интерфейс. Хотя я не думаю, что это волшебная пуля для того, что мне нужно, но, безусловно, похоже, что она делает балансировку намного проще, чем другие подходы, так что это может быть путь. Спасибо за ваш вклад!
gkimsey

3

Я не знаю, читаете ли вы это по-прежнему, но вот как я это делаю сейчас (код основан на UE4 и C ++). Обдумав проблему в течение более двух недель (!!), я наконец нашел это:

http://gamedevelopment.tutsplus.com/tutorials/using-the-composite-design-pattern-for-an-rpg-attributes-system--gamedev-243

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

В любом случае, я начал с упаковки атрибута в одну структуру:

USTRUCT(BlueprintType)
struct GAMEATTRIBUTES_API FGAAttributeBase
{
    GENERATED_USTRUCT_BODY()
public:
    UPROPERTY()
        FName AttributeName;
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float BaseValue;
    /*
        This is maxmum value of this attribute.
    */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float ClampValue;
protected:
    float BonusValue;
    //float OldCurrentValue;
    float CurrentValue;
    float ChangedValue;

    //map of modifiers.
    //It could be TArray, but map seems easier to use in this case
    //we need to keep track of added/removed effects, and see 
    //if this effect affected this attribute.
    TMap<FGAEffectHandle, FGAModifier> Modifiers;

public:

    inline float GetFinalValue(){ return BaseValue + BonusValue; };
    inline float GetCurrentValue(){ return CurrentValue; };
    void UpdateAttribute();

    void Add(float ValueIn);
    void Subtract(float ValueIn);

    //inline float GetCurrentValue()
    //{
    //  return FMath::Clamp<float>(BaseValue + BonusValue + AccumulatedBonus, 0, GetFinalValue());;
    //}

    void AddBonus(const FGAModifier& ModifiersIn, const FGAEffectHandle& Handle);
    void RemoveBonus(const FGAEffectHandle& Handle);

    void InitializeAttribute();

    void CalculateBonus();

    inline bool operator== (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName == AttributeName);
    }

    inline bool operator!= (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName != AttributeName);
    }

    inline bool IsValid() const
    {
        return !AttributeName.IsNone();
    }
    friend uint32 GetTypeHash(const FGAAttributeBase& AttributeIn)
    {
        return AttributeIn.AttributeName.GetComparisonIndex();
    }
};

Это еще не закончено, но основная идея состоит в том, что эта структура отслеживает свое внутреннее состояние. Атрибуты могут быть изменены только с помощью эффектов. Попытки изменить их напрямую небезопасны и не доступны для дизайнеров. Я предполагаю, что все, что может взаимодействовать с атрибутами - это Effect. В том числе плоские бонусы от предметов. Когда новый предмет экипирован, создается новый эффект (вместе с дескриптором), который добавляется на выделенную карту, которая обрабатывает бонусы бесконечной продолжительности (те, которые игрок должен удалить вручную). Когда применяется новый эффект, создается новый дескриптор для него (дескриптор просто int, обернутый структурой), а затем этот дескриптор передается как средство взаимодействия с этим эффектом, а также отслеживается, если эффект все еще активен. Когда эффект удален, его дескриптор транслируется на все заинтересованные объекты,

Действительно важной частью этого является TMap (TMap - хешированная карта). FGAModifier - это очень простая структура:

struct FGAModifier
{
    EGAAttributeOp AttributeMod;
    float Value;
};

Содержит тип модификации:

UENUM()
enum class EGAAttributeOp : uint8
{
    Add,
    Subtract,
    Multiply,
    Divide,
    Set,
    Precentage,

    Invalid
};

И Значение, которое является окончательным рассчитанным значением, которое мы собираемся применить к атрибуту.

Мы добавляем новый эффект, используя простую функцию, а затем вызываем:

void FGAAttributeBase::CalculateBonus()
{
    float AdditiveBonus = 0;
    auto ModIt = Modifiers.CreateConstIterator();
    for (ModIt; ModIt; ++ModIt)
    {
        switch (ModIt->Value.AttributeMod)
        {
        case EGAAttributeOp::Add:
            AdditiveBonus += ModIt->Value.Value;
                break;
            default:
                break;
        }
    }
    float OldBonus = BonusValue;
    //calculate final bonus from modifiers values.
    //we don't handle stacking here. It's checked and handled before effect is added.
    BonusValue = AdditiveBonus; 
    //this is absolute maximum (not clamped right now).
    float addValue = BonusValue - OldBonus;
    //reset to max = 200
    CurrentValue = CurrentValue + addValue;
}

Предполагается, что эта функция пересчитывает весь стек бонусов, каждый раз, когда эффект добавляется или удаляется. Функция еще не закончена (как вы можете видеть), но вы можете получить общее представление.

Моя самая большая проблема сейчас связана с обработкой атрибута Damaging / Healing (без перерасчета всего стека), я думаю, что это несколько решено, но все равно требуется больше тестов, чтобы быть на 100%.

В любом случае атрибуты определяются следующим образом (+ нереальные макросы, здесь опущены):

FGAAttributeBase Health;
FGAAttributeBase Energy;

и т.п.

Также я не уверен на 100% в обработке CurrentValue атрибута, но он должен работать. Они так и есть сейчас.

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


Спасибо за ссылку и объяснение вашей работы! Я думаю, что вы движетесь к тому, о чем я просил. Несколько вещей, которые приходят на ум, - это порядок операций (например, 3 эффекта «добавить» и 2 эффекта «умножения» для одного и того же атрибута, что должно произойти первым?), И это чисто поддержка атрибутов. Также существует понятие триггеров (например, типа «потерять 1 AP при попадании»), но это, вероятно, будет отдельным расследованием.
gkimsey

Порядок работы, в случае просто расчета бонуса атрибута, сделать несложно. Вы можете видеть здесь, что у меня есть для и переключения. Для перебора всех текущих бонусов (которые можно складывать, вычитать, умножать, делить и т. Д.), А затем просто накапливать их. Вы делаете что-то вроде BonusValue = (BonusValue * MultiplyBonus + AddBonus-SubtractBonus) / DivideBonus, или как вы хотите посмотреть это уравнение. Благодаря единой точке входа с ней легко экспериментировать. Что касается триггеров, я не писал об этом, потому что это еще одна проблема, которую я обдумываю, и я уже пробовал 3-4 (предел)
Лукаш Баран

решения, ни один из них не работал так, как я хотел (моя главная цель - чтобы они были дружелюбны к дизайнеру). Моя общая идея - использовать теги и проверять входящие эффекты на теги. Если тег совпадает, эффект может вызвать другой эффект. (тег - это простое читаемое имя, например Damage.Fire, Attack.Physical и т. д.). По сути это очень простая концепция, проблема заключается в организации данных, чтобы они были легко доступны (быстрый для поиска) и в простоте добавления новых эффектов. Вы можете проверить код здесь github.com/iniside/ActionRPGGame (GameAttributes - модуль, который вас заинтересует)
Łukasz Baran

2

Я работал над небольшой MMO, и все предметы, способности, баффы и т. Д. Имели «эффекты». Эффектом был класс, в котором были переменные для AddDefense, InstantDamage, HealHP и т. Д. Силы, предметы и т. Д. Будут обрабатывать длительность этого эффекта.

Когда вы накладываете силу или надеваете предмет, он будет применять эффект к персонажу в течение указанного времени. Тогда основная атака и т. Д. Расчеты будут учитывать примененные эффекты.

Например, у вас есть бафф, который добавляет защиту. Для этого баффа будут как минимум EffectID и Duration. При приведении его он будет применять EffectID к персонажу в течение указанной продолжительности.

Другой пример для элемента, будет иметь те же поля. Но длительность будет бесконечной или до тех пор, пока эффект не будет снят, снимая предмет с персонажа.

Этот метод позволяет перебирать список эффектов, которые в настоящее время применяются.

Надеюсь, я объяснил этот метод достаточно четко.


Насколько я понимаю из своего минимального опыта, это традиционный способ реализации статов в RPG играх. Это хорошо работает и легко понять и реализовать. Недостатком является то, что мне не остается места для таких вещей, как «шипы» или что-то более продвинутое или ситуативное. Это также исторически было причиной некоторых подвигов в ролевых играх, хотя они довольно редки, и, поскольку я делаю однопользовательскую игру, если кто-то находит эксплойт, меня это не особо волнует. Спасибо за вклад.
gkimsey

2
  1. Если вы являетесь пользователем Unity, вот с чего начать: http://www.stevegargolinski.com/armory-a-free-and-unfinished-stat-inventory-and-buffdebuff-framework-for-unity/

Я использую ScriptableOjects как баффы / заклинания / таланты

public class Spell : ScriptableObject 
{
    public SpellType SpellType = SpellType.Ability;
    public SpellTargetType SpellTargetType = SpellTargetType.SingleTarget;
    public SpellCategory SpellCategory = SpellCategory.Ability;
    public MagicSchools MagicSchool = MagicSchools.Physical;
    public CharacterClass CharacterClass = CharacterClass.None;
    public string Description = "no description available";
    public SpellDragType DragType = SpellDragType.Active; 
    public bool Active = false;
    public int TargetCount = 1;
    public float CastTime = 0;
    public uint EffectRange = 3;
    public int RequiredLevel = 1;
    public virtual void OnGUI()
    {
    }
}

используя UnityEngine; using System.Collections.Generic;

public enum BuffType {Buff, Debuff} [System.Serializable] открытый класс BuffStat {public Stat Stat = Stat.Strength; public float ModValueInPercent = 0.1f; }

public class Buff : Spell
{
    public BuffType BuffType = BuffType.Buff;
    public BuffStat[] ModStats;
    public bool PersistsThroughDeath = false;
    public int AmountPerTick = 3;
    public bool UseTickTimer = false;
    public float TickTime = 1.5f;
    [HideInInspector]
    public float Ticktimer = 0;
    public float Duration = 360; // in seconds
    public float ModifierPerStack = 1.1f;
    [HideInInspector]
    public float Timer = 0;
    public int Stack = 1;
    public int MaxStack = 1;
}

BuffModul:

using System;
using RPGCore;
using UnityEngine;

public class Buff_Modul : MonoBehaviour
{
    private Unit _unit;

    // Use this for initialization
    private void Awake()
    {
        _unit = GetComponent<Unit>();
    }

    #region BUFF MODUL

    public virtual void RUN_BUFF_MODUL()
    {
        try
        {
            foreach (var buff in _unit.Attr.Buffs)
            {
                CeckBuff(buff);
            }
        }
        catch(Exception e) {throw new Exception(e.ToString());}
    }

    #endregion BUFF MODUL

    public void ClearBuffs()
    {
        _unit.Attr.Buffs.Clear();
    }

    public void AddBuff(string buffName)
    {
        var buff = Instantiate(Resources.Load("Scriptable/Buff/" + buffName, typeof(Buff))) as Buff;
        if (buff == null) return;
        buff.name = buffName;
        buff.Timer = buff.Duration;
        _unit.Attr.Buffs.Add(buff);
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
    }

    public void RemoveBuff(Buff buff)
    {
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat]  /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
        _unit.Attr.Buffs.Remove(buff);
    }

    void CeckBuff(Buff buff)
    {
        buff.Timer -= Time.deltaTime;
        if (!_unit.IsAlive && !buff.PersistsThroughDeath)
        {
            if (buff.ModStats != null)
                foreach (var stat in buff.ModStats)
                {
                    _unit.Attr.StatsBuff[stat.Stat] = 0;
                }

            RemoveBuff(buff);
        }
        if (_unit.IsAlive && buff.Timer <= 0)
        {
            RemoveBuff(buff);
        }
    }
}

0

Это был актуальный вопрос для меня. У меня есть одна идея об этом.

  1. Как было сказано ранее, нам нужно реализовать Buffсписок и логику обновления для баффов.
  2. Затем нам нужно изменить все настройки конкретного игрока в каждом кадре в подклассах Buffкласса.
  3. Затем мы получаем текущие настройки игрока из изменяемого поля настроек.

class Player {
  settings: AllPlayerStats;

  private buffs: Array<Buff> = [];
  private baseSettings: AllPlayerStats;

  constructor(settings: AllPlayerStats) {
    this.baseSettings = settings;
    this.resetSettings();
  }

  addBuff(buff: Buff): void {
    this.buffs.push(buff);
    buff.start(this);
  }

  findBuff(predcate(buff: Buff) => boolean): Buff {...}

  removeBuff(buff: Buff): void {...}

  update(dt: number): void {
    this.resetSettings();
    this.buffs.forEach((item) => item.update(dt));
  }

  private resetSettings(): void {
    //some way to copy base to settings
    this.settings = this.baseSettings.copy();
  }
}

class Buff {
    private owner: Player;        

    start(owner: Player) { this.owner = owner; }

    update(dt: number): void {
      //here we change anything we want in subclasses like
      this.owner.settings.hp += 15;
      //if we need base value, just make owner.baseSettings public but don't change it! only read

      //also here logic for removal buff by time or something
    }
}

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


0

Я знаю, что это довольно старый, но он был связан в более новом сообщении, и у меня есть некоторые мысли по этому поводу, которыми я хотел бы поделиться. К сожалению, в данный момент у меня нет с собой заметок, поэтому я постараюсь дать общий обзор того, о чем я говорю, и я отредактирую детали и пример кода, когда они будут перед меня.

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

Что я имею в виду? Неважно, является ли что-то баффом или дебаффом, они оба являются модификаторами, которые просто влияют на что- то положительное или отрицательное. Коду не важно, что есть что. В этом отношении в конечном счете не имеет значения, добавляет ли что-то статистику или умножает ее, это просто разные операторы, и опять-таки, в коде все равно, какой именно.

Так куда я иду с этим? Создание хорошего (читай: простого, изящного) класса баффа / дебаффа не так уж и сложно, сложно разработать системы, которые рассчитывают и поддерживают состояние игры.

Если бы я проектировал систему баффов / дебаффов, вот несколько вещей, которые я бы рассмотрел:

  • Класс баффа / дебаффа для представления самого эффекта.
  • Класс типа бафф / дебафф, содержащий информацию о том, на что влияет бафф и как.
  • Персонажи, Предметы и, возможно, Местоположения должны иметь свойства list или collection, содержащие баффы и дебаффы.

Некоторые особенности того, что типы баффов / дебаффов должны содержать:

  • К кому / к чему это может быть применено, IE: игрок, монстр, локация, предмет и т. Д.
  • Какой это тип эффекта (положительный, отрицательный), мультипликативный или аддитивный, и на какой тип статистики он влияет, IE: атака, защита, движение и т. Д.
  • Когда это следует проверить (бой, время суток и т. Д.).
  • Может ли это быть удалено, и если так, как это может быть удалено.

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

Пока я поместил правильные типы на место, просто создать запись баффа, которая говорит:

  • Тип: Проклятие
  • ObjectType: Item
  • StatCategory: Утилита
  • StatActed: MovementSpeed
  • Продолжительность: Бесконечно
  • Триггер: OnEquip

И так далее, и когда я создаю бафф, я просто назначаю ему BuffType of Curse, а все остальное зависит от движка ...

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.