Quantcast
Channel: Envato Tuts+ Game Development
Viewing all articles
Browse latest Browse all 728

How to Code Unlockable Achievements for Your Game (A Simple Approach)

$
0
0

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 TwitterFacebook, or Google+.

  • Make Them Work for It: Designing Achievements for Your Games
  • Don’t Just Give It Away: Designing Unlocks for Your Games

  • Viewing all articles
    Browse latest Browse all 728

    Trending Articles