Який спосіб реалізувати гнучку систему "буф / налагодження"?


66

Огляд:

Безліч ігор зі статистикою RPG-подібної статистики дозволяють створювати символи "відхилень", починаючи від простого "Наносіть додатковий збиток на 25%" і закінчуючи складнішими речами, такими як "Нанесіть 15 збитків назад нападникам при попаданні".

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

Деталі:

У моєму конкретному випадку я маю декілька символів у покроковій бойовій обстановці, тому я передбачив прихильників прив'язки до таких подій, як "OnTurnStart", "OnReceiveDamage" і т. Д. Можливо, кожен баф є підкласом головного абстрактного класу Buff, де перевантажені лише відповідні події. Тоді для кожного символу може бути застосований вектор буфів.

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

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

Як відносно недосвідчений програміст на C ++ (я зазвичай використовую C у вбудованих системах), я відчуваю, що моє рішення спрощене і, ймовірно, не в повній мірі використовує об'єктно-орієнтовану мову.

Думки? Хтось тут раніше розробив досить надійну баф-систему?

Редагувати: Щодо відповідей (и):

Я вибрав відповідь насамперед на основі доброї деталізації та ґрунтовної відповіді на запитання, яке я задав, але читання відповідей дало мені ще трохи розуміння.

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

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

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

У будь-якому випадку, я все ще сподіваюсь, що хтось прийде разом із фантазійною магічною кулею "OO", яка дозволить мені застосувати відстань переміщення +2 за поворот на поворот, завдавши 50% шкоди, завданої назад нападнику , і автоматично переміщуватися до сусідньої плитці при атаці з 3 або більше плиток геть любителя в одній системі , не повертаючи +5 сили Баффі в свій власний підклас.

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


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

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

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

Правильно, я очікував, що кожен шаблон події для абстрактного класу Buff включає такі відповідні параметри. Це, безумовно, спрацює, але я вагаюся, бо відчуваю, що він не буде масштабуватись добре. Мені важко уявити MMORPG з декількома сотнями різних прихильників, має окремий клас, визначений для кожного буфера, вибираючи зі ста різних подій. Справа не в тому, що я роблю так багато прихильників (можливо, ближче до 30), але якщо є простіша, більш елегантна або більш гнучка система, я б хотів її використовувати. Більш гнучка система = цікавіші помилки / здібності.
gkimsey

4
Це не є гарною відповіддю на проблему взаємодії, але мені здається, що декоративний малюнок тут добре застосовується; просто нанесіть більше бізонів (декораторів) один на одного. Можливо, із системою для управління взаємодією шляхом "злиття" бафів разом (наприклад, 10x 25% зливаються в один 100% buff).
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 на Effect і передайте значення UNDEAD лише get_current_attribute_value()тоді, коли ви обчислюєте рулон пошкоджень проти нежить.

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

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

# 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 матиме обробник подій для кожного типу подій, і ви можете призначити функції обробника стільки, скільки вам потрібно в кожному конкретному випадку. Ефект підкласу не потрібно, оскільки кожен визначається складом модифікаторів атрибутів та обробниками подій, які він містить. (Напевно, він також буде містити ім'я, тривалість тощо)


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

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

22

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

Ідея була простою, і поки ми застосовували її в Python, вона була досить ефективною.

В основному, ось як це пішло:

  • Користувач мав список поточно застосованих бафів і налагоджень (зауважте, що буф і налагодження відносно однакові, просто ефект має різний результат)
  • Буфери мають різні атрибути, такі як тривалість, ім'я та текст для відображення інформації та час життя. Найважливішими є живий час, тривалість і посилання на актора, до якого цей баф застосовується.
  • Для Buff, коли він прикріплений до гравця через player.apply (buff / debuff), він би викликав метод start (), це застосує критичні зміни до гравця, такі як збільшення швидкості або уповільнення.
  • Потім ми переглянемо кожен баф у циклі оновлення, і бафи оновляться, це збільшить час їх життя. Підкласи можуть реалізовувати такі речі, як отруєння гравця, надання гравцеві HP з часом тощо.
  • Коли баф було зроблено протягом, тобто timeAlive> = тривалість, логіка оновлення видалить баф і викликає метод закінчення (), який може відрізнятися від зняття обмежень швидкості на гравці до спричинення невеликого радіусу (подумайте про ефект бомби після DoT)

Тепер, як насправді застосовувати мешканців світу - це вже інша історія. Ось моя їжа для роздумів.


1
Це звучить як краще пояснення того, що я намагався описати вище. Це порівняно просто, зрозуміло легко зрозуміти. Ви, по суті, згадали про три "події" там (OnApply, OnTimeTick, OnExpired), щоб ще більше пов'язати це з моїм мисленням. Так і є, він не підтримує такі речі, як повернення шкоди при попаданні тощо, але це робить масштаб краще для великої кількості потвор. Я вважаю за краще не обмежувати, що можуть робити мої помилки (які = обмеження кількості подій, які я придуму, повинні називатися основною логікою гри), але масштабність бафу може бути важливішою. Дякуємо за ваш внесок!
gkimsey

Так, ми нічого подібного не реалізували. Це звучить по-справжньому акуратно і чудовою концепцією (на кшталт хижість терна).
Росс

@gkimsey Для таких речей, як Торни та інші пасивні похибки, я застосував би логіку у вашому класі Mob як пасивну статистику, подібну шкоді чи здоров'ю, і збільшив би цю статистику при застосуванні баффа. Це значно спрощує випадок, коли у вас є кілька відхилень шипів, а також підтримка інтерфейсу в чистоті (10 бафів показували б 1 пошкодження повернення, а не 10), а система баффа залишається простою.
3Doubloons

Це майже контрінтуїтивно простий підхід, але я почав думати про себе, граючи в Diablo 3. Я помітив, що вкрали життя, постраждали від життя, пошкодження нападників ближнього бою тощо були всі власні статистичні дані у вікні персонажа. Зрозуміло, D3 не має найскладнішої системи буферизації або взаємодій у світі, але навряд чи банальна. Це має багато сенсу. Тим не менш, є потенційно 15 різних прихильників з 12 різними ефектами, які могли б потрапити в це. Здається, дивним
прошивкою з

11

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

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


Статичні модифікатори

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

Я ніколи не кешував створені значення для цього типу системи. Наприклад, якби я хотів показати максимальне здоров'я чогось, я б генерував значення на місці. Це не дало речей схильності до помилок і просто простіше зрозуміти всім, хто бере участь.

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

Загалом, це дуже добре працює з просто вирівняними статичними модифікаторами. Хоча код повинен існувати у відповідних місцях для модифікаторів, які будуть використовуватися: getAttack, getMaxHP, getMeleeDamage тощо, тощо.

Якщо цей метод не вдається (для мене), це дуже складна взаємодія між любителями. Немає справжнього простого способу взаємодії, за винятком того, як трохи його гетто. У нього є кілька простих можливостей взаємодії. Для цього потрібно внести зміни в спосіб зберігання статичних модифікаторів. Замість використання enum в якості ключа ви використовуєте String. Ця рядок буде ім'ям Enum + додатковою змінною. У 9 разів з 10 додаткова змінна не використовується, тому ви все одно зберігаєте ім'я enum як ключ.

Зробимо короткий приклад: Якщо ви хотіли б змінити збитки від неживих істот, у вас може бути впорядкована пара на зразок цього: (DAMAGE_Undead, 10) УРОК - це Enum, а Undead - додаткова змінна. Тож під час бою ви можете зробити щось на кшталт:

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

