Codes: Character Profiles

Another thing the game has (and will build on in the next release) is unlockable character profiles with some information about each character, in addition to some bonus features unlocked from them. As far as things I’ve posted here go, this is a fairly straightforward one, but I figured I’d share anyway.

First of all, I defined a block of data for each character’s information. In addition, because these are linked from the title screen via a sketched sprite, I included coordinates for the appropriate sprite.

python early:
# list in reverse order of rendering on title screen
real_profile_names = (
"nick", "hannah", "elliot", "nate", "lizb", "lizg", "adam", "juan", "jake", "dan", "james",
)
profile_profile_info = {
"adam": ("Adam", (950,340),
"Sophomore", "Biology", "August 15, 1986 (age 20)", "6'2\"", "California, United States",
"Adam James Prewitt",
"Profile text for Adam"),
"juan": ("Juan", (850,340),
"Junior", "Biology", "April 22, 1986 (age 20)", "6'0\"", "Costa Rica",
"Juan Rodrigo Salas Valverde",
"Profile text for Juan"),
…etc…
}

Next we actually put these on the title screen.

screen main_menu():
…Stuff…
fixed:
if "completed" in persistent.profiles:
imagebutton auto "images/title/leigh_%s.png" action Show("char_profile", chname="ashleigh") xanchor 0.0 yanchor 0.0 pos (650, 140)
imagebutton auto "images/title/leigh_%s.png" action Show("char_profile", chname="leigh") xanchor 0.0 yanchor 0.0 pos (850, 140)
imagebutton auto "images/title/steve_%s.png" action Show("char_profile", chname="steve") xanchor 0.0 yanchor 0.0 pos (950, 140)
imagebutton auto "images/title/robert_%s.png" action Show("char_profile", chname="robert") xanchor 0.0 yanchor 0.0 pos (1050, 140)
for profname in real_profile_names:
if profname in persistent.profiles:
imagebutton auto "images/title/" + profname + "_%s.png" action Show("char_profile", chname=profname) xanchor 0.0 yanchor 0.0 pos profile_profile_info[profname][1]

Next, we define the screen for character profiles. Note the additional features that I’ll get into shortly.

screen char_profile(chname):
tag charprofile
xpos 0
ypos 0
default pmode = "normal"
fixed:
add "images/sp/profilebg.png" xpos 0 ypos 0
if pmode == "best":
add "images/title/profile_" + chname + "best.png" xalign 0.5 yalign 1.0 xpos 175 ypos 720
elif pmode == "good":
add "images/title/profile_" + chname + "good.png" xalign 0.5 yalign 1.0 xpos 175 ypos 720
else:
add "images/title/profile_" + chname + ".png" xalign 0.5 yalign 1.0 xpos 175 ypos 720
if (chname == "james" and persistent.permacc_james) or (…other guys’ checks…):
add "images/title/profile_" + chname + "acc.png" xalign 0.5 yalign 1.0 xpos 175 ypos 720
if chname + "best" in persistent.profiles or chname + "good" in persistent.profiles:
imagebutton auto "images/sp/proftab_normal_%s.png" action SetScreenVariable("pmode", "normal") xanchor 0.0 yanchor 0.0 pos (375, 631)
imagebutton auto "images/sp/proftab_good_%s.png" action SetScreenVariable("pmode", "good") xanchor 0.0 yanchor 0.0 pos (375, 559)
if chname + "best" in persistent.profiles:
imagebutton auto "images/sp/proftab_best_%s.png" action SetScreenVariable("pmode", "best") xanchor 0.0 yanchor 0.0 pos (375, 480)
imagebutton auto "images/sp/profextra_%s.png" action Start("epilogue_bonus_" + chname + "1") xanchor 0.0 yanchor 0.0 pos (350, 0)
imagebutton auto "images/sp/profpermacc_%s.png" action ToggleField(persistent, "permacc_" + chname) xanchor 0.0 yanchor 0.0 pos (363, 171)
vbox xpos 500 ypos 30 xalign 0.0 xsize 480:
spacing 10
text "{size=40}" + profile_profile_info[chname][0] + "{/size}"
text "{size=25}Name: " + profile_profile_info[chname][7] + "\nYear: " + profile_profile_info[chname][2] + "\nMajor: " + profile_profile_info[chname][3] + "\nBirthday: " + profile_profile_info[chname][4] + "\nHeight: " + profile_profile_info[chname][5] + "\nHome: " + profile_profile_info[chname][6] + "{/size}"
text "{size=25}" + profile_profile_info[chname][8] + "{/size}"
vbox xpos 1250 ypos 30 xalign 1.0 xsize 400:
for k in sorted(profile_profile_info):
if k in persistent.profiles or ("completed" in persistent.profiles and k in ("ashleigh", "leigh", "steve", "robert")):
textbutton profile_profile_info[k][0] action [Show("char_profile", chname=k), SelectedIf(chname == k)] xalign 1.0
textbutton "Return to Main Menu" action Hide("char_profile") xalign 0.0 xpos 500 ypos 675

