55
\$\begingroup\$

Specifically, what is the best way to implement an achievement system flexible enough to handle going beyond simple statistics-driven achievements such as "kill x enemies."

I'm looking for something more robust than a statistics based system and something more organized and maintainable than "hardcode them all as conditions." Some examples that are impossible or unwieldy in a statistics based system: "Slice a watermelon after a strawberry", "Go down a pipe while invincible", etc.

\$\endgroup\$
0

7 Answers 7

39
\$\begingroup\$

I think a kind of robust solution would be to go the object oriented way.

Depending on what kind of achievement you want to support, you need a way to query the current state of your game and/or the history of actions/events the game objects (like the player) have made.

Let's say you have a base Achievement class such as:

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

AbstractAchievement holds a reference to the state of the game. It's used to query the things that are happening.

Then you make concrete implementations. Let's use your examples:

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

Then it's up to you to decide when to check using the IsEarned() method. You could check at each game update.

A more efficient way would be, for instance, to have some kind of event manager. And then register events (such as PlayerHasSlicedSomethingEvent or PlayerGotInvicibleEvent or simply PlayerStateChanged) to a method that would take the achievement in parameter. Example:

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);
        }
    }
};
\$\endgroup\$
4
  • 14
    \$\begingroup\$ style nitpick: if(...) return true; else return false; is the same as return (...) \$\endgroup\$ Jun 17, 2011 at 18:04
  • 2
    \$\begingroup\$ That is a very good template for implementing an achievement system. Moreover, it still demonstrates the idea that you have to have a way to track game-states. I find that probably the most complicated idea. \$\endgroup\$
    – Nayrb
    Jul 7, 2011 at 13:53
  • \$\begingroup\$ @Spio you are a master men...! :D Simple and elegant solution. Congrat. \$\endgroup\$ Oct 3, 2013 at 17:04
  • \$\begingroup\$ +1 I think an event emit/collect system is a great way to handle this problem. \$\endgroup\$
    – ashes999
    Mar 15, 2014 at 20:55
14
\$\begingroup\$

Well, in short, achievements are unlocked when a certain condition is met. So you need to be able to produce if statements to check the condition you want.

For example, if you want to know that a level was completed or a boss was defeated, you'd need to have boolean flag turn true when these events happen.

Then:

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

You can make this as complex or simplistic as needed to match the condition that you're wanting.

Some info on Xbox 360 achievements can be found here.

\$\endgroup\$
4
  • 2
    \$\begingroup\$ +1 Great article, and essentially what I was going to suggest. Although I would make the Modal get its text from the achievement itself... just to avoid text hunting if you want to change something. \$\endgroup\$ Jul 21, 2010 at 20:16
  • \$\begingroup\$ @Noctrine - don't forget any code posted here should be treated as pseudo code - it's often necessary to use simplified code to get the point across. \$\endgroup\$
    – ChrisF
    Jul 21, 2010 at 20:21
  • 1
    \$\begingroup\$ Xbox 360 achievements link is dead. \$\endgroup\$
    – hangy
    Jan 8, 2011 at 14:48
  • 3
    \$\begingroup\$ Cache here: web.archive.org/web/20101030214905/http://www.xbox.com/en-US/… \$\endgroup\$
    – Matsemann
    Apr 30, 2012 at 16:50
8
\$\begingroup\$

What if you every action the player takes posts a message to the AchievementManager? Then the manager can check internally whether certain conditions have been met. First objects post messages:

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

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

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

And then the AchievementManager checks if it needs to do anything:

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

You'll probably want to do this with enumerations instead of strings though. ;)

\$\endgroup\$
3
  • \$\begingroup\$ This is along the lines of what I was thinking, but it still relies on a bunch of stuff being hardcoded in one function. Maintainability aside, at the very least every achievement has to be checked for eligibility every time through the function. \$\endgroup\$
    – lti
    Jul 22, 2010 at 17:29
  • 2
    \$\begingroup\$ Does it? You can put the conditionals in an external script, as long as you have some way of tracking what happened in the game. \$\endgroup\$
    – knight666
    Jul 22, 2010 at 19:06
  • 7
    \$\begingroup\$ -1 this reeks of bad design. If you're going to call the AchivementManager directly, just make each of those "messages" a separate function. If you're going to use messages, make a message-manager so that other classes can use the messages as well (I'm sure the Goomba would be interested in knowing he's been killed), and to remove that coupling to the AchievementManager in every class (which is what OP was asking how to avoid in the first place). And use an enum or separate classes for your messages, not string-literals - using string literals to pass around state is always a bad idea. \$\endgroup\$ Jun 17, 2011 at 18:16
3
\$\begingroup\$

The last design I used was based around having a set of per-user persistent counters and then have achievements key off a certain counter hitting a certain value. Most were a single achievement/counter pair where the counter would only ever be 0 or 1 (and the achievement triggered on >=1), but you can use this for "killed X dudes" or "found X chests" too. It also means you can setup counters for something that has no achievements, and it will still be tracked for future use.

\$\endgroup\$
3
\$\begingroup\$

When I implemented achievements in my last game, I made it all statistic based. Achievements are unlocked when our statistics reach a certain value. Consider Modern Warfare 2: the game is tracking tons of stats! How many shots have you taken with the SCAR-H? How many miles have you sprinted while using the Lightweight perk?

So in my implementation, I simply created a statistics engine, then created an achievement manager that runs really simple queries to check the status of achievements throughout gameplay.

While my implementation is pretty simplistic, it gets the job done. I wrote about it and shared my queries here.

\$\endgroup\$
2
\$\begingroup\$

Use Event Calculus. Then make some preconditions and actions which are applied after preconditions are met:

  • preconditions: you killed 1000 enemies, you have 2 legs
  • actions: give me lollypop, give me super-duper-shotgun-13
  • first-time-actions: say "you are so awesome!"

Use it like (non-optimized for speed!):

  • Store all statistics.
  • Query statistics for preconditions.
  • Apply actions.
  • Apply one-time actions once.

If You want to make it fast:

  • Cache whatever You want, store parts of it in some trees, hashes ...
  • Make changes incremental so You dont apply all the actions all the time but these which are new ...)

Note

It is hard to give best advice since all things have pros and cons.

  • "What is best datastructure?" implies "What operations You want to do with it most? Searching, removing, adding ..."
  • You basically think in these properties: ease of coding, speed, clarity, size ...
\$\endgroup\$
0
\$\begingroup\$

What's wrong with an IF checks after the achievement event occurs?

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);
\$\endgroup\$
1
  • 4
    \$\begingroup\$ 'I'm looking for something more organized and maintainable than "hardcode them all as conditions." '. Though this is definitely a good K.I.S.S. method for a small game. \$\endgroup\$ Nov 16, 2010 at 16:49

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .