Introduction
I LOVE the boats! Such beautifully drawn graphics, it’s a shame to whoever drew them that it took some 35 years for them to be discovered 😓
Thanks to the free open source Phaser framework, we can create a desktop and mobile compatible version of various parts of the Goldfish Game.
Build The Scene
When I build my Skoolkit disassemblies, I tend to scale the images up by “4”. This is simply because the Spectrum screen is pretty low resolution, and should we keep a 1x1 ratio, the image assets would be tiny!
With that in mind, the Spectrum screen is 256x192 pixels, hence to initialise
our Phaser window I’ll use 1024x768 (which is 256x4 and 192x4) to keep the same
aspect ratio as the Spectrum screen. But, as the window this produces is a bit
too large - let’s use zoom: 0.5
to half the size of the initialised window.
const config = {
type: Phaser.AUTO,
width: 1024,
height: 768,
scene: Boats,
backgroundColor: '#CDC6CD',
physics: {
default: 'arcade',
arcade: {
debug: false
}
},
scale: {
mode: Phaser.Scale.FIT,
parent: 'gameContainer',
autoCenter: Phaser.Scale.CENTER_BOTH,
zoom: 0.5
}
};
const game = new Phaser.Game(config);
I won’t explain any of the other configs here, as the guides on the Phaser website do a great job of explaining the inner workings of the framework.
As I’ve defined my scene class, let’s go ahead and write it:
class Boats extends Phaser.Scene {
constructor() {
super();
// Simply because I've made the assets use scale 4 in the disassembly.
this.gameScale = 4;
}
preload() {
// Preload the boat assets.
this.load.image('boat-1', '/images/booty/boat-02.png');
this.load.image('boat-2', '/images/booty/boat-01.png');
}
create() {
// Add the "sea"!
this.add.rectangle(0, 8 * 8 * this.gameScale, config.width, 8 * 12 * this.gameScale, 0x0000C5).setOrigin(0, 0);
// Store widths of the background items.
this.boatWidth = this.textures.get('boat-1').getSourceImage().width;
// Create two boats on screen.
let boat1 = this.add.image(0, 0, 'boat-1').setOrigin(0, 0);
let boat2 = this.add.image(config.width - this.boatWidth - 80, 0, 'boat-2').setOrigin(0, 0);
}
}
There are a few things to note here:
- I’ve added
this.gameScale
as a class-wide “constant” for ease of reuse (all my UDG images use “4” for the scale). - Both boats are the same height/ width, so we only need to track a single width to cover them both.
- There are 8 bits in a byte, so where I have calculations like:
8 * 16 * this.gameScale
this means 8 pixels * 16 character blocks * the scale. This is to select the same area as would be the 16th column in the Spectrum display.
Let’s see what we have so far:
So far so good!
Animation
So now we run into our first problem, it’s not a huge problem but it’s definitely something which is going to take a bit of thought…
In the game, and TBH, quite a few Spectrum games, the display “wraps-around”. Think of JetPac, where if you fly off the left or right of the screen, you “re-appear” on the opposite side. Only … it’s also seamless, so if one pixel of your character goes over the threshold of the display, then one pixel will appear on the other side. Now, this is kind of tricky to achieve but it’s a lot easier when you write it with a “ghost”!
Currently we have two boats … but, we can create a third boat! One which “pretends” to be the first boat and is initially rendered off-screen. Once the first boat begins to leave the screen, our “ghost” will begin to enter the screen on the right-hand side and take it’s place. Now the issue is more one of timing than of implementation.
But wait, the boats differ … plus, there is a gap between them. We could manage this by tracking which boat is which and blah, blah, blah … but this rapidly becomes very technical. The easiest solution which springs to mind is; TWO GHOST boats! Let’s see how this looks:
class Boats extends Phaser.Scene {
constructor() {
super();
// Simply because I've made the assets use scale 4 in the disassembly.
this.gameScale = 4;
}
preload() {
// Preload the boat assets.
this.load.image('boat-1', '/images/booty/boat-02.png');
this.load.image('boat-2', '/images/booty/boat-01.png');
}
create() {
// Add the "sea"!
this.add.rectangle(0, 8 * 8 * this.gameScale, config.width, 8 * 12 * this.gameScale, 0x0000C5).setOrigin(0, 0);
// Store widths of the background items.
this.boatWidth = this.textures.get('boat-1').getSourceImage().width;
// Create two boats on screen.
let boat1 = this.add.image(0, 0, 'boat-1').setOrigin(0, 0);
let boat2 = this.add.image(config.width - this.boatWidth - 80, 0, 'boat-2').setOrigin(0, 0);
// Create ghost boats off-screen at precisely the right distance so they scroll in.
let boat1Ghost = this.add.image(config.width, 0, 'boat-1').setOrigin(0, 0);
let boat2Ghost = this.add.image((config.width * 2) - this.boatWidth - 80, 0, 'boat-2').setOrigin(0, 0);
// Store the boats in an array.
this.boats = [boat1, boat2, boat1Ghost, boat2Ghost];
}
update() {
// Move each boat.
this.boats.forEach((boat, index) => {
// Slow the boats down.
boat.x -= 0.5;
// Wrap around logic to cycle boats and ghosts seamlessly.
if (boat.x < -this.boatWidth) {
boat.x = (config.width * 2) - this.boatWidth;
}
});
}
}
So, the additions here are that we’ve now initialise FOUR boats and stored them
in an array. We’ve also added a loop in the update()
method to cycle through
all the boats and move them left slowly-ish.
When each boat is completely off-screen on the left-hand side
boat.x < -this.boatWidth
(the currently processed boat X position is less
than a boat width casted negatively), so when the WHOLE of the width of the boat
is off-screen, then we update the X position to the right-hand side ghost area
off-screen. If this was viewable, then the whole of the boat would just
“appear” out of nowhere, but as this is already off-screen, we don’t have to
worry about being neat.
The effect looks pretty accurate to the game, albeit a lot smoother! We could use our scale to make it completely exact but as this is a background effect let’s keep it nice looking.