As you can see, the profile screen has a few unlockable features. The first feature is buttons that let you view alternate poses for the character image, depending on which ending you’ve completed with the given person. This is implemented simply, with a screen variable indicating the mode.

The second feature is the ability to access a bonus scene for the given character, once you’ve found their HEA ending. Just define a label with the appropriate name (epilogue_bonus_james1 for example).

The last feature is a toggleable item on the guy that will persist on their sprite during subsequent playthroughs. For example, if you get James’ best ending, you can toggle his enagement ring on or off, and have it show up whenever he appears in the game. The associated code for that builds off of our sprite code and simply adds a new check for the persistent variable, and a layer.

image james jamesnew = LiveComposite(
(266, 700),
(0, 0), "chnew/james/[spposes_data[james].pose]/base.png",
(0, 0), "chnew/james/[spposes_data[james].pose]/basehair.png",
(0, 0), "chnew/james/[spposes_data[james].expression].png",
(0, 0), "chnew/james/[spposes_data[james].pants].png",
(0, 0), "chnew/james/[spposes_data[james].shirt].png",
(0, 0), "chnew/james/[spposes_data[james].permacc].png",
(0, 0), "chnew/james/[spposes_data[james].extras].png",
)
    def execute_showsp(o):
…old code…
# Handle epilogue accessories
if who == "james" and persistent.permacc_james:
store.spposes_data[who].permacc = help_get_filepathsp(who, pose, "x", "ring")
…other guys' accessories…
else:
store.spposes_data[who].permacc = help_get_filepathsp(who, pose, "x", None)
…old code…

And… that’s it. Obviously, your profiles don’t have to be nearly this complicated. If you just want to show some basic information for each character, the screen and associated logic will be much simpler.

Sprites and Expressions

One thing my sprite artist (the awesome @stollcomics) had been doing was varying the head, as needed, in different expressions. For example, here’s a sample of (inked!) Adam heads, and I think it makes for a much more expressive sprite than the usual “static head and moving facial features” thing.

However! In the process I think we also discovered why head movements aren’t a normal thing in visual novels – because it takes really long to draw and ink so many heads, which multiplies out even more when you want hair and facial hair variations. (We were expecting to have a final Adam sprite, colors and all, this weekend, but instead he only got around 70% done with inks.)

Given that, I think we’re going to be going back to the “usual” visual novel style with a static head and moving facial features. It won’t look as good as these, but it’ll be a heck of a lot easier on him, and generally means this process won’t take as long given the 13 other sprites we have to get through.

It’s particularly unfortunate that a lot of this awesome art isn’t going to see the light of day (other than this post, I suppose), but I’m really glad that we ran into this early in the process (on the first sprite) instead of after doing a couple of simple ones and then having to go back and redo those, as well.

Learning experiences, I guess!

(This also means, of course, that the next game release is delayed for a couple weeks while we redo the expressions and get the new sprite colored.)

Sprites: More Adam Sketches, James Preview

More from the talented hands of @stollcomics. Here’s Adam looking good. 🙂 I think his sprite design is done at this point, and he’ll soon get inked and then colored.


Here’s a preview of a couple iterations in design on James.

The second is the more recent one. I think he’s going to be repositioned, and have some slight tweaks, but that’s the general idea.

Codes: Sprite expression changes

Last time, we added a custom showsp command to easily show sprites that obey game settings automatically. But that can be tedious to use if all you want to do in a scene is change a character’s expression when they’re already onscreen. Cue showexp!

A couple of changes to the old code are necessary. First of all, we need to store the raw names for the clothing (you’ll see why in a moment).

python early:
class CharPoses:
pose = ""
pants = ""
shirt = ""
expression = ""
extras = ""
oldclothes = ""
basepantsstr = ""
baseshirtstr = ""
baseextrasstr = ""
def execute_showsp(o):
…same as before…
if not who in store.spposes_data:
store.spposes_data[who] = CharPoses()
store.spposes_data[who].pose = pose
store.spposes_data[who].baseshirtstr = shirt
store.spposes_data[who].basepantsstr = pants
store.spposes_data[who].baseextrasstr = extras
store.spposes_data[who].shirt = help_get_filepathsp(who, pose, "s", shirt)
store.spposes_data[who].pants = help_get_filepathsp(who, pose, "p", pants)
store.spposes_data[who].expression = help_get_filepathsp(who, pose, "e", expr)
store.spposes_data[who].extras = help_get_filepathsp(who, pose, "x", extras)
…same as before…

Next, we define the parser and lint for showexp. Note that we’re allowing changes to the pose, as well, so this can be called as either showexp adam happy or showexp adam happy crossed.

python early:
def parse_showexp(lex):
who = lex.simple_expression()
expr = lex.simple_expression()
if lex.eol():
return (who, None, expr)
pose = lex.simple_expression()
return (who, pose, expr)
def lint_showexp(o):
who, pose, expr = o
if len(o) != 3 or expr is None:
renpy.error("Invalid showsp declaration")
return
if not who in showsp_valid_combos:
renpy.error("Invalid person " + who)
return
if pose is None:
# don't know the pose so check across poses
# may fail if a given expr only exists for one pose
# but we can't track current pose state
hasExpr = "e_" + expr in showsp_valid_combos[who]["!any"]
if not hasExpr:
for pose in showsp_valid_combos[who]:
hasExpr = "e_" + expr in showsp_valid_combos[who][pose]
if hasExpr:
break
if not hasExpr:
renpy.error("Invalid expression " + expr + " for " + who)
else:
if not pose in showsp_valid_combos[who]:
renpy.error("Invalid pose " + pose + " for " + who)
return
if not "e_" + expr in showsp_valid_combos[who][pose] and not "e_" + expr in showsp_valid_combos[who]["!any"]:
renpy.error("Invalid expression " + expr + " for " + who + pose)

Note the documented limitation in linting. Since we don’t always have the pose information, if you have an expression that exists only for one pose, this will be missed by linting. (So make sure all expressions exist in some base form, and override them on a per-pose basis as necessary, to prevent this.)

Finally, the actual execute command, where we use the information we’d previously stored in the new fields:

    def execute_showexp(o):
who, pose, expr = o
if not who in store.spposes_data:
renpy.error("Invalid person " + who)
if pose is not None:
if store.spposes_data[who].pose != pose:
# force all other components to re-render as necessary when pose changes
store.spposes_data[who].shirt = help_get_filepathsp(who, pose, "s", store.spposes_data[who].baseshirtstr)
store.spposes_data[who].pants = help_get_filepathsp(who, pose, "p", store.spposes_data[who].basepantsstr)
store.spposes_data[who].extras = help_get_filepathsp(who, pose, "x", store.spposes_data[who].baseextrasstr)
store.spposes_data[who].pose = pose
else:
pose = store.spposes_data[who].pose
store.spposes_data[who].expression = help_get_filepathsp(who, pose, "e", expr)
renpy.register_statement("showexp", parse=parse_showexp, execute=execute_showexp, lint=lint_showexp)

This will probably also fail if a particular shirt, pants, or extra exists only for one pose and you change pose as part of the command. Again, to be safe, make sure everything a character could be wearing exists at some form at the base level, or just don’t change poses around when using the command.

In any case, now you can more easily carry on a conversation without having to remember what a character was wearing in the same scene (or even where they’re standing, which is an improvement over the basic game’s show command, as well).

