Porting MYTH, Part II: Doing Very, Very Naughty Things

Hey, BlackDragonHunt here again with another look into the process of porting MYTH. This is part two in a three-part series.

In my first post, I talked a bit about the game’s original engine, NScripter, how I eventually decided I was going to port it to Ren’Py, and one of the first challenges I ran into.

This time, we’re going to be looking at the “visual” aspect of visual novels and how our two engines differ on that front.

Images

Images in NScripter are identified by a number from 0 to 999, with lower-numbered images displaying on top of higher-numbered images. When you show an image, you give a number, a filename, and a position. And when you hide it again, you use that same number.

In Ren’Py, however, images are identified by tags, such as “bg” for a background or “sou” for the character Sou’s sprites. Images are, by default, all displayed on the same layer, with images shown later in the script displaying on top of images shown earlier.

In short, there are two important differences in how these two engines handle images—display order and image names don’t matter in NScripter, while they do in Ren’Py. To properly simulate NScripter’s behavior, I had to create individual layers for every numeric identifier used in the script (of which there were only 14, fortunately), so I could show and hide the layers at will, not having to care about the Ren’Py name of the images being displayed there.

So, for example, these two lines of NScripter code:

lsp 901,":a;image\ivent\furo_w.png",0,0
lsp 902,":c;image\ivent\furo2.png",-480,0

become this in Ren’Py:

scene bg ivent_furo_w onlayer sp901:
  xpos 0
  ypos 0
scene bg ivent_furo2 onlayer sp902:
  xpos -480
  ypos 0
It's bath time!]

It’s bath time!

Even though the fog image (furo_w) is shown first, because it’s been assigned a lower number than the CG (furo2), it gets rendered on top of it.

Sprites and background images are a special case, behaving more similarly to Ren’Py. So aside from some boring bookkeeping code, they look sort of like you would expect in a hand-written Ren’Py script.

Transitions

NScripter comes with 18 built-in transition effects, some of which can be customized using mask images. It’s all fairly standard stuff, the only real quirk being that you can’t use the transitions directly—you have to define instances of the effects you want to use, assigning each one a number and specifying the length of time it takes. This means that when you use effect #15, for example, it could very well be an instance of the built-in effect #10. Not confusing at all.

A vertical blinds transition.

A vertical blinds transition.

Most of these effects (of which MYTH defines 52 variations) were easily recreated using native Ren’Py transitions, with a couple exceptions: NScripter built-in effects 11-14 (directional scroll) and 16-17 (pixellate and un-pixellate).

The latter effect is essentially Ren’Py’s Pixellate transition, except split into two parts, meaning I could dig into the source code, take the existing transition, and create two new individual transitions from it—one for each half of the effect.

The scrolling effect, though, I couldn’t manage to replicate using Ren’Py’s built-in transitions. Essentially, the entire scene slides off one edge of the screen while the new scene slides in to take its place. Ren’Py’s CropMove transition does something similar, but only one of the new or old scenes slides in at a time—no matter how much I mixed and matched, I couldn’t get it to move one out and the other in at the same time.

One image slides out while the other slides in.

So I did the same thing as I did with the Pixellate transition—I yanked the source code for CropMove and reworked it to be able to do both at the same time. This became the PushMove transition—which I thought other people would find useful, so I submitted the code (with the kind permission of my awesome bosses) and it was integrated into Ren’Py with version 6.99.8.

Animation

Generally speaking, most visual novels are not very “animated.” Sprites jump up and down, the camera zooms in and out, the screen shakes. But with a few exceptions, it’s mostly fairly simple stuff.

MYTH has a few such animated segments—mostly the camera panning across CG larger than the game window (such as the bath scene above). Here’s how a pan is accomplished in the NScripter code.

lsp 902,":c;image\ivent\furo2.png",-480,0
print 1:delay 300
mov %6,20
for %5 = 0 to 16
inc %6
resettimer
msp 902,%6,-%6/2:print 1
waittimer 20
next
waittimer 400
for %5 = 0 to 5
inc %6
resettimer
msp 902,-%6,-10-%6:print 1
waittimer 30
next

And here’s what it’s doing:

  • show a CG, as described above, at (-480, 0)
  • wait for 300 ms
  • set the value of %6 to 20
  • enter a BASIC-style for loop, range 0 to 16 (inclusive)
    • increment %6 by 1
    • move image %6 pixels to the right and %6/2 pixels up
    • wait for 20 ms
    • repeat x16
  • wait for 400 ms, the image currently located at (13, -242)
  • enter another for loop, range 0 to 5
    • increment %6 by 1
    • move image %6 pixels to the left and %6+10 pixels up
    • wait 30 ms
    • repeat x5
  • animation complete, the image located at (-230, -545)

You’ll notice the CG isn’t moving at a constant speed—it gets gradually faster with every iteration of the loop, moving (21, -10) pixels, then (22, -11), then (23, -11), up to (37, -18) on its final iteration. And because the second loop doesn’t reset the offset counter, it rockets across (-243, -303) pixels in only 180 ms.

