Click here to Skip to main content
12,510,818 members (50,601 online)
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

9.3K views
10 bookmarked
Posted

Simple Achievement System in C#

, 8 May 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
Achievements are becoming more and more usual in games. They provide the player a sense of accomplishment and progress by rewarding them with badges that proves their skill and experience. Some achievements are simple and other require a combination of particular actions to unlock.

Achievements are becoming more and more usual in games. They provide the player a sense of accomplishment and progress by rewarding them with badges that proves their skill and experience. Some achievements are simple and other require a combination of particular actions to unlock. In this article I show you how to make a simple Achievement System using C# and will demonstrate it using Unity3D, but this should be easy enough for you to port it to whatever language you’re more familiar with for your games.

The main idea behind this achievement system is to count how many occurrences a player has done a particular action and keep track of it in a AchievementManager class. When the AchievementManager counts an action a specific number of times (e.g, deaths, new games, trades, etc..), or, depending on the event type, counts a higher number than a particular amount (e.g, score, or damage), an event is fired to unlock the achievement, reward is given, i.e, medal, score bonus, coin, a message, etc.

I’m going to create a simple data structure to hold the achievement data:

public class Achievement
{
	public int countToUnlock { get; set; }
	public bool isUnlocked { get; set; }
    public string Message { get; set; }
}

For the sake of simplicity, I’m not declaring private variables, nor constructors. This will do just fine for the purpose of this article. Might as well have declared that as a structure. Oh well.

I’ll have an achievement type enumerator and a achievement argument for the events.

public enum AchievementType
{
	Tap,
	Score,
	Die,
	Start
};

This is not the most elegant way to creating Achievement Types. I could have set them dynamically as Strings, but then I would have to search the dictionary against strings which is less efficient as this. For the simplicity of this system, I’ll be using “hard coded” achievement types.

public class AchievementEventArg : EventArgs
{
	public Achievement Data;
	public AchievementEventArg( Achievement e )
	{
		Data = e;
	}
}

With this, we can start to think about our AchievementManager class. But first, how do we want to handle the actions made by the user. I think one easy enough method would be to call the AchivementManager class, and Register a particular action occurrence, as such:

_achievementManager.RegisterEvent(AchievementType.Die);

So whenever my player dies, I’ll call the RegisterEvent on AchievementManager and chose the Die achievement type.

The code behind RegisterEvent would look like this:

public void RegisterEvent( AchievementType type )
{
	if( !_achievementKeeper.ContainsKey(type) )
	    return;

	switch(type)
	{
	case AchievementType.Tap:
		_achievementKeeper[type]++;
		break;
	case AchievementType.Start:
		_achievementKeeper[type]++;
		break;
	case AchievementType.Score:
		_achievementKeeper[type]++;
		break;
	case AchievementType.Die:
		_achievementKeeper[type]++;
		break;
	}
	ParseAchievements(type);
}

RegisterEvent will check the achievement type and increment it on the _achievementkeeper data structure. This _achievementkeeper  is a simple Dictionary declared as such:

private Dictionary<AchievementType, int> _achievementKeeper;

Since our achievement keeper is a dictionary with the key being the Achievement type, we just have to increment the dictionary’s value at that key. As you can see, I switch over all Achievement types, and increment the occurrences of the achievements. I also check whether the achievement type is present in the dictionary. Should the dictionary have to key, it would return a exception, which I am not handling in a try catch, again for the sake of simplicity.

At the end of the RegisterEvent method, I call ParseAchievements of that same type. ParseAchievements will go through all achievements that are on the dictionary of the given type. As there may be more than one achievement per type, I loop through all achievements of that type and select only the ones that are still locked. After that, I differentiate the Score achievement type from all the others. If the achievement keeper for Score is Greater than or equal to the current iteration of the achievement, then, I’ll raise the event to unlock that achievement. For the other types of achievements, I just compare for equality. I am differentiating this because scores can change rapidly, and I think of it like comparing two floats for equality, you might get it or not, so better safe than sorry.

Here’s the code for ParseAchievement:

public void ParseAchievements( AchievementType type )
	{
		foreach( var kvp in _achievements.Where( a => a.Key == type ) ) // use System.Linq.
		{
			foreach( var ach in kvp.Value.Where( a => a.isUnlocked == false ) ) 
			{
				if( type == AchievementType.Score ) {
					if( _achievementKeeper[type] >= ach.countToUnlock )
						RaiseAchievementUnlocked(ach);
				}
				else if( _achievementKeeper[type] == ach.countToUnlock ) 
				{
					RaiseAchievementUnlocked(ach);
				}
			}
		}
	}

As mentioned, the second loop will go through all the achievements of that type. They are stored in another dictionary declared as such:

private Dictionary<AchievementType, List<Achievements>> _achievements;

As you can see, I have all achievements per type linked to lists of achievements of that type. You could do it in other ways, but I found this one to work pretty nicely. This is how I load all achievements:

var listStart = new List();
		listStart.Add(new Achievement() { countToUnlock = 1, isUnlocked = false, Message = "First Time Playing!" });
		listStart.Add(new Achievement() { countToUnlock = 5, isUnlocked = false, Message = "Fifth Time is the Charm?" });
		listStart.Add(new Achievement() { countToUnlock = 19, isUnlocked = false, Message = "Hello and Welcome Back!" });
		listStart.Add(new Achievement() { countToUnlock = 25, isUnlocked = false, Message = "Tapping Time!!" });
		listStart.Add(new Achievement() { countToUnlock = 50, isUnlocked = false, Message = "Perseverance Lvl 1!" });