showsp adam sides greent jeans happy
adam "This is only a test. How do I look?"
showexp adam worried
adam "Does this look okay?"
showsp adam crossed bluet slacks curious jacket left
adam "Now I'm over here, and in blue. Huh."
showexp adam confused
adam "Strange."
showexp adam default crossed
adam "Oh well."
showexp adam grin sides
adam "It's nice to meet you anyway."

Yay! One last thing: Calling showexp before showsp will probably do nothing, since changing variables around while a sprite is offscreen has no effect. So if your script has a lot of jumps in it, you probably want to lead each label with a showsp for all of your characters, again, just to be safe.

Codes: Sprites, the YAGS way

One thing I needed was the ability to easily show sprites that obeyed various game settings automatically. For example, there’s a toggle in preferences to revert to the legacy stick figure sprites, there’s a nudity toggle, and there may eventually be an option to toggle body hair. In addition, the guys have a lot of different possible combinations of clothing and expressions.

There are a lot of “automatic” sprite scripts out there already, this oneprobably being one of the best ones. And for most people, I’m sure that will work just fine.

My requirements would have made that script tedious to use, though, since I would have had to case on different flags every time I wanted to show a sprite. Instead, I threw together a custom show command (showsp) that gives me the flexibility to change sprites depending on persistent and game state.

The first thing to do is to define the actual sprites. Since I’m supporting both the new style and old stick figures, each character has to be defined twice. Note the file structure requirements in the comment. It’s also worth noting that each expression must exist both in the old and new sprite directories. 

default spposes_data = {}

#file structure: base folder adam
#under that, folders with name of poses (sides, crossed) with base.png under it
#then either in base folder or in poses subfolder
#s_.png, p_.png, e_.png, x_.png
#in poses folder if differs by pose, base folder if not
#x_blank.png and p_cover.png is required for all characters
#s_shirtless, p_underwear, and p_naked are handled specially for legacy sprites, if they exist
image adam adamnew = LiveComposite(
(342, 600),
(0, 0), "chnew/adam/[spposes_data[adam].pose]/base.png",
(0, 0), "chnew/adam/[spposes_data[adam].pants].png",
(0, 0), "chnew/adam/[spposes_data[adam].shirt].png",
(0, 0), "chnew/adam/[spposes_data[adam].expression].png",
(0, 0), "chnew/adam/[spposes_data[adam].extras].png",
)
#c_blank.png and c_basic.png are required for clothing in old sprites
image adam adamold = LiveComposite(
(500, 600),
(0, 0), "chold/adam/base.png",
(0, 0), "chold/adam/c_[spposes_data[adam].oldclothes].png",
(0, 0), "chold/adam/[spposes_data[adam].expression].png",
)

Also note we’re making use of dynamic images, which is particularly useful, and would work on its own, except you don’t get linting checks for invalid images, and it would make assignment within the game a bit of a pain.

Next, we actually define a command to show sprites. This is the heart of the framework, so I’ll go over it piece by piece.

First is the file processing. This automatically picks up all images from the chnew directory, sorts them by pose, and builds a map of all allowable assets. 

python early:
class CharPoses:
pose = ""
pants = ""
shirt = ""
expression = ""
extras = ""
oldclothes = ""
showsp_valid_combos = {}
showsp_valid_locations = ("left", "right", "slightleft", "moreright", "lessright")
def process_file(charname, pose, filename):
itemname = filename.replace(".png", "")
if itemname != "base":
if not pose in showsp_valid_combos[charname]:
showsp_valid_combos[charname][pose] = set()
showsp_valid_combos[charname][pose].add(itemname)
def process_all():
for path in renpy.list_files():
if path.startswith("images/chnew/"):
pathlist = path.split("/")
charname = pathlist[2]
if not charname in showsp_valid_combos:
showsp_valid_combos[charname] = {}
showsp_valid_combos[charname]["!any"] = set()
if pathlist[3].endswith(".png"):
process_file(charname, "!any", pathlist[3])
else:
process_file(charname, pathlist[3], pathlist[4])
process_all()

Note the un-ideal manual declaration of supported Positions. This will show up again later, but I’m not sure how to take a position string and convert it to a Position object automatically.

Next, the custom command itself. The parsing logic is slightly complex, because we want to be able to omit the position and omit extras, in any combination. (Note that this technically means you can’t have both a Position and an extra with the same name, but you probably don’t want something like x_right.png anyway.)

python early:
def parse_showsp(lex):
who = lex.simple_expression()
pose = lex.simple_expression()
shirt = lex.simple_expression()
pants = lex.simple_expression()
expr = lex.simple_expression()
if lex.eol():
return (who, pose, None, shirt, pants, expr, None)
extraorpos = lex.simple_expression()
if lex.eol():
if extraorpos in showsp_valid_locations:
return (who, pose, extraorpos, shirt, pants, expr, None)
else:
return (who, pose, None, shirt, pants, expr, extraorpos)
else:
return (who, pose, lex.simple_expression(), shirt, pants, expr, extraorpos)

Next, the linting. It’s technically unnecessary, but it’s invaluable unless you want to be tracking down runtime errors from invalid clothing or expressions in your game. Note the returns that prevent errors when you try to access a map item that doesn’t exist (due to invalid pose or character).

    def lint_showsp(o):
who, pose, where, shirt, pants, expr, extras = o
if where is not None and where not in showsp_valid_locations:
renpy.error("Invalid location " + where)
if not who in showsp_valid_combos:
renpy.error("Invalid person " + who)
return
if not pose in showsp_valid_combos[who]:
renpy.error("Invalid pose " + pose + " for " + who)
return
if not "s_" + shirt in showsp_valid_combos[who][pose] and not "s_" + shirt in showsp_valid_combos[who]["!any"]:
renpy.error("Invalid shirt " + shirt + " for " + who + pose)
if not "p_" + pants in showsp_valid_combos[who][pose] and not "p_" + pants in showsp_valid_combos[who]["!any"]:
renpy.error("Invalid pants " + pants + " for " + who + pose)
if not "e_" + expr in showsp_valid_combos[who][pose] and not "e_" + expr in showsp_valid_combos[who]["!any"]:
renpy.error("Invalid expression " + expr + " for " + who + pose)
if extras is not None and not "x_" + extras in showsp_valid_combos[who][pose] and not "x_" + extras in showsp_valid_combos[who]["!any"]:
renpy.error("Invalid extra " + extras + " for " + who + pose)

Finally, the actual command logic:

    def help_get_filepathsp(who, pose, prefix, item):
if prefix == "x" and item is None:
return "x_blank"
if prefix + "" + item in showsp_valid_combos[who][pose]: return pose + "/" + prefix + "" + item
elif prefix + "" + item in showsp_valid_combos[who]["!any"]: return prefix + "" + item
else:
renpy.error("Could not find resource " + prefix + "_" + item + " for " + who + pose)
def execute_showsp(o):
who, pose, where, shirt, pants, expr, extras = o
whereobj = None
if where is not None:
if where == "left":
whereobj = left
elif where == "right":
whereobj = right
elif where == "slightleft":
whereobj = slightleft
elif where == "moreright":
whereobj = moreright
elif where == "lessright":
whereobj = lessright
else:
renpy.error("Unknown location " + where)
else:
whereobj = default
# TODO: Special variable checks to override things here
# possibly on a character-specific basis
if pants == "naked" and not persistent.nudes:
pants = "cover"
# End special checks
if not who in store.spposes_data:
store.spposes_data[who] = CharPoses()
store.spposes_data[who].pose = pose
store.spposes_data[who].shirt = help_get_filepathsp(who, pose, "s", shirt)
store.spposes_data[who].pants = help_get_filepathsp(who, pose, "p", pants)
store.spposes_data[who].expression = help_get_filepathsp(who, pose, "e", expr)
store.spposes_data[who].extras = help_get_filepathsp(who, pose, "x", extras)
# handle old sprites; either they're dressed or they're not
if pants == "naked" or pants == "underwear" or pants == "cover" or shirt == "shirtless":
store.spposes_data[who].oldclothes = "blank"
else:
store.spposes_data[who].oldclothes = "basic"
if persistent.oldsprites:
renpy.show(who + " " + who + "old", at_list=[whereobj])
else:
renpy.show(who + " " + who + "new", at_list=[whereobj])
renpy.register_statement("showsp", parse=parse_showsp, execute=execute_showsp, lint=lint_showsp)

A few things to note here: The position stuff is really non-ideal (you need to manually handle every position you support for sprites in the game), but you can’t just do at_list=[where] as where is a string and not a position. There’s special logic to handle overrides, such as putting a covering over the character if they’re naked and nudity is turned off; it would be straightforward to handle body hair the same way.  You can also override special cases manually (for example, I’ll probably handle Juan’s cast this way by reading the current game state instead of having to put the cast as an extra every time).

This should also be easily extensible if you want to build expressions from pieces (eyebrows, eyes, mouth) by adding support for things like b_, y_, and m_.

In any case, all of this mess of code allows you to write your game script like this:

showsp adam sides greent jeans happy jacket left
adam "This is only a test. How do I look?"
showsp adam crossed bluet slacks curious right
adam "Now I'm over here"
showsp adam crossed shirtless jeans embarassed
adam "And losing my clothing."
showsp adam sides shirtless underwear embarassed jacket
adam "That's not really any better."

Which is really nice given it’s obeying all of your game state and preferences automatically.

Future enhancement: A showexp command that changes just the expression on the given sprite, without having to re-specify the rest of the clothing or position.

Edit: The showexp command has been defined here. Also, I found a way to grab the location object based on the string name for it.

        if where is not None:
if where in showsp_valid_locations:
whereobj = globals()[where]
else:
renpy.error("Unknown location " + where)
else:
whereobj = default

I kept the showsp_valid_locations list in place, because it seemed useful. (After all, you want to make sure what you’re grabbing is actually a Position object that you expect, and not some arbitrary RenPy object sitting there.) But this means adding a new position is just a matter of defining it and updating the list, instead of actually updating code.

Codes: Achievements, Take Two

Last time, we added achievements to our game, in a rather crappy and user un-friendly way, but that should theoretically integrate with Steam properly. Today, we improve that by defining a custom achievement command and showing a message to the user!

The first thing to do is to define a command:

python early:
basic_achievements = (
("First Steps", "Starting your college life", "Starting your college life"),
…More achievements…
)
def parse_achieve(lex):
what = lex.rest()
return what
def lint_achieve(what):
if not any(what == a for a,lkd,unk in basic_achievements):
renpy.error("Invalid achievement " + what)
def execute_achieve(what):
if achievement.has(what):
return
achievement.grant(what)
renpy.show_screen("gain_achieve", _layer="screens", achievetext=what)
renpy.register_statement("achieve", parse=parse_achieve, execute=execute_achieve, lint=lint_achieve)

Note the screen that we show to notify the user. That screen is defined as follows:

screen gain_achieve(achievetext):
tag achievementalert
fixed:
image "sp/achievementbg.png" at Position(xpos=0.85, ypos=0.1, xanchor=0.5, yanchor=0.5)
text "{size=22}You unlocked an achievement:\n" + achievetext + "{/size}" id "achievetext" at Position(xpos=0.85, ypos=0.1, xanchor=0.5, yanchor=0.5) xmaximum 290 ymaximum 90
timer 3.0 action Hide("gain_achieve")

Basically, we’ve added a new screen that that shows up in the upper right of the screen, tells the user they unlocked an achievement, and then dismisses itself after 3 seconds.

One important thing I want to point out here is the _layer=“screens”parameter when you show the screen with renpy.show_screen(). Assuming I understand RenPy properly, by default, there are a few different threads to render the GUI. In particular, when you show a screen, the main rendering thread is blocked on the screen, and will not do anything else until you dismiss it. In this case, because we’re showing a screen that dismisses itself, RenPy freaks out and throws an error on the next call that does anything other than hides the screen.

When we direct the screen to be displayed in the overlay layer, instead, it does not tie up the main thread. Indeed, gameplay continues normally and the user can keep playing.

The final part of this is to actually grant achievements within your game script. Because we’ve defined a custom command, this is now easy:

