Ren'Py Tutorial: Add achievements to your game

Ren'Py Tutorial: Add achievements to your game

As I’ve mentioned before, I really enjoy achievements in games, and I think that they add a lot of extra content to games. Even if you don’t enjoy them yourself, I’d recommend that you consider adding them to your visual novel - plenty of other people will like them, and it will give them more incentive to see everything in your game.

In this tutorial, we’ll be going over how to add achievements to your game, including hidden achievements, Steam achievements, and how to display them. In the examples given, we will be demonstrating with a game that has four achievements, but you can create as many or as few as you want for your game.

1. Set up your achievements

To start with, create a list of dictionaries which contain the information about each of your achievements.

default achievements = [

    {

        "id": 1,

        "name": "First Chapter Completed",

        "description": "You completed the first chapter!",

        "hidden": False

    },

    {

        "id": 2,

        "name": "Second Chapter Completed",

        "description": "You completed the second chapter!",

        "hidden": False

    },

    {

        "id": 3,

        "name": "Good Ending",

        "description": "You unlocked the good ending",

        "hidden": True

    },

    {

        "id": 4,

        "name": "Bad Ending",

        "description": "You unlocked the bad ending",

        "hidden": True

    },

]

 

Let’s look at one of these achievements in more detail:

{

    "id": 1,

    "name": "First Chapter Completed",

    "description": "You completed the first chapter!",

    "hidden": False

}

 

It contains the following data:

  • id: A number we will be using to identify and refer to this achievement

  • name:  The name of this achievement

  • description: The description shown, describing how the player can unlock this achievement

  • hidden: Whether this achievement’s description is hidden or not, for more spoiler-y achievements.

 

If your game has more than four achievements, continue adding dictionary entries like this to cover all of your achievements.

Next, add in a variable to contain the information which achievements we’ve unlocked - an empty list.

default persistent.achievements_unlocked = []

 

2. Add a screen to view the achievements

Before getting into how to unlock the achievements, let’s add in a screen where people can view the achievements, both before and after unlocking them. For this, you’ll need two images for each of your achievements - one for when it’s locked, and one for when it’s unlocked. In this example:

  • Each image is stored with a game/gui/achievements folder

  • Each image is named after the achievement’s ID in the format X_active.png or X_inactive.png where X is the ID of the achievement. For example, the icons for your first achievement will be named 1_active.png and 1_inactive.png.

 

screen achievements:

    tag menu

    default achievements = achievements

    default unlocked_achievements = persistent.achievements_unlocked

    default hover_item = None

    style_prefix "achievements"

 

    frame:

        text _("Achievements"):

            xalign 0.5

            yalign 0.1

 

        hbox:

            xfill True

            frame:

                yalign 0.5

                ymaximum 0.5

                xmaximum 0.6667

                grid 2 2:

                    xalign 0.5

                    yalign 0.5

                    spacing 10

 

                    for achievement in achievements:

                        button:

                            if (hover_item == achievement):

                                at transform:

                                    on idle:

                                        matrixcolor None

                                    on hover:

                                        matrixcolor TintMatrix("#0099FF")

                            xsize 150

                            ysize 150

                            action NullAction()

                            background Frame("gui/achievements/" + str(achievement["id"]) + "_active.png" if achievement["id"] in unlocked_achievements else ("gui/achievements/" + str(achievement["id"]) + "_inactive.png" if hover_item is not achievement else "gui/achievements/" + str(achievement["id"]) + "_active.png"))

                            hovered SetScreenVariable("hover_item", achievement)

                            unhovered SetScreenVariable("hover_item", None)

 

            frame:

                xmaximum 1.0

                if (hover_item is not None):

                    image ("gui/achievements/" + str(hover_item["id"]) + "_active.png" if hover_item["id"] in unlocked_achievements else "gui/achievements/" + str(hover_item["id"]) + "_inactive.png"):

                        xalign 0.5

                        yalign 0.4

                        xsize 300

                        ysize 300

 

                    text _(hover_item["name"]):

                        color ("#0099ff" if hover_item["id"] in unlocked_achievements else "#707070")

                        xalign 0.5

                        yalign 0.62

                        textalign 0.5

 

                    if (hover_item["id"] in unlocked_achievements or hover_item["hidden"] == False):

                        text _(hover_item["description"]):

                            color ("#0099ff" if hover_item["id"] in unlocked_achievements else "#707070")

                            xalign 0.5

                            yalign 0.7

                            textalign 0.5

                            size 25

                    else:

                        text _("Keep playing through the game to unlock this achievement"):

                            color "#707070"

                            xalign 0.5

                            yalign0.7

                            textalign 0.5

                            size 25

 

        bar value StaticValue(len(unlocked_achievements), len(achievements)):

            xmaximum 600

            xalign 0.125

            yalign 0.8

 

        text _(str(len(unlocked_achievements)) + "/" + str(len(achievements)) + " (" + str(int(len(unlocked_achievements) / len(achievements) * 100)) + "%) Unlocked"):

            color "#0099ff"

            xalign 0.1

            xoffset 5

            yalign 0.85

 

    textbutton _("Back"):

        xalign 1.0

        yalign 1.0

        text_size 30

        text_hover_color "#0099ff"

        action ShowMenu("main_menu")

 