Ren’Py, to the best of my knowledge, does not have any way to (easily) shift an image an arbitrary number of pixels. You can show an image with an x/y offset, but that’s relative to its initial position. Showing it again with the same offset will put it in the same place, rather than the offsets stacking. I would have to keep some sort of temporary variable around to accumulate offsets into, which wasn’t going to happen in a ~1000 line transpilation script without a lot of headache.

Ren’Py does, however, have its own richly featured system for performing exactly the kind of animations we need—which it calls ATL, or the Animation and Transformation Language. As there were only a handful of these animations to begin with, I decided it would be simpler (and much more efficient) to rewrite them entirely.

Here’s what the above code looks like in ATL:

show bg ivent_furo2 onlayer sp902:
  xpos -480
  ypos 0
  pause 0.3
  easeout_quad 0.4 xpos 13 ypos -242
  pause 0.9
  easeout_quad 0.2 xpos -230 ypos -545

Which does this:

  • show a CG at (-480, 0)
  • wait for 0.3 seconds
  • move the image to (13, -242) over 0.4 seconds
  • wait for 0.9 seconds
  • move the image to (-230, -545) over 0.2 seconds

A couple things to note about this: first, I tweaked the timing slightly, partly for aesthetic reasons and partly to better match how the animation actually runs in NScripter, which has a bit of trouble with really short times.

Second, the “easeout_quad” is what’s called a “warper“. It essentially tells Ren’Py that we don’t want the motion to happen linearly, but to follow a quadratic curve. This isn’t 100% accurate to the original (which is also quadratic, just not the same function), but the difference is negligible on such a small timescale.

Full-Screen Filters

NScripter offers two filters that affect the whole screen: “monocro” and “nega“. They both do about what you’d expect them to do—monocro turns the entire screen monochrome, taking a color to use as a base. White makes everything grayscale, red a fiery hellscape, green, well, green. You get the idea. Likewise, “nega” inverts the colors.

Cycling between a few different monocro colors.

Ren’Py, however, doesn’t support any filters that apply to the entire screen. There has been talk about possibly adding the feature at some point in the future, but as of now, full-screen filters are still unsupported.

To get around this limitation, I substituted the “monocro” and “nega” commands for variable assignments, and then I created a custom displayable—what Ren’Py calls any object that can be shown to the user, such as an image—that watches for changes to these variables and applies the appropriate filter to itself. I then made every single image in the game an instance of this custom displayable (with the exception of the menus and some UI stuff, which were left unaffected in the original game), effectively simulating NScripter’s full-screen filtering.

Those of you familiar with Ren’Py may be wondering, why didn’t I just use a DynamicDisplayable? Well, at first, I did. But there was a slight problem with that approach: in NScripter, the monocro effect is only applied to the next scene update (i.e. a sprite being shown or the background changed). A DynamicDisplayable, on the other hand, applies any changes at the start of every interaction (i.e. text being shown or menus being displayed).

It’s a subtle difference, but enough to cause an issue—for example, in one of the first scenes of the game, Meito’s room is shown in grayscale. The game then disables the monocro filter and transitions to white. Using a DynamicDisplayable causes the room to snap back into color before the transition begins, where we actually want the old scene to remain in grayscale while the new one transitions in.

Custom displayable vs DynamicDisplayable.

One potential problem with deferring the change until the displayable is explicitly told to update is that Ren’Py only redraws displayables it’s been told to redraw. Each individual displayable has no way of knowing whether something else is changing and it needs to follow suit, leaving the possibility of creating inconsistent, half-filtered scenes.

This is further complicated by the fact that MYTH has animated, blinking sprites, which are constantly redrawing themselves regardless of what else is happening. And since the eyes are a separate graphic, they would go grayscale immediately, leaving the rest of the sprite behind.

Colored sprite with gray eyes and gray background.

Luckily, MYTH only ever uses monocro before fully clearing and recreating the scene, meaning I could safely ignore this flaw in my implementation. It’s far from elegant, any maybe there’s a better way to do it, but it does the trick.

Well, I think I’ve rambled on long enough, so I’ll cut it off here this time. I hope you’re enjoying the posts, and do look forward to the next one!


Part I: Introduction | Part III: How Not to Code a Game in Ren’Py

Bookmark the permalink.

2 Comments

  1. I love technical post like this, as a hobbyist programmer they are quite fascinating and entertaining to read, so I really appreciate that you guys take the time to write these.

    I’m also quite happy that you decided to push some of your new Ren’Py features back to the Ren’Py code base, I am always a big fan of companies contributing back to open source projects that they benefit from. Especially since there often is no direct financial benefit to do so, so good on you and your boss for allowing it.

    I am looking forward to the next post, and hope that you guys keep doing blog features like this for other games that you port in the future.

  2. Pretty sweet stuff here.

    Don’t forget to mention any errors you saw in the original script people do make mistakes but it would be nice to know what the mistake was and if it had an effect.

    For example, while helping a TL group edit a kirikiri game I noticed it was using the wrong image in the save/load menu screen which doesn’t matter game play wise but I got a good chuckle out of it.

Leave a Reply