У будь-якому випадку, це працює досить добре і швидко. Але це не вдається при складних взаємодіях і наявності "спеціального" коду скрізь. Наприклад, розглянемо ситуацію "25% шансу телепортувати смерть". Це "досить" складний. Вищеописана система може впоратися з нею, але не легко, оскільки вам потрібно наступне:

  1. Визначте, чи є у гравця цей мод.
  2. Десь є якийсь код для виконання телепортації, якщо це вдасться. Розташування цього коду - дискусія сама по собі!
  3. Отримати потрібні дані на карті Mod. Що означає значення? Це кімната, де вони також телепортуються? Що робити, якщо у гравця є два модники телепорту ?? Чи не додаватимуться суми разом ?????? ЗБУД!

Отже, це приводить мене до наступного:


Кінцева складна система баффу

Я колись спробував написати 2D MMORPG власноруч. Це була жахлива помилка, але я багато чому навчився!

Я переписав афектну систему 3 рази. Перший використовував менш потужну варіацію вищезазначеного. Другий був про що я буду говорити.

Ця система мала ряд класів для кожної модифікації, тому такі речі, як: ChangeHP, ChangeMaxHP, ChangeHPByPercent, ChangeMaxByPercent. У мене було мільйон цих хлопців - навіть такі речі, як TeleportOnDeath.

На моїх заняттях було зроблено наступне:

  • застосовувати вплив
  • removeAffect
  • checkForInteraction <--- важливо

Застосовуйте та видаляйте пояснення себе (хоча для таких речей, як відсоток, афект буде відслідковувати, наскільки він збільшив HP, щоб забезпечити, коли афект знищився, він би видалив лише додану суму. Це було баггі, лол та ін. мені знадобилося багато часу, щоб переконатися, що це правильно. Я все ще не мав хорошого почуття з цього приводу.).

Метод checkForInteraction був жахливо складним фрагментом коду. У кожному з класів афектів (тобто: ChangeHP), він повинен мати код, щоб визначити, чи слід це змінювати за допомогою вхідного афекту. Так, наприклад, якщо у вас було щось на кшталт…

  • Buff 1: Наносить 10 вогневих збитків під час нападу
  • Buff 2: Збільшує всю шкоду від пожежі на 25%.
  • Buff 3: Збільшує всю шкоду від пожежі на 15.

Метод checkForInteraction обробляє всі ці впливи. Для цього потрібно перевірити кожен вплив на ВСІХ гравців, які знаходяться поруч !! Це тому, що тип афектів я мав справу з кількома гравцями протягом певного проміжку площі. Це означає, що за кодом НІКОЛИ НЕ БУДЬ будь-яких спеціальних тверджень, як вище - "якщо ми щойно померли, нам слід перевірити наявність телепорту на смерть". Ця система автоматично обробляла б її правильно в потрібний час.

Спроба написати цю систему мені зайняла як 2 місяці, і кілька разів зробила голову вибухом. ЯКЩО це було ДІЙСНО потужно і могло зробити божевільну кількість речей - особливо якщо взяти до уваги наступні два факти щодо здібностей у моїй грі: 1. Вони мали цільові діапазони (тобто: одиночний, самостійна, лише група, PB AE self , Ціль PE AE, націлена на AE тощо). 2. Здібності можуть впливати на них більше ніж 1.

Як я вже згадував вище, це була 2-я система 3-го афекту для цієї гри. Чому я відійшов від цього?

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

Тож ми підійшли до моєї третьої версії (та іншого типу буф-системи):


Комплексний клас афектів з обробниками

Таким чином, це в значній мірі поєднання перших двох: ми можемо мати статичні змінні в класі Affect, що містить багато функціональності та додаткових даних. Тоді просто зателефонуйте обробникам (для мене, як правило, деякі статичні корисні методи замість підкласів для конкретних дій. Але я впевнений, що ви можете піти з підкласами для дій, якщо ви хочете теж), коли ми хочемо щось зробити.

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

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

Взаємодія може бути здійснена, якщо ви теж хочете. Було б просто написати код, щоб шукати конкретні бафи на програвачах / тощо. Оскільки він має хороші показники (див. Нижче), це слід досить ефективно робити. Просто знадобляться складніші обробники тощо.

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

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

