Jak wdrożyć elastyczny system buff / debuff?

66

Przegląd:

Wiele gier ze statystykami podobnymi do RPG pozwala na „wzmocnienie” postaci, od prostego „Zadaj dodatkowe 25% obrażeń” do bardziej skomplikowanych rzeczy, takich jak „Zadaj 15 obrażeń atakującym z powrotem po trafieniu”.

Specyfika każdego typu wzmocnienia nie jest tak naprawdę istotna. Szukam (prawdopodobnie zorientowanego obiektowo) sposobu obsługi dowolnych wzmocnień.

Detale:

W moim szczególnym przypadku mam wiele postaci w turowym środowisku bitewnym, więc wyobrażałem sobie wzmocnienia związane z wydarzeniami takimi jak „OnTurnStart”, „OnReceiveDamage” itp. Być może każde wzmocnienie jest podklasą głównej abstrakcyjnej klasy Buff, gdzie przeciążone są tylko odpowiednie zdarzenia. Wówczas każda postać może mieć aktualnie zastosowany wektor wzmocnień.

Czy to rozwiązanie ma sens? Z pewnością widzę, że potrzebne są dziesiątki typów zdarzeń, wydaje się, że utworzenie nowej podklasy dla każdego wzmocnienia jest przesadą i nie wydaje się, aby pozwalało to na jakiekolwiek „interakcje” wzmocnienia. Oznacza to, że gdybym chciał wprowadzić ograniczenie doładowań, nawet jeśli miałbyś 10 różnych wzmocnień, z których wszystkie dają dodatkowe 25% obrażeń, robisz tylko 100% więcej zamiast 250% dodatkowych.

Są też bardziej skomplikowane sytuacje, które idealnie mógłbym kontrolować. Jestem pewien, że każdy może wymyślić przykłady, w jaki sposób bardziej wyrafinowani buffy mogą potencjalnie ze sobą współdziałać w sposób, którego jako twórca gier nie chcę.

Jako stosunkowo niedoświadczony programista C ++ (generalnie używałem C w systemach wbudowanych), uważam, że moje rozwiązanie jest uproszczone i prawdopodobnie nie w pełni wykorzystuje język obiektowy.

Myśli? Czy ktoś tutaj wcześniej zaprojektował dość solidny system wzmocnień?

Edycja: w odniesieniu do odpowiedzi:

Wybrałem odpowiedź opartą przede wszystkim na szczegółach i solidnej odpowiedzi na zadane pytanie, ale czytanie odpowiedzi dało mi więcej wglądu.

Być może nic dziwnego, że różne systemy lub ulepszone systemy wydają się lepiej stosować w pewnych sytuacjach. To, który system będzie najlepszy dla mojej gry, będzie zależeć od typów, wariancji i liczby wzmocnień, które zamierzam móc zastosować.

W przypadku gry takiej jak Diablo 3 (wymienionej poniżej), w której prawie każdy element wyposażenia może zmienić siłę wzmocnienia, wzmocnienia to system statystyk postaci, gdy tylko jest to możliwe, wydaje się dobrym pomysłem.

W sytuacji turowej, w której się znajduję, bardziej odpowiednie może być podejście oparte na zdarzeniu.

W każdym razie nadal mam nadzieję, że ktoś przyjdzie wraz z fantazyjną magiczną kulą „OO”, która pozwoli mi zastosować dystans +2 do ruchu na turę , zadać 50% obrażeń z powrotem do wzmocnienia atakującego oraz automatycznie przeniesie się do pobliskiego dachówki gdy zaatakowany z 3 lub więcej płytek z dala buff w jednym systemie bez konieczności włączania do +5 siła buff do własnej podklasy.

Myślę, że najbliższą rzeczą jest odpowiedź, którą zaznaczyłem, ale podłoga jest nadal otwarta. Dziękujemy wszystkim za wkład.

