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 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
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.
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.
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.
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.
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.
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.
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.
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.
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!