Co mogę zrobić, aby uniknąć jednorazowych flag i kontroli w całym kodzie?

18

Rozważ grę karcianą, taką jak Hearthstone .

Istnieją setki kart, które robią wiele różnych rzeczy, z których niektóre są unikalne nawet dla jednej karty! Na przykład, istnieje karta (zwana Nozdormu), która zmniejsza turę gracza do zaledwie 15 sekund!

Kiedy masz tak wiele potencjalnych efektów, jak możesz uniknąć magicznych liczb i jednorazowych kontroli w całym kodzie? Jak można uniknąć metody „Check_Nozdormu_In_Play” w klasie PlayerTurnTime? Jak można zorganizować kod w taki sposób, aby dodając jeszcze więcej efektów, nie trzeba było refaktoryzować podstawowych systemów, aby obsługiwały rzeczy, których nigdy wcześniej nie obsługiwały?

Sable Dreamer
źródło
Czy to naprawdę problem z wydajnością? Mam na myśli, że możesz zrobić szaloną ilość rzeczy za pomocą nowoczesnych procesorów w krótkim czasie.
Jari Komppa
11
Kto powiedział coś o problemach z wydajnością? Głównym problemem, jaki dostrzegam, jest ciągła potrzeba dostosowania całego kodu za każdym razem, gdy tworzysz nową kartę.
jhocking
2
więc dodaj język skryptowy i napisz każdą kartę.
Jari Komppa
1
Nie ma czasu na udzielenie prawidłowej odpowiedzi, ale zamiast mieć np. Czek Nozdormu i 15-sekundową regulację w kodzie klasy „PlayerTurnTime”, który obsługuje tury gracza, możesz zakodować klasę „PlayerTurnTime”, aby wywołać [klasę - jeśli chcesz ] funkcja dostarczana z zewnątrz w określonych punktach. Następnie kod karty Nozdormu (i wszystkie inne karty, które muszą wpływać na tę samą liczbę) mogą zaimplementować funkcję dla tej korekty i w razie potrzeby wstrzyknąć tę funkcję do klasy PlayerTurnTime. Przydatne może być przeczytanie o wstrzykiwaniu strategii i zależności z klasycznej książki Design
Designs
2
W pewnym momencie muszę się zastanawiać, czy dodanie kontroli ad-hoc do odpowiednich fragmentów kodu jest najprostszym rozwiązaniem.
user253751,

Odpowiedzi:

12

Czy sprawdziłeś systemy komponentów encji i strategie przesyłania komunikatów o zdarzeniach?

Efekty statusu powinny być jakimś składnikiem, który może zastosować swoje trwałe efekty w metodzie OnCreate (), wygasić ich efekty w OnRemoved () i subskrybować komunikaty o zdarzeniach gry, aby zastosować efekty, które występują jako reakcja na coś, co się dzieje.

Jeśli efekt jest trwale warunkowy (trwa przez X zwojów, ale ma zastosowanie tylko w pewnych okolicznościach), może być konieczne sprawdzenie tych warunków na różnych etapach.

Następnie upewnij się, że twoja gra również nie ma domyślnych magicznych liczb. Upewnij się, że wszystko, co można zmienić, jest zmienną opartą na danych, a nie zakodowanymi domyślnymi wartościami domyślnymi ze zmiennymi używanymi dla wyjątków.

W ten sposób nigdy nie zakładasz, jaka będzie długość skrętu. Jest to zawsze stale sprawdzana zmienna, którą można zmienić dowolnym efektem i być może cofnąć później przez efekt po wygaśnięciu. Nigdy nie sprawdzasz wyjątków przed ustawieniem domyślnej liczby magicznej.

