Jak skonfigurować elastyczne ramy do obsługi osiągnięć?

54

W szczególności jaki jest najlepszy sposób na wdrożenie systemu osiągnięć wystarczająco elastycznego, aby poradzić sobie z wykraczaniem poza proste osiągnięcia oparte na statystykach, takie jak „zabij x wrogów”.

Szukam czegoś solidniejszego niż system oparty na statystykach i czegoś bardziej uporządkowanego i łatwiejszego w utrzymaniu niż „zakodować je wszystkie jako warunki”. Niektóre przykłady, które są niemożliwe lub nieporęczne w systemie opartym na statystykach: „Pokrój arbuza po truskawce”, „Zejdź fajką, gdy jesteś niezwyciężony” itp.

lti
źródło

Odpowiedzi:

39

Myślę, że pewnego rodzaju solidnym rozwiązaniem byłoby pójście w kierunku obiektowym.

W zależności od rodzaju osiągnięcia, które chcesz wesprzeć, potrzebujesz sposobu na sprawdzenie aktualnego stanu gry i / lub historii działań / zdarzeń, które dokonały obiekty gry (takie jak gracz).

Załóżmy, że masz podstawową klasę osiągnięć, taką jak:

class AbstractAchievement
{
    GameState& gameState;
    virtual bool IsEarned() = 0;
    virtual string GetName() = 0;
};

AbstractAchievementzawiera odniesienie do stanu gry. Służy do sprawdzania, co się dzieje.

Następnie wykonujesz konkretne wdrożenia. Wykorzystajmy twoje przykłady:

class MasterSlicerAchievement : public AbstractAchievement
{
    string GetName() { return "Master Slicer"; }
    bool IsEarned()
    {
        Action lastAction = gameState.GetPlayerActionHistory().GetAction(0);
        Action previousAction = gameState.GetPlayerActionHistory().GetAction(1);
        if (lastAction.GetType() == ActionType::Slice &&
            previousAction.GetType() == ActionType::Slice &&
            lastAction.GetObjectType() == ObjectType::Watermelon &&
            previousAction.GetObjectType() == ObjectType::Strawberry)
            return true;
        return false;
    }
};
class InvinciblePipeRiderAchievement : public AbstractAchievement
{
    string GetName() { return "Invincible Pipe Rider"; }
    bool IsEarned()
    {
        if (gameState.GetLocationType(gameState.GetPlayerPosition()) == LocationType::OVER_PIPE &&
            gameState.GetPlayerState() == EntityState::INVINCIBLE)
            return true;
        return false;
    }
};

Następnie to Ty decydujesz, kiedy sprawdzić za pomocą tej IsEarned()metody. Możesz sprawdzić przy każdej aktualizacji gry.

Bardziej efektywnym sposobem byłoby na przykład posiadanie jakiegoś menedżera zdarzeń. A następnie zarejestruj zdarzenia (takie jak PlayerHasSlicedSomethingEventlub PlayerGotInvicibleEventpo prostu PlayerStateChanged) w metodzie, która zajęłaby osiągnięcie parametru. Przykład:

class Game
{
    void Initialize()
    {
        eventManager.RegisterAchievementCheckByActionType(ActionType::Slice, masterSlicerAchievement);
        // Each time an action of type Slice happens,
        // the CheckAchievement() method is invoked with masterSlicerAchievement as parameter.
        eventManager.RegisterAchievementCheckByPlayerState(EntityState::INVINCIBLE, invinciblePiperAchievement);
        // Each time the player gets the INVINCIBLE state,
        // the CheckAchievement() method is invoked with invinciblePipeRiderAchievement as parameter.
    }
    void CheckAchievement(const AbstractAchievement& achievement)
    {
        if (!HasAchievement(player, achievement) && achievement.IsEarned())
        {
            AddAchievement(player, achievement);
        }
    }
};
Splo
źródło
13
styl nitpick: if(...) return true; else return false;jest taki sam jakreturn (...)
BlueRaja - Danny Pflughoeft
2
To bardzo dobry szablon do wdrożenia systemu osiągnięć. Co więcej, wciąż pokazuje, że musisz mieć sposób na śledzenie stanów gry. Uważam, że to chyba najbardziej skomplikowany pomysł.
Bryan Harrington,
@Spio jesteś mistrzem mężczyzn ...! : D Proste i eleganckie rozwiązanie. Gratulacje.
Diego Palomar
+1 Myślę, że system emisji / odbioru zdarzeń to świetny sposób na poradzenie sobie z tym problemem.
ashes999
14