gkimsey
źródło
Nie zamieszczam tego jako odpowiedzi, ponieważ po prostu burzę mózgów, ale co z listą wzmocnień? Każde wzmocnienie ma stałą i modyfikator współczynnika. Stała byłaby równa +10 obrażeń, współczynnik wynosiłaby 1,10 dla wzmocnienia + 10% obrażeń. W swoich obliczeniach obrażeń iterujesz wszystkie wzmocnienia, aby uzyskać całkowity modyfikator, a następnie nakładasz dowolne ograniczenia. Zrobiłbyś to dla dowolnego modyfikowalnego atrybutu. Potrzebna byłaby jednak specjalna metoda przypadku skomplikowanych rzeczy.
William Mariager
Nawiasem mówiąc, zaimplementowałem już coś takiego dla mojego obiektu Stats, kiedy tworzyłem system dla broni i akcesoriów, które można wyposażać. Jak powiedziałeś, jest to dość przyzwoite rozwiązanie dla wzmocnień, które modyfikują tylko istniejące atrybuty, ale oczywiście nawet wtedy chcę, aby niektóre wzmocnienia wygasały po X turach, inne wygasają, gdy efekt pojawi się Y razy itp. Nie zrobiłem tego wspomnij o tym w głównym pytaniu, ponieważ robiło się już naprawdę długo.
gkimsey
1
jeśli masz metodę „onReceiveDamage”, która jest wywoływana przez system przesyłania wiadomości, ręcznie lub w inny sposób, powinno być wystarczająco łatwe podanie odniesienia do tego, od kogo / od czego otrzymujesz obrażenia. Więc możesz udostępnić tę informację swojemu
Tak, spodziewałem się, że każdy szablon zdarzenia dla abstrakcyjnej klasy Buff będzie zawierał odpowiednie parametry tego typu. To z pewnością zadziałałoby, ale waham się, ponieważ wydaje mi się, że nie będzie dobrze skalować. Trudno mi sobie wyobrazić, że MMORPG z setkami różnych wzmocnień ma osobną klasę zdefiniowaną dla każdego wzmocnienia, wybierając spośród setki różnych zdarzeń. Nie dlatego, że robię tyle wzmocnień (prawdopodobnie bliżej 30), ale jeśli istnieje prostszy, bardziej elegancki lub bardziej elastyczny system, chciałbym go użyć. Bardziej elastyczny system = więcej interesujących wzmocnień / umiejętności.
gkimsey
4
To nie jest dobra odpowiedź na problem interakcji, ale wydaje mi się, że wzór dekoratora ma tutaj zastosowanie; po prostu nakładaj na siebie więcej wzmocnień (dekoratorów). Może z systemem do obsługi interakcji poprzez „łączenie” wzmocnień razem (np. 10x 25% łączy się w jeden 100% wzmocnienia).
ashes999

Odpowiedzi:

32

Jest to skomplikowany problem, ponieważ mówisz o kilku różnych rzeczach, które (w dzisiejszych czasach) są skupione jako „wzmocnienia”:

  • modyfikatory atrybutów gracza
  • efekty specjalne, które mają miejsce podczas niektórych wydarzeń
  • kombinacje powyższych.

Zawsze implementuję pierwszy z listą aktywnych efektów dla określonej postaci. Usunięcie z listy, niezależnie od tego, czy jest oparte na czasie trwania, czy też jest jawne, jest dość trywialne, więc nie omówię tego tutaj. Każdy efekt zawiera listę modyfikatorów atrybutów i może zastosować ją do wartości bazowej poprzez proste pomnożenie.

Następnie owijam go funkcjami, aby uzyskać dostęp do zmodyfikowanych atrybutów. na przykład.:

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

Pozwala to na łatwe stosowanie efektów multiplikatywnych. Jeśli potrzebujesz również efektów addytywnych, zdecyduj, w jakiej kolejności zamierzasz je zastosować (prawdopodobnie dodatek jest ostatni) i dwukrotnie przejrzyj listę. (Prawdopodobnie miałbym osobne listy modyfikatorów w Effect, jedną dla multiplikatywnej, drugą dla addytywnego).

Wartość kryterium pozwala wdrożyć „+ 20% w stosunku do Nieumarłych” - ustaw wartość UNDEAD na Efekt i przekaż wartość UNDEAD tylko get_current_attribute_value()wtedy, gdy obliczasz rzut obrażeń przeciwko nieumarłemu wrogowi.

Nawiasem mówiąc, nie kusiłoby mnie, aby napisać system, który stosuje i nie stosuje wartości bezpośrednio do podstawowej wartości atrybutu - końcowy rezultat jest taki, że twoje atrybuty prawdopodobnie odchodzą od zamierzonej wartości z powodu błędu. (np. jeśli pomnożysz coś przez 2, ale potem dokonasz tego, gdy ponownie podzielisz przez 2, będzie niższy niż początkowo.)

Jeśli chodzi o efekty oparte na zdarzeniach, takie jak „Zadaj atakującym 15 obrażeń z powrotem po trafieniu”, możesz do tego dodać metody w klasie Efekt. Ale jeśli chcesz wyraźnego i arbitralnego zachowania (np. Niektóre efekty dla powyższego zdarzenia mogą odzwierciedlać obrażenia z powrotem, niektóre mogą cię wyleczyć, może cię teleportować losowo, cokolwiek) będziesz potrzebował niestandardowych funkcji lub klas, aby sobie z tym poradzić. Możesz przypisać funkcje do obsługi zdarzeń dla efektu, a następnie możesz po prostu wywołać funkcje obsługi zdarzeń dla dowolnych aktywnych efektów.

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

Oczywiście twoja klasa Effect będzie miała moduł obsługi zdarzeń dla każdego rodzaju zdarzenia i możesz przypisać funkcje modułu obsługi do tylu, ile potrzebujesz w każdym przypadku. Nie musisz podklasować efektu, ponieważ każdy z nich jest zdefiniowany przez skład modyfikatorów atrybutów i procedur obsługi zdarzeń, które zawiera. (Prawdopodobnie będzie również zawierać nazwę, czas trwania itp.)

