Achievements are extremely popular among gamers. They can be used in a variety of ways, from teaching to measuring progress, but how can we code them? In this tutorial, I will present a simple approach for implementing achievements.
Note: Although this tutorial is written using AS3 and Flash, you should be able to use the same techniques and concepts in almost any game development environment.
You can download or fork the final code from the GitHub repo: https://github.com/Dovyski/Achieve
Achivements Code: Trick or Treat?
At first glance, programming an achievements system seems trivial – and that’s partially true. They are generally implemented with counters, each one representing an important game metric, such as the number of enemies killed, or the player’s lives.
An achievement is unlocked if those counters match specific tests:
killedEnemies = killedEnemies + 1; if(killedEnemies >= 10 && lives >= 2) { // unlock achievement }
There is nothing wrong with that approach, but imagine a test with 10 or more counters. Depending on the number of achievements (and counters), you may end up with spaghetti code replicated all over the place:
if(killedEnemies > 5 && lives > 2 && deaths <= 3 && perfectHits > 20 && ammo >= 100 && wrongHits <= 1) { // unlock achievement }
A Better Idea
My approach is also based on counters, but they are controlled by simple rules:
Achievements based on properties. Gray rectangles are active properties/achievements.
An achievement is unlocked when all of its related properties are active. An achievement may have one or more related properties, each one being managed by a single class, so there is no need to write if()
statements all over the code.
Here is the idea:
- Identify any interesting game metric (kills, deaths, mistakes, matches, etc).
- Every metric becomes a property, guided by an update constraint. The constraint controls whether the property should be changed when a new value arrives.
- The constraints are: ”update only if new value is greater then current value”; ”update only if new value is less then current value”; and “update no matter what the current value is”.
- Every property has an activation rule – for instance “kills is active if its value is greater than 10″.
- Check activated properties periodically. If all related properties of an achievement are active, then the achievement is unlocked.
In order to implement an achievement, one must define which properties should be active to unlock that achievement. After that the properties must be updated during the gameplay, and you’re done!
The next sections present an implementation for that idea.
Describing Properties and Achievements
The first implementation step is the representation of properties and achievements. The class Property
can be the following:
public class Property { private var mName :String; private var mValue :int; private var mActivation :String; private var mActivationValue :int; private var mInitialValue :int; public function Property(theName :String, theInitialValue :int, theActivation :String, theActivationValue :int) { mName = theName; mActivation = theActivation; mActivationValue = theActivationValue; mInitialValue = theInitialValue; } }
A property has a name (mName
), a value (mValue
, which is the counter), an activation value (mActivationValue
) and an activation rule (mActivation
).
The activation rule is something like “active if greater than” and it controls whether a property is active (more on that later). A property is said to be active when its value is compared to the activation value and the result satisfies the activation rule.
An achievement can be described as follows:
public class Achievement { private var mName :String; // achievement name private var mProps :Array; // array of related properties private var mUnlocked :Boolean; // achievement is unlocked or not public function Achievement(theId :String, theRelatedProps :Array) { mName = theId; mProps = theRelatedProps; mUnlocked = false; } }
An achievement has a name (mName
) and a flag to indicate whether it is already unlocked (mUnlocked
). The array mProps
contains an entry for every property needed to unlock the achievement. When all those properties are active, then the achievement should be unlocked.
Managing Properties And Achievements
All properties and achievements will be managed by a centralized class named Achieve
. This class should behave as a black box that receives property updates and tells whether an achievement was unlocked. Its basic structure is:
public class Achieve { // activation rules public static const ACTIVE_IF_GREATER_THAN :String = ">"; public static const ACTIVE_IF_LESS_THAN :String = "<"; public static const ACTIVE_IF_EQUALS_TO :String = "=="; private var mProps :Object; // dictionary of properties private var mAchievements :Object; // dictionary of achievements public function Achieve() { mProps = { }; mAchievements = { }; } }
Since all properties will be updated using their name as the lookup index, it’s convenient to store them in a dictionary (the mProps
attribute in the class). The achievements will be handled similarly, so they are stored in the same way (mAchievements
attribute).
In order to handle the addition of properties and achievements, we create the methods defineProperty()
and defineAchievement()
:
public function defineProperty(theName :String, theInitialValue :int, theaActivationMode :String, theValue :int) :void { mProps[theName] = new Property(theName, theInitialValue, theaActivationMode, theValue); } public function defineAchievement(theName :String, theRelatedProps :Array) :void { mAchievements[theName] = new Achievement(theName, theRelatedProps); }
Both methods simply add an entry to the property or the achievement dictionary.
Updating Properties
Now that the Achieve
class can handle properties and achievements, it’s time to make it able to update property values. A property will be updated during the game and it will act as a counter. For instance the property killedEnemies
should be incremented every time an enemy is destroyed.
Just two methods are needed for that: one to read and another to set a property value. Both methods belong to the Achieve
class and can be implemented as follows:
public function getValue(theProp :String) :int { return mProps[theProp].value; } private function setValue(theProp :String, theValue :int) :void { mProps[theProp].value = theValue; }
It’s also useful to have a method to add a value to a group of properties, something like a batch increment/decrement:
public function addValue(theProps :Array, theValue :int) :void { for (var i:int = 0; i < theProps.length; i++) { var aPropName :String = theProps[i]; setValue(aPropName, getValue(aPropName) + theValue); } }
Checking For Achievements
Checking for unlocked achievements is simple and easy: iterate over the achievements dictionary, checking whether all the related properties of an achievement are active.
In order to perform that iteration, first we need a method to check whether a property is active:
public class Property { // // the rest of the class code was omitted... // public function isActive() :Boolean { var aRet :Boolean = false; switch(mActivation) { case Achieve.ACTIVE_IF_GREATER_THAN: aRet = mValue > mActivationValue; break; case Achieve.ACTIVE_IF_LESS_THAN: aRet = mValue < mActivationValue; break; case Achieve.ACTIVE_IF_EQUALS_TO: aRet = mValue == mActivationValue; break; } return aRet; } }
Now let’s implement the checkAchievements()
method in the Achieve
class:
public function checkAchievements() :Vector { var aRet :Vector = new Vector(); for (var n :String in mAchievements) { var aAchivement :Achievement = mAchievements[n]; if (aAchivement.unlocked == false) { var aActiveProps :int = 0; for (var p :int = 0; p < aAchivement.props.length; p++) { var aProp :Property = mProps[aAchivement.props[p]]; if (aProp.isActive()) { aActiveProps++; } } if (aActiveProps == aAchivement.props.length) { aAchivement.unlocked = true; aRet.push(aAchivement); } } } return aRet; }
The checkAchievements()
method iterates over all achievements. At the end of every iteration it tests whether the number of active properties for that particular achievement equals the amount of related properties. If that is true, then 100% of the related properties for that achievement are active, so the player has unlocked a new achievement.
For convenience, the method returns a Vector
(which acts like a typed Array or List) containing all achievements that were unlocked during the check. Also, the method marks all achievements found as “unlocked”, so they will not be analysed again in the future.
Adding Constraints To Properties
So far properties have no constraints, which means any value passed through setValue()
will update the property. Imagine a property named killedWithASingleBomb
, which stores the number of enemies the player killed using a single bomb.
If its activation rule is “if greater than 5″ and the player kills six enemies, it should unlock the achievement. However, assume the checkAchievements()
method was not invoked right after the bomb exploded. If the player detonates another bomb and it kills three enemies, the property will be updated to 3
.
That change will cause the player to miss the achievement. In order to fix that, we can use the property activation rule as a constraint. It means a property with “if greater then 5″ will be updated only if the new value is greater than the current one:
private function setValue(theProp :String, theValue :int) :void { // Which activation rule? switch(mProps[theProp].activation) { case Achieve.ACTIVE_IF_GREATER_THAN: theValue = theValue > mProps[theProp].value ? theValue : mProps[theProp].value; break; case Achieve.ACTIVE_IF_LESS_THAN: theValue = theValue < mProps[theProp].value ? theValue : mProps[theProp].value; break; } mProps[theProp].value = theValue; }
Resetting and Tagging Properties
Often achievements are not related to the whole game, but specific to periods such as levels. Something like “beat a level killing 40 or more enemies” must be counted during the level, then reset so the player can try again in the next level.
A possible solution for that problem is the addition of tags to properties. The use of tags allows the manipulation of a groups of properties. Using the previous example, the killedEnemies
property can be tagged as levelStuff
, for instance.
As a consequence, it’s possible to check for achievements and reset properties based on tags:
// Define the property using a tag defineProperty("killedEnemies", ..., "levelStuff"); if(levelIsOver()) { // Check for achievements, but only the ones based on properties // tagged with "levelStuff". All other properties will be ignored, // so it will not interfere with other achievements. checkAchievements("levelStuff"); // Reset all properties tagged with 'levelStuff' resetProperties("levelStuff"); }
The method checkAchievements()
becomes much more versatile with tags. It can be invoked any time now, as long as it operates the correct group of properties.
Usage Demonstration
Below is a code snippet demonstrating how to use this achievement implementation:
var a :Achieve = new Achieve(); function initGame() :void { a.defineProperty("killedEnemies", 0, Achieve.ACTIVE_IF_GREATER_THAN, 10, "levelStuff"); a.defineProperty("lives", 3, Achieve.ACTIVE_IF_EQUALS_TO, 3, "levelStuff"); a.defineProperty("completedLevels", 0, Achieve.ACTIVE_IF_GREATER_THAN, 5); a.defineProperty("deaths", 0, Achieve.ACTIVE_IF_EQUALS_TO, 0); a.defineAchievement("masterKill", ["killedEnemies"]); // Kill 10+ enemies. a.defineAchievement("cantTouchThis", ["lives"]); // Complete a level and don't die. a.defineAchievement("nothingElse", ["completedLevels"]); // Beat all 5 levels. a.defineAchievement("hero", ["completedLevels", "deaths"]); // Beat all 5 levels, do not die during the process } function gameLoop() :void { if(enemyWasKilled()) { a.addValue(["killedEnemies"], 1); } if(playerJustDied()) { a.addValue(["lives"], -1); a.addValue(["deaths"], 1); } } function levelUp() :void { a.addValue(["completedLevels"], 1); a.checkAchievements(); // Reset all properties tagged with 'levelStuff' a.resetProperties("levelStuff"); }
Conclusion
This tutorial demonstrated a simple approach for implementing achievements in code. Focusing on managed counters and tags, the idea tries to eliminate several tests spread all over the code.
You can download or fork the code from its GitHub repo: https://github.com/Dovyski/Achieve
I hope this approach helps you implement achievements in a simpler and better way. Thank you for reading! Don’t forget to keep up to date by following us on Twitter, Facebook, or Google+.