Working on a little 3D racing game called Trackminia

Ahoy! I've been making a small racing game, primarily inspired by "Trackmania 2" (my favorite in the series, because of its lovely drifting mechanics).

playdate-20240511-181645

This clip is recorded in the simulator - on the actual device, it wavers between 40 and 50 fps, varying depending on your viewpoint. It'd be sick if I could get it to maintain 50 fps on the Playdate, but it's feeling less and less likely as I add more stuff, lol. I'll probably end up locking it to either 30 or 40 fps, even if it's "sometimes" able to go higher.

Happy to answer any questions about wut's going on here, but to start, here's some general techy info:

  • Everything is written in C, mostly for performance reasons but also because I wanted more practice with native languages
  • The renderer is primarily a raycaster, like the original wolfenstein or doom
  • Extra objects are rasterized 3D meshes (mostly the car, in the clip above - I'm trying to avoid the "pre-rendered sprites for different viewing angles" style). Currently I'm relying on the SDK's built-in fillTriangle() helper to draw the meshes, but I'm gonna need to write my own at some point so that objects can get occluded by the walls
  • The dithering is based on a fullscreen blue-noise texture

Here's a twitter thread showing the development process of the game so far, including a bunch of video clips which were too big to embed in here.

Thanks for reading!

16 Likes

This looks incredible, love me some trackmania! Bird thread doesn’t load for me, but did you try other dither styles? I think this fullsvreen effect works really well, and it probably looks great on the screen!

Ope yeah i guess twitter requires an account to view a thread now (cooooool). Here's a mastodon thread instead, it's slightly shorter but has most of the videos.

When first starting to get the dithering to work, I did a white-noise pattern just to get something on the screen...it changed every frame, so it was basically nightmare mode.

Next, I tried traditional Bayer dithering, and that looked better, but it reminded me that I don't actually like the look of Bayer dithering very much (but, to each their own).

So then I tried a 16x16 blue-noise texture, and that was an improvement, but either my texture was bad or 16x16 is too small for this, because it had some pretty noticeable tiling artifacts...

...and that brings us to the current approach: a giant fullscreen blue-noise texture.

I'm only allowed to add one image per post since my account is new, so here's all four versions in one pic:

4 Likes

Just mentioned this on twitter, but don't miss the faster blit routine by @RPDev

1 Like

I'm definitely interested in fitting some more-standard blitting into the renderer, but it's tough because pretty much everything depends on per-pixel info! Walls/floors are based on raycasting, and those plus the sky are using the dither texture as per-pixel thresholds instead of it being a blittable sprite - and then for the 3D mesh parts like the car, I currently have to do per-pixel occlusion checks against the walls...when I wasn't doing that, I was able to do a similar "splat a pattern to the screen in 32-bit strips" trick (meshes use blittable patterns instead of the blue noise comparisons) and that was definitely faster, but the per-pixel occlusion is important for stuff like this finish-line banner:

Honestly though, I may need to figure out a way to incorporate some blitting to shave off more per-frame cost...at the moment it feels like I can either do that, or settle for locking to 30fps. 30 wouldn't be terrible, especially on a battery-powered handheld, and I do like the smoothness I get from all the per-pixel shenanigans...

Maybe I can store a "min/max z for each 32-pixel-wide column" (along with the z for each 1-pixel-wide column, like it does already), and then mesh scanline-strips could use that as a broadphase (if a scanline-strip's z range doesn't intersect the 32-wide column's z-range, either cull the whole strip or blit the whole strip, and only do full per-pixel occlusion checks if the ranges overlap).

1 Like

I've been working on replay recording and playback - aside from being able to watch your own best time for a course, this also means you can race against a recorded ghost, and to me that does some really nice things for the general feel of the game. It also lets you see the car from more viewing angles, which I can mark down as a win for my "draw the car as a full 3D mesh" decision!

2024-05-1322-09-14-ezgif.com-cut

Full video (~50 seconds):

Replay data is stored as input-keyframes using run-length encoding: each keyframe is a bitmask of the buttons you're holding (just a direct copy of the getButtonState() value, as one byte) and the number of frames that you held that particular combination of inputs (also one byte). With this setup, an average 30-second replay is only about 200 bytes! The downside is that any time the game's simulation changes, old replays becomes invalid (so I need to get that stuff Very Locked Down before releasing the game). It's unlikely that I'll do any networking stuff with this due to playdate limitations, but the small size still might be relevant if I can find a comfy way for people to make and share maps with each other (since each map will need at least one ghost-replay attached to it, and the replays are likely to be the heftiest part of a map file).

5 Likes

Improved the floor-sampling (sharper checkerboard with fewer samples, by doing some texel-space raycasting to figure out how far along the current scanline you'd have to go before hitting the next texel), and more interestingly, added a shadow to the player's car (draw an all-black version of the car mesh with a modified local-to-world projection and a mask-pattern).

2024-05-1422-08-37-ezgif.com-optimize

Longer clip (~45 seconds):

Pretty settled on targeting 30fps at this point so I can do more of these fun effects, maybe 40 if I can find more optimizations - currently it mostly stays at or above 40, but it dips a little below that in certain situations...so it feels worthwhile to settle for 30 and keep the extra breathing room just in case.

4 Likes

Getting into some main-menu stuff, so today I put together a (joyfully) overengineered title-card, featuring 3D text and a cloth sim. lol

2024-05-1701-29-14-ezgif.com-video-to-gif-converter

3 Likes

Very cool.

Trackminia was one of my early names for Daily Driver. And maybe Track Minier. I even did the chequered flag title screen (no longer used).

I also talked to the Trackmania creator at Nadeo, but he couldn't see how we could get the game through the Ubisoft corporate machine so it fizzled out. Maybe things would be different today, especially with your game being 3D?

At the time this was on the cards I talked with an American guy in Japan who did Trackmania style decals for cars. He was open to doing the graphics. Big ideas! First big game kind of scope. :joy:

Another codename I had was Crank Turismo. Fun times.

Eventually I went in my own direction.

2 Likes

lol, what a coincidence! Honestly "Trackminia" was the first title I thought of before even writing any code...but I haven't thought of anything I liked more, so I think it's feeling appropriate to lean into the demake vibe.

"Daily Driver" is a nice title, too - good find!

1 Like

You should totally lean into the demake!

All this happened for me back in 2020, and Nadeo in 2021, which feels like an eternity ago!

1 Like