Kylotan
źródło
2
+1 za doskonałe szczegóły. Jest to najbliższa odpowiedź na oficjalne udzielenie odpowiedzi na moje pytanie, jakie widziałem. Podstawowa konfiguracja tutaj wydaje się zapewniać dużą elastyczność i niewielką abstrakcję tego, co w innym przypadku mogłoby być nieporządną logiką gry. Jak już powiedziałeś, bardziej funkowe efekty nadal potrzebowałyby własnych klas, ale myślę, że to zaspokaja większość potrzeb typowego systemu „buffa”.
gkimsey
+1 za wskazanie ukrytych tutaj różnic koncepcyjnych. Nie wszystkie z nich będą działać z tą samą logiką aktualizacji opartą na zdarzeniach. Zobacz odpowiedź @ Ross na zupełnie inną aplikację. Oba będą musiały istnieć obok siebie.
ctietze
22

W grze, nad którą pracowałem z koleżanką dla klasy, stworzyliśmy system wzmocnień / debuffów, gdy użytkownik zostaje uwięziony w wysokiej trawie i płytkach przyspieszających, a co nie, a także niektóre drobne rzeczy, takie jak krwawienia i trucizny.

Pomysł był prosty i chociaż zastosowaliśmy go w Pythonie, był raczej skuteczny.

Zasadniczo oto, jak poszło:

  • Użytkownik miał listę aktualnie stosowanych wzmocnień i osłabień (zauważ, że wzmocnienie i osłabienie są względnie takie same, to tylko efekt, który ma inny wynik)
  • Premie mają różne atrybuty, takie jak czas trwania, nazwa i tekst do wyświetlania informacji oraz czas życia. Ważne są czas życia, czas trwania i odniesienie do aktora, którego dotyczy to wzmocnienie.
  • W przypadku wzmocnienia, gdy jest ono dołączone do odtwarzacza za pośrednictwem player.apply (buff / debuff), wywołałoby metodę start (), zastosowałoby to krytyczne zmiany w odtwarzaczu, takie jak zwiększenie prędkości lub spowolnienie.
  • Następnie powtarzalibyśmy każdy buff w pętli aktualizacji, a buffy aktualizowałyby się, co wydłużyłoby ich czas życia. Podklasy wprowadzałyby takie rzeczy, jak zatruwanie gracza, dawanie mu HP z czasem itp.
  • Gdy wzmocnienie zostanie wykonane, co oznacza timeAlive> = czas trwania, logika aktualizacji usunie wzmocnienie i wywoła metodę finish (), która będzie się różnić od usunięcia ograniczeń prędkości gracza do spowodowania małego promienia (pomyśl o efekcie bombowym po DoT)

Teraz, jak faktycznie zastosować wzmocnienia ze świata, to inna historia. Oto moje jedzenie do przemyślenia.

Ross
źródło
1
To brzmi jak lepsze wyjaśnienie tego, co próbowałem opisać powyżej. Jest stosunkowo prosty, z pewnością łatwy do zrozumienia. Wspomniałeś zasadniczo o trzech „zdarzeniach” tam (OnApply, OnTimeTick, OnExpired), aby dalej powiązać je z moim myśleniem. W tej chwili nie wspierałby takich rzeczy, jak powrót obrażeń po trafieniu i tak dalej, ale skaluje się lepiej dla wielu wzmocnień. Wolałbym nie ograniczać możliwości moich wzmocnień (co = ograniczanie liczby zdarzeń, które wymyślam, które muszą być wywoływane przez główną logikę gry), ale skalowalność wzmocnienia może być ważniejsza. Dzięki za wkład!
gkimsey
Tak, nie wdrożyliśmy czegoś takiego. Brzmi naprawdę schludnie i jest świetnym konceptem (trochę jak wzmocnienie cierni).
Ross
@gkimsey W przypadku takich rzeczy, jak Ciernie i inne pasywne wzmocnienia, zaimplementowałbym logikę w twojej klasie Mobów jako statystykę pasywną podobną do obrażeń lub zdrowia i zwiększyłem tę statystykę podczas stosowania wzmocnienia. Upraszcza to dużo w przypadku, gdy masz wiele ciernie buffy, jak również utrzymanie interfejs czyste (10 buffy pokaże 1 obrażenie zwrotny zamiast 10) i pozwala system buff pozostają proste.
3Doubloons
Jest to prawie sprzeczne z intuicją podejście, ale zacząłem myśleć o sobie, grając w Diablo 3. Zauważyłem, że kradzież życia, życie przy trafieniu, obrażenia atakujących w zwarciu itp. Były własnymi statystykami w oknie postaci. To prawda, że ​​D3 nie ma najbardziej skomplikowanego systemu buforowania ani interakcji na świecie, ale nie jest to wcale banalne. To ma sens. Mimo to istnieje potencjalnie 15 różnych wzmocnień z 12 różnymi efektami. Wydaje się dziwne wypełnianie arkusza statystyk postaci ....
gkimsey,
11

Nie jestem pewien, czy nadal to czytasz, ale od dłuższego czasu zmagam się z tego rodzaju problemami.

Zaprojektowałem wiele różnych rodzajów systemów afektu. Omówię je teraz krótko. Wszystko opiera się na moim doświadczeniu. Nie twierdzę, że znam wszystkie odpowiedzi.


Modyfikatory statyczne