RobStone
źródło
2
„Upewnij się, że wszystko, co można zmienić, jest zmienną opartą na danych, a nie kodami domyślnymi ze zmiennymi używanymi dla wyjątków”. - Och, raczej to lubię. Myślę, że to bardzo pomaga!
Sable Dreamer
Czy mógłbyś rozwinąć kwestię „zastosowania ich trwałych efektów”? Czy zapisanie się do turnStarted, a następnie zmiana wartości Length spowoduje, że kod będzie niemożliwy do debugowania lub, co gorsza, spowoduje niespójne wyniki (podczas interakcji między podobnymi efektami)?
wondra,
Tylko dla subskrybentów, którzy zakładają dowolny przedział czasu. Musisz starannie modelować. Może być dobrze, aby aktualny czas tury był inny niż czas tury gracza. PTT zostanie sprawdzone, aby utworzyć nową turę. CTT można sprawdzić za pomocą kart. Jeśli efekt powinien zwiększyć bieżący czas, interfejs użytkownika timera powinien naturalnie podążać za nim, jeśli jest bezstanowy.
RobStone
Aby lepiej odpowiedzieć na pytanie. Nic innego nie przechowuje czasu tury ani niczego na tej podstawie. Zawsze to sprawdzaj.
RobStone
11

RobStone jest na dobrej drodze, ale chciałem to rozwinąć, ponieważ właśnie to zrobiłem, kiedy napisałem Dungeon Ho !, Roguelike, który miał bardzo złożony system efektów dla broni i zaklęć.

Każda karta powinna mieć dołączony zestaw efektów, zdefiniowany w taki sposób, aby wskazywał, jaki jest efekt, do czego jest skierowany, jak i na jak długo. Na przykład efekt „obrażeń przeciwnika” może wyglądać mniej więcej tak;

Effect type: deal damage (enumeration, string, what-have-you)
Effect amount: 20
Source: my weapon
Target: opponent
Effect Cost: 20
Cost Type: Mana

Następnie, gdy zadziała efekt, przygotuj ogólną procedurę obsługi efektu. Jak idiota użyłem ogromnej instrukcji case / switch:

switch (effect_type)
{
     case DAMAGE:

     break;
}

Ale o wiele lepszy i bardziej modułowy sposób to zrobić poprzez polimorfizm. Utwórz klasę Effect, która otacza wszystkie te dane, utwórz podklasę dla każdego rodzaju efektu, a następnie poproś tę klasę o zastąpienie metody onExecute () specyficznej dla tej klasy.

class Effect
{
    Object source;
    int amount;

    public void onExecute(Object target)
    {
          // Do nothing
    }
}

class DamageEffect extends Effect
{
    public void onExecute(Object target)
    {
          target.health -= amount;
    }
}

Mielibyśmy więc podstawową klasę Effect, a następnie klasę DamageEffect z metodą onExecute (), więc w naszym kodzie przetwarzania po prostu poszliśmy;

Effect effect = card.getActiveEffect();

effect.onExecute();

Sposobem radzenia sobie ze świadomością tego, co jest w grze, jest utworzenie Vector / Array / link list / etc. aktywnych efektów (typu Efekt, klasa podstawowa) dołączonych do dowolnego obiektu (w tym pola gry / „gry”), więc zamiast konieczności sprawdzania, czy dany efekt jest w grze, wystarczy przejrzeć wszystkie efekty przypisane do obiekty i pozwól im się wykonać. Jeśli efekt nie jest dołączony do obiektu, nie jest odtwarzany.

Effect effect;

for (int o = 0; o < objects.length; o++)
{
    for (int e = 0; e < objects[o].effects.length; e++)
    {
         effect = objects[o].effects[e];

         effect.onExecute();
    }
}
Sandalfoot
źródło
Właśnie tak to zrobiłem. Piękno polega na tym, że masz zasadniczo system oparty na danych i możesz łatwo dostosować logikę na podstawie efektu. Zwykle będziesz musiał wykonać pewne sprawdzenie stanu w logice wykonania efektu, ale nadal jest to o wiele bardziej spójne, ponieważ te kontrole dotyczą tylko danego efektu.
manabreak
1

Przedstawię garść sugestii. Niektóre z nich są ze sobą sprzeczne. Ale może niektóre są przydatne.

Rozważ listy kontra flagi