Ну, сподіваємось, хтось десь знайде кілька цих поглядів корисними.


6

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

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

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

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

Важливо зауважити, що чим раніше буде застосовано баф, тим більше ефекту він матиме на результат . Отже, якщо ви хочете більш "тактичного" бою (де навичка гравця важливіша за рівень персонажа), створіть багато бафів / налагоджувачів на базовій статистиці. Якщо ви хочете більш "врівноваженого" бою (де рівень важливіший - важливо в MMOG, щоб обмежити швидкість прогресу), використовуйте лише бафи / налагодження пізніше в циклі.

Відмінність між "Модифікаціями" та "Баффами", про які я згадував раніше, має мету: рішення щодо правил та балансу можуть бути здійснені за першим, тому будь-які зміни щодо них не потрібно відображати в змінах до кожного класу останнього. ОТОХ, кількість та види прихильників обмежені лише вашою уявою, оскільки кожен з них може висловити бажану поведінку, не враховуючи жодної можливої ​​взаємодії між ними та іншими (або взагалі навіть існуванням інших).

Отже, відповідаючи на запитання: не створюйте клас для кожного Buff, а один для кожної (типу) модифікації, а прив'язуйте модифікацію до циклу атаки, а не до символу. Буффи можуть бути просто списком (модифікація, ключ, значення) кортежів, і ви можете застосувати баф до символу, просто додавши / видаливши його до набору символів. Це також зменшує вікно для помилок, оскільки статистику персонажа не потрібно змінювати взагалі при застосуванні бафів (тому менше ризику відновити статистику до неправильного значення після закінчення терміну закінчення).


Це цікавий підхід, оскільки він потрапляє десь між двома розглянутими нами реалізаціями - тобто або просто обмежуючи буфери досить простими модифікаторами статистики і призводять до пошкоджень, або робить дуже надійною, але високонадійною системою, яка може працювати з чим завгодно. Це свого роду розширення колишнього, щоб дозволити "шипи", зберігаючи простий інтерфейс. Хоча я не думаю, що це чарівна куля для того, що мені потрібно, але, безумовно, схоже, що це робить балансування набагато простіше, ніж інші підходи, тому це може бути шлях. Дякуємо за ваш внесок!
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();
    }
};