Ten typ systemu opiera się głównie na prostych liczbach całkowitych w celu określenia jakichkolwiek modyfikacji. Na przykład +100 do Max HP, +10 do ataku i tak dalej. Ten system może również obsługiwać procenty. Musisz tylko upewnić się, że układanie nie wymknie się spod kontroli.

Tak naprawdę nigdy nie buforowałem wygenerowanych wartości dla tego typu systemu. Na przykład, jeśli chciałbym wyświetlić maksymalne zdrowie czegoś, wygenerowałbym wartość na miejscu. Zapobiegło to podatności na błędy i było łatwiejsze do zrozumienia dla wszystkich zaangażowanych.

(Pracuję w Javie, więc to, co następuje, jest oparte na Javie, ale powinno działać z pewnymi modyfikacjami dla innych języków). Ten system można łatwo wykonać za pomocą wyliczeń dla typów modyfikacji, a następnie liczb całkowitych. Wynik końcowy można umieścić w jakiejś kolekcji, która zawiera pary uporządkowane według klucza i wartości. Będzie to szybkie wyszukiwanie i obliczenia, więc wydajność jest bardzo dobra.

Ogólnie rzecz biorąc, działa bardzo dobrze z prostymi modyfikatorami statycznymi. Chociaż kod musi istnieć w odpowiednich miejscach, aby można było użyć modyfikatorów: getAttack, getMaxHP, getMeleeDamage i tak dalej.

Gdy ta metoda zawodzi (dla mnie), jest to bardzo złożona interakcja między wzmocnieniami. Nie ma naprawdę łatwego sposobu na interakcję, chyba że trochę ją poprawisz. Ma kilka prostych możliwości interakcji. W tym celu należy zmodyfikować sposób przechowywania modyfikatorów statycznych. Zamiast używać wyliczenia jako klucza, używasz ciągu znaków. Ten ciąg byłby nazwą Enum + dodatkowa zmienna. 9 razy na 10 dodatkowa zmienna nie jest używana, więc nadal zachowujesz nazwę wyliczenia jako klucz.

Zróbmy szybki przykład: jeśli chcesz mieć możliwość modyfikowania obrażeń zadawanych nieumarłym stworzeniom, możesz mieć taką uporządkowaną parę: (DAMAGE_Undead, 10) USZKODZENIE to Enum, a Nieumarli to dodatkowa zmienna. Podczas walki możesz zrobić coś takiego:

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

W każdym razie działa dość dobrze i jest szybki. Ale zawodzi przy złożonych interakcjach i wszędzie ma „specjalny” kod. Weźmy na przykład sytuację „25% szans na teleportację po śmierci”. Jest to „dość” złożony proces. Powyższy system może to obsłużyć, ale nie łatwo, ponieważ potrzebujesz następujących elementów:

  1. Sprawdź, czy gracz ma ten mod.
  2. Gdzieś, jeśli uda się uzyskać teleportację, przygotuj kod. Lokalizacja tego kodu jest dyskusją samą w sobie!
  3. Uzyskaj odpowiednie dane z mapy Mod. Co oznacza wartość? Czy to w pokoju też się teleportują? Co jeśli gracz ma na sobie dwa mody teleportacji? Czy kwoty się nie sumują ?????? NIEPOWODZENIE!

To prowadzi mnie do następnego:


The Ultimate Complex Buff System

Kiedyś sam próbowałem napisać 2D MMORPG. To był okropny błąd, ale wiele się nauczyłem!

Przepisałem system afektu 3 razy. W pierwszym zastosowano mniejszą odmianę powyższego. Drugi był tym, o czym zamierzam mówić.

Ten system miał szereg klas dla każdej modyfikacji, więc rzeczy takie jak: ChangeHP, ChangeMaxHP, ChangeHPByPercent, ChangeMaxByPercent. Miałem milion takich ludzi - nawet takie rzeczy jak TeleportOnDeath.

Moje zajęcia miały rzeczy, które mogłyby wykonać następujące czynności:

  • ApplyAffect
  • removeAffect
  • checkForInteraction <--- ważne

Zastosuj i usuń wyjaśnienia same (chociaż w przypadku takich rzeczy, jak procenty, efekt będzie śledził, o ile zwiększył on HP, aby upewnić się, że kiedy efekt się skończy, usunie tylko dodaną kwotę. To był błąd, lol i zajęło mi dużo czasu, aby upewnić się, że wszystko jest w porządku. Nadal nie czułem się dobrze.).

Metoda checkForInteraction była przerażająco złożonym fragmentem kodu. W każdej klasie wpływów (tj .: ChangeHP) miałby kod określający, czy należy to zmodyfikować przez wpływ wejściowy. Na przykład, jeśli masz coś takiego ...

  • Wzmocnienie 1: Zadaje 10 obrażeń od ognia podczas ataku
  • Wzmocnienie 2: Zwiększa wszystkie obrażenia od ognia o 25%.
  • Wzmocnienie 3: Zwiększa wszystkie obrażenia od ognia o 15.

