Codes: Collectibles

Another thing I wanted to include in my game was little items that were both fun to collect, and also gave some indication of how much of the game you’d explored.

So the game has collectibles: Small named items that you acquire throughout the game that you can view descriptions of in a dedicated section of the menu. This isn’t a novel idea, by any means, but it also wasn’t entirely straightforward to set up. (Probably because I was really new to RenPy at the time.)

First, the core code that makes it work. There’s two sections here: First is the declaration of the actual items you can pick up, to keep it in one place, along with the associated persistent storage code:

init python:
if persistent.collection is None:
persistent.collection = set()
def merge_collections(old, new, current):
current.update(old)
current.update(new)
return current
renpy.register_persistent('collection', merge_collections)
python early:
collectitems = {
"adamid": ("Adam's Student ID", "A student ID for one Adam James Prewitt. It looks well-worn."),
"jalapeno": ("Jalapeno", "A whole jalapeno from dinner with Adam."),
…other items here…
}

Second, because I wanted to be able to easily grant these items as part of the game script, there’s a custom command that grants items. Again, note the Lint check, which is super useful to catch typos and missed sections when you rename items:

python early:
def parse_collect(lex):
what = lex.rest()
return what
def lint_collect(what):
if not what in collectitems:
renpy.error("Invalid collectible " + what)
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)
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)
renpy.register_statement("collect", parse=parse_collect, execute=execute_collect, lint=lint_collect)

This can then be straightforwardly called as part of your game script. The best part is that you don’t have to put it behind a conditional, because the player will only be notified if they don’t already have the item.

adam "Anyway…"
"He smiles at you before quickly walking out the door."
collect adamid
hide adam

So that grants collectibles, and tells the player when they receive a new one, but how to show the collection to the player? You have to make a new screen, which is thankfully straightforward. First, the screen that actually shows item descriptions, pulling from the map of valid collectible items you defined earlier:

screen itemdesc(itemid):
tag itemd
modal True
image "sp/itembg.png" at Position(xpos=0.5, ypos=0.5, xanchor=0.5, yanchor=0.5)
vbox at Position(xpos=0.5, ypos=0.5, xanchor=0.5, yanchor=0.5, xmaximum=1280, ymaximum=250):
text collectitems[itemid][1] id "collectitemtext" xalign 0.5 yalign 0.5 xmaximum 570 ymaximum 200
null height 10
textbutton "Dismiss" action Hide("itemdesc") xalign 0.5 yalign 0.5

Then the screen that lists the collectibles and makes them clickable to show their descriptions:

screen mycollection():
tag menu
use game_menu(_("Collection"), scroll="viewport"):
vbox:
spacing 10
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)

Finally, add your screen as an item in the standard menu:

textbutton _("Load") action ShowMenu("load")
textbutton _("My Collection") action ShowMenu("mycollection")

Note that this doesn’t feed into the achievements system, but you probably don’t want it to… hundreds of achievements for every little thing in your game is probably overkill. But doing it this way allows the ability to easily add new collectibles (just add a new row to your map, and then collect itemname in your script) that persist between different plays of your game.

Codes: Instant Messaging

One thing I knew I wanted in the game from the beginning was instant messenger conversations – I remember using it basically exclusively for short-form communication from middle school through college, and it seemed appropriate that college students in 2006 would be using it.

This would be straightforward to do normally in RenPy. You can make a ParameterizedText object and just show it with your desired text whenever you need a new line.

image imwindow = Image("sp/imwindow.png", xanchor=210, ypos=0.69)
image im1 = renpy.ParameterizedText(xanchor=190, ypos=0.1)
image im2 = renpy.ParameterizedText(xanchor=190, ypos=0.15)
…etc

And then

 show imwindow
show im1 "{color=#f00}Diocusin:{/color} {color=#000}This is a test.{/color}"
$ renpy.pause()
hide imwindow
hide im1

However, that gets very verbose very fast. I wanted to be able to easily script IM conversations using character names instead of raw text, and I wanted to automatically handle coloring and showing and hiding, as appropriate.

RenPy allows you to define your own commands, so I defined one to hide and show the background window. (The one to show it is technically not necessary, but it’s here for completeness.)

python early:
def parse_hideim(lex):
return "”
def execute_hideim(o):
renpy.hide("imwindow")
renpy.hide("im1")
renpy.hide("im2")
…etc…
_window_show(trans=None)
renpy.register_statement("hideim", parse=parse_hideim, execute=execute_hideim)
def parse_showim(lex):
return "”
def execute_showim(o):
_window_hide(trans=None)
renpy.show("imwindow")
renpy.register_statement("showim", parse=parse_showim, execute=execute_showim)

Then it’s fairly straightforward to define a command that shows a message.

    def parse_im(lex):
num = lex.simple_expression()
who = lex.simple_expression()
pause = lex.simple_expression()
msg = lex.rest()
return (num, who, pause, msg)
def lint_im(o):
num, who, pause, msg = o
if(who != "mc" and who != "janet" and …etc…):
renpy.error("Invalid IM speaker " + who)
if(pause != "p" and pause != "n"):
renpy.error("Invalid pause value " + pause)
if(int(num) < 1):
renpy.error("Invalid IM number " + num)
def execute_im(o):
num, who, pause, msg = o
formatmsg = ""
if(who == "mc"):
formatmsg += "\"{color=#f00}Diocusin:{/color} {color=#000}"
elif(who == "janet"):
formatmsg += "\"{color=#00f}CrazyCatLady2588:{/color} {color=#000}"
…etc…
else:
renpy.error("Invalid IM person " + who)
formatmsg += "\"" + msg + "\""
formatmsg += "{/color}\""
if(not renpy.showing("imwindow")):
execute_showim("foo")
imnum = "im"+num
renpy.show((imnum, formatmsg))
if(pause == "p"):
renpy.pause()
renpy.register_statement("im", parse=parse_im, execute=execute_im, lint=lint_im)

This command can then be used to carry on a conversation within a script.

im 1 mc p "This is a test."
im 2 janet n "this is only a test, and clicking just once"
im 3 janet p "shows both of these lines together"
hideim

Note that, if I were doing this now, I would keep a counter variable that automatically increments the im number, and reset it in the hideim command. It would simplify IM conversations to something like

im mc p "This is a test."
im janet p "this is only a test"
hideim

but there’s something to be said about knowing exactly how many lines into a conversation you are at any given moment. 

There are probably better ways to do this, and someone more comfortable with Python and the RenPy engine could undoubtedly make this cleaner. But this got the job done for me.

Also never underestimate the power of lint. It’s a most excellent way to catch stupid mistakes when using your custom commands.