Wave Racer | Dev Log

Hello!

Wave Racer tl;dr

A top-down, 2d, 1-bit racing game heavily inspired by (but legally distinct from) Wave Race 64.

Note: name may change before release.

Goals of this dev log

I would like to share some of the interesting approaches I've taken, as well as practice more technical writing.

I have a long way to go, but I've been developing occasionally for the last few months. I've built enough core behavior and can now see a path to actually finishing up the game. My goal would be to launch 6-9 months from now.

Tech overview

My goal is to use only the PlayDate Lua SDK. The SDK provides a lot of great building blocks, but I wound up needing to write my own collisions and deal with lots of complexities around rotations. The end result is a decent amount of game-istic physics and plenty of trigonometry.

Future posts

  • How to rotate all the the things, all the time!
  • Infinitely scrollable, rotating, 360 degree tiled backrounds
  • What I learned about performance tuning Lua
  • Wakes? oof.
  • Creating a CPU/AI to race against
  • Collisions!
  • General progress updates

Sneak peak

For now, introducing the game with a little demo of some racing action, to give a sense of what the game is all about. I have been focusing on the core racing mechanics first, and over the coming months hope to build up the surrounding game mechanics (several courses, menus, racer selections, championship modes, lots TBD).

first_demo

4 Likes

Very cool!

Have you experimented with speed? I'm imagining it being slower would shift the gameplay from arcade more towards ... something not arcade? It would require multiple cranks to turn, meaning more forward planning would be required. Just a thought!

Have you experimented with speed?

Definitely! Sharing more about how I've been building out the controls:

Play only requires steering with the crank. When the crank is vertical (parallel to the console body, pointed up), the jetski goes straight. Pulling the crank towards you will steer left, pushing away will steer right. The more you pull/push on the crank, the more you turn. This is similar to a joystick control (or steering wheel on a car), where you are frequently returning to the center position.

