Wie kann ich ein flexibles Framework für den Umgang mit Erfolgen einrichten?

54

Was ist der beste Weg, um ein Erfolgssystem zu implementieren, das flexibel genug ist, um über einfache statistische Erfolge wie "Töte x Feinde" hinauszugehen?

Ich suche etwas Robusteres als ein statistikbasiertes System und etwas organisierteres und wartbareres als "sie alle als Bedingungen fest zu codieren". Einige Beispiele, die in einem statistischen System unmöglich oder unhandlich sind: "Schneiden Sie eine Wassermelone nach einer Erdbeere", "Gehen Sie eine Pfeife hinunter, während Sie unbesiegbar sind" usw.

lti
quelle

Antworten:

39

Ich denke, eine Art robuste Lösung wäre, den objektorientierten Weg zu gehen.

Abhängig davon, welche Art von Leistung Sie unterstützen möchten, benötigen Sie eine Möglichkeit, den aktuellen Status Ihres Spiels und / oder den Verlauf von Aktionen / Ereignissen abzufragen, die die Spielobjekte (wie der Spieler) ausgeführt haben.

Angenommen, Sie haben eine Basis-Leistungsklasse wie:

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

AbstractAchievemententhält einen Verweis auf den Stand des Spiels. Es wird verwendet, um die Ereignisse abzufragen.

Dann machen Sie konkrete Umsetzungen. Lassen Sie uns Ihre Beispiele verwenden:

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

Dann liegt es an Ihnen, zu entscheiden, wann die IsEarned()Methode angewendet werden soll . Sie können bei jedem Spiel Update überprüfen.

Ein effizienterer Weg wäre zum Beispiel, eine Art Event-Manager zu haben. Registrieren Sie dann Ereignisse (wie PlayerHasSlicedSomethingEventoder PlayerGotInvicibleEventoder einfach PlayerStateChanged) für eine Methode, die die Leistung als Parameter annehmen würde. Beispiel:

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
quelle
13
Stil Nitpick: if(...) return true; else return false;ist das gleiche wiereturn (...)
BlueRaja - Danny Pflughoeft
2
Das ist eine sehr gute Vorlage für die Implementierung eines Leistungssystems. Darüber hinaus zeigt es immer noch die Idee, dass Sie eine Möglichkeit haben müssen, Spielzustände zu verfolgen. Ich finde das wahrscheinlich die komplizierteste Idee.
Bryan Harrington
@Spio du bist ein Meistermann ...! : D Einfache und elegante Lösung. Gratuliere
Diego Palomar
+1 Ich denke, ein System zum Senden / Sammeln von Ereignissen ist eine großartige Möglichkeit, dieses Problem zu lösen.
Asche999
14

Kurz gesagt, Erfolge werden freigeschaltet, wenn eine bestimmte Bedingung erfüllt ist. Sie müssen also in der Lage sein, if-Anweisungen zu erstellen, um die gewünschte Bedingung zu überprüfen.

Wenn Sie beispielsweise wissen möchten, dass ein Level abgeschlossen oder ein Boss besiegt wurde, muss die Boolesche Flagge auf "Wahr" gesetzt sein, wenn diese Ereignisse eintreten.

Dann:

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

Sie können dies so komplex oder simpel gestalten, wie es für die gewünschte Bedingung erforderlich ist.

Einige Informationen zu den Erfolgen von Xbox 360 finden Sie hier .

Bryan Denny
quelle
2
+1 Großartiger Artikel und im Wesentlichen das, was ich vorschlagen wollte. Obwohl ich den Modal dazu bringen würde, seinen Text von der Leistung selbst zu erhalten ... nur um eine Textjagd zu vermeiden, wenn Sie etwas ändern möchten.
Jesse Dorsey
@Noctrine - Vergessen Sie nicht, dass hier eingegebener Code als Pseudo-Code behandelt werden sollte. Oft ist es erforderlich, vereinfachten Code zu verwenden, um den Punkt zu verdeutlichen.
ChrisF
1
Der Link für die Xbox 360-Errungenschaften ist nicht mehr verfügbar.
Hangy
8

Was passiert, wenn Sie bei jeder Aktion, die der Spieler ausführt, eine Nachricht an die AchievementManagersenden? Dann kann der Manager intern prüfen, ob bestimmte Bedingungen erfüllt sind. Erste Objekte posten Nachrichten:

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

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

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

Und dann die AchievementManagerÜberprüfung, ob etwas getan werden muss:

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

Wahrscheinlich möchten Sie dies jedoch mit Aufzählungen anstelle von Zeichenfolgen tun. ;)

