Porting MYTH, Part III: How Not to Code a Game in Ren’Py

Hello again, everyone! BlackDragonHunt here, and welcome to the third and final installment in my series of posts about porting MYTH to Ren’Py. If you missed them, you can find the first one here, and the second one here.

Last time, I focused on the “visual” elements of our visual novel. This time, I’ll be focusing on the “novel” half and the various components involved in putting text on the screen.

Translation

As I mentioned in my first post, Ren’Py provides facilities for including multiple languages in a single game, as well as switching between defined languages at the click of a button. The base translation system is, however, intended for use on a finalized (or at least mostly finalized) script, which I most definitely didn’t have at any stage in the porting process.

When generating files for translation, Ren’Py assigns each “block” (generally a single line of text) a unique identifier—a combination of the label preceding it and a hash of the code inside the block. Ren’Py uses these identifiers to match a translation with the original block it’s translating, allowing it to execute the translated block in its place.

But because I was going to be constantly regenerating the script as I fixed bugs and implemented missing features, the hashes used to generate the translation identifiers would be in constant flux. This meant that unless I exactly replicated the hashing Ren’Py performed on the code, I wouldn’t be able to make use of its automatic translation identifiers.

This, combined with the fact that I was generating a significant chunk of code that needed to differ between English and Japanese, but that Ren’Py had no way of knowing was “translatable”, made the built-in, automatic identifiers unsuitable for my purposes.

Ren’Py does, however, allow you to assign your own identifiers to blocks for translation, so that’s what I ended up doing instead. When I generate the script, I create predictable identifiers (a combination of the filename and line number), assigning them to the Japanese and English blocks simultaneously, so I don’t have to worry about translations breaking because the code is constantly changing.

Text and the Text Box

Text is an important part of any visual novel (obviously), and how an engine handles text plays a significant role in shaping how the games that use it are designed and function. In some engines, such as NScripter or Kirikiri, any part of the script that isn’t explicitly code or a comment is considered text (except blank lines and, sometimes, newlines). In others, text is part of the code, usually shown by calling some sort of dedicated function. Ren’Py falls in the latter category.

In NScripter, any line beginning with a half-width alphabetic character and a few specific symbols (asterisk, semicolon, tilde) is considered a command, and anything else is displayed as text. A newline at the end of a line of text is included as part of the text, unless explicitly excluded by ending the line with a forward slash. Additional newlines can be inserted with the “br” command, as blank lines are ignored. An “@” symbol causes the game to wait for the user to click, and any subsequent text is shown on the same screen. A backslash does the same, except clears the screen of text before showing any more. Code and text can be mixed freely, so NScripter will happily execute whatever you want it to while it’s in the middle of printing text.

Here’s a short block of NScripter code:

name "奏",1
HB0
ld 1,":a;tati\sou\niya.png",12
HB1
「というか、私達に興味があるのですか?
delay 200
HB0
ld 1,":a;tati\sou\ikaridefo.png",12
HB1
 …このドスケベッ!!」\

After showing the first part of the line, it pauses for 200 ms, hides the text box, changes the sprite, reshows the text box, and then finishes the line, all without any user interaction.

This is how the above code plays out in-game.

Text in Ren’Py is represented by Python strings. A line of text can optionally be preceded by a speaker, either in the form of a character object (used to customize the appearance of the text), or a raw string representing the speaker’s name.

Ren’Py’s default behavior with regard to text display is essentially the opposite of NScripter’s. A line of text comes with an implicit wait for the user to click, and then the screen is cleared for the next line. If you want to execute code in the middle of a line, you have to explicitly tell Ren’Py not to wait using the {nw} tag, and then you have to explicitly tell it to put the previous text back in the box before you add more. The latter can be accomplished by duplicating the previous text and tacking {fast} onto it, which tells Ren’Py to show everything up to that point instantly.

A special “extend” speaker is provided by the engine to do that for you automatically, but for translation purposes, I needed to make each “line” wholly self-contained. Switching languages on a line that uses “extend” causes Ren’Py to show half the line in one language and half the line in the other, so my generated text contains the entirety of what needs to be shown on the screen at any given time.

Here’s what the above block of code looks like transpiled to Ren’Py:

set_name "奏" 1
window hide effect12
set_sprite 1 "sou niya"
show sou niya as sp1 at sprite1 zorder 99
with effect12
window show effect12
translate None _R1_MYTH03_line_00134:
  blank_say "「というか、私達に興味があるのですか?\n{fast}"
  spk "「というか、私達に興味があるのですか?\n{nw}"
$ renpy.pause(200 / 1000., hard = False)
window hide effect12
set_sprite 1 "sou ikaridefo"
show sou ikaridefo as sp1 at sprite1 zorder 99
with effect12
window show effect12
unstore_say
blank_say ""
translate None _R1_MYTH03_line_00135:
  spk "「というか、私達に興味があるのですか?\n{fast} …このドスケベッ!!」"

As you can see, it’s kind of ballooned a bit.

I mentioned earlier that lines in Ren’Py can have a speaker attached to them, which is used to customize the appearance of the text. The speaker used in the above excerpt is simply “spk”.

NScripter has no concept of discrete speakers, though. In MYTH, the speaker tag is set using a custom “name” function, which stores the character’s name and a flag used for two purposes: to distinguish between different characters using the same name, and to determine whether to show a portrait on the left side of the text box.

Additionally, the game switches back and forth between five distinct text boxes, each with different margins, font size, color, placement, and UI. With 102 unique speakers in the game, that’s potentially 510 different characters, not accounting for variations given by the nametag flag.