Getting to some of the physics of the controls/speed etc, there's a few knobs that I have abstracted out into different boats/drivers:

  • Acceleration - There is a constant thrust applied, the throttle is always on. However, I can tune how much thrust is applied on each tick to control the acceleration.
  • Top speed - This is the max speed the boat can go. Thrusting at top speed has no effect. Of course, can raise/lower this limit to increase the overall speed of the game.
  • Handling - The angle of the crank is not 1:1 applied to the boat. The short version: when the crank is turned I can throttle how much of the angle gets applies to the boat. However turning the crank more will always apply more angle to the boat, up to 180 degress (going past vertical on the bottom doesn't give any more turning). For a very snappy turning I could go closer to 1:1, or I can throttle the crank angle quite a bit to give sluggish control.
  • Grip - When the boat steers, due to angular momentum, the new direction is not immediately applied. To an extreme case, thinking of gliding on ice: you could steer the boat 45 degrees while the direction of the boat remains unchanged. This would be a zero/low grip. A high grip would have zero lag. This is an area I will likely keep tuning, as it contributes a lot to the "feel" I am after. When turning, I want have the boat feel like it slides a little. There's an element of "mass" here that could be it's own bullet but for now I've rolled this into "grip" as "close enough".

I am aiming for 3 boats:

  • Neutral - average across all aspects
  • Tight but slow - High handling and grip will make navigating the course easy since the controls will be very responsive to the crank, but with a lower speed to level set. This is the high floor, low ceiling boat.
  • Loose but fast - Low grip and less responsive handling, but high speed. This boat will be the hardest to control but if handled right should be the best overall. This is a low floor, high ceiling boat.

Combining handling & grip: the more the boat is turned the more speed is lost, so taking sharp turns slow down quite a bit, shallow turns not so much.

The speed and controls are critical, as I am not likely to introduce a lot of other core game mechanics. For an old-school example, think more Wave Race 64 and less Mario Kart - more about how you run a course and not about items/powerups/big obstactles.

  • I want the controls to "feel" fun, and to feel like you are controlling a tiny little jet ski in the palm of your hand.
  • I am hoping the controls reward skill: being able to handle speed with a bit of a loose grip will require taking a challenging racing line through a course to make sure that the boat does not slow down due to inefficient turning. There should be a challenge in terms of avoiding collisions to bouys/CPU players, as well as planning & timing passes.

This all might seem obvious for a racing game.

All that said...I am a pro web developer and an amateur game developer, I know I'll likely come up short in certain areas. I did not use a physics engine since I am actually interested in getting into the maths myself and really have control over the feel and perf, this might be a classic downfall.

Oh! The speed makes sense if it is jetski. I thought it was boats.

Are you going to simulate waves? Perlin noise might be useful.

:sweat_smile:

Ok so, one of the main features of Wave Race 64 is the waves themselves. Navigating the contours of the waves is a huge part of racing, since the waves will limit the areas and timing of turns (wave pushing you in the air, etc).

Ignoring the graphics for a bit - I did play with having random moments where the grip is 0, so the boat turns but there's no actual turning. This would simulate being up in the air due to a wave, making turning/planning hard. Going further, I'd also not need to account just for air-time but also just generally being pushed around by waves.

This actually had a decent effect, there is something there. I got conceptually stuck on needing to have some visual feedback on where the waves were. I thought about maybe a clever background graphic alpha collisions to trigger the the push/pull/air time if I could get it within perf. In my 1 minute of googling, perlin noise might part of a trick there in terms of applying wave-like gradient effects.

Maybe there's a simplified version of this I can re-visit, or maybe just a bit of ambient random push/pull/air times if done small enough would give a nice effect without a high simulation cost.

(Also apologies I'll likely interchange boat & jetski frequently)

Wave Race 64 is amazing, phenomenal game.

I also love Wave Rally on PS2 (see this video of mine) the developer of which even published information about their water simulation.

So there are many ways that I think this could work on Playdate.

Visually: I think particles are out, too many would be needed. But you could overlay multiple transparent images scrolling at different speeds and directions.

Height: you could represent things as Perlin noise generated height maps (generate it once, not continually!). Again with multiple overlaid to make the water more dynamic. Then you would do simple "get pixel" to test the height at player position. I'd use this information to add vertical offset with momentum. When the player is in the air you'd turn of steering and grip. etc. If the player is stopped they would simply bob up and down.

Nothing would be random, everything would be deterministic.

In fact, I do pretty much exactly this in Daily Driver for road surfaces (using drawn layouts rather than perlin noise height map). Most of the time cars on the ground but I also have hover cars and drones that are mostly airborne.

I really like these ideas!

I think I could try to play with a static version of this even with just my current water effect graphics, having a non-visible height map that corresponds roughly to the wave that I could use to impact the player jetski.

The tiled background has been the hardest part so far (even more so than collisions) due to the rotations & scroll, and keeping it within a decent frame rate. I hope to do a full write on this soon. Only mentioning this as an example of limitations of myself or myself in combination with the playdate and having everything constantly rotate (not complaining just admitting I lack some skills). I have heavy usage of pre-rotated sprites and image tables, I am thinking I will need an image table equivalent of the height map.

1 Like

The rotating background is very impressive.

Personally I'd try a darker image for the water to improve contrast, that way the wave breaks and wakes would be drawn in white which would probably feel more natural.

Even if you don't use Perlin noise you can just use multiple sin() waves, which is perhaps what Wave Race 64 does.

And you could use the debug drawing colour overlay in the Simulator to draw wave information that won't be drawn on the device. Eg. You could draw the contours.

This link goes to the middle of a thread:

Lander, Zarch/Virus, Elite all did it similarly too:

1 Like

Quick n dirty version of of straight-up inverting the colors. Not saying this is the final version but I also like how this is coming out.

Maybe a little concern it's looking too much like space, but reworking the water pattern would help.

inverted

Edit: on device, the contrast is trickier with the dark background. I am going to keep the darker theme in mind but might need to riff on it a bit.

1 Like

Animated turning jet skis

In general I am trying to sprinkle in graphics polish as I work on the more "core" mechanics, to make sure that I don't stray too far from the look n feel I am after. My overall aim is for the graphics to not be a detriment and occasionally be good.

That said... sharing a quick update from last night: beefing up the animation of the jet skis while turning.

turning

I went with a 4 step turning animation: neutral + 3 increasing amounts of tilting/turning. The more you turn the crank the more the jets tilts.

I also applied the same to the CPU - the more it turns the more it tilts, etc. The only interesting bit for the CPU is needing to account for the rotation via image tables, so now instead of a single image table for the rotations I have 7.

The 4 steps were all drawn in GIMP by hand. At the scale of graphics, and with my own artistic ability, I am hoping to abstract over the human driver. I plan on making slightly different shapes/designs (ie paint jobs) for each player using this framework.

The effect came out better than I hoped. As with all things, I think there's infinite fine tuning to be had, but compared to the "before" this feels like a real step-up.

2 Likes

Nice! It certainly adds a lot!
Because you have the 'camera' locked to the player, you're likely going to need to do the rotations for everything you put in the world right?

Rotating all the things all the time

The camera for Wave Racer is fixed on the playable jetski. When a player interacts with the controls, it's not the jetski that moves, but every other point (aka Sprite) in the game.

Since this is a racing game with lots of turning, this means that every sprite is constantly moving and rotating, in order to give the effect of the jetski moving through the world.

As the player rotates the crank, I calculate how much rotation should be applied to the world in terms of an offset. For example, rotate the world by 3 degrees, and not, rotate the world to 177 degrees in some absolute measure. This allows me to place sprites all over the map on setup (buoys & obstacles), having sprites moving on their own accord (ie a CPU controlled jetski) and rotate them all by an offset from the input of the crank.

The steps of the algorithm for rotation, using a buoy for example and with no other movement (eg just rotating the world):

  • Calculate the current distance to the point of rotation. For the game, this is always the center of playable jetski towards the bottom center.

    image

  • Calculate the current angle to the the point of rotation.

    image

  • Figure out the new angle by adding the offset from the crank to the original angle.

    image

  • Move to the new angle at the prior distance.

    image

Here is a slightly modifed version of the function I am using:

function GameState:translate(x, y)
    local a = x - self.centerX
    local b = y - self.centerY
    local rad = math.atan2(b, a)
    rad += self.crankPositionRad
    local sin = math.sin(rad)
    local cos = math.cos(rad)

    local dist = math.sqrt(a * a + b * b)
    local translatedX = self.centerX + (dist * cos)
    local translatedY = self.centerY + (dist * sin) 

    return translatedX, translatedY, dist
end

All sprites must use this in their update() to be part of the world:

function sampleSprite:update()
    local x, y = s:translate(sampleSprite.x, sampleSprite.y)
    sampleSprite:moveTo(x, y)
end

I showed the movement of a buoy above, but this is being applied to all the background tiles, the wakes, and the CPU jetski - literally every sprite!

However, the world is not just rotating, it is also moving as the jetski propels itself.

After applying the rotation, we need to also apply the movement of the jetski to all points.

In the simplest example: moving straight up - we would subtract from the y axis for all points, to move all points downwards, to give the impression that the jetski is moving upwards.

I have additional logic for handling the speed, momentum, reaction to collisions, and some random choppiness, etc etc.

To combine these with the rotations, I can simply add these to the new x, y coordinates, eg:

    local translatedX = self.centerX + (dist * cos) + self.momentumX + self.bumpX + self.choppyX
    local translatedY = self.centerY + (dist * sin) + self.momentumY + self.bumpY + self.choppyY

Now, if the jetski is moving, sliding, or got bumped in a collision, all sprites will move accordingly, and in unison, preserving the effect I am after.

Rotating images

As each sprite is being rotated through the world, it's graphical representation also needs to be rotated.

Naively/logically, this could be done via the built-in sprite:setRotation(angle) - doc

However, I have confirmed the docs do not lie:

This function should be used with discretion, as it’s likely to be slow on the hardware. Consider pre-rendering rotated images for your sprites instead.

So... I am pre-rendering by creating a sprite sheet containing all the rotations of my image, and loading those into an image table. Once I have the angle of rotation for the sprite, I lookup the angle in the image table to get a pre-rotated version of the graphics.

I am using the spriterot utility that came from the playdate community.

Tow great write-ups on this forum that really helped me out:

Here is a snippet of one of the jetski sprite sheets at 2 degree increments:

image

And here is a reduced version of the code I am running, just to give a sense of how this is used end to end:


local jetskiImageTable = playdate.graphics.imagetable.new("images/image_tables/jetski")
function jetskiSprite:update()
    local angle = ... -- omitted for bevity
    local image = jetskiImageTable[((angle + 1) // 2) % 180 + 1] -- for 180 degreee steps
    jetskiSprite:setImage(image)
end

Challenges

Summing up the approaches above, there are some inherent challenges.

Performance: Every sprite is running through the distance formula and angle calculations shown above, as well as calling :moveTo(x, y) and :setImage(image). Both of these limit the max number of sprites that I can contain in my world before starting to drop frames. I am also generally re-drawing the entire screen.

Memory: My images are simple and small, however needing to create the sprite sheet means that I am ostensibly multiplying the size of the images by 180 or 360. For most game objects I am creating 180 frames, for 2 degree increments, but for the backgrounds, in order to have a smoother effect, I am using single degree increments, so 360 versions of the images.

This makes the image files larger, and also the image tables themselves grow due to needing to have 180 or 360 entries.

Combining the above, I am always mindful of headroom and tradeoffs for all approaches. Some early bottlenecks show that I have roughly 100 sprites to work with total before dropping frames. That might sound like a lot but I have an animated, tiled background, buoys for the course, the (now animated) jetskis, and the trail left behind for the wakes of jetskis.

I will go deeper into perf in another post but right now CPU is sitting around 50% and memory after loading up the image tables is about 11-12mb. I originally wanted more "frames" of animation for the background tiles, but quickly ran out of memory, so the backgrounds are the biggest tradeoff as well, and running into memory issues appears to be my looming bottleneck.

As aside: I am already reducing the sizes of my final images via pngcrush, and summing up the sizes of all my sprite sheet PNGs, I am not at 1mb, so consuming ~11mb on startup is a little confusing. I think there's more to investigate here about memory consumption, I am hoping I have overlooked something obvious.

I am going to add in more obstacles as I get into the course design: rocks, shores, piers, etc. I still have headroom to fit some in, but I will have to keep these constraints in mind throughout.

That said, I enjoy the constraints, for me it's part of the fun of the Playdate.

Knowing I have a limited budget for memory and CPU clock time in each frame makes me really mindful about any pieces of functionality or features I add, which I hope will lead to not only a better game, but one I might "finish" some day since there isn't a world in infinite possibilities to pursue.

Finally, I see some truly impressive games being built for the playdate: physics, 3d, beautiful imagery, etc. There's likely a macro approach or rookie mistakes I am making, but I get the sense I am also not taking the "easy" path by going for a fully rotated game.

1 Like

Playdate compiler converts images to its own format (black + white + alpha), so anything you do to the PNG files is lost during the conversation. Crushing will only make your source files take up less disk space.

Regarding memory usage, and rotated sprites, check out: https://blog.gingerbeardman.com/2021/08/07/how-big-is-a-sprite-sheet/#memory-usage

You might investigate managing and drawing images to the screen or a single sprite, or sprite BG, rather than using the Sprite system with hundreds of sprites. There's a lot of overhead per-sprite which will add up. Only you know how much benefit sprite system is giving you.

1 Like

Playdate compiler converts images to its own format

That makes total sense.

Thanks for sharing your write-up, that's super helpful.

(Also I had seen your thread on Daily Driver and thought about attempting 3d modeling for my jetski since the effect you achieved is awesome and I have some CAD experience...maybe another day.)

Right now, my background image tiles are the biggest memory hog. I think I can start to tradeoff tile size (more tiles means more CPU consumed on rotations & updates) and/or the number of steps/angles in the image table (less smooth rotations) as I need to buy more memory headroom.

Checking the malloc log, each 360-step background image is consuming 3.6mb, and I animate the background (adding a little wiggle to the waves). I am using 3 frame animation but created 8 frames - 8 * 3.6mb not being able to fit, so went down to 3.

Re: not using Sprites. I am not leveraging ton of the features of Sprite. Mostly treating it as a coordinate and an image (movement/rotations and collisions done outside Sprites). There's some book keeping/mechanical bits I'd have to work through - I make use of :add()/:update()/gfx.sprite.update() for example - I'd need to replace that with a small pattern of my own (not a huge deal but it was a nice pattern to reach for).

I'll share a write-up of my background approach next/soon, that might expose some alternative ideas!

2 Likes

I open sourced my setup for that. If I get a spare moment tomorrow I'll throw together a jetski and render it using my setup. I'll be sure to straighten the camera so it's overhead view.

the tilting is a nice touch - feels less static.

just to be sure: you only pre-calculate 1/4 of the images? all other quadrants are just mirrors of that initial set

I personally render all of them because my cars are shown at an angle, the wheels are turning, etc. At its most basic I could have mirrored some things, but once I started rendering I just kept adding more unique details and little touches.

was not obvious in above pics but yeah, if slanted/perspective - you are stuck

1 Like

That's incredibly gracious, thank you.

Oh! I could totally do this for my backgrounds, great idea.

1 Like