"You feel an immense sense of relief, as well."
achieve Nothing But Love
show adam grin
"Adam turns to you, grinning."

One caveat that I have not figured out yet: What to do if your achievement name contains quote characters. For example, I have an achievement called “Journey’s End” with an apostrophe in the name. Trying to grant it directly like this results in an error:

achieve Journey's End

Instead, I’ve special-cased that achievement, and grant it as JourneysEnd instead. I’m sure there’s a way to escape the apostrophe, but I also didn’t care to keep banging my head against it tonight.

One other important thing to note: These statements are notequivalent.

achieve Nothing But Love
achieve "Nothing But Love"

The version without quotes is almost certainly what you want. (I’m assuming the quotes are read literally, instead of denoting a string, but I also did not test to see exactly why it was failing to work properly.)

Musak: YAGS Theme (Preliminary lyrics)

As I’ve probably mentioned elsewhere, this game has been a lot of firsts for me. Not only is it my first time using RenPy (or even writing Python code, at all), it’s also my first non-school-related creative writing project as well as my first video game.

To that list can be added songwriting. I threw together some lyrics for a theme song, and my esteemed musical colleague is helping fix it, record it, and put it to a pop/rock track.

Like most things I post here, there’s no guarantees about its quality. But hopefully it does a passable job at conveying the general sentiment of the game. This is also just the first cut, so it’s particularly rough.

It is, appropriately and unoriginally, titled Yearning.

Feeling alone, Off on my own
Kinda for the first time, and a
Secret inside, like something to hide
Like a mountain I must climb, and there’s
Looking back to who I’ve been
And finding comfort in my skin
So soon with just some time
I guess that I will find…
That I’m

(Chorus)
Yearning
To be who I’ll be
Finding myself, Finally free, I’m just
Yearning
So that I can see
That maybe what I neeeeeed….
Could be right here in front of me

(Instrumental)

People I meet, talks that I seek
Friendships forming as I go, and there’s
Times of support, sorrow cut short
Laughter, joy, and thoughts that flow, from my
Mind as I find something more
In those friends that I adore
Still with each and every day
Wish I could get away…
From this

(Chorus)
Yearning
To be who I’ll be
Finding myself, Finally free, I’m just
Yearning
So that I can see
That maybe what I neeeeeed…
Is what I’m finding in front of me

(Bridge)
Through thick and thin and broken limbs
And even times of trial
As we grow close, what helps the most
Is when I see you smile

Yeah, I love to see your smile
I’m still…

(Chorus)
Yearning
To be who I’ll be
Finding myself, Finally free, I’m just
Yearning
But now I can see
That all this time you’ve been there for me,
With this

(Chorus)
Yearning
To be who I’ll be
Finding myself, Finally free, I’m just
Yearning
So that I can see
That maybe what I needed, in the end…
Was you there standing in front of me

(Instrumental)

Thank you for being in front of me

(Instrumental)

I’m glad that you’re here in front of me

(Strum, end)

Will post demos and backing track stuffs once I have them. But songwriting is hard, yo.

Codes: Achievements

I actually haven’t seen a full writeup of using RenPy’s achievements module. Thankfully, it’s super easy to set up, but I have no idea if this will actually integrate into Steam and such properly.

In any case: The game had collectibles. Yay. But I wanted some larger accomplishments (such as finding lots of collectibles) to be noted in a more prominent way.

The first thing to note is that RenPy’s achievements module is part of core RenPy and not its game scripts. (This is probably obvious, but I figured I’d point it out anyway.) As such, you have to call it from python code blocks.

To simplify things, I defined two kinds of achievements: Ones in a set list that are just boolean (you have it or you don’t) toggles, and ones that have a bit more complicated tracking logic. (I believe the latter will matter more if you actually integrate into Steam, but again, I have not actually tested any of this.)

First off: Defining your achievements. They just need a name, but I put a small description with them for UI display. Note the tuple instead of map, this time, so ordering is maintained:

python early:
basic_achievements = (
("First Steps", "Starting your college life", "Starting your college life"),
("Nothing But Love", "???", "Coming out to your parents"),
("Achievement Name", "Description when not unlocked", "Description when unlocked"),
…other achievements…
)