Krótko mówiąc, osiągnięcia są odblokowane, gdy spełniony zostanie określony warunek. Musisz więc mieć możliwość tworzenia instrukcji if, aby sprawdzić żądany warunek.

Na przykład, jeśli chcesz wiedzieć, że poziom został ukończony lub boss został pokonany, musisz mieć flagę boolowską, która stanie się prawdziwa, gdy te zdarzenia się zdarzą.

Następnie:

if(GlobalFlags.MasterBossDefeated == true && AchievementClass.MasterBossDefeatedAchievement == false)
{
    AchievementClass.MasterBossDefeatedAchievement = true;
    showModalPopUp("You defeated the Master Boss!  30 gamerscore");
}

Możesz uczynić to tak złożonym lub uproszczonym, jak to konieczne, aby dopasować się do pożądanego warunku.

Niektóre informacje o osiągnięciach Xbox 360 można znaleźć tutaj .

Bryan Denny
źródło
2
+1 Świetny artykuł i zasadniczo to, co zamierzałem zasugerować. Chociaż sprawiłbym, że Modal pobierał tekst z samego osiągnięcia ... tylko po to, aby uniknąć szukania tekstu, jeśli chcesz coś zmienić.
Jesse Dorsey
@Noctrine - nie zapomnij, że jakikolwiek kod tutaj zamieszczony powinien być traktowany jako pseudo-kod - często konieczne jest użycie uproszczonego kodu, aby uzyskać sens.
ChrisF
1
Link do osiągnięć Xbox 360 jest martwy.
hangy
8

Co się stanie, jeśli po każdej akcji, którą gracz wykona, opublikuje wiadomość na adres AchievementManager? Następnie menedżer może wewnętrznie sprawdzić, czy zostały spełnione określone warunki. Pierwsze obiekty wysyłają wiadomości:

AchievementManager::PostMessage("Jump", "162");

AchievementManager::PostMessage("Slice", "Strawberry");
AchievementManager::PostMessage("Slice", "Watermelon");

AchievementManager::PostMessage("Kill", "Goomba");

A następnie AchievementManagersprawdza, czy musi coś zrobić:

if (!strcmp(m_Message.name, "Slice") && !strcmp(m_LastMessage.name, "Slice"))
{
    if (!strcmp(m_Message.value, "Watermelon") && !strcmp(m_LastMessage.value, "Strawberry"))
    {
        // achievement unlocked!
    }
}

Prawdopodobnie będziesz chciał to zrobić z wyliczeniami zamiast ciągów. ;)

knight666
źródło
Jest to zgodne z tym, o czym myślałem, ale wciąż opiera się na kilku rzeczach zakodowanych na stałe w jednej funkcji. Pomijając łatwość utrzymania, przynajmniej każde osiągnięcie musi być sprawdzane pod kątem kwalifikowalności za każdym razem poprzez funkcję.
lti
2
Czy to? Możesz umieścić warunkowe w zewnętrznym skrypcie, o ile masz jakiś sposób na śledzenie tego, co wydarzyło się w grze.
knight666,
6
-1 to cuchnie złym projektem. Jeśli zamierzasz wywołać bezpośrednio AchivementManager, po prostu ustaw każdy z tych „komunikatów” jako osobną funkcję. Jeśli zamierzasz używać wiadomości, utwórz menedżera wiadomości, aby inne klasy również mogły z nich korzystać (jestem pewien, że Goomba byłby zainteresowany wiedząc, że został zabity) i aby usunąć to połączenie z AchievementManagerkażdym klasa (o co OP pytał przede wszystkim, jak tego uniknąć). I używaj wyliczeń lub oddzielnych klas dla swoich wiadomości, a nie literałów łańcuchowych - używanie literałów łańcuchowych do przekazywania stanu jest zawsze złym pomysłem.
BlueRaja - Danny Pflughoeft
4