Metoda checkForInteraction poradziłaby sobie z tymi wszystkimi skutkami. Aby to zrobić, każdy wpływ na WSZYSTKICH graczy w pobliżu musiał zostać sprawdzony !! Wynika to z tego, że rodzaj wpływów, z którymi miałem do czynienia w przypadku wielu graczy na danym obszarze. Oznacza to, że kod NIGDY NIE MIAŁ JAKICHKOLWIEK specjalnych stwierdzeń jak wyżej - „jeśli właśnie umarliśmy, powinniśmy sprawdzić teleportację po śmierci”. Ten system automatycznie obsłużyłby go poprawnie we właściwym czasie.

Próba napisania tego systemu zajęła mi około 2 miesięcy i kilka razy wybuchła głową. JEDNAK, był NAPRAWDĘ potężny i potrafił robić szaloną ilość rzeczy - szczególnie gdy weźmiesz pod uwagę następujące dwa fakty dotyczące umiejętności w mojej grze: 1. Miały zakresy docelowe (tj .: pojedyncze, własne, tylko grupowe, własne PB AE , Cel PB AE, docelowa AE itd.). 2. Zdolności mogą mieć na nie więcej niż 1 wpływ.

Jak wspomniałem powyżej, był to system wpływów 2 i 3 dla tej gry. Dlaczego się od tego odsunąłem?

Ten system miał najgorszą wydajność, jaką kiedykolwiek widziałem! To było strasznie wolne, ponieważ musiało tak dużo sprawdzać każdą rzecz, która się wydarzyła. Próbowałem to poprawić, ale uznałem to za porażkę.

Przechodzimy do mojej trzeciej wersji (i innego rodzaju systemu buffów):


Złożona klasa afektów z programami obsługi

Jest to więc prawie kombinacja dwóch pierwszych: możemy mieć zmienne statyczne w klasie Affect, która zawiera wiele funkcji i dodatkowe dane. Następnie po prostu wywołaj procedury obsługi (dla mnie, prawie niektóre statyczne metody narzędzi zamiast podklas dla określonych akcji. Ale jestem pewien, że możesz przejść z podklasami dla akcji, jeśli chcesz), kiedy chcemy coś zrobić.

Klasa afektu miałaby wszystkie soczyste dobre rzeczy, takie jak typy celów, czas trwania, liczba zastosowań, szansa na wykonanie itd.

Wciąż musielibyśmy dodać specjalne kody, aby poradzić sobie z sytuacjami, na przykład teleportować się po śmierci. Nadal będziemy musieli to sprawdzić ręcznie w kodzie walki, a jeśli tak, to dostaniemy listę wpływów. Ta lista afektów zawiera wszystkie aktualnie stosowane afekty na graczu, który zajmował się teleportacją po śmierci. Następnie spojrzeliśmy na każdy z nich i sprawdziliśmy, czy wykonał się on i był udany (zatrzymalibyśmy się przy pierwszym udanym). Udało się, po prostu zadzwoniliśmy do przewodnika, aby się tym zajął.

Jeśli chcesz, możesz również wykonać interakcję. Musiałby tylko napisać kod, aby wyszukać określone wzmocnienia w odtwarzaczach / etc. Ponieważ ma dobrą wydajność (patrz poniżej), powinno być dość wydajne. Potrzebowałby po prostu bardziej złożonych programów obsługi i tak dalej.

