Comment puis-je mettre en place un cadre flexible pour gérer les réalisations?

54

Plus précisément, quel est le meilleur moyen de mettre en œuvre un système de réalisation suffisamment souple pour pouvoir aller au-delà de simples réalisations basées sur des statistiques telles que "tuer x ennemis".

Je cherche quelque chose de plus robuste qu'un système basé sur des statistiques et quelque chose de plus organisé et plus facile à gérer que "les coder en dur en tant que conditions". Quelques exemples impossibles ou difficiles à manier dans un système basé sur des statistiques: "Découpez une pastèque après une fraise", "Descendez un tuyau alors qu’il est invincible", etc.

lti
la source

Réponses:

39

Je pense qu'une solution solide consisterait à adopter une approche orientée objet.

Selon le type de réalisation que vous souhaitez soutenir, vous avez besoin d’un moyen d’interroger l’état actuel de votre jeu et / ou l’historique des actions / événements créés par les objets du jeu (comme le joueur).

Supposons que vous ayez une classe de réussite de base telle que:

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

AbstractAchievementdétient une référence à l'état du jeu. Il est utilisé pour interroger les choses qui se passent.

Ensuite, vous faites des implémentations concrètes. Utilisons vos exemples:

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

Ensuite, c’est à vous de décider quand vérifier avec cette IsEarned()méthode. Vous pouvez vérifier à chaque mise à jour du jeu.

Un moyen plus efficace serait, par exemple, d’avoir une sorte de gestionnaire d’événements. Et puis enregistrez les événements (tels que PlayerHasSlicedSomethingEventou PlayerGotInvicibleEventou simplement PlayerStateChanged) à une méthode qui prendrait la réalisation en paramètre. Exemple:

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
la source
13
style nitpick: if(...) return true; else return false;c'est pareil quereturn (...)
BlueRaja - Danny Pflughoeft le
2
C'est un très bon modèle pour la mise en œuvre d'un système de réalisation. De plus, cela démontre toujours l’idée qu’il faut un moyen de suivre les états de jeu. Je trouve ça probablement l’idée la plus compliquée.
Bryan Harrington
@Spio vous êtes un maître des hommes ...! : D Solution simple et élégante. Féliciter
Diego Palomar
+1 Je pense qu'un système d'émission / collecte d'événements est un excellent moyen de gérer ce problème.
ashes999
14

En bref, les réalisations sont déverrouillées lorsqu'une certaine condition est remplie. Vous devez donc pouvoir produire des déclarations if pour vérifier la condition souhaitée.

Par exemple, si vous voulez savoir qu'un niveau est terminé ou qu'un boss est vaincu, vous devez activer le drapeau booléen lorsque ces événements se produisent.

Ensuite:

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

Vous pouvez rendre cela aussi complexe ou simpliste que nécessaire pour répondre à la condition souhaitée.

Vous trouverez quelques informations sur les réalisations de la Xbox 360 ici .

Bryan Denny
la source
2
+1 Grand article, et essentiellement ce que j'allais suggérer. Bien que je ferais en sorte que le modal tire son texte de la réalisation elle-même ... juste pour éviter de chercher du texte si vous voulez changer quelque chose.
Jesse Dorsey
@ Noctrine - n'oubliez pas que tout code publié ici doit être traité comme un pseudo-code - il est souvent nécessaire d'utiliser un code simplifié pour faire passer le message.
ChrisF
1
Le lien des réalisations Xbox 360 est mort.
hangy
3
Cachez
8

Que se passe-t-il si chaque action du joueur envoie un message à AchievementManager? Ensuite, le responsable peut vérifier en interne si certaines conditions sont remplies. Les premiers objets postent des messages:

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

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

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

Et puis les AchievementManagervérifications s’il faut faire quelque chose:

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

Vous voudrez probablement faire cela avec des énumérations au lieu de chaînes. ;)