Możesz iterować po całym świecie i sprawdzać flagę na każdym elemencie, aby zdecydować, czy wykonać flagę. Lub możesz zachować listę tylko tych przedmiotów, które powinny zrobić flagę.

Rozważ listy i wyliczenia

Możesz dodawać pola boolowskie do swojej klasy przedmiotów, isAThis i isAThat. Lub możesz mieć listę ciągów znaków lub elementów wyliczających, takich jak {„isAThis”, „isAThat”} lub {IS_A_THIS, IS_A_THAT}. W ten sposób możesz dodawać nowe w wyliczeniach (lub stałych ciągów) bez dodawania pól. Nie dlatego, że dodawanie pól jest naprawdę złe…

Rozważ wskaźniki funkcji

Zamiast listy flag lub wyliczeń, może mieć listę akcji do wykonania dla tego elementu w różnych kontekstach. (Entity-ish…)

Rozważ obiekty

Niektóre osoby preferują podejścia oparte na danych, skryptach lub elementach składowych. Ale warto też wziąć pod uwagę staromodne hierarchie obiektów. Klasa podstawowa musi zaakceptować akcje, takie jak „zagraj tę kartę w fazie F” lub cokolwiek innego. Następnie każdy rodzaj karty może zastąpić i odpowiednio zareagować. Prawdopodobnie istnieje także obiekt gracza i obiekt gry, więc gra może wykonywać takie czynności, jak (player-> isAllowedToPlay ()) {wykonaj grę…}.

Rozważ możliwość debugowania

Kiedyś dobrą cechą stosu flag jest to, że możesz sprawdzić i wydrukować stan każdego przedmiotu w ten sam sposób. Jeśli stan jest reprezentowany przez różne typy lub pakiety komponentów, wskaźniki funkcji lub znajdowanie się na różnych listach, może nie wystarczyć samo spojrzenie na pola elementu. To wszystko kompromisy.

Ostatecznie refaktoryzacja: rozważ testy jednostkowe

Bez względu na to, jak bardzo uogólniasz swoją architekturę, będziesz w stanie wyobrazić sobie rzeczy, których nie obejmuje. Następnie będziesz musiał dokonać refaktoryzacji. Może trochę, może dużo.

Sposobem na zwiększenie bezpieczeństwa jest szereg testów jednostkowych. W ten sposób możesz być pewny, że nawet jeśli przestawiłeś rzeczy pod spodem (może o wiele! Każdy test jednostkowy wygląda ogólnie tak:

void test1()
{
   Game game;
   game.addThis();
   game.setupThat(); // use primary or backdoor API to get game to known state

   game.playCard(something something).

   int x = game.getSomeInternalState;
   assertEquals(“did it do what we wanted?”, x, 23); // fail if x isn’t 23
}

Jak widać, utrzymanie stabilnych połączeń API najwyższego poziomu w grze (lub odtwarzaczu, karcie i c) jest kluczem do strategii testów jednostkowych.

David Van Brink
źródło
0

Zamiast myśleć o każdej karcie osobno, zacznij myśleć w kategoriach kategorii efektów, a karty zawierają jedną lub więcej z tych kategorii. Na przykład, aby obliczyć czas w rundzie, możesz przejrzeć wszystkie karty w grze i sprawdzić kategorię „manipuluj czasem trwania tury” każdej karty zawierającej tę kategorię. Każda karta następnie zwiększa lub zastępuje czas trwania tury, zgodnie z ustalonymi regułami.

Zasadniczo jest to system mini-komponentowy, w którym każdy obiekt „karciany” jest po prostu pojemnikiem na kilka elementów efektów.

jhocking
źródło
Ponieważ karty - i także przyszłe - mogą zrobić wszystko, oczekiwałbym, że każda karta będzie zawierała skrypt. Nadal jestem pewien, że to nie jest prawdziwy problem z wydajnością.
Jari Komppa
4
zgodnie z głównymi komentarzami: Nikt (oprócz ciebie) nic nie powiedział o problemach z wydajnością. Jeśli chodzi o pełne skrypty jako alternatywę, opisz je w odpowiedzi.
jhocking