To do this Ren’Py-style, I would have to figure out exactly which character objects I even needed (the easy part), and then keep track of enough state to determine the appropriate character to use for any given line (the hard part). I very quickly determined that that was never happening, and I decided to go with a single, unified character object, “spk”, which I would use for every single line of text and modify on the fly as the game demanded.

So I created five “template” characters, each with the style of one of the different text boxes used in the game. When the game switches between text boxes, I copy that template over to the “spk” object. Then, when a speaker’s name is specified, I inject that name into the “spk” object, as well as the appropriate nametag color.

This way (as with most of the solutions to my problems) I don’t have to worry about keeping track of what the game’s doing, since it does it all for me.

Choices

While some VNs are very literally what the name implies, just pictures and text—what you often hear referred to as a “kinetic novel”—many have interactivity in the form of simple “A, B, C, or D?” choices. You pick something to say, or you pick somewhere to go, or you pick someone to spend time with, and the culmination of these choices changes the course of the story.

MYTH defines a custom function to handle its choices. It first stores the text of each option and a target label in a sequential series of variables, then jumps to the function, creates buttons for each option, and then jumps to the label assigned to whatever option the user selects.

Here’s what the first choice in the game looks like in NScripter:

mov %50,2
mov $51,"・寄り道する          "
mov $52,"*R1_MYTH00_sel_1"
mov $53,"・帰宅する           "
mov $54,"*R1_MYTH00_sel_2"
goto *csel_main

The value of %50 tells the csel_main function that there are two choices coming—up to a maximum of five.

This translates into:

$ nums[50] = 2
$ strs[51] = "・寄り道する          {#r1_myth00_sel}"
$ strs[52] = "*R1_MYTH00_sel_1"
$ strs[53] = "・帰宅する           {#r1_myth00_sel}"
$ strs[54] = "*R1_MYTH00_sel_2"
jump csel_main

…which is pretty much exactly the same thing. (The extra {#r1_myth00_sel} is simply for translation purposes and not relevant here.)

A typical choice in MYTH, with the bottom option highlighted.

And then csel_main, which is about 170 lines of NScripter code laboriously creating buttons and monitoring for user input, looks something like this in Ren’Py:

label csel_main:
  
  menu:
    
    # Some text here, so it will show the text box.
    spk ""
    
    "[strs[51]!t]" if nums[50] >= 1:
      $ target = strs[52][1:]
      
    "[strs[53]!t]" if nums[50] >= 2:
      $ target = strs[54][1:]
      
    "[strs[55]!t]" if nums[50] >= 3:
      $ target = strs[56][1:]
      
    "[strs[57]!t]" if nums[50] >= 4:
      $ target = strs[58][1:]
      
    "[strs[59]!t]" if nums[50] >= 5:
      $ target = strs[60][1:]
  
  jump expression target

Ren’Py’s got a nice menu system built-in, so all I had to do was feed it the text for the choices, set the appropriate label for each one, and jump to whichever one the user selected. Easy.

Or so I thought. But this solution quickly proved rather problematic. Like many games, MYTH keeps track of what choices you’ve previously selected, displaying them for you in differently colored text.

The bottom choice has been selected before.

Ren’Py also keeps track of what choices have been selected, and it does so using two pieces of information: where the menu is being run from, and the raw text of the choice. However, because I’m using a generic menu modeled after the original NScripter code, it sees every single menu as being in the same place.

Secondly, Ren’Py allows you to put a variable name enclosed in square brackets in strings, and it will substitute that with the value of the variable when the string is displayed to the user. That’s what the menu code above is doing—telling Ren’Py to use the values of the variables we assigned beforehand as the text. !t is a flag saying we also want to translate the string, allowing us to show the equivalent English text when playing in English.

The problem is, substitution doesn’t occur until the string is actually shown. Internally, Ren’Py sees the first choice as the string literal “[strs[51]!t]” no matter what text we have stored in strs[51]. This means, if you choose the first choice, every single first choice on every single options menu after that will be considered “chosen.”

This functionality is perfectly reasonable for most games, which will have separate menus for all their choices, and any substitution will be cosmetic (i.e. a changeable character name). It would be extremely counter-intuitive if a choice was considered not selected because the user went through the game a second time with a different character name.

Obviously, at this point, the smart thing to do would have been to detect choices while transpiling and creating individual menus, allowing me to operate within Ren’Py’s existing framework. But I’m a programmer, and what kind of programmer actually does the smart thing when they have a 90% functional, decidedly not-smart solution?

To make my menus work, I had to come up with some other way to determine if a choice has been seen before. The NScripter code uses the target label to decide, but as far as I can tell, there’s no easy way to pass extra data to a menu in Ren’Py. So I did perhaps the ugliest thing you could do in this situation: I sliced the index out of the raw option string, converted it to an integer, incremented it by one, and used that to grab the label, which I then asked Ren’Py if we had been to before.

Yeah. I know. But it works! And that’s ultimately what matters, right?

…Right?

Conclusion

Anyway.

While this is far from everything that went into creating the MYTH port, it’s about everything I could think of that people might find interesting—and/or a good warning about what not to do when making a game in Ren’Py.

I’d like to give special thanks to PyTom for creating Ren’Py, which really is a fantastic engine, and for always being so quick to respond to the nigh uselessly obscure bug reports I filed as I beat and abused his poor engine into submission over the last year.

Thank you for reading, and in conclusion: who thought any of this was a good idea? I am a terrible programmer and no one should ever let me touch their games again.

Bookmark the permalink.

One Comment

  1. Why don’t you add Android versions for your Ren’Py games?

Leave a Reply