Ostatni zastosowany przeze mnie projekt opierał się na posiadaniu zestawu trwałych liczników na użytkownika, a następnie na osiągnięciu pewnej wartości licznika osiągniętego klucza. Większość stanowiła pojedynczą parę osiągnięć / liczników, gdzie licznik wynosiłby zawsze 0 lub 1 (a osiągnięcie uruchomiono przy> = 1), ale można tego również użyć do „zabitych X kolesi” lub „znalezionych X skrzyń”. Oznacza to również, że możesz skonfigurować liczniki dla czegoś, co nie ma osiągnięć, i nadal będzie śledzone do wykorzystania w przyszłości.

koderanger
źródło
3

Kiedy zaimplementowałem osiągnięcia w mojej ostatniej grze, stworzyłem to w oparciu o statystyki. Osiągnięcia są odblokowywane, gdy nasze statystyki osiągną określoną wartość. Zastanów się nad Modern Warfare 2: gra śledzi mnóstwo statystyk! Ile zdjęć wykonałeś za pomocą SCAR-H? Ile mil przebiegłeś podczas korzystania z lekkiego profitu?

Więc w swojej implementacji po prostu stworzyłem silnik statystyk, a następnie stworzyłem menedżera osiągnięć, który uruchamia naprawdę proste zapytania, aby sprawdzić status osiągnięć w trakcie gry.

Chociaż moja implementacja jest dość uproszczona, wykonuje zadanie. Napisałem o tym i udostępniłem tutaj moje zapytania .

Reed Olsen
źródło
2

Użyj rachunku zdarzeń . Następnie wykonaj kilka warunków wstępnych i działań, które zostaną zastosowane po spełnieniu warunków wstępnych:

  • warunki wstępne: zabiłeś 1000 wrogów, masz 2 nogi
  • akcje: daj mi Lollypop, daj mi super-duper-shotgun-13
  • pierwsze działania: powiedz „jesteś taki niesamowity!”

Używaj go jak (niezoptymalizowany pod kątem prędkości!):

  • Przechowuj wszystkie statystyki.
  • Statystyki zapytań o warunki wstępne.
  • Zastosować działania.
  • Zastosuj jednorazowe działania raz.

Jeśli chcesz zrobić to szybko:

  • Buforuj co chcesz, przechowuj jego części na niektórych drzewach, hashe ...
  • Wprowadzaj zmiany przyrostowe, abyś nie stosował wszystkich działań przez cały czas, ale te, które są nowe ...)

Uwaga

Trudno dać najlepszą radę, ponieważ wszystkie rzeczy mają zalety i wady.

  • „Jaka jest najlepsza struktura danych?” oznacza „Jakie operacje chcesz z tym zrobić najbardziej? Wyszukiwanie, usuwanie, dodawanie ...”
  • Zasadniczo myślisz w tych właściwościach: łatwość kodowania, szybkość, przejrzystość, rozmiar ...
użytkownik712092
źródło
0

Co jest nie tak z czekami JEŻELI po wystąpieniu zdarzenia osiągnięcia?

if (Cinimatics.done)
   Achievement.get(CINIMATICS_SEEN);

if (EnemiesKiled > 200)
   Achievement.get(KILLER);

if (Damage > 2000 && TimeSinceFirstDamage < 2000)
   Achievement.get(MEAT_SHIELD);

InvitationAccepted = Invite.send(BestFriend);
if (InvitationAccepted)
   Achievement.get(A_FRIEND_IN_NEED);
MrValdez
źródło
3
„Szukam czegoś bardziej uporządkowanego i łatwego w utrzymaniu niż„ zakodować je wszystkie jako warunki ”. „. Chociaż jest to zdecydowanie dobra metoda KISS dla małej gry.
Kaczka komunistyczna