_achievements.Add(AchievementType.Start, listStart );

First, I create a list of Achievements, where I manually create them ( they should be loaded from a file or database ). Then, I add them into the _achievements dictionary and set all the achievements in the list of type Start ( to be used when the player starts the game ).

And finally the RaiseAchievementUnlocked event handler and event raiser:

public event EventHandler AchievementUnlocked;
	protected virtual void RaiseAchievementUnlocked( Achievement ach )
	{
		// unlock the event
		ach.isUnlocked = true;

		var del = AchievementUnlocked as EventHandler;
		if( del != null )
		{
			del( this, new AchievementEventArg(ach) );
		}
	}

The EventHandler only declares a public event, so another class, say the GameManager class, can register this event, and when a event is fired, get the info on the achievement. The RaiseAchievementUnlocked method accepts the Achievement as parameter to be sent through the delegate, wrapped in a AchievementEventArgs class, only after setting the Achievement as unlocked.  When the GameManager class, received the event, it will get the data available from the Achievement, namely, countToUnlock, isUnlocked, and in this case, the only useful one for the player, Message. Then its up to the GameManager to show a message to the user letting him know he/she has unlocked the Achievement.

Here’s the full code for the AchievementManager.cs class:

public class AchievementsManager
{
    private Dictionary<AchievementType, List<Achievement>> _achievements;
	private Dictionary<AchievementType, int> _achievementKeeper;

	public event EventHandler AchievementUnlocked;
	protected virtual void RaiseAchievementUnlocked( Achievement ach )
	{
		// unlock the event
		ach.isUnlocked = true;

		var del = AchievementUnlocked as EventHandler;
		if( del != null )
		{
			del( this, new AchievementEventArg(ach) );
		}
	}

    public AchievementsManager( )
    {
		_achievementKeeper = new Dictionary<AchievementType, int>();
		// ideally, load here previous, saved values.
		// tap = 0
		// die = 1
		// start = 12
		// score = 1231

		_achievements = new Dictionary<AchievementType, List<Achievement>>();

		var listStart = new List();
		listStart.Add(new Achievement() { countToUnlock = 3, isUnlocked = false, Message = "First Time Playing!" });
		listStart.Add(new Achievement() { countToUnlock = 8, isUnlocked = false, Message = "Fifth Time is the Charm?" });
		listStart.Add(new Achievement() { countToUnlock = 10, isUnlocked = false, Message = "Hello and Welcome Back!" });
		listStart.Add(new Achievement() { countToUnlock = 16, isUnlocked = false, Message = "Tapping Time!!" });
		listStart.Add(new Achievement() { countToUnlock = 50, isUnlocked = false, Message = "Perseverance Lvl 1!" });

		_achievements.Add(AchievementType.Start, listStart );
    }

	public void RegisterEvent( AchievementType type )
	{
		if( !_achievementKeeper.ContainsKey(type) )
			return;

		switch(type)
		{
		case AchievementType.Tap:
			_achievementKeeper[type]++;
			break;
		case AchievementType.Start:
			_achievementKeeper[type]++;
			break;
		case AchievementType.Score:
			_achievementKeeper[type]++;
			break;
		case AchievementType.Die:
			_achievementKeeper[type]++;
			break;
		}

		ParseAchievements(type);
	}

	public void ParseAchievements( AchievementType type )
	{
		foreach( var kvp in _achievements.Where( a => a.Key == type ) )
		{
			foreach( var ach in kvp.Value.Where( a => a.isUnlocked == false ) ) 
			{
				if( type == AchievementType.Score ) {
					if( _achievementKeeper[type] >= ach.countToUnlock )
						RaiseAchievementUnlocked(ach);
				}
				else if( _achievementKeeper[type] == ach.countToUnlock ) 
				{
					RaiseAchievementUnlocked(ach);
				}
			}
		}
	}
}

Here is an example usage of the AchievementManager class. Declaration is trivial:

private AchievementsManager _achievementManager;

In the constructor or initialization method for the GameManager class, I instantiate the AchievementManager, register to the AchievementUnlocked event and specify I want to call a method to show the achievement, which is defined as a lambda expression ( That I will talk about in the second part of this tutorial ). Finally, as I am starting the game, I call the register event method on the AchievementManager and register a Start Achievement Type.

_achievementManager = new AchievementsManager();
_achievementManager.AchievementUnlocked += (sender, e) =>
{ 
	ShowAchievement(e.Data); 
};
_achievementManager.RegisterEvent(AchievementType.Start);

That pretty much covers this simple achievement system that can be used in any game. I hoped you enjoyed this tutorial, feel free to use this, add to it, comment and give suggestions. Part 2 will cover how to effectively use this in Unity3D engine. To make your life easier, here the gist of the whole file. Enjoy.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Mikea15
Software Developer SkyBelow
Portugal Portugal
I am a regular guy who likes to design and write software and games. I'm currently living in Covilhã, Portugal, where I'm finishing my MSc in Computer Science and Engineering.

In my spare time I develop and design video games for PC and mobile platforms using Unity3D. I also like to develop tools to help developers.

I also like to workout, play sports, go to the movies and hangout with my friends.

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionVery good article! Pin
Volynsky Alex8-May-14 23:47
professionalVolynsky Alex8-May-14 23:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.160929.1 | Last Updated 9 May 2014
Article Copyright 2014 by Mikea15
Everything else Copyright © CodeProject, 1999-2016
Layout: fixed | fluid