This will create a grid of images of your achievements, which can be hovered over to see in more detail. Let’s break it down.

The start of the screen is simply setting up variables that are relevant to the screen. Nothing too complicated there.

screen achievements:

    tag menu

    default achievements = achievements

    default unlocked_achievements = persistent.achievements_unlocked

    default hover_item = None

    style_prefix "achievements"

 

Next, we add the title of the screen at the top of the screen.

    frame:

        text _("Achievements"):

            xalign 0.5

            yalign 0.1

 

The bulk of the screen is in a horizontal box. The first two-thirds of this box contains a grid which displays the achievements. The main thing to note here is the grid 2 2 line - this sets up a grid with two rows and two columns. If your game has a different number of achievements, you’ll need to update the numbers so that the count of rows (the first number) multiplied by the count of columns (the second number) equal how many achievements you have.

        hbox:

            xfill True

            frame:

                yalign 0.5

                ymaximum 0.5

                xmaximum 0.6667

                grid 2 2:

                    xalign 0.5

                    yalign 0.5

                    spacing 10

 

                    for achievement in achievements:

                        button:

                            if (hover_item == achievement):

                                at transform:

                                    on idle:

                                        matrixcolor None

                                    on hover:

                                        matrixcolor TintMatrix("#0099FF")

                            xsize 150

                            ysize 150

                            action NullAction()

                            background Frame("gui/achievements/" + str(achievement["id"]) + "_active.png" if achievement["id"] in unlocked_achievements else ("gui/achievements/" + str(achievement["id"]) + "_inactive.png" if hover_item is not achievement else "gui/achievements/" + str(achievement["id"]) + "_active.png"))

                            hovered SetScreenVariable("hover_item", achievement)

                            unhovered SetScreenVariable("hover_item", None)

 

The last third of the screen will display a larger icon when we hover over an achievement, along with its description. If the achievement is hidden and hasn’t been unlocked yet, we’ll just display text saying that the player needs to keep playing through the game to unlock the achievement.

            frame:

                xmaximum 1.0

                if (hover_item is not None):

                    image ("gui/achievements/" + str(hover_item["id"]) + "_active.png" if hover_item["id"] in unlocked_achievements else "gui/achievements/" + str(hover_item["id"]) + "_inactive.png"):

                        xalign 0.5

                        yalign 0.4

                        xsize 300

                        ysize 300

 

                    text _(hover_item["name"]):

                        color ("#0099ff" if hover_item["id"] in unlocked_achievements else "#707070")

                        xalign 0.5

                        yalign 0.62

                        textalign 0.5

 

                    if (hover_item["id"] in unlocked_achievements or hover_item["hidden"] == False):

                        text _(hover_item["description"]):

                            color ("#0099ff" if hover_item["id"] in unlocked_achievements else "#707070")

                            xalign 0.5

                            yalign 0.7

                            textalign 0.5

                            size 25

                    else:

                        text _("Keep playing through the game to unlock this achievement"):

                            color "#707070"

                            xalign 0.5

                            yalign 0.7

                            textalign 0.5

                            size 25

 

 