Ma więc dużą wydajność pierwszego systemu i wciąż dużo złożoności, jak drugi (ale nie tak bardzo). Przynajmniej w Javie możesz zrobić kilka trudnych rzeczy, aby uzyskać wydajność prawie pierwszej w większości przypadków (np. Mając mapę enum ( http://docs.oracle.com/javase/6/docs/api/java) /util/EnumMap.html ) z Enums jako kluczami i ArrayList afektów jako wartości. To pozwala zobaczyć, czy szybko masz afekty [ponieważ lista wynosiłaby 0 lub mapa nie miałaby enum] i nie posiadając do ciągłego iterowania list afektów bez powodu. Nie mam nic przeciwko iteracji nad afektami, jeśli ich potrzebujemy w tej chwili. Zoptymalizuję później, jeśli stanie się to problemem).

Obecnie ponownie otwieram (przepisuję grę w Javie zamiast bazy kodu FastROM, w której była pierwotnie), mój MUD, który zakończył się w 2005 roku, a ostatnio natknąłem się na to, jak chcę wdrożyć mój system buffów? Będę używać tego systemu, ponieważ działał dobrze w mojej poprzedniej nieudanej grze.

Cóż, mam nadzieję, że ktoś gdzieś znajdzie przydatne informacje.

dayrinni
źródło
6

Inna klasa (lub funkcja adresowalna) dla każdego wzmocnienia nie jest nadmierna, jeśli zachowanie tych wzmocnień różni się od siebie. Jedną rzeczą byłoby posiadanie + 10% lub + 20% wzmocnień (które, oczywiście, byłyby lepiej reprezentowane jako dwa obiekty tej samej klasy), inne wdrażałyby zupełnie odmienne efekty, które i tak wymagałyby niestandardowego kodu. Uważam jednak, że lepiej jest mieć standardowe sposoby dostosowywania logiki gry, zamiast pozwolić każdemu wzmocnieniu robić to, co mu się podoba (i może zakłócać się nawzajem w nieprzewidziany sposób, zaburzając równowagę gry).

Sugerowałbym podzielenie każdego „cyklu ataku” na etapy, w których każdy krok ma wartość podstawową, uporządkowaną listę modyfikacji, które można zastosować do tej wartości (być może ograniczone), oraz ostateczne ograniczenie. Każda modyfikacja ma domyślnie transformację tożsamości i może mieć na nią wpływ zero lub więcej wzmocnień / osłabień. Specyfika każdej modyfikacji zależy od zastosowanego kroku. To, jak cykl zostanie wdrożony, zależy od ciebie (włączając w to opcję architektury opartej na zdarzeniach, o której mówiłeś).

Jednym z przykładów cyklu ataku może być:

  • oblicz atak gracza (baza + mody);
  • obliczyć obronę przeciwnika (baza + mody);
  • zrób różnicę (i zastosuj mody) i określ podstawowe obrażenia;
  • obliczyć wszelkie efekty parowania / zbroi (mody na obrażenia podstawowe) i zadawać obrażenia;
  • obliczyć dowolny efekt odrzutu (mody na obrażenia podstawowe) i zastosować do atakującego.

Ważną rzeczą do zapamiętania jest to, że im wcześniej w cyklu zostanie zastosowane wzmocnienie, tym większy efekt będzie miał w wyniku . Więc jeśli chcesz bardziej „taktycznej” walki (w której umiejętność gracza jest ważniejsza niż poziom postaci), stwórz wiele wzmocnień / osłabień na podstawowych statystykach. Jeśli chcesz bardziej „zrównoważonej” walki (gdzie poziom ma większe znaczenie - ważne w MMOG, aby ograniczyć tempo postępu), używaj tylko wzmocnień / osłabień na późniejszym etapie cyklu.

Rozróżnienie między „Modyfikacjami” a „Wzmocnieniami”, o których wspomniałem wcześniej, ma cel: decyzje dotyczące zasad i równowagi można wdrożyć na tym pierwszym, więc wszelkie zmiany na nim nie muszą odzwierciedlać zmian w każdej klasie tego drugiego. OTOH, liczba i rodzaje wzmocnień są ograniczone tylko twoją wyobraźnią, ponieważ każdy z nich może wyrazić swoje pożądane zachowanie bez konieczności uwzględnienia jakiejkolwiek możliwej interakcji między nimi a innymi (lub nawet istnienia innych).

Odpowiadając na pytanie: nie twórz klasy dla każdego wzmocnienia, ale po jednej dla każdej (typu) modyfikacji i powiąż modyfikację z cyklem ataku, a nie z postacią. Wzmocnienia mogą być po prostu listą krotek (Modyfikacja, klucz, wartość) i można zastosować wzmocnienie do postaci, po prostu dodając / usuwając ją z zestawu wzmocnień postaci. Zmniejsza to również okno błędu, ponieważ statystyki postaci nie muszą być wcale zmieniane po zastosowaniu wzmocnień (więc istnieje mniejsze ryzyko przywrócenia statystyki do niewłaściwej wartości po wygaśnięciu wzmocnienia).

mgibsonbr
źródło
Jest to interesujące podejście, ponieważ mieści się gdzieś pomiędzy dwiema implementacjami, które rozważałem - to znaczy albo ogranicza wzmocnienia do dość prostych modyfikatorów statystyk i obrażeń wynikowych, albo tworzy bardzo solidny, ale wysokowydajny system, który poradziłby sobie z wszystkim. Jest to swego rodzaju rozwinięcie tego pierwszego, aby umożliwić „ciernie” przy zachowaniu prostego interfejsu. Chociaż nie sądzę, że jest to magiczna kula dla tego, czego potrzebuję, z pewnością wygląda na to, że znacznie ułatwia balansowanie niż inne podejścia, więc może to być droga. Dzięki za wkład!
gkimsey
3

Nie wiem, czy nadal go czytasz, ale oto jak teraz to robię (kod oparty na UE4 i C ++). Po zastanowieniu się nad problemem przez ponad dwa tygodnie (!!) w końcu znalazłem to:

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

Pomyślałem, że dobrze, że enkapsulacja pojedynczego atrybutu w klasie / strukturze nie jest wcale takim złym pomysłem. Pamiętaj jednak, że naprawdę korzystam z wbudowanego systemu odzwierciedlania kodu UE4, więc bez pewnych przeróbek może to nie być odpowiednie wszędzie.

W każdym razie zacząłem od zawijania atrybutu do pojedynczej struktury:

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

To wciąż nie jest ukończone, ale podstawową ideą jest to, że ta struktura śledzi swój stan wewnętrzny. Atrybuty mogą być modyfikowane tylko przez Efekty. Próba ich bezpośredniej modyfikacji jest niebezpieczna i nie jest narażona na działanie projektantów. Zakładam, że wszystkim, co może oddziaływać z atrybutami, jest Efekt. W tym płaskie premie od przedmiotów. Po wyposażeniu nowego przedmiotu tworzony jest nowy efekt (wraz z uchwytem) i dodawany do dedykowanej mapy, która obsługuje premie na nieskończoność (te, które gracz musi ręcznie usunąć). Po zastosowaniu nowego efektu tworzony jest nowy uchwyt (uchwyt jest po prostu int, owinięty strukturą), a następnie ten uchwyt jest przekazywany dookoła jako środek do interakcji z tym efektem, a także śledzenia, czy efekt jest wciąż aktywny. Po usunięciu efektu jego uchwyt jest nadawany do wszystkich zainteresowanych obiektów,

Naprawdę ważną częścią tego jest TMap (TMap to mapa mieszana). FGAModifier jest bardzo prostą strukturą:

struct FGAModifier
{
    EGAAttributeOp AttributeMod;
    float Value;
};

Zawiera rodzaj modyfikacji:

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

    Invalid
};

I wartość, która jest ostateczną obliczoną wartością, którą zastosujemy do atrybutu.

Dodajemy nowy efekt za pomocą prostej funkcji, a następnie wywołujemy:

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

Ta funkcja ma na celu ponowne obliczenie całego stosu bonusów za każdym razem, gdy efekt zostanie dodany lub usunięty. Funkcja wciąż nie jest ukończona (jak widać), ale można uzyskać ogólny pomysł.

Moim największym problemem jest teraz posługiwanie się atrybutem Obrażanie / Leczenie (bez konieczności przeliczania całego stosu), myślę, że mam to nieco rozwiązane, ale wciąż wymaga więcej testów, aby uzyskać 100%.

W anycase Atrybuty są zdefiniowane w następujący sposób (+ makra Unreal, tutaj pominięte):

FGAAttributeBase Health;
FGAAttributeBase Energy;

itp.

Nie jestem również w 100% pewien, czy poradzę sobie z wartością CurrentValue atrybutu, ale powinien on działać. Tak już jest.

W każdym razie mam nadzieję, że uratuje to pamięć podręczną niektórych osób, nie jestem pewien, czy jest to najlepsze, czy nawet dobre rozwiązanie, ale podoba mi się to bardziej niż śledzenie efektów niezależnie od atrybutów. Sprawienie, by każdy atrybut śledził swój własny stan, jest w tym przypadku znacznie łatwiejszy i powinien być mniej podatny na błędy. Zasadniczo istnieje tylko jeden punkt awarii, który jest dość krótką i prostą klasą.

Łukasz Baran
źródło
Dziękujemy za link i wyjaśnienie swojej pracy! Myślę, że zmierzasz w kierunku tego, o co prosiłem. Kilka rzeczy, które przychodzą na myśl, to kolejność operacji (na przykład 3 efekty „dodaj” i 2 efekty „zwielokrotnienia” dla tego samego atrybutu, co powinno się zdarzyć najpierw?), I jest to wyłącznie obsługa atrybutów. Istnieje również pojęcie wyzwalaczy (takich jak efekty typu „stracić 1 AP po trafieniu”), które mogłyby zostać rozwiązane, ale prawdopodobnie byłoby to osobne dochodzenie.
gkimsey
Kolejność operacji w przypadku obliczania premii atrybutu jest łatwa do wykonania. Możesz tutaj zobaczyć, że mam tam i zmienić. Aby iterować po wszystkich obecnych bonusach (które można dodawać, odejmować, mnożyć, dzielić itp.), A następnie po prostu je kumulować. Robisz coś takiego jak BonusValue = (BonusValue * MultiplyBonus + AddBonus-SubtractBonus) / DivideBonus, Lub jakkolwiek chcesz spojrzeć na to równanie. Dzięki jednemu punktowi wejścia łatwo z nim eksperymentować. Jeśli chodzi o wyzwalacze, nie pisałem o tym, ponieważ jest to kolejny problem, nad którym się zastanawiam, i już próbowałem 3-4 (limit)
Łukasz Baran
rozwiązania, żadne z nich nie działało tak, jak chciałem (moim głównym celem jest, aby były przyjazne dla projektantów). Moją ogólną ideą jest używanie tagów i sprawdzanie przychodzących efektów w stosunku do tagów. Jeśli tag pasuje, efekt może wywołać inny efekt. (tag jest prostą, czytelną dla człowieka nazwą, taką jak Damage.Fire, Attack.Physical itp.). Podstawą jest bardzo łatwa koncepcja, problemem jest porządkowanie danych, aby były łatwo dostępne (szybkie wyszukiwanie) i łatwość dodawania nowych efektów. Możesz sprawdzić kod tutaj github.com/iniside/ActionRPGGame (GameAttributes to moduł, który Cię zainteresuje)
Łukasz Baran
2

Pracowałem nad małym MMO, a wszystkie przedmioty, moce, wzmocnienia itp. Miały „efekty”. Efektem była klasa, która miała zmienne dla „AddDefense”, „InstantDamage”, „HealHP” itp. Moce, przedmioty itp. Poradziłyby sobie z czasem trwania tego efektu.

Kiedy rzucisz moc lub umieścisz przedmiot, zastosuje on efekt do postaci na określony czas. Następnie główny atak itp. Obliczenia uwzględniłyby zastosowane efekty.

Na przykład masz buff, który dodaje obronę. Dla tego wzmocnienia będzie co najmniej identyfikator efektu i czas trwania. Podczas rzucania zastosowałby EffectID do postaci na określony czas.

Kolejny przykład dla przedmiotu miałby te same pola. Ale czas trwania byłby nieskończony lub do momentu usunięcia efektu poprzez zdjęcie przedmiotu z postaci.

Ta metoda pozwala na iterację listy aktualnie stosowanych efektów.

Mam nadzieję, że wyjaśniłem tę metodę wystarczająco jasno.

Stopień
źródło
Rozumiem to przy moim minimalnym doświadczeniu, jest to tradycyjny sposób na implementację modów statystycznych w grach RPG. Działa dobrze i jest łatwy do zrozumienia i wdrożenia. Minusem jest to, że nie pozostawia mi miejsca na robienie czegoś takiego jak wzmocnienie „cierni” lub coś bardziej zaawansowanego lub sytuacyjnego. Historycznie było to również przyczyną niektórych exploitów w grach RPG, chociaż są one dość rzadkie, a ponieważ tworzę grę dla jednego gracza, jeśli ktoś znajdzie exploita, to tak naprawdę mnie to nie martwi. Dzięki za wkład.
gkimsey
2
  1. Jeśli jesteś użytkownikiem jedności, tutaj jest coś na początek: http://www.stevegargolinski.com/armory-a-free-and-unfinished-stat-inventory-and-buffdebuff-framework-for-unity/

Używam ScriptableOjects jako buffów / zaklęć / talentów

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

using UnityEngine; using System.Collections.Generic;

public enum BuffType {Buff, Debuff} [System.Serializable] public class 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);
        }
    }
}
użytkownik22475
źródło
0

