How achievements are achieved

by Tom on

(Almost) no Steam game comes without achievements, and Roche Fusion isn't any different. Achievements are a way to challenge the player in different, sometimes unconventional, ways. Of course, achievements also allow us to incorporate a whole lot of puns, easter eggs and other silliness, so there was no question that Roche Fusion would incorporate the Steam achievement system.

In this dev log, I will explain the framework we built for the achievements. I won't go into details about the Steam achievement interface (I am legally not allowed to). There will be some technical things and implementation details, but I'll leave those for the end, so feel free to stop reading when things start going over your head too much.

Achievements and hardcoding

Achievements are triggered by a large variety of different events, for example activating an ultimate, picking an upgrade, or dying. On first sight, it seems that we have to scatter our achievement code all over our program to deal with this. We could easily put a line of code in the ultimate activation method that unlocks the achievement.

There are a few major disadvantages to this approach however. First and foremost: it clutters the source a lot. Maybe one line for unlocking an achievement is fine, but what if we have a separate achievement for the different type of ultimates. Then the achievement code would already become a larger block. Now add a few achievement with special conditions (e.g. activating an ultimate during bullet time while there is an epic boss on the screen) and your code pretty much starts to become a huge mess.

Another disadvantage is that the code becomes heavily decentralised. Is there a bug in an achievement? That means first spending time finding where the achievement is actually defined. Want to change the achievement interface? Changes have to be made all over the source code.

I hope it has become clear how this solution is very suboptimal, so we have to be a bit more smart about this.

Let's forget about achievements

As the title already says: let's completely forget about achievements. Remember how I said that achievements are triggered by certain events? Let's take another look at this. If there would be a complete newsfeed of everything that happens during a game, we could just look at the feed and when we notice it is our cue, trigger the achievement.

Maybe this gets more clear by using a metaphor: let's say I want to research how many car fires happen during New Year's Eve. As the police is busy enough as it is, they won't just tell me about a car fire whenever they encounter one, but I can listen in on the radio. Most of the events announced on the radio will be completely unrelated, but I can filter these out and thus count the amount of car fires based on that.

The achievement system in Roche Fusion works no different. Achievements subscribe to certain events and change their state based on that.

So how does this solution compare to hardcoding all achievements. The observant readers will immediately tell me that we still need to add code all over the place to actually trigger these events. This is definitely true, but this code is limited to calling an event. The actual logic triggered is completely elsewhere in the code. People with knowledge of design patterns will probably have already recognised the observer pattern here.

There are more advantages to this system. By only triggering the event, the game code no longer has any idea or expectations about what is going to happen with that event. This means that we are free to hook up more systems to this newsfeed. In analogy with counting car fires, our statistic and unlock systems also use the same event system.

Technical details: the event feed

Since we have the architecture laid down, we can now look into how it is implemented. We start by looking at the event feed. This feed is actually a very straight-forward object that contains a long list of events. The C# event system would work great for this, but has one major flaw: you can't pass (references to) events around easily. To solve this issue, we encapsulated the events in a class. In essence, these classes look like this:
Code: Select all
sealed class Event
{
    private event VoidEventHandler handler;

    internal void TryInvoke()
    {
        if (this.handler != null)
            this.handler();
    }

    public void Subscribe(VoidEventHandler sub)
    {
        this.voidHandler += sub;
    }

    public void Unsubscribe(VoidEventHandler sub)
    {
        this.voidHandler -= sub;
    }
}


The actual class we use is slightly more complicated to make sure we don't count certain events for background games and to allow subscribers to also get a copy of the current game state if they so desire.

Some achievements want some extra information to their events. For example the event that is triggered when an ultimate is activated should also include which ultimate was activated. To allow for this, we also have generic variations of the Event class that look very similar to class shown bfefore:

Code: Select all
sealed class Event<T>
{
    private event GenericEventHandler<T> handler;

    internal void TryInvoke(T t)
    {
        if (this.handler != null)
            this.handler(t);
    }

    public void Subscribe(GenericEventHandler<T> sub)
    {
        this.handler += sub;
    }

    public void Unsubscribe(GenericEventHandler<T> sub)
    {
        this.handler -= sub;
    }
}


It probably doesn't take a lot of imagination to see how this class looks for even more type parameters.

With this in place, we have a central system from which we can send notifications about these events. The events in turn can be passed around by reference, as they are encapsulated by a class.

Technical details: the 'chievs

One of the first achievements to be implemented was the achievement that triggers once you activated the phoenix. In our game, all achievements are defined in a central place and one of the first lines of that file reads as follows:
Code: Select all
Achievement.Define("ach_ulti_beard")
    .AchieveOn(dispatcher.UltimateActivated, (p, ulti) => ulti is Phoenix);

There is quite a lot going on in this line of code, so we will disect it into parts:

  • Achievement.Define("ach_ulti_beard"): Achievement is (unsurprisingly) the type that contains all achievement logic. Using the static Define method creates a new instance of the Achievement class. We give it the id of the Steam achievement, so it knows what achievement to unlock. This piece of code is actually equivalent to calling new Achievement("ach_ulti_beard"), but I liked how it looked better and it allows us to insert more logic easily if necessary.
    The static method returns the Achievement it created, so we can do some stuff with it.

  • .AchieveOn: as opposed to Define, AchieveOn is actually an instance method. What it does is it takes an event and optionally a condition, and it will take care of unlocking the achievement when the event is called with arguments that fulfil the given condition.
    The AchieveOn method is a chaining method. This means that it will make some changes to the instance it is called on, and then return the same instance. Therefore we could continue writing code to make the achievement more complex. I will give an example of that below.

  • dispatcher.UltimateActivated: the dispatcher is the news feed we have been talking about earlier. UltimateActivated is one of the events listed in the dispatcher. In this case it is an instance of Event<Player, Ultimate>.

  • (p, ulti) => ulti is Phoenix: what you see here is a so-called lambda expression. This expression is equivallent to the following method:
    Code: Select all
    bool isUltiPhoenix(Player p, Ultimate ulti)
    {
        return ulti is Phoenix;
    }

    Using a lambda expression saves us defining the method explicitly and referring to it, making the code more concise and also make it immediately obvious what the condition encompasses.

The implementation of the AchieveOn method looks like this:

Code: Select all
public Achievement AchieveOn<T1, T2>(Event<T1, T2> @event, EventCondition<T1, T2> condition)
{
    if (!this.achieved)
        @event.Subscribe((t1, t2) => this.achieveIf(condition, t1, t2));
    return this;
}

We can see how it accepts an event and a condition (the latter is optional, there is also an overload without a condition parameter). We subscribe to the event (note how we use another lambda-expression here) so that once the event is fired, the achieveIf method is called. If this method detects that the condition is fulfilled, the achievement is awarded to the player.

Chaining

Before checking out, I would like to revisit the concept of chaining. As is clear from the implementation of AchieveOn the instance is returned by the method. This allows for the chaining of methods. If we want to for example award an achievement for reaching an ultimate or dying, the definition could look like this:
Code: Select all
Achievement.Define("ach_nonsense")
    .AchieveOn(dispatcher.UltimateActivated)
    .AchieveOn(dispatcher.PlayerDied);

In addition to the AchieveOn methods, there are several other methods that allow for more complex conditions to trigger achievements. Consider the (hypothetical) achievement of not losing a life before reaching the first upgrade. In code, this could be achieved (no pun intended) as follows:

Code: Select all
Achievement.Define("ach_more_nonsense")
    .AchieveOn(dispatcher.PlayerUpgraded)
    .FailOn(dispatcher.PlayerDied)
    .ResetOn(dispatcher.GameStarted);

The FailOn allows me to define events that will mark the achievement as failed. Even if the player reaches the upgrade later on, the achievement will not be awarded. To make sure that the player gets to try another time in the next game, the ResetOn method completely resets the state of the achievement.

These are just two examples of additional methods, but there are even more complex conditions that we have implemented. As the framework as described in this dev log is very flexible, the possibilities for making more complex achievements are very numerous. In this dev log, I had to omit many implementation details to not make it any longer than it already is. I hope that based on the information given, you can get a general idea about the structures and design patterns we have used to make the achievement system as flexible and least ugly as possible.

If you have any questions based on this dev log, feel free to ask them on our forums and I will do my best to get back to you as soon as possible. For now I challenge you to find all the achievements in the game, so you can show them off on your Steam profile.