9 Exotic Coding Tricks used in the C64 Game, Seawolves
Updated on:
Introduction
With the release of my first ever commercial game on the Commodore 64, Seawolves,
I thought it might be of interest to the coders among you as to how the game was constructed.
From the outset, brace yourself to read about some "code less travelled", as the game required several strange or quirky methods that are perhaps more
associated with the madness that goes in the demo scene.
- NMIs + IRQs running in sync.
- Real-time torpedoes thanks to "splites".
- Real-time implosion animations.
- Real-time ocean waves.
- Real-time water distortion effects.
- FLD shift + upward correction.
- GFX stream-ins.
- Quick logic.
- Branch-jumping.
Let’s check them out in turn.
#1: NMIs + IRQs Running in Synchronisation
I first combined NMIs and IRQs inside a game environment in
Parallaxian and again in
The Wild Wood, to great effect, because it offers the following benefits:
- You can easily interrupt long tasks in a raster IRQ (IRST) with the NMI to perform short scanline-exact tasks without the need for nesting IRQs; it's simpler and more elegant.
- It can be used as a safety net to minimise the effects of raster stall events, in which freak load conditions on an IRQ (say a 1 in 1000 alignment of circumstances) would cause the IRQ schema to stall / collapse for a screen refresh frame before recovering. In this case, the final NMI handler, rather than the last IRQ handler, sets the IRQ pointers / vectors for the top-of-the-screen IRQ. This way, if (for example), IRQ handler #3 out of 7 IRQ handlers stalls, the contagion only spread to the bottom of the screen before normal service is resumed at the top during the next frame. Otherwise, the stall effects would not recover until the next time IRQ handler #3 fires.
- NMIs, being timer interrupts, can be set up to trigger at pretty much any cycle along a scanline, making them more efficient in terms of managing raster time than IRSTs are; typically with a raster IRQ, you will have to carefully place NOPs or otherwise juggle code before changing a register (e.g. changing background colour) and that's after you have lost time In stabilising the IRST, whereas with NMIs, you have better control over where on the desired scanline they fire.
If the foregoing sounds horrendous and esoteric, I can only apologise for making it thus through poor explanation skills, but really, it boils down to giving the
developer a more coder-friendly way of slicing the screen up into horizontal layers that collectively form a useful game environment.
NMIs are timer interrupts, meaning that unlike IRQs, they can't be triggered by $D012 on the VIC-II chip, but instead are controlled by either of the two
timers on CIA chip #2 (likewise, timer IRQs can be set up using either of the 2 timers on CIA #1).
The timers hold the number of cycles between each NMI instance in the form of a lo-byte, hi-byte 16-bit number stored in $DD04 + $DD05 (for timer A)
or $DD06 + $DD07 (for timer B).
Those cycle counts are referred to, unhelpfully, in the Commodore 64 Programmer's Reference Guide as "frequencies".
Critical to setting up NMIs is a consistent start cycle on the same scanline each time the game is initiated.
Like IRQs, NMIs can stall too, but the effect is different; whereas an IRST stall consists of a fleeting collapse in the IRQ schema, an NMI stall event
looks more like a regrouping of all NMIs in the chain down-screen from their proper position and is caused either by (a) unanticipated cycle steal caused
by the presence of sprites, which take priority over NMIs every bit as much as they do over IRQs, and (b) an NMI handler not completing its tasks before
the next NMI is scheduled to fire.
You really have to use a spreadsheet to calculate the timer 16-bit number for each NMI instance, which is the only very awkward aspect of using them.
For more, see my in-depth guide to setting up NMIs.
#2: Real-Time Torpedoes thanks to "Splites"
The torpedoes are, fundamentally, sprites... but not in the usual sense.
This is because they are rendered in real time on a multiplexed blank "canvas" consisting of a column of 8 sprites, each split into 3 horizontal slices that
I call "splites" (from "split sprite").
Each splite is 7px deep (because 3 x 7 = 21 = the height of a standard sprite).
Thanks to an interrupt firing every 7 lines during the full vertical range of the torpedoes, each splite is assigned its own unique x-position
(including, obviously, unique MSB value).
Next, we are free to render (in real-time) 7px high (or smaller) torpedo shapes on the blank sprite gfx data of the splites, but we have to ensure that
there is a minimum of 7px (i.e. 7 scanlines) of vertical space between each torpedo.
This is to prevent ugly artefacts when the rendered torpedo moves from one splite to another.
We also have to ensure that when a torpedo is crossing between splites, both affected splites share the same x-position, to keep smooth continuity.
The diagram below shows how 8 sprites are vertically stacked to produced 24 splites.
Finally, here is a short video of me trying to explain the entire splite concept:
After the splite torpedo is moved up by 1px, instead of wiping the last line of the torpedo's gfx data, we leave it alone so that a vertical trace is formed
on the canvas.
Then we have a routine that scans all the canvas data for trailing traces and gradually makes them thinner until they vanish, so that we have a wake effect
following each torpedo.
At the same time, we flicker the torpedoes in front of and behind the char data for the water blending in the foreground, to make the wakes looks more "frothy".
#3: Real-Time Implosion Animations
When a player submarine dies, instead of a clichéd explosion I thought it might be interesting to have it disintegrate under pressure.
To do that, the player sub is switched to hi-res mode and then we simply use some nice bit-shifting instructions to destroy the sub's displayed gfx data in real-time.
#4: Real-Time Ocean Waves Effect
The same bit-shifting used for the real-time implosion (or rather, bit-rotating in this case) is used to make the distant waves on the sea animate.
This principle also applies, but in a vertical pattern, for the foreground rippling of the water, an effect that I modelled on the "get ready" screens of
Ecco the Dolphin.
With the foreground ripples, there is also a horizontal component performed by using $D016 to rock left and right at varying rates.
It's all very simple stuff, but I think it works well in the game!
#5: Real-Time Water Distortion Effects
The very first special effect developed for Seawolves was a real-time water distortion effect for submerged or partially submerged objects in the foreground.
This was done simply through short vertical bands in which the y-expand of the affected sprites was activated, and enhanced by wobbling said bands up and down.
For a time during development this was taken further and full y-expand sprite stretching was used (i.e. more than 2x vertical expand), but the gains were minimal
and too resource-hungry to be justified for the final game.
#6: FLD Shunt + Upward Y-Scroll Correction
If you have experience moving multiple sprites vertically in the play area, at some point around mid-screen you may have run into the problem of a
bad line
leaving you with insufficient CPU cycles to render your sprites on time (I won't attempt to explain the problem further as you either know from experience
what I am referring to or not).
To circumvent the issue, Seawolves performs one line of
FLD,
that is, it stalls the bad line at the affected sprite y-position so that the scheduled bad line
then occurs on the next scanline, leaving us time to render our sprites (in this case, the "splite" columns).
However, this has the undesired side effect of shunting the characters on the screen below that point downwards, so on the very next scanline we have to compensate
with a one line vertical shift back upwards again using Y-Scroll on $D011.
#7: GFX Stream-ins
Even if we had enough RAM to have unique sprite definitions for all the animation frames of the enemy ships and the player subs, I still would not have done it that way.
Rather, it is far more RAM-efficient to stream-in the gfx data for the turning radars on the spy ship (for example), or the spray on the hydrofoil,
or the helicopter rotor blades, etc.
This means that we only need predefined gfx for the parts of the sprite definition that get changed, not the whole sprite.
When the player subs change direction, they too are swiftly redrawn "in the blink of an eye" to their mirrored counterpart (except, if you look carefully,
they are not literally mirrored, as that would make the lighting inconsistent and we can't have that!)
#8: Quick Logic
Again and again in the code for Seawolves, there are cases where a subroutine is only executed if multiple "logic gates" permit it.
You can do this the slow and ugly way by LDA CONDITION_n for n conditions with a branch instruction on the next line, or you can use the logical operators to save CPU time and RAM by stacking them as LDA CONDITION_0 then ORA CONDITION_1 to n, for n conditions with one shared branch instruction at the end.
Not understanding what I mean?
Check out my blog article on the subject, ORA: A Special Use in Branch Testing.
#9: Branch-Jumping
Often in 6502 coding you will want to jump ahead by a few bytes and the popular way to do that is JMP $****.
However, you can save 1 byte of RAM by using the branch instructions instead, as long as you know which flag(s), if any, are guaranteed to be on or off at the jump point.
For example, if you know the carry flag will always be clear at the jump point, and if the jump distance is within branching range, you can replace JMP with BCC.
Closing Comments
The above are just some of the methods used in Seawolves to make the game come to life, and I left out the parallax scrolling on the "sea mist" levels as well details
of the bespoke SFX player, which was developed with deployment in Parallaxian in mind also.
Despite its simple looks, Seawolves has very technical inner workings that might not be that normal outside of the kind of crazy stuff that the demo scene lunatics produce.
There are some other little tricks hidden away in the code, but I can leave that for others to discover at a later stage, so for now I hope the foregoing
has been of interest to those of you with a coder mindset!
Finally, if you have not yet bought the game, it would be nice support for the developer (i.e. me!) if you did... it only costs £4.99 and yes,
I know everyone expects stuff for free on the C64 these days, but this game was not knocked out in a few weeks by any means, as hopefully should be obvious
when you play it.
PS - If you value my work and want to support me, a small donation via PayPal would be nice (and thanks if you do!)