Це ще не закінчено, але основна ідея полягає в тому, щоб ця структура відстежувала свій внутрішній стан. Атрибути можна змінювати лише за допомогою ефектів. Намагання змінити їх безпосередньо небезпечно і не піддається дизайнерам. Я припускаю, що все, що може взаємодіяти з атрибутами - це ефект. У тому числі плоскі бонуси від предметів. Коли новий елемент оснащений, створюється новий ефект (разом з ручкою), і він додається до спеціальної карти, яка обробляє бонуси безмежної тривалості (ті, які гравцеві потрібно видалити вручну). Коли застосовується новий ефект, створюється нова ручка для нього (ручка просто 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%.

У будь-якому випадку Atttributes визначаються так (+ Нереальні макроси, опущені тут):

FGAAttributeBase Health;
FGAAttributeBase Energy;

тощо.

Також я не на 100% впевнений в обробці атрибута CurrentValue, але це має працювати. Вони так, як зараз.

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


Дякуємо за посилання та пояснення вашої роботи! Я думаю, що ти рухаєшся по суті до того, про що я просив. Деякі речі, які спадають на думку, - це порядок операцій (наприклад, 3 "додавання" ефекту та 2 "множення" ефектів на той самий атрибут, що має відбутися спочатку?), І це суто підтримка атрибутів. Існує також поняття тригерів (наприклад, "втратити 1 AP при попаданні" типу ефектів), але це, ймовірно, буде окремим розслідуванням.
gkimsey

Порядок роботи, у випадку лише обчислення бонусу атрибуту, легко зробити. Ви можете бачити тут, що я там і перемикаюся. Повторіть усі поточні бонуси (які можна додавати, віднімати, множити, ділити тощо), а потім просто накопичуйте їх. Ви робите щось на кшталт BonusValue = (BonusValue * MultiplyBonus + AddBonus-SubtractBonus) / DivideBonus, або як би ви хочете подивитися це рівняння. Через єдину точку входу експериментувати з цим легко. Щодо тригерів, я не писав про це, тому що це ще одна проблема, над якою я розмірковую, і я вже спробував 3-4 (ліміт)
Łukasz Baran

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

2

Я працював над невеликим MMO, і всі предмети, повноваження, помилки тощо мали «наслідки». Ефектом був клас, у якому були змінні для 'AddDefense', 'InstantDamage', 'HealHP' і т. Д. Повноваження, елементи тощо будуть обробляти тривалість цього ефекту.

Коли ви передаєте силу або ставите на предмет, він буде застосовувати ефект до символу протягом зазначеної тривалості. Тоді в основній атаці тощо розрахунки враховували б застосовані ефекти.

Наприклад, у вас є баф, який додає захист. Існує мінімум ефектуID та тривалості для цієї групи. При передачі його буде застосовано EffectID до символу протягом зазначеної тривалості.

Інший приклад для елемента, матиме ті самі поля. Але тривалість буде нескінченною або до тих пір, поки ефект не буде усунутий, знявши елемент із символу.

Цей метод дозволяє перебирати список застосованих на даний момент ефектів.

Сподіваюся, я пояснив цей метод досить чітко.


Як я розумію це зі своїм мінімальним досвідом, це традиційний спосіб впроваджувати статичні моди в ігри RPG. Він добре працює і його легко зрозуміти та реалізувати. Мінус полягає в тому, що, здається, не залишається мені жодного місця робити такі речі, як "шипи", або щось більш розвинене або ситуативне. Це також історично було причиною деяких подвигів у RPG, хоча вони досить рідкісні, і оскільки я роблю одиночну гру, якщо хтось знайде подвиг, мене це не дуже хвилює. Дякуємо за вклад.
gkimsey

2
  1. Якщо ви є користувачем єдності, ось що для початку: 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; використовуючи System.Collections.Generic;

public enum BuffType {Buff, Debuff} [System.Serializable] public class BuffStat {public Stat Stat = Stat.Strength; публічний поплавок 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

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

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

Що я маю на увазі? Насправді не важливо, чи є щось баф або налагодження, вони обидва модифікатори, які просто впливають на щось позитивно чи негативно. Коду не важливо, який це. З цього приводу не має значення, чи додає щось статистика чи множує їх, це просто різні оператори, і знову ж таки код не має значення, який є.

То куди я йду з цим? Таке конструювання хорошого (читання: простого, елегантного) класу "buff / debuff" не все так складно. Найважчим є проектування систем, які обчислюють та підтримують ігровий стан.

Якби я розробляв систему "buff / debuff", я б врахував деякі речі:

  • Клас buff / debuff для представлення самого ефекту.
  • Клас типу "buff" / "debuff", який містить інформацію про те, на що впливає баф та як.
  • Персонажі, елементи та, можливо, місця розташування повинні мати список властивостей списку чи колекції, щоб містити помилки та налагодження.

Деякі особливості типів баф / налагодження повинні містити:

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

Це лише початок, але звідти ви просто визначаєте, чого хочете, і дієте на нього, використовуючи звичайний ігровий стан. Наприклад, скажіть, що ви хочете створити проклятий предмет, який зменшує швидкість руху ...

Поки я поставив належні типи на місце, просто створити баф-запис, який говорить:

  • Тип: Прокляття
  • Тип об’єкта: елемент
  • StatCategory: Утиліта
  • StatAffected: MovementSpeed
  • Тривалість: Нескінченна
  • Тригер: OnEquip

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

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