Don't lose your way
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.
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 = []
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.
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.
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!
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.