knight666
quelle
Dies ist in etwa so, wie ich es mir vorgestellt habe, aber es hängt immer noch davon ab, dass eine Menge Dinge in einer Funktion fest codiert sind. Abgesehen von der Wartbarkeit muss mindestens jede Leistung jedes Mal durch die Funktion auf ihre Eignung überprüft werden.
lti
2
Macht es? Sie können die Bedingungen in ein externes Skript einfügen, sofern Sie eine Möglichkeit haben, den Verlauf des Spiels zu verfolgen.
Knight666
6
-1 Das stinkt nach schlechtem Design. Wenn Sie den AchivementManager direkt aufrufen möchten, müssen Sie nur für jede dieser "Nachrichten" eine eigene Funktion festlegen. Wenn Sie Nachrichten verwenden möchten, erstellen Sie einen Nachrichten-Manager, damit auch andere Klassen die Nachrichten verwenden können (ich bin sicher, dass der Goomba daran interessiert wäre, zu wissen, dass er getötet wurde), und entfernen Sie diese Kopplung mit dem AchievementManagerin every Klasse (das ist es, was OP gefragt hat, wie es zu vermeiden ist). Verwenden Sie eine Aufzählung oder separate Klassen für Ihre Nachrichten und keine Zeichenfolgenliterale. Es ist immer eine schlechte Idee , Zeichenfolgenliterale zum Weitergeben des Status zu verwenden .
BlueRaja - Danny Pflughoeft
4

Das letzte Design, das ich verwendete, basierte darauf, eine Reihe von beständigen Leistungsindikatoren pro Benutzer zu haben und dann Leistungsschlüssel von einem bestimmten Leistungsindikator zu entfernen, der einen bestimmten Wert erreicht. Die meisten waren ein einzelnes Erfolgs- / Gegenpaar, bei dem der Zähler immer nur 0 oder 1 war (und der Erfolg bei> = 1 ausgelöst wurde), aber Sie können dies auch für "getötete X-Typen" oder "gefundene X-Truhen" verwenden. Dies bedeutet auch, dass Sie Zähler für etwas einrichten können, das keine Erfolge erzielt hat, und dass diese weiterhin für die zukünftige Verwendung verfolgt werden.

coderanger
quelle
3

Als ich in meinem letzten Spiel Erfolge erzielt habe, habe ich alles statistikbasiert gemacht. Erfolge werden freigeschaltet, wenn unsere Statistiken einen bestimmten Wert erreichen. Betrachten Sie Modern Warfare 2: Das Spiel verfolgt Unmengen von Statistiken! Wie viele Aufnahmen haben Sie mit der SCAR-H gemacht? Wie viele Meilen sind Sie mit dem Lightweight-Vorteil gesprintet?

In meiner Implementierung habe ich einfach eine Statistik-Engine erstellt und dann einen Leistungs-Manager, der wirklich einfache Abfragen ausführt, um den Status der Leistungen während des gesamten Spiels zu überprüfen.

Während meine Implementierung ziemlich simpel ist, erledigt sie die Arbeit. Ich habe darüber geschrieben und meine Fragen hier geteilt .

Reed Olsen
quelle
2

Verwenden Sie die Ereignisberechnung . Machen Sie dann einige Vorbedingungen und Aktionen, die angewendet werden, nachdem die Vorbedingungen erfüllt sind:

  • Voraussetzungen: Sie haben 1000 Feinde getötet, Sie haben 2 Beine
  • Aktionen: Gib mir Lollypop, gib mir Super-Duper-Shotgun-13
  • first-time-actions: sag "du bist so geil!"

Verwenden Sie es wie folgt (nicht für Geschwindigkeit optimiert!):

  • Speichern Sie alle Statistiken.
  • Abfragestatistik für Voraussetzungen.
  • Aktionen anwenden.
  • Einmalige Aktionen anwenden.

Wenn Sie es schnell machen wollen:

  • Cache was immer du willst, speichere Teile davon in einigen Bäumen, Hashes ...
  • Nehmen Sie die Änderungen inkrementell vor, damit Sie nicht ständig alle Aktionen anwenden, sondern nur die neuen ...)

Hinweis

Es ist schwierig, die besten Ratschläge zu geben, da alle Dinge Vor- und Nachteile haben.

  • "Was ist die beste Datenstruktur?" impliziert "Welche Operationen möchten Sie am häufigsten ausführen? Suchen, Entfernen, Hinzufügen ..."
  • Grundsätzlich denken Sie in diesen Eigenschaften: einfache Codierung, Geschwindigkeit, Klarheit, Größe ...
user712092
quelle
0

Was ist los mit einer IF- Prüfung nach dem Erfolg?

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
quelle
3
"Ich bin auf der Suche nach etwas besser organisiertem und wartbarem als" sie alle als Bedingungen fest zu codieren ". '. Dies ist jedoch definitiv eine gute KISS-Methode für ein kleines Spiel.
Die kommunistische Ente