chevalier666
la source
Cela va dans le sens de ce que je pensais, mais cela repose toujours sur un tas de choses codées en dur dans une fonction. La maintenabilité mise à part, au moins chaque réalisation doit faire l’objet d’une vérification de son éligibilité à chaque fois par le biais de la fonction.
lti
2
Le fait-il? Vous pouvez insérer les conditions dans un script externe, à condition de pouvoir suivre ce qui s'est passé dans le jeu.
knight666
6
-1 cette odeur de mauvais design. Si vous appelez directement AchivementManager, faites de chacun de ces "messages" une fonction distincte. Si vous allez utiliser des messages, créez un gestionnaire de messages afin que les autres classes puissent également les utiliser (je suis sûr que le Goomba voudrait savoir qu'il a été tué), et supprimer ce couplage à AchievementManagerchaque fois. classe (qui est ce que OP demandait comment éviter en premier lieu). Et utilisez une énumération ou des classes séparées pour vos messages, et non des chaînes de caractères. L'utilisation de chaînes de chaînes pour transmettre l'état est toujours une mauvaise idée.
BlueRaja - Danny Pflughoeft
4

La dernière conception que j'ai utilisée reposait sur le fait de disposer d'un ensemble de compteurs persistants par utilisateur, puis de permettre à des réalisations de désactiver un certain compteur atteignant une certaine valeur. La plupart étaient une seule paire réalisation / compteur où le compteur ne serait jamais que 0 ou 1 (et la réalisation déclenchée sur> = 1), mais vous pouvez également l'utiliser pour "mecs X tués" ou "coffres X trouvés" aussi. Cela signifie également que vous pouvez configurer des compteurs pour quelque chose qui n'a pas de succès et qu'il sera toujours suivi pour une utilisation future.

coderanger
la source
3

Lorsque j'ai mis en œuvre des réalisations dans mon dernier jeu, j'ai tout basé sur des statistiques. Les réalisations sont débloquées lorsque nos statistiques atteignent une certaine valeur. Considérez Modern Warfare 2: le jeu contient des tonnes de statistiques! Combien de coups avez-vous pris avec le SCAR-H? Combien de miles avez-vous sprinté en utilisant le bonus Lightweight?

Ainsi, dans mon implémentation, j'ai simplement créé un moteur de statistiques, puis un gestionnaire de réalisations qui exécute des requêtes très simples pour vérifier l'état des réalisations tout au long du jeu.

Bien que ma mise en œuvre soit assez simpliste, elle fait le travail. J'ai écrit à ce sujet et partagé mes questions ici .

Reed Olsen
la source
2

Utilisez le calcul d'événement . Ensuite, établissez des conditions préalables et des actions à appliquer une fois les conditions préalables remplies:

  • conditions préalables: vous avez tué 1000 ennemis, vous avez 2 jambes
  • actions: donnez-moi sucette, donnez-moi super-duper-shotgun-13
  • première action: dites "vous êtes si génial!"

Utilisez-le comme (non optimisé pour la vitesse!):

  • Stocker toutes les statistiques.
  • Statistiques de requête pour les conditions préalables.
  • Appliquer des actions.
  • Appliquez des actions uniques une fois.

Si vous voulez faire vite:

  • Cachez ce que vous voulez, stockez des parties dans des arbres, des hachages ...
  • Effectuez des modifications incrémentielles afin de ne pas appliquer toutes les actions tout le temps mais celles-ci sont nouvelles ...)

Remarque

Il est difficile de donner les meilleurs conseils car toutes les choses ont des avantages et des inconvénients.

  • "Quelle est la meilleure structure de données?" implique "Quelles opérations tu veux en faire le plus? Chercher, supprimer, ajouter ..."
  • Vous pensez fondamentalement dans ces propriétés: facilité de codage, rapidité, clarté, taille ...
utilisateur712092
la source
0

Quel est le problème avec un contrôle IF après l'événement de réalisation?

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
la source
3
"Je cherche quelque chose de plus organisé et facile à gérer que" les coder en dur en tant que conditions ". '. Bien que ce soit définitivement une bonne méthode KISS pour un petit jeu.
Le canard communiste