Wednesday, December 2, 2015

architecture - How can I set up a flexible framework for handling achievements?


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.



Answer



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

}
};

No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...