Link's Awakening: Rendering the opening cutscene

10 septembre 2016

This article is part of an ongoing “Disassembling Link’s Awakening” series, where I attempt to gain some understanding on how special effects where implemented in this game.

When powering-up the console, after displaying the iconic “Nintendo™” logo, many Game Boy titles jump you right to a title screen.

Zelda: Link’s Awakening does better, and open with a nice opening cutscene, which introduces the plot and the characters of the story. Several smaller animated sequences are also sprinkled through the game – and roughly half-a-dozen were added during the DX remake, as a part of the Photographer sub-quest. The Ending sequence, with its impressive 5-minutes length, also have some nice visual effects exclusive to this segment of the game.

This opening cutscene, as the first sequence of the game, is a good starting point for understanding various special effects used through the game.

The sea sequence, stripped

The original Game Boy was first released in 1989, and has quite basic capabilities. The graphic primitives are based on tiles, background and sprites. Tiles are 8x8 bitmaps, arranged into the grid of a large scrollable background.

This grid is very rigid: that’s 8x8 for you, and nothing else. Fortunately, sprites are objects that can move with smaller increments, positioned over the background.

Note that there is no “direct drawing” mode of some sort: you can’t draw individual pixels on the Game Boy screen, it has to be part of a 8x8 tile.

This severely limits the drawing possibilities. Any advanced effects will have to use complex workarounds.

To understand, let’s have a look at the introduction sea sequence. We’re going to strip it of all special effects, and only use background scrolling, tiles and sprites.

(This can’t normally be seen in the game. But now that we have a limited source code, we can replace some code to disable specific features, re-assemble the ROM, and run it into our favorite emulator.)

Link's Awakening Sea Sequence without special effects

Less dramatic, isn’t it?

However the reduced motion allows us to see more clearly what is going on. What have we here?

Now if we were the developers of Link’s Awakening, what could we add to this scene to spice it up?

Waves in motion

For now the sea looks very static, almost like a solid surface. Nothing remotely menacing, like a deadly sea ready to sink our ship. Breaking the waves into different overlapping parts would be much better.

Unfortunately the Game Boy doesn’t has much control over how the tiles are arranged over the background: a simple 8x8 grid, nothing else.

But luckily we can animate the content of the tiles themselves! And that’s what the developers used: they made a small looping animation of overlapping waves, like an animated GIF.

Link's Awakening Sea Sequence animation loop

Here is how our scene looks with this wave animation.

Link's Awakening Sea Sequence with moving waves

Much better: we can see the waves going one behind each other.

However something looks strange. Due to the way the animation is made, the horizon is now moving up and down. Not very convincing. To avoid this, we’ll need a more complex trick.

Compensating for sea movement

Remember that we are drawing our scene over a scrollable background. The “camera” moving to the right is actually our background window being scrolled on the horizontal axis.

What we could do is also scroll the background up and down over the vertical axis, to compensate the horizon motion. This way the horizon position would appear constant. And this is actually what the game does.

Link's Awakening Sea Sequence Background movement

Nice trick. But one issue remains: although the horizon position looks now fixed, the clouds now appear to be moving up and down.

If only we could move only the lower part of the background, and left the upper part untouched…

Wait, the Game Boy can actually do this – by using the LCD STAT interrupt.

Abusing HBlank

The Game Boy LCD screen renders graphics line by line, sequentially from the top to the bottom. When reaching the end of an horizontal line, the LCD controller makes a small pause (of more-or-less 300 cycles) before drawing the next line: this period between two lines is the Horizontal Blank, or HBlank. (The name comes from the old CRT screens, where the electron beam physically needed to move back from the right of the line to the left of the new line.)

Likewise, when the last line has been rendered, the LCD controller makes a longer pause before jumping again to the top line: this period is the Vertical Blank, or VBlank.

Game Boy scanlines

Manipulating the Video Memory while the screen is being drawn is quite restricted: during this period many operations on the video memory are not possible, or simply ignored. This is why the logic of most games runs during the VBlank period, when nothing is being rendered, and all the video memory can be manipulated freely.

To make it easier, the Game Boy hardware can fire an interrupt when the Vertical Blank period begins (appropriately named the VBLANK interrupt).

But the video memory can also be manipulated at the end of each line, during the HBlank period — although you have very few cycles to do so. One popular manipulation is to change the x coordinates of the background for each line: this allows to create a wave-effect seen in many games (like this one).

Firing an interrupt for every HBlank would be quite expensive though – especially considered that usually most lines will be left unmodified. Fortunately the Game Boy provides a smarter primitive for this: the LYC register.

LYC stands for LCD Y Compare: put the number of a scanline in this register, and the LCD Status interrupt (or LCD STAT) will fire whenever HBlank occurs on this line specifically. Almost like setting a breakpoint.

And this is what the game does here.

When the interrupt occurs, the execution pointer will jump to the address $0388, the hardcoded value for this interrupt. The code can then shift the position of the background position after the clouds have been drawn, but before the sea appears.

Link's Awakening Moving the background offset during HBlank

By moving in sync with the waves animation, this gives the illusion that the horizon is stable. Here is what the scene becomes with vertical motion compensated.

Link's Awakening Sea Sequence Background movement

Note that the horizon motion is not perfectly compensated for: sometimes it moves one pixel up or down. I’m not sure if this is a bug or an intentional feature left there on purpose ; but it arguably looks better than having a perfectly stable horizon.

Differential scrolling

This is nice, but still lacks motion. To add some depth, an effect often see in 2D games is differential scrolling: scrolling portions of the background at different rates, to give an illusion of perspective and depth.

But remember, we still only have a static tiled background here. How can we add a differential scrolling effect on this constrained grid?

Well, we previously shifted the position of the background when the rendering reached a specific scanline: the same mechanism can be used again, but to shift the background horizontally.

This time we need to break at several different positions, one for each screen section. For this the game will divide the screen into five horizontal sections – and assign a scanline to each section.

Here is the relevant section of the game code that performs this effect.

    ; List of scanlines to divide the screen in horizontal sections.
    ; This is used to enable differential scrolling during the sea intro sequence.
    db $20, $30, $40, $60, $0  ; upper clouds, lower clouds, sea, upper waves, lower waves

; snip...

    ; Setup the next HBlank interrupt for the Sea intro sequence.
    ; e = Section Index
    ld   hl, $037F     ; hl = $037F + SectionIndex
    add  hl, de        ;
    ld   a, [hl]       ; a = next section scanline
    ld   [rLYC], a     ; Fire LCD Y-compare interrupt when reaching
                       ;   the scanline for the next section.
    ld   a, e          ; a = SectionIndex
    inc  a             ; Increment section index
    cp   $05           ; If SectionIndex != 5
    jr   nz, .return   ;     return
    ; If SectionIndex == 5
    xor  a             ; Reset the section index to 0

What all this means? When the scanline for the next section is reached, the LCD STAT interrupts fire, and the background is moved. Then the game increments the section index, retrieves in a list the scanline for the next section, and reprogram the LYC register with the new value to break when reaching the next section.

The code for this is actually quite straightforward. I was so happy when I found out the meaning of these numbers stored at $037F! It’s actually a table, mapping the screen sections indices with a scanline.

Here is how the background is shifted and moved around when rendering a single frame of the Introduction sequence.

Link's Awakening Sea Sequence Differential scrolling

And here is how the differential scrolling looks in the game.

Link's Awakening Sea Sequence Background movement

Random rain

The last thing missing is the heavy rain pouring over the ship.

These are just three different sprites (long, short, thick) arranged in several horizontal sections, following randomly-chosen predefined patterns.

By the way, the random number generator of the game is quite simple, but does its job well. It’s a function of the global frame counter, the previously generated random number, and the current LY register value (the number of the scanline being rendered at this time). Random enough.

    ; Return a random number in `a`
    push hl
    ld   a, [hFrameCounter]
    ld   hl, WR0_RandomSeed
    add  a, [hl]
    ld   hl, rLY
    add  a, [hl]
    ld   [WR0_RandomSeed], a ; WR0_RandomSeed += FrameCounter + rrca(rLY)
    pop  hl

And finally, with the rain added, here is the Sea intro sequence at it looks like in the game!

Link's Awakening Sea Sequence Background movement

All these effects are not unique to Link’s Awakening: they are of course found in many other games as well. But this game shows a remarkable combination of technical and artistic skills, associated to create a great atmosphere.

For more details, you can browse the annotated assembly code for the LCD Status Interrupt, or the code for the Introduction sequence gameplay.

Discussion, liens, et tweets

J’écris des sites web, des logiciels, des applications mobiles. Vous me trouverez essentiellement sur ce blog, mais aussi sur Mastodon, Twitter, parmi les Codeurs en Liberté, ou en haut d’une colline du nord-est de Paris.