To było prawdziwe pytanie do mnie. Mam jeden pomysł na ten temat.

  1. Jak już powiedziano, musimy zaimplementować Bufflistę i aktualizator logiki dla buffów.
  2. Następnie musimy zmienić wszystkie określone ustawienia odtwarzacza w każdej ramce w podklasach Buffklasy.
  3. Następnie uzyskujemy bieżące ustawienia odtwarzacza z pola zmiennych ustawień.

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

W ten sposób można łatwo dodawać nowe statystyki graczy, bez zmiany logiki Buffpodklas.

Dantalia N.
źródło
0

Wiem, że to dość stare, ale zostało połączone w nowszym poście i mam kilka przemyśleń na ten temat, którym chciałbym się podzielić. Niestety w tej chwili nie mam ze sobą swoich notatek, więc postaram się przedstawić ogólny przegląd tego, o czym mówię, i wyedytuję szczegółowe informacje oraz przykładowy kod, gdy będę go miał przed mnie.

Po pierwsze, myślę, że z punktu widzenia projektowania większość ludzi jest zbyt pochłonięta tym, jakie typy buffów można stworzyć i jak są stosowane, i zapominając o podstawowych zasadach programowania obiektowego.

Co mam na myśli? Tak naprawdę nie ma znaczenia, czy coś jest wzmocnieniem, czy debuffem, oba są modyfikatorami, które wpływają na coś w pozytywny lub negatywny sposób. Kod nie dba o to, który jest który. W tym przypadku nie ma znaczenia, czy coś dodaje statystyki, czy pomnaża je, są to po prostu różne operatory i ponownie kod nie dba o to, która z nich.

