Geo-Referenced Sprites with Lateral Motion
NOTE: The following section assumes the reader has above average technical knowledge of sprite rendering on the Commodore 64.
I think it's fair to say that games that scroll in two directions are much more challenging to program than those that only scroll in one direction.
Primarily, it has to do with the additional coding considerations necessary in ensuring that the things that have been scrolled off-screen are still there when you perform a u-turn and scroll back to where you last saw them.
As mentioned in the previous article regarding the mountains scroller in Parallaxian, joining up a scrolling background in a wraparound planet model can be tricky enough, but having geo-stationary hardware sprites reappear where you left them also poses its own set of difficulties, compounded when you want them to additionally move about laterally and not just vertically.
As if to underline this point, the great John Rowlands mentioned the unexpected and frustrating delays he encountered when trying to synchronise the monsters walking back and forth with the scroller in Mayhem in Monsterland, despite having what was relatively recent experience with such sequencing in the "island hopping" sections of Creatures 2. (Then again, Mayhem was scrolling at 8 pixels per frame at times, whereas Creatures 2's island hopping was a 1 pixel per frame speed, which may have explained his sprite sequencer woes with the newer game).
It's possibly telling, therefore - and this is just conjecture on my part - that several high profile bi-directionally scrolling games, such as Dropzone and
Uridium, apparently don't do this kind of hardware sprite geo-anchoring at all; Uridium rolls attack waves in from either
side, but they're not geo-referenced (plus the "terrain" over which you fly isn't wrapped as it's a spaceship, not a planet); meanwhile, Dropzone doesn't even use hardware sprites for the
enemies, other than the "Nmeye" and "Blunderstorms", employing char-based software sprites instead,
but in fairness that seems more likely a direct consequence of the C64 version being a straight port from the Atari original.
So what's the answer? Well, given that there's more than one way to skin a cat, let's consider (in simplified terms) some potential methods:
- Scan a table holding the unique ID of each block in which the sprite type (e.g. helicopter) is to be anchored and then activate said sprite and its motions if the associated block scrolls onto the screen - see diagram below.
- Forget about the blocks and instead create an overlay sprite map synced to the scroller... This is how John Rowlands did it with Mayhem, although I think it's unnecessary and bloaty for Parallaxian.
- As in 1. above, but instead of the roving sprite anchored to the table, have a table-referenced de facto axis either side of which the sprite horizontally moves.
- As in 3. above, but make it so that the sprite moves back and forth horizontally on only one side of a datum line of reference.
Getting this right for Parallaxian was always going to necessitate a fast-to-execute / lightweight solution placing minimal burden on the IRST, given the previously stated importance of minimising clock cycle overhead in what is a CPU-battering game with a multitude of tasks to perform every frame.
As usual, I did it the wrong way first and ran - understandably perhaps - with option 1, having been greatly encouraged by the relative ease of previously making geostationary sprites sync with the scrolling landscape.
In the interests of keeping it as simple as possible, I used pretty much the same code for sliding the helicopter sprite left and right of its geostationary reference point as that used to sync it with the landscape's horizontal scroller, so sometimes the helicopter would be instructed to scroll with the landscape to the right, yet corrected to scroll at a different rate to the left, leaving a net gain or loss of lateral position by the time the IRST's plexor got round to updating the sprite's x-position.
However, things became dicey at the boundary conditions (as they inevitably do), especially when the helicopter had to roam from left to right partially off-screen and partially on-screen at the far left hand side, leading to code bloat and needless complication... plus it was taking ages to debug.
(As a point of relevance - although maybe it's somewhat remiss of me to point this out - Mayhem's lateral motion handler for the monsters roaming back and forth is technically inconsistent in places if you take the time to experiment with the same far left boundary condition as it crosses the edge of the screen).
So, to avoid experiencing frustrating delays in progress, I cut code on option 2 having sketched out the various conditions the axis and roving sprite could be in, but, after around an hour of work on it, it too was starting to look overly complex with both addition and subtraction operations going on (depending on which side of the geostationary axis the helicopter was) and maybe not as clock efficient as it should be so I stopped before it even made it to its first test compile!
I took a break, thought about it a bit while I was far away from the screen, and thankfully - given that these epiphanies seldom arrive so quickly - found option 3
floating into view.
Right away it became apparent that it would be the lightest, fastest solution with no subtraction operations involved, only addition... even though it meant discarding several days'
worth of programming (but as I believe Andrew Braybrook once said, it doesn't matter if you sweated blood to code something; if it doesn't work as required, out it goes - or words to that effect).
Inevitably, my first implementations of it were plagued by unnecessary complexity, caused by treating the datum lime as a "ghost" sprite complete with its own MSB variable that was checked using the BIT instruction... No, no, no... just no!!!
A DATUM INSTEAD OF AN AXIS
A cleaner, faster, lighter way is to treat the datum as having 3 simple conditions and work everything out from there:
- Datum is to the left of the x-position = 0, MSB = 0 - in other words, to the left of the far LHS of the screen; this is condition 1.
- Datum is to the right of the x-position = 0, MSB = 0; this is condition 2.
- Datum is to the right of the x-position = 0, MSB = 1, but to the left of the x-position = 89 (the far RHS of the screen); this is condition 3.
So, with those 3 datum conditions settled on, the code would read the helicopter sprite's relative x-position from a pre-calculated range table and simply add that tabular value
to the present datum x-position on every screen refresh.
Obviously, this meant no fancy or complex (i.e. bloaty and slow) on-the-fly calculations; instead, because the back-and-forth motion had to have inertia built in as the helicopter turns back on itself, the lateral positions would be based on the SINE values from an angular sweep of a circle, all worked out by a simple Excel (well, Open Office!) spreadsheet, which you can download here, and manually entered into the range table.
The testing schema then looked like this:
- If datum condition = 0 and datum x-position = rightmost limit of helicopter's roaming range from the datum, set helicopter mode = 0 (i.e. deactivate helicopter).
- If datum condition = 0 and result of addition from range table sets carry flag, then set helicopter x-position = said result and set helo MSB = 0.
- If datum condition = 0 and result of addition from range table doesn't set carry flag, then don't update helicopter x-pos.
- If datum condition = 1 and result of addition from range table sets carry flag, then set helicopter x-position = said result and set helo MSB = 1.
- If datum condition = 1 and result of addition from range table doesn't set carry flag, then set helicopter x-position = said result and set helo MSB = 0.
- If datum condition = 2 and result of addition from range table doesn't set carry flag, then set helicopter x-position = said result and set helo MSB = 1.
- If datum condition = 2 and result of addition from range table doesn't set carry flag and datum x-position < 89, then set helicopter x-position = said result and set helo MSB = 1.
- If datum condition = 2 and datum x-position >= 89, then set helicopter mode = 0 (i.e. deactivate helicopter).
This worked beautifully, as the video below shows, although I am not saying the finished game will have the helicopters move about like that; rather, the primary objective was to produce a method that can be used anywhere that geo-referenced sprites are required in the game.
As for the helicopters, their true role as friendly aircraft should become clearer in the forthcoming gameplay update article.
NON GEO-REFERENCED SPRITES
Of course, not every enemy sprite has to be geo-referenced.
The Hunter Killer point-defence drones work more on the "Uridium Principle", that is, they are activated when the plane reaches a certain location and then their lateral positions are updated in response to the plane's actions, so that they can pursue the plane over great distances.
Likewise, the Backfires, which the player has to hunt down, roam the planet at low speed until the plane catches up with one, whereupon it panics and attempts to flee, making sharp turns in an effort to throw the plane off its tail... As with winning a girl's heart, the thrill is in the chase ;-)
Lest there be an accusation of parsimony in revealing source code, a workable example has been provided, which you can
download here as a text file and then copy, paste
and modify as required... it's not optimised so you can play around with that yourself, but it provided the basis for the cool "swarm" effect on Parallaxian.
In the interests of making the source code broadly accessible to coders using different assemblers, hieroglyphic macros have been shunned and the code has been broken up into "bite-sized" pieces to make it as readable as possible, again, a habit I use with my own code anyway as it makes debugging much easier than is the case with long, unbroken lines of instructions.
- It can easily be stripped down by any competent coder to work for one-directional horizontally scrolling games, if that's what you require.
- You should call the LEFT or RIGHT subroutines every time the game's scroller is executed in the relevant direction.
- Since #2 above implies JSRs from an IRST, all variables should ideally be zero-paged for faster execution and, if you're scrolling at high speeds, e.g., 8 pixels per frame, you should think of self-modifying the "step" by which the x-position of the sprite is updated, from ADC #01 to whatever gap you need to keep raster time consumption down... the code will not fail if you do this.
- The feeder subroutine could, one supposes, be called from a game's main loop, not just from an interrupt, as long as the frequency of the main loop exceeds that of the screen refresh; that said, it's generally preferable to call it from an IRST.
- The "roaming" subroutine works fine when executed from the main loop, as it performs its maths and leaves the results for the interrupt to collect and apply when it gets round to it, so to speak; that said, it can run from the interrupt if that's what you prefer.
- It is assumed you will write your own IRST-based plexor / sprite updater to write the results to the relevant VIC-II locations... The lower border or, if using open borders, the lower VBLANK is, in my view, an ideal place to perform such writes.
- The code also uses an "illegal" op code in the roaming subroutine, again just for speed of execution, even though that subroutine was designed to run from the main loop.
- There is a lot of self-modifying in the source code; I often find it's the best / most efficient way to get things done, so no need to shun it.
- You can also download the Excel spreadsheet file used to generate the sine wave values for the back-and-forth motion used in the video.
VIDEO: DEMONSTRATION + METHOD
In the video below I talk for around 17 minutes on my approach to the subject. Enjoy.
Interested in coding games on the C64? Check out the following on Amazon (and yes, I get a tiny pittance if you buy via the banner below):
Help Make Parallaxian Happen
I would ask you if you could consider a small, recurring monthly donation (and depending on your tax situation, you might even be able to designate it as a charitable donation rather than let the taxman have it).
And don't worry, you can cancel at any time... but in the meantime, it would be a welcome contribution, however petite.
Oh, and as a special thank you, all who do this will be credited in the game (unless you opt out of it if you have the same kind of incognito hermit tendencies I do).