Developing for Modern Windows

Tips, tricks, and guides for developing on modern Windows platforms

Add Achievements to your Windows 8.1 Store Game (C#/XAML)

Achievements make games more fun and engaging. This tutorial shows how to add achievements to your own game, with the delight of toast notifications when your players earn an achievement.

toast

Introduction

This tutorial covers a generic achievement add-on, which you can adapt to your own games easily. The example is for Windows 8.1 Store apps (in C# and XAML), but the basic ideas can be easily adapted elsewhere. You’ll need a minimum of Visual Studio Express 2013 for Windows to follow this tutorial.

We’ll create five things:

  1. An Achievement class, used to give each achievement a name, image, points, etc.
  2. An AchievementsManager static class to test achievements and display notifications
  3. An AchievementsList class, where we define all our achievements
  4. A “game” with three sample achievements to reward button clicks
  5. A method to display toast notifications for unlocked achievements.

Overview

We will set up our app so that the core game code only ever needs to call methods that check for achievements progress. Everything else is kept separate from the main code. AchievementsManager checks the achievements with a predefined list, then displays toast notifications when one is unlocked.

flowchart

 

Step 0

Create a new Visual Studio project with C#/XAML and call it Achievements.

Detailed Instructions

new_project_achievements

  1. Open Visual Studio.
  2. From the top menu bar select FILE > New Project…
  3. In the New Project window, select Templates > Visual C# > Store Apps > Windows Apps from the left-hand panel.
  4. From the list of templates in the middle section, click once on Blank App (Windows) to highlight it.
  5. In the Name: field near the bottom of the screen replace the default (usually App1) with Achievements.
  6. Click OK in the lower-right corner.

Step 1 – the Achievement Class

Each achievement will inherit from a base achievement class containing how many points it’s worth, what it’s called, and so on, as well as the code that tests if the achievement conditions are met.

Add a new class to your solution called Achievements.cs.

Detailed Instructions

  1. In the Solution Explorer pane, right-click on the  project name, select Add, then Class…

create_new_class

  1. In the Add New Item window rename the new class Achievement.cs by changing the Name: field:

name_class

Open the class you just created (double-click Achievement.cs in the Solution Explorer pane). Paste in the following code (replace the automatic code that Visual Studio put in there):


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Storage;

namespace Achievements
{
    public class Achievement
    {
        public string Image { get; set; } // small 60x60 image displayed in the toast notification
        public string Description { get; set; } // used to describe how the achievement is earned
        public int Points { get; set; } // how many game points this achievement is worth
        public AchievementsManager.AchievementCategory Category; // category for lumping achievements together for efficiency
        public string Name { get; set; } // title - try to have fun, witty titles

        public bool Achieved
        // is this achievement earned yet?
        // used to make sure that conditions are not checked for achievements that are already won
        // does not test the achievement conditions, only returns whether this is marked as gained or not
        // use when listing achievements and check BEFORE testing achievements
        {
            get
            {
                // stored in LocalSettings as ("achieved_" + achievement name)
                // if the value is not stored the default will be false
                if (ApplicationData.Current.LocalSettings.Values.ContainsKey("achieved_" + Name))
                {
                    string achStatus = ApplicationData.Current.LocalSettings.Values["achieved_"; + Name].ToString();
                    if (achStatus == "True")
                        return true;
                    else
                        return false;
                }
                else
                {
                    return false;
                }

            }

            set
            {
                if (ApplicationData.Current.LocalSettings.Values.ContainsKey("achieved_" + Name))
                    ApplicationData.Current.LocalSettings.Values["achieved_" + Name] = value.ToString();
                else
                    ApplicationData.Current.LocalSettings.Values.Add("achieved_" + Name, value.ToString());

            }

        }

        public bool CheckIfAchieved()
        // run the when you want to actually test for the achievement conditions, such as when a level is completed
        // if not, it goes on to test the achievement progress
        {
             if (Achieved) // check if the achievement was already marked as achieved
                return true;
            else // if not marked as achieved test if the conditions are now met to unlock it
                 return AchievedTest;

        }

    public virtual bool AchievedTest // virtual, must always be overridden by each achievemnt, which must have unique requisites
        // only call this if the progress has not been marked done (via Achieved() )
        // DO NOT use this when displaying a list of achievements, as it will test ALL achievements and reduce performance

    {
        get
        {

            return false; // code here doesn't matter as it will be overridden
        }

    }

    }
}

Note: AchievementCategory will be described in the AchievementsManager class (so ignore the ‘red squiggly’ underneath it for now).

Step 2 – Managing Achievements

I like to use ‘manager’ classes to implement modular functionality. My convention is to allow the main program to only communicate directly with the manager class, and let the manager class do all the actual work. Our manager class will check achievements and notify the user when one is earned; the main program will ask the manager to check achievement progress when required.

We’ll include the following in AchievementsManager:

  • The definition of an enum type that holds the different achievement categories
  • Methods to check achievements by category
  • A method to display toast notifications.

We use categories so we can check only the achievements that could be triggered by a particular event. For example, if we have a category ‘Levels’ that contains achievements for ’10 Levels Completed’, ’20 Levels Completed’, etc., we should only check those achievements when the player completes a level, not every time they kill a goblin (which would have its own category, such as ‘GoblinsKilled’ or ‘EnemiesSlain’).

Add another new class to your solution, and call it AchievementsManager.cs. Paste in the following code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Notifications;
using Windows.Data.Xml.Dom;

namespace Achievements
{
    public static class AchievementsManager
    {
        public enum AchievementCategory
        {
        // each achievement has a category, which determines when it is checked for completion
        // for example achievements that check the number of completed levels only get checked when a level is completed
        // this will save on checking ALL achievements whenever something in the app occurs that may trigger an achievement

            ButtonOne, // checked whenever button 1 is pressed
            ButtonTwo, // checked whenever button 2 is pressed
            TotalButtons // checked when any button is pressed

        } ;

        public static void TestButtonOneAchievements()
        {
            // check each achievement with the ButtonOne category
            foreach (Achievement cheev in AchievementsList.AllAchievements)
            {
                if (cheev.Category == AchievementCategory.ButtonOne)
                {
                    // this checks the achievement by running the code that tests its completedness
                    var c = cheev.CheckIfAchieved();
                }
            }
        }

        public static void TestButtonTwoAchievements()
        {
            foreach (Achievement cheev in AchievementsList.AllAchievements)
            {
                if (cheev.Category == AchievementCategory.ButtonTwo)
                {
                    var c = cheev.CheckIfAchieved();
                }
            }
        }

   public static void TestTotalButtonsAchievements()
        {
            foreach (Achievement cheev in AchievementsList.AllAchievements)
            {
                if (cheev.Category == AchievementCategory.TotalButtons)
                {
                    var c = cheev.CheckIfAchieved();
                }
            }
        }

    public static void DisplayAchievementToast(Achievement cheev)
    // displays a toast notification using data from an achievement that has just been unlocked
    {
        var toastTemplate = ToastTemplateType.ToastImageAndText04;
        XmlDocument toastXml = ToastNotificationManager.GetTemplateContent(toastTemplate);

        XmlNodeList toastTextElements = toastXml.GetElementsByTagName("text");
        toastTextElements[0].AppendChild(toastXml.CreateTextNode(cheev.Name));
        toastTextElements[1].AppendChild(toastXml.CreateTextNode(cheev.Description));
        toastTextElements[2].AppendChild(toastXml.CreateTextNode(cheev.Points.ToString() + " pts"));

        // to add an image, put some images in your solution and add their file paths to the .Image properties of your achievements
        XmlNodeList toastImageAttributes = toastXml.GetElementsByTagName("image");
        ((XmlElement)toastImageAttributes[0]).SetAttribute("src", cheev.Image);

        var toasty = new ToastNotification(toastXml);

        ToastNotificationManager.CreateToastNotifier().Show(toasty);
        }
    }
}

As before, ignore the ‘red squiggles’ that refer to code we haven’t yet implemented.

Note: An alternative it to replace all of the checking methods with a generic one, and pass the category from your main code.

Step 3 – Creating Achievements

Now that we have an achievement class, and a manager to handle checking for achievements, it’s time to create a few achievements. In your own game you will have all kinds of wonderful things to make your players strive for: gems, levels, hidden items, points, etc., but in our example we’ll just have three achievements that reward the player for clicking some buttons.

Create a new class called AchievementsList.cs and enter the code below:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Storage;

namespace Achievements
{
            public static class AchievementsList
            {
                // initialise the list of achievements
                public static List<Achievement> AllAchievements = new List<Achievement>();

                static AchievementsList()
                {
                    // add an instance of each achievement into our list
                    AllAchievements.Add(new Achievement1());
                    AllAchievements.Add(new Achievement2());
                    AllAchievements.Add(new Achievement3());
                }

                public class Achievement1 : Achievement
                // refer to the Achievement class earlier in this tutorial for descriptions of these properties
                {
                    public Achievement1()
                    {
                        Name = &quot;Button One Master&quot;;
                        Points = 5;
                        Description = "Pressed Button One 5 times!";
                        Image = "/Assets/AchievementsImages/star_1.png";
                        Category = AchievementsManager.AchievementCategory.ButtonOne;
                    }

                    public override bool AchievedTest
                    {
                        get
                        {
                            if (ApplicationData.Current.LocalSettings.Values.ContainsKey("button1presses"))
                            {
                                string numOfPresses = ApplicationData.Current.LocalSettings.Values["button1presses"].ToString();
                                int numAsInt = Convert.ToInt16(numOfPresses);
                                if (numAsInt >= 5)
                                {
                                    AchievementsManager.DisplayAchievementToast(this);
                                    Achieved = true;
                                    return true;
                                }
                                else
                                    return false;
                            }
                            else
                                return false;
                        }
                    }
                }

                public class Achievement2 : Achievement
                {
                    public Achievement2()
                    {
                        Name = "Button Two Presser!";
                        Points = 5;
                        Description = "Pushed Button Two 10 times!";
                        Image = "/Assets/AchievementsImages/star_2.png";
                        Category = AchievementsManager.AchievementCategory.ButtonTwo;

                    }

                    public override bool AchievedTest
                    {
                        get
                        {
                            if (ApplicationData.Current.LocalSettings.Values.ContainsKey("button2presses"))
                            {
                                string numOfPresses = ApplicationData.Current.LocalSettings.Values["button2presses"].ToString();
                                int numAsInt = Convert.ToInt16(numOfPresses);
                                if (numAsInt >= 10)
                                {
                                    AchievementsManager.DisplayAchievementToast(this);
                                    Achieved = true;
                                    return true;
                                }
                                else
                                    return false;
                            }
                            else
                                return false;

                        }
                    }

                }

                public class Achievement3 : Achievement
                {
                    public Achievement3()
                    {
                        Name = "Mega Presser!";
                        Points = 10;
                        Description = "Pressed a total of 20 times!";
                        Image = "/Assets/AchievementsImages/star_2.png";
                        Category = AchievementsManager.AchievementCategory.TotalButtons;

                    }

                    public override bool AchievedTest
                    {
                        get
                        {
                            int totalPresses = 0;
                            if (ApplicationData.Current.LocalSettings.Values.ContainsKey("button1presses"))
                            {
                                string numOfPresses = ApplicationData.Current.LocalSettings.Values["button1presses"].ToString();
                                int numAsInt = Convert.ToInt16(numOfPresses);
                                totalPresses += numAsInt;
                            }
                            if (ApplicationData.Current.LocalSettings.Values.ContainsKey("button2presses"))
                            {
                                string numOfPresses = ApplicationData.Current.LocalSettings.Values["button2presses"].ToString();
                                int numAsInt = Convert.ToInt16(numOfPresses);
                                totalPresses += numAsInt;
                            }
                            if (totalPresses >= 20)
                            {
                                AchievementsManager.DisplayAchievementToast(this);
                                Achieved = true;
                                return true;
                            }
                            else
                                return false;
                        }

                    }

                }
            }
}

Note: these achievements have image properties. To get images to display in the notifications, put images into your project with matching filenames. You can use the image from the sample project or make your own. If you don’t add images, this code will still work, but your toasts won’t have pictures.

Step 4 – The game

Let’s put these pieces into some context. We’ll create a simple game with two buttons. Each button press will get recorded, and we’ll check each of the achievements for each button when that button is pressed, displaying a toast notification whenever an achievement is reached. Add two buttons to your app.

Copy the code below into your MainPage.xaml (within the <Grid> control) or manually create two buttons called ButtonOne and ButtonTwo.

&lt;Button x:Name=&quot;ButtonOne&quot; Content=&quot;1&quot; HorizontalAlignment=&quot;Left&quot; Margin=&quot;437,381,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;161&quot; Height=&quot;65&quot; Click=&quot;ButtonOne_Click&quot;/&gt;
        &lt;Button x:Name=&quot;ButtonTwo&quot; Content=&quot;2&quot; HorizontalAlignment=&quot;Left&quot; Margin=&quot;787,429,0,0&quot; VerticalAlignment=&quot;Top&quot; Height=&quot;59&quot; Width=&quot;91&quot; Click=&quot;ButtonTwo_Click&quot;/&gt;

Now add the following event handlers to the MainPage.xaml.cs. Here we keep track of the number of presses for each button, and we also call our achievement tests. Paste the following code after the public MainPage() constructor method:

         // these two methods record how many times each button is pressed in application settings data
        private void ButtonOne_Click(object sender, RoutedEventArgs e)
        {
            if (Windows.Storage.ApplicationData.Current.LocalSettings.Values.ContainsKey("button1presses"))
            {
                string numOfPresses = Windows.Storage.ApplicationData.Current.LocalSettings.Values["button1presses"].ToString();
                int numAsInt = Convert.ToInt16(numOfPresses);
                numAsInt++;
                Windows.Storage.ApplicationData.Current.LocalSettings.Values["button1presses"] = numAsInt.ToString();
            }
            else
                Windows.Storage.ApplicationData.Current.LocalSettings.Values.Add("button1presses", "1");
                AchievementsManager.TestButtonOneAchievements();
                AchievementsManager.TestTotalButtonsAchievements();

        }

        private void ButtonTwo_Click(object sender, RoutedEventArgs e)
        {
            if (Windows.Storage.ApplicationData.Current.LocalSettings.Values.ContainsKey("button2presses"))
            {
                string numOfPresses = Windows.Storage.ApplicationData.Current.LocalSettings.Values["button2presses"].ToString();
                int numAsInt = Convert.ToInt16(numOfPresses);
                numAsInt++;
                Windows.Storage.ApplicationData.Current.LocalSettings.Values["button2presses"] = numAsInt.ToString();
            }
            else
                Windows.Storage.ApplicationData.Current.LocalSettings.Values.Add("button2presses", "1");

                AchievementsManager.TestButtonTwoAchievements();
                AchievementsManager.TestTotalButtonsAchievements();
        }

When each button is pressed we check for two categories of achievement – the one for that button and the one that counts all button presses. We don’t check for ButtonTwo achievements when ButtonOne is pressed because it’s impossible for a ButtonOne press to trigger a ButtonTwo achievement. This is how using categories lets us avoid checking all the achievements all the time, improving efficiency and performance.

Next, add this single line to the end of the MainPage() constructor. It will reset the stored button presses each time the game is run so we can play it over and over:

 Windows.Storage.ApplicationData.Current.LocalSettings.Values.Clear();

We’re using isolated storage user settings to keep track of player progress (button presses in this example), because we never want that information to disappear when the game is closed. You wouldn’t reset progress like this in a proper game.

Step 5 – enable toast notifications

One last thing: allow your app to display toast notifications (this is disabled by default):

  1. Double-click Package.appxmanifest in the Solution Explorer
  2. In the Application tab, Notifications area, set Toast capable: to Yes.

Now run the app. Press the buttons to earn achievements. Even with a simple, dumb game like this you can see how much value and fun achievements add.

Overview

What did we do in all that code above?

We started with a class to hold our achievements and gave it the usual properties you’d expect – a name, an image, a points value, and most importantly some code to check when the achievement is earned. We later made three sample achievements inheriting this class, and these achievements tested button press totals.

Our manager class is how the game interacts with achievements – by requesting to check achievements of a certain category when events in the game demand it (e.g. check ButtonOne achievements when ButtonOne is pressed). We use categories so we can avoid checking achievements that we know couldn’t be triggered by the specific event that led us to check for achievements (e.g. we don’t check for the number of stars collected when the player has just killed a goblin); this also makes for neater code.

Troubleshooting

  • If you have any ‘red squigglies’, make sure you’ve named all your classes correctly. ‘Achievement’ is a tricky word to type, so make sure you haven’t mis-spelled it anywhere. I typed it wrong about 100 times writing this tutorial… make sure that ‘v’ has an ‘e’ on either side!
  • If the code is running fine but you don’t get the notification toasts, make sure you’ve done Step 5 to enable toasts.
  • Download the project from the link at the bottom and compare its code with your own.
  • If you have created your own achievements, make sure to include them in the AchievementsList or the app won’t ever check them.
  • If you’re not using Windows 8.1/Visual Studio 2013 you might need to adjust some of the code. Send a comment if you’re stuck. This code should work on Windows 8.0, and with some modifications on Windows Phone.

What Next?

Try adding a few more achievements to the example, and maybe even another measure of player progress (a third button, a timer, etc.). Some ideas for extra achievements:

  • Press both buttons at least 12 times each
  • Press ButtonOne, ButtonOne, ButtonTwo, ButtonTwo in that sequence.

In a game you’ll want a page where the player can view which achievements they have and have not earned. When you  do this, be sure to bind the ‘achieved’ status (e.g. to a ‘tick’ icon) to the Achieved property, not the AchievedTest property (because that would test every achievement every time the list is displayed, which would be inefficient). Remember to check achievements only when something happens that could trigger them. The more complex your game and achievements, the more important that efficiency will be.

You can add a method to count the total achievements points earned. Loop through the achievements and add their points value to a total if the Achieved property is true. This total would be great on a Windows live tile.

You can check out my game Puzzle Detective to see the above method in action, including an achievements list page with a running points total.

Links
Achievements sample app
Puzzle Detective for Windows 8.1

Leave a Reply

Your email address will not be published.