Więc gdzie idę z tym? To, że zaprojektowanie dobrej (czytaj: prostej, eleganckiej) klasy buff / debuff nie jest wcale takie trudne, co trudne, to zaprojektowanie systemów, które obliczają i utrzymują stan gry.

Gdybym projektował system buff / debuff, oto kilka rzeczy, które rozważę:

  • Klasa buff / debuff do reprezentowania samego efektu.
  • Klasa typu buff / debuff, która zawiera informacje o tym, na co wpływa i w jaki sposób buff.
  • Postacie, Przedmioty i ewentualnie Lokalizacje musiałyby mieć właściwość list lub kolekcja zawierającą wzmocnienia i osłabienia.

Niektóre szczegóły dotyczące tego, jakie typy buff / debuff powinny zawierać:

  • Do kogo / do czego może być zastosowany, np. Gracz, potwór, lokalizacja, przedmiot itp.
  • Jaki to rodzaj efektu (pozytywny, negatywny), czy jest multiplikatywny czy addytywny i jaki rodzaj statystyki ma wpływ, IE: atak, obrona, ruch itp.
  • Kiedy należy to sprawdzić (walka, pora dnia itp.).
  • Czy można go usunąć, a jeśli tak, to w jaki sposób można go usunąć.

To dopiero początek, ale od tego momentu określasz, czego chcesz i działasz w oparciu o normalny stan gry. Powiedzmy na przykład, że chcesz stworzyć przeklęty przedmiot, który zmniejsza prędkość ruchu ...

Tak długo, jak umieściłem odpowiednie typy, łatwo jest utworzyć rekord wzmocnienia, który mówi:

  • Typ: Klątwa
  • ObjectType: Item
  • StatCategory: Utility
  • StatAffected: MovementSpeed
  • Czas trwania: Nieskończony
  • Trigger: OnEquip

I tak dalej, a kiedy tworzę buff, po prostu przypisuję mu BuffType of Curse i wszystko inne zależy od silnika ...

Aithos
źródło