Then, you’ll want to register them with the backend. I believe this isn’t strictly necessary, but it doesn’t hurt to do.

init python:
for a, lockdesc, unlockdesc in basic_achievements:
achievement.register(a)
achievement.register("Growing Pains", stat_max=100, stat_modulo=1)
achievement.register("Compulsive Hoarder", stat_max=100, stat_modulo=1)

Note the manually-registered ones that include stat maxes. You could probably just register them with the actual number of collectibles, but I figured it’d be cleaner to deal with completion in terms of percentage, especially as I was still increasing the number of collectibles in the game.

Next, you actually want to grant these achievements in the game. This is, thankfully, straightforward.

"You feel an immense sense of relief, as well."
$ achievement.grant("Nothing But Love")
show adam grin
"Adam turns to you, grinning."

So this grants achievements (silently, to the player). How do you display them? I added them to the collectibles screen I defined in the previous post.

screen mycollection():
tag menu
default collectshow = "collectibles"
use game_menu(("Collection"), scroll="viewport"): vbox: spacing 10 hbox: spacing 20 textbutton ("Collectibles") action SetScreenVariable("collectshow", "collectibles")
textbutton _("Achievements") action SetScreenVariable("collectshow", "achievements")
if collectshow == "achievements":
for aname, lockdesc, unlockdesc in basic_achievements:
if achievement.has(aname):
text aname + ": {color=#777}" + unlockdesc + "{/color}"
else:
text "{color=#ccc}???: " + lockdesc + "{/color}"
if achievement.has("Growing Pains"):
text "Growing Pains: {color=#777}Finding " + str(int(len(collectitems)/2)) + " collectibles{/color}"
else:
text "{color=#ccc}???: Finding " + str(int(len(collectitems)/2)) + " collectibles{/color}"
if achievement.has("Compulsive Hoarder"):
text "Compulsive Hoarder: {color=#777}Finding all " + str(int(len(collectitems))) + " collectibles{/color}"
else:
text "{color=#ccc}???: Finding all " + str(int(len(collectitems))) + " collectibles{/color}"
else:
text "Found " + str(len(persistent.collection)) + "/" + str(len(collectitems)) + " collectibles" id "collectiblescount"
for k in sorted(persistent.collection):
if k in collectitems:
textbutton collectitems[k][0] action Show("itemdesc", itemid=k)

The last part is to increment progress as you acquire collectibles. This is also straightforward.

    def execute_collect(what):
if what in persistent.collection:
return
formatmsg = "{color=#090}{b}You found a collectible:{/b} "
if what in collectitems:
formatmsg += collectitems[what][0]
else:
renpy.error("Invalid collectable " + what)
persistent.collection.add(what)
totitemcount = len(collectitems)
curitemcount = len(persistent.collection)
if curitemcount >= (totitemcount):
if not achievement.has("Compulsive Hoarder"):
achievement.grant("Compulsive Hoarder")
else:
achievement.progress("Compulsive Hoarder", int(totitemcount100/curitemcount)) if curitemcount >= (totitemcount/2): if not achievement.has("Growing Pains"): achievement.grant("Growing Pains") else: achievement.progress("Growing Pains", int(totitemcount50/curitemcount))
if len(persistent.collection) == 1:
formatmsg += "\n(Access your collection from the main menu or pause menu. Collectible items will persist between games.)"
formatmsg += "{/color}"
renpy.say("", formatmsg)

As usual, I make no claims that this code is the best way to do things, or even a good way to do things. But it works. And, assuming achievements work the way I assume they will, doing it this way should also hook properly into things like Steam.

I was expecting the achievement framework to already put up some sort of notification, but it looks like RenPy doesn’t do this by default, so using this doesn’t really have any immediate advantages over directly writing persistent data.

This could be improved by adding a custom command to wrap achievement.grant() that can lint against the known list of achievements, and that can also show a notification to the player that an achievement was unlocked (some new screen that displays temporarily in some corner). This is perhaps an enhancement I will make at some point. 

Edit: Improved achievements, building on this code, is here.