Jak zaprojektować wiele różnych rodzajów ataków, które można łączyć?

17

Tworzę odgórną grę 2D i chcę mieć wiele różnych rodzajów ataków. Chciałbym, aby ataki były bardzo elastyczne i łączone tak, jak działa The Binding of Isaac. Oto lista wszystkich przedmiotów kolekcjonerskich w grze . Aby znaleźć dobry przykład, spójrzmy na przedmiot Spoon Bender .

Spoon Bender daje Izaakowi możliwość strzelania do łez naprowadzających.

Jeśli spojrzysz na sekcję „synergie”, zobaczysz, że można ją łączyć z innymi przedmiotami kolekcjonerskimi, aby uzyskać ciekawe, ale intuicyjne efekty. Na przykład, jeśli łączy się z Wewnętrznym okiem , „pozwoli Izaakowi na oddanie wielu strzałów naprowadzających jednocześnie”. Ma to sens, ponieważ Wewnętrzne oko

Daje Izaakowi potrójny strzał

Jaka jest dobra architektura do projektowania takich rzeczy? Oto rozwiązanie brutalnej siły:

if not spoon bender and not the inner eye then ...
if spoon bender and not the inner eye then ...
if not spoon bender and the inner eye then ...
if spoon bender and the inner eye then ...

Ale to wymknie się spod kontroli bardzo szybko. Jaki jest lepszy sposób zaprojektowania takiego systemu?

Daniel Kaplan
źródło
Osobiście trzymałbym wszystkie wyposażone przedmioty na liście i zaimplementowałby jakiś wspólny interfejs, który przenosi obiekt, który mutujesz z obiektu na obiekt. „dla każdego przedmiotu zmodyfikuj planowany atak”, aby jeden element mógł powielić ilość pocisków, jeden mógł dodać jego odcień i zmienić obrażenia (więc jeśli jeden przedmiot wytworzyłby czerwone śruby, a drugi zrobił żółty, po obu modyfikacjach miałbyś atak pomarańczowy) . Możesz też mieć tylko jedną ogólną klasę przedmiotów, która ma parametry decydujące o tym, jak modyfikuje planowany atak.
Benjamin Danger Johnson
2
Chciałbym zauważyć, że Kirby 64 zastosował podejście brutalnej siły i zaprogramował różne efekty dla wszystkich możliwych kombinacji umiejętności, więc jest to wykonalne.
Kevin

Odpowiedzi:

16

Zupełnie nie musisz ręcznie kombinować kodów. Zamiast tego możesz skupić się na właściwościach, które daje każdy element. Na przykład, pozycja A ustawia Projectile=Fireball,Targetting=Homing. Pozycja B zestawy FireMode=ArcShot,Count=3. ArcShotLogika jest odpowiedzialny za wysyłanie Countliczbę Projectileelementów w łuk.

Te dwa elementy można łączyć z dowolnymi innymi elementami, które dowolnie modyfikują te (lub inne) właściwości. Jeśli dodasz nowy typ pocisku, będzie on automatycznie działał ArcShot, a jeśli dodasz nowy tryb strzelania, będzie on automatycznie działał z Fireballpociskami. Podobnie Targettingjest właściwością, która ustawia kontroler dla pocisków podczas FireModetworzenia pocisków, dzięki czemu można je łatwo i trywialnie łączyć w dowolną kombinację, jeśli ma to sens.

Możesz także ustawić zależności właściwości i tym podobne. Na przykład ArcShotwymaga posiadania dostawcy Projectile(który może być tylko domyślny). Możesz ustawić priorytety, aby mieć dwa aktywne elementy, z których oba dostarczają Projectilekod, wiedział, którego użyć. Możesz też podać interfejs użytkownika, aby użytkownik mógł wybrać typ pocisku, którego chcesz użyć, lub po prostu zażądać od gracza wyposażyć przedmioty o wysokim priorytecie, których nie chce, lub użyć najnowszego przedmiotu itp. Możesz dodatkowo dopuścić system niezgodności , np. takie, że dwa przedmioty, które tylko oba modyfikują, Projectilenie mogą być jednocześnie wyposażone.

Zasadniczo, jeśli to możliwe, preferuj podejście oparte na danych (lub deklaratywne ) nad podejściem proceduralnym (duży bałagan jeśli-inaczej), jeśli chodzi o przedmioty i takie w twojej grze. Ogólna logika wyższego poziomu, którą można konfigurować za pomocą prostych danych, jest znacznie lepsza niż zakodowane na stałe listy reguł o specjalnej obudowie.

Sean Middleditch
źródło
Mała nitka, ale wydaje się, że nie masz poprawnych właściwości dla przykładów, których używasz. „Spoon Bender” dodaje bazowanie, a „Inner Eye” dodaje potrójny strzał. Ani dodaj łuku, a oba są łzami. Jeśli używasz dowolnych właściwości w swoim przykładzie do wyodrębnienia projektu, łatwiej byłoby je odczytać, gdyby nie zostały nazwane w sposób wprowadzający w błąd. Wolę od tego „pozycję A” i „pozycję B”.
Daniel Kaplan
1
@tieTYT: Uogólniłem nazwy i rozszerzyłem przykład o tryby celowania oprócz typów pocisków i trybów ostrzału. Nigdy nie grałem w BoI dłużej niż kilka minut, więc nie mam tak zinternalizowanych nazw, jak inni. :)
Sean Middleditch
Wydaje mi się, że trudną częścią jest identyfikacja kategorii właściwości. np .: Targetowanie to jeden, FireMode to inny, Count może być trzeci ... albo nie. Może 3 pociski mogą być kulą ognistą, a 5 granatami, chociaż jest to jedna broń.
Daniel Kaplan
@tieTYT: absolutnie. Oczywiście możesz rozszerzyć system, aby umożliwić specjalne kombinacje lub logikę, ale to raczej wyjątek niż reguła. Zoptymalizuj pod kątem zwykłego przypadku, a nie narożnego.
Sean Middleditch,
Oto świetny film o tym, dlaczego się mylisz w odniesieniu do programowania proceduralnego youtu.be/QM1iUe6IofM , a także oparty na danych sposób opisywania mapuje dowolne bloki if / else do poszczególnych zestawów danych, zasadniczo nie robiąc nic, aby zmniejszyć liczbę specjalnych scenariuszy przypadków program, ponieważ musisz zaprogramować je wszystkie ...
RenaissanceProgrammer
8

Jeśli używasz języka OOP, brzmi to jak dobre miejsce do zastosowania Wzorca Dekoratora . Jeśli chcesz zmodyfikować sposób ataku, udekoruj go odpowiednim rozszerzeniem.

Surowy c ++ Przykład:

class AttackBehaviour
{
    /* other code */
    virtual void Attack(double angle);
};

class TearAttack: public AttackBehaviour
{
    /* other code */
    void Attack(double angle);
};

class TripleAttack: public AttackBehaviour
{
    /* other code */
    AttackBehaviour* baseAttackBehaviour;
    void Attack(double angle);
};

void TripleAttack::Attack(angle)
{
    baseAttackBehaviour->Attack(angle-30);
    baseAttackBehaviour->Attack(angle);
    baseAttackBehaviour->Attack(angle+30);
}

Ta metoda byłaby najlepsza, jeśli masz bardzo dużą liczbę ataków i potrzebujesz, aby wszystkie zachowywały się mniej więcej w ten sam sposób. Jeśli chcesz znacząco zmienić sposób, w jaki atak odbywa się za pomocą modyfikatora (np. Nowa animacja z modyfikatorem), ta metoda nie jest dla Ciebie.

Mila morska
źródło
Jeśli chcę zmienić animację, dlaczego nie jest to dla mnie? A co jeśli będę pracować w bardziej funkcjonalnym języku? Czy znasz wzór, który jest dla nich odpowiedni?
Daniel Kaplan
Jest bardziej sztywny, ponieważ modyfikator zawsze wywołuje Attackmetodę agregowanego obiektu. TripleAttackKlasa nie powinna wiedzieć o TearAttackklasie. Gdyby to była prawda, doprowadziłoby to do tylu bólów głowy, co else-ifblok. Oznacza to, że wszelkie animacje łez muszą znajdować się wewnątrz TearAttackBehaviourobiektu. Ten obiekt nie wie (i nie powinien) wiedzieć, że został ozdobiony przez TripleAttackprzedmiot. W rezultacie 3 animacje łez przebiegają niezależnie, ponieważ niezależne.
NauticalMile
Trudno mi to wytłumaczyć słowami, jeśli ktoś inny chce się w to dźgnąć, bądź moim gościem.
NauticalMile
Jeśli chodzi o wdrożenie tego w bardziej funkcjonalnym języku, zastanowię się przez chwilę i poprawię odpowiedź, gdy będę gotowy.
NauticalMile
1

Jako fan Binding of Isaac również zastanawiałem się, jak poradzić sobie z czymś takim. System w grze jest wystarczająco solidny, gdy pojawiające się zachowania powstają w wyniku kombinacji efektów (ten, który przychodzi mi do głowy, to uzyskanie lustra, łyżki do gięcia i niektórych wzmacniaczy zasięgu, które powodują zawirowanie, naprowadzanie ściany łez wokół Izaaka w stylu Magneto ). Sama ich liczba sprawiłaby, że blok „jeśli” byłby niepraktyczny.

Doszedłem do wniosku, że Izaak i jego łzy są dwoma bytami znajdującymi się w centrum ogromnej ramy Component-Entity . Istoty mają pewne podstawowe statystyki (prędkość ruchu, życie, zasięg, obrażenia, duszek itp.), A każdy składnik przyniesie ze sobą modyfikator statystyk i czasownik.

W kodzie Izaak i jego łzy mieliby listę zawierającą elementy interfejsu. Izaak miałby listę rzeczy, które subskrybują interfejs IsaacMutatora, a jego łzy tearMutator. IsaacMutator miałby funkcje modyfikujące zdrowie, szybkość, zasięg, wygląd i niektóre specjalne czasowniki Izaaka. TearMutator byłby podobny. Raz na pętlę gry Izaak przechodził przez wszystkich swoich Izaaków, a także wszystkie żywe łzy. Przykładowy angielski brzmi następująco:

Isaac has IsaacMutators:
--spoonbender which gives no stat change and: Tears are made homing
--MeatEater which give +1 health, +1 damage and: nothing
--MagicMirror which gives no stat change and: Tears are made reflecting

Tears have tearMutators:
--(depends on MeatEater) +1 damage and: nothing
--(depends on MagicMirror) no stat change and: +1 vector towards isaac
--(depnds on spoonbender) no stat change and: +1 vector towards enemytype

i tak dalej. Ponieważ typy są addytywne, możesz układać w stosy, dodawać i usuwać zawartość swoich serc.

Kirbinator
źródło
-5

Myślę, że twoja droga działa najlepiej. Każdy z tych elementów daje warunek, jeśli użyte razem dają inny warunek, wówczas efektywnie potrzebujesz wszystkich 3 możliwych warunków.

Możesz także przejść do tego, tworząc nowy typ definicji, gdy oba elementy są obecne, ale tak naprawdę dodaje to splotu:

if spoon bender and the inner eye then new spoon bender inner eye

if spoon bender inner eye then ...
RenaissanceProgrammer
źródło