If you can get all of the above into your game and running without issue, you’ve done the majority of the work - there are only two more things remaining. Firstly, we’ll display a bar and percentage which indicate how many achievements players have unlocked.

        bar value StaticValue(len(unlocked_achievements), len(achievements)):

            xmaximum 600

            xalign 0.125

            yalign 0.8

 

        text _(str(len(unlocked_achievements)) + "/" + str(len(achievements)) + " (" + str(int(len(unlocked_achievements) / len(achievements) * 100)) + "%) Unlocked"):

            color "#0099ff"

            xalign 0.1

            yalign 0.85

 

Lastly, we add a button into the bottom-right corner of the screen which allows us to return to the main menu.

    textbutton _("Back"):

        xalign 1.0

        yalign 1.0

        text_size 30

        text_hover_color "#0099ff"

        action ShowMenu("main_menu")

 

The achievement screen is complete, but we’ll still need something to show players when they unlock an achievement. Below the above code, add in the following:

screen achievement_notification(achievement):

    zorder 10

    style_prefix "achievement_notification"

    timer 6:

        repeat False

        action [Hide('achievement_notification')]

    frame at notification_show_and_hide:

        image "gui/achievements/" + (str(achievement["id"])) + "_active.png":

            xsize 72

            ysize 72

            xoffset 10

            yoffset 20

        text "Achievement unlocked!" style "achievement_notification_header"

        text achievement["name"] style "achievement_notification_description"

        background Frame("gui/frame.png", xsize=370, ysize=150)

 

style achievement_notification_header:

    xoffset 100

    yoffset 25

    size 20

    color "#0099ff"

style achievement_notification_description:

    xoffset 100

    yoffset 55

    size 15  

 

Quickly breaking it down, we start out setting up two variables for the screen, as well as a timer to hide the screen after six seconds.

screen achievement_notification(achievement):

    zorder 10

    style_prefix "achievement_notification"

    timer 6:

        repeat False

        action [Hide('achievement_notification')]

 

When we call this screen, we want to show an image of the achievement along with text saying that we’ve unlocked it.

    frame:

        at transform:

            yalign 1.0

            xalign 1.0

            xsize 370

            ysize 110

            yoffset 200

            linear 0.5 yoffset 0

            pause 5.0

            linear 0.5 yoffset 200

        image "gui/achievements/" + (str(achievement["id"])) + "_active.png":

            xsize 72

            ysize 72

            xoffset 10

            yoffset 20

        text "Achievement unlocked!" style "achievement_notification_header"

        text achievement["name"] style "achievement_notification_description"

        background Frame("gui/frame.png", xsize=370, ysize=150)

 

 

The last few lines simply move the styling for the items on this screen to cut down on the clutter.

style achievement_notification_header:

    xoffset 100

    yoffset 25

    size 20

    color "#0099ff"

style achievement_notification_description:

    xoffset 100

    yoffset 55

    size 15  

 

It’s worth noting that the achievement_notification screen we’re displaying is only designed to show one achievement at a time. If it’s possible to unlock multiple achievements at the same time, or within a short period of time, you’ll need to add in further coding for that yourself.

3. Unlocking Achievements

Next, we’ll need to set up a function to unlock the achievement and display a notification when this happens. Thankfully, we’ve done most of the work at this point.

Add the following code into your game. For the record, I usually create a separate file to store all of my python variables, called python_variables.rpy.

init python:

    def unlock_achievement(achievement_id):

        if (achievement_id not in persistent.achievements_unlocked):

            persistent.achievements_unlocked.append(achievement_id)

            for achievement in achievements:

                if (achievement["id"] == achievement_id):

                    renpy.show_screen("achievement_notification", achievement)

 

Now, let’s assume that you have some code that looks something like this:

alice "Little did I know, that was the last time I would ever see them alive..."

 

#Begin Chapter Two

alice "The next day, I woke up to find that the fire had gone out. Fog had come in overnight, and visibility was low."

 

You probably want to unlock an achievement after the first line, since it’s the end of chapter one. Looking at our list of achievements, you can see that we’ve given the corresponding achievement an id of 1. This means that all you need to do is add in the following line into this code:

alice "Little did I know, that was the last time I would ever see them alive..."

$ unlock_achievement(1)

 

#Begin Chapter Two

alice "The next day, I woke up to find that the fire had gone out. Fog had come in overnight, and visibility was low."

 

To unlock any other achievements, simply replace the 1 parameter in the unlock_achievement function with the id of the corresponding achievement.

4. Accessing Achievements

At this stage, we can define achievements, unlock achievements, and display a notification when they’re unlocked. There’s just one thing missing: a way to view the list of available and unlocked achievements. We already set up the screen for this back in the second step - now we just need access to the screen itself.

In order to do this, find the following line in your screens.rpy file:

screen navigation():

 

Then, within the indentation of the vbox block, add in the following:

if (main_menu):

    textbutton _("Achievements") action ShowMenu("achievements")

 

And you’re done!

5. Steam Achievements

All of the code so far assumes that you’re just setting up local achievements for your game, and the code provided will work fine for that. But what if you’re releasing your game on Steam, and want to add in support for achievements there?

Firstly, you’ll need to set up your achievements in Steam. That’s a little beyond the scope of this guide, but you can read the documentation on how to do so here. The main thing to note is the API Name attribute of each achievement that you’re adding. 

Next, you’ll need to add Steam support to your game. There’s more in-depth information on how to do this in the Ren’Py docs, but long story short, you want to choose the “Install Steam Support” option from your Ren’Py launcher, under preferences -> Install libraries. Once you’ve done so, go to your options.rpy file, and add in the following line:

define config.steam_appid = 1111111

 

The value here should be the actual App ID of your Steam game, which you will have access to through the Steam control panel.

Remember how I said that you need to note the API Name of each achievement that you added into Steam? Here’s where we use them. Find your default achievements section, where we’ve defined our achievements. Each achievement should still look something like this:

{

    "id": 1,

    "name": "First Chapter Completed",

    "description": "You completed the first chapter!",

    "hidden": False

}

 

For each achievement, add in a steam_code attribute, and the API Name of the achievement on Steam. For instance, if your API Name for your first achievement is CHAPTER_ONE_COMPLETE, your achievement would now look like this:

{

    "id": 1,

    "name": "First Chapter Completed",

    "description": "You completed the first chapter!",

    "hidden": False,

    "steam_code": "CHAPTER_ONE_COMPLETE"

}

 

There’s just one final step to add: update your unlock_achievement function to implement Steam functionality.

def unlock_achievement(achievement_id):

        if (achievement_id not in persistent.achievements_unlocked):

            persistent.achievements_unlocked.append(achievement_id)

            for achievement_item in achievements:

                if (achievement["id"] == achievement_id and not achievement.has(achievement_item["steam_code"])):

                    achievement.grant(achievement_item["steam_code"])

                    achievement.sync()

 

 

Essentially, we’re checking for whether we’ve unlocked the achievement, and if not, we tell Steam to unlock it using the API Name provided.

You may have noticed that I’ve removed showing our achievement_notification screen from the updated function - this is because Steam’s UI already does everything that we need (notably, it also accounts for unlocking multiple achievements at a time). If you want to keep it, however, you’re more than welcome to. You might also want to consider removing the achievements screen button from the main menu if you think that Steam’s pages for showing achievements are sufficient for your purposes.

 


 

With a bit of luck, this tutorial has helped you with setting up achievements in your game, whether you’re integrating with Steam or not. While it is quite a bit to do and can be a little overwhelming if you’re not very experienced with Ren’Py screens, it’s absolutely worth putting in achievements in order to appeal to more players and encourage them to find more content in your game. Still having difficulty, or have improvements to suggest? Contact me, and let me know your thoughts.




Related News