Welcome back to the dev diary! After a month of pause, we have some catching up to do. We’re going to show some gameplay, talk a lot about opponents AI, game performance and some more, so grab a drink and make yourself comfortable for this one!
What’s up?
During the past month I’ve recovered nicely from the January development stint. It has been an absolutely frustrating period, where development slowed down to an halt while I was searching for solutions both for opponents AI and for some critical performance issues, which I will explain more in detail below. Now these problems are solved and are finally behind me, but they took a hit to my psyche and made me reach a new low during that time. At some point I started believing I had no other choice that settle with terrible AI and start stripping down graphic features to recover performance, but I’m happy to say nothing of that will happen. I just needed some time to find solutions, and luckily I did.
Making games is a constant learning process, and at times it can be frustrating because resources on specific topics are very sparse, or sometimes just don’t exist publicly. Lately I’ve been kinda regretting my decision to go for a simcade racing game like Downtown Club: I learned the hard way this is not something that can be sanely done by one individual and just some extra help. I swear this will be the last project of this complexity I’m going to work on for the rest of my life… But I’d be damned if I’m not proud of what I have built so far!
New gameplay videos
Let’s begin with some eye candy! Me and the testers have published some new videos of the game in action, recording the gameplay from our Quest 2. Enjoy!
AI behind the wheel
A necessary clarification before we dive further, given the times we are living today: all the mentions of AI in this post are not related to neural networks, machine learning, or generative AI. The AI I will be talking about is just a manually programmed set of instructions, algorithms and formulas, in the classic way that has always been done for videogames. I’m not interested in using neural networks for opponents AI because with my limited resources, time and knowledge, there is no way it will work better than manually writing it. After all, it’s no coincidence the first ever full machine-learning-driven opponent AI was made available on a racing game only recently.
I didn’t really start working on the opponents AI for DTC until I had a clear setup for the physics of the cars, so only recently. Until that moment, the AI logic was still the same identical as V-Speedway, where the opponents sure leave something to be desired… So the obvious thing to do was rethink it all from the beginning for DTC. Allow me to shorten V-Speedway in “VS” from now on.
In fact, even if the core idea for the AI logic is the same as VS, I ended up rewriting everything from scratch and changing a number of things. At the start it didn’t take much time to have a single car driving alone on the racetrack, following closely the ideal trajectory, knowing when to brake according by the upcoming corners, also handling oversteer and understeer, but there’s more to it. In VS the cars drive almost on rails and you can’t lose them no matter what you do, but this is vastly different in DTC, where you can lose grip and spin the car if you push too hard. This of course applies to AI too, so it was suddenly much more difficult to have the AI react to certain corners without losing the car. No matter how much I fine tuned the values, the AI would still spin out every now and then. A human player can react and keep the car on the road with swift micro corrections, but this is not something I was able to code into the AI, not in a reliable way at least.
I don’t consider myself an expert in AI and even less in physics, so this was the first important roadblock for me. The only solution to move forward without losing days of experimentation was to resort to some cheating, and so I did: I gave AI drivers a physical steer/rotation assist, but wait! This doesn’t mean they corner faster now: this is really just to avoid them losing the car too easily. They still approach and drive through corners at the same speed an human player would, and from the outside you can’t really notice the difference. I believe this is a good solution for a game that is a middle ground between arcade and realistic, and gameplay-wise it’s still a fair fight between human driver and AI opponents.
AI against AI
That was just the tip of the iceberg though. The real challenge came when I added multiple AI drivers on the racetrack and had them race together. Imagine the context of DTC compared to VS: the game is no longer purely arcade and so races have to feel plausible, opponents have to behave logically, and the player should feel like racing against experienced drivers, not against newbies who don’t really know what they are doing. Even if the underlying AI logic in VS was still valid for to DTC, all the code has changed noticeably.
The AI in DTC is better under every aspect, but the most important difference is how opponents behave when going side to side and overtaking: in VS, they will leave the ideal trajectory and start following the exact shape of the track until one of the cars is clearly ahead. This is a “safe” strategy that will keep cars from hitting each other and from hitting the walls, but doesn’t result in realistic racing and looks more like a parade of sorts. In DTC instead, opponents will keep following the ideal trajectory at all times, cutting every corner as best as they can and squeezing through the available space between each other, something you would expect from real competitive drivers. This behaviour is not “safe” and of course opens up a long series of problems that I’m not going to explain here (trust me, it’s a rabbit hole), but the one problem that gave me the most headaches was the following: opponents overtaking on the inside in sharp corners, in the attempt to avoid other cars while following along the ideal trajectory, would cut the corner too much and hit the inside walls, causing big accidents.
I knew perfectly why it was happening, but at first I had no idea how to avoid it unless instructing the opponents to just not overtake in corners, which would be boring and illogical. I also tried to instruct opponents to steer away from walls when too close to them, but that caused all sorts of side effects that ruined the whole race in multiple points. I’ve spent days thinking about the problem, trying different combinations of values to prevent it from happening, but nothing worked and that was when I’ve hit the morale low, around late January. I felt like I had no solutions at hand and there was nothing I could do with my somewhat limited knowledge on AI. I even started brainstorming the problem with my brother and sister, drawing concepts on paper (the one pic I’ve posted last month!), and taking a bunch of days off in an attempt to clear my mind. I came back to the project with new ideas, implemented those, only to discover they were useless and made everything even worse. This time I had some more sleep so I reverted the AI to the latest functional system I had, suppressed all the negative feelings, and once again squeezed my brain on the problem until I was done.
It’s anticlimactic, but there was no magic help of sorts there. I just eventually reached the much needed “AHA!” moment and came up with a very functional idea on my own, after thinking and overthinking it night and day. Opponents are now able to adjust their trajectory before entering a corner or even abort overtaking, if they are side by side in a risky position. And if they have cars on the sides, these too will leave more space for everyone to make sure nobody hits the inside wall of the corner. To clarify, I knew from the start this was the behaviour I needed, but until then I had no idea on how to code it in the game. It was a tiring effort but I finally went over the long streak of depressing and inconclusive coding days, and started having faith in the project once again- it’s been really this intense for me. Since then, I continued working on the AI some more, refined what I had, and I believe I’ve finally reached a point where I’m actually happy with how it works.
Is there still room for improvement? I’m sure it is. But do I need that improvement right now? Honestly, no. We all know that Downtown Club is crazy late on the original schedule, so truly there is no need to spend more time to reach perfection for something that works well enough at the moment. It sure works a ton better than in VS!
Final considerations on racing AI
I guess while reading the two sections above you might have asked: “Can’t you just tell the car to do X?” or: “Can’t you just ask Google or ChatGPT for help?”. Well, let me answer those.
If you are unfamiliar with AI or even games in general, consider you can’t just tell the car “go there” or “avoid that”. The concepts of “go” and “avoid”, but also the concepts of “there” and “that”, are all different and unknown the the AI, and you have to define those manually. There is no direct way to tell the AI to brake before a corner or to avoid a wall, because at the core the AI has no idea of what a corner is, what a wall is, or even what the brake is. And after you create the basic commands for acceleration, brake and steer, you then need to factor in all the other variables of the race: car performance, current velocity, position on the track, position of the opponents, intent of the opponents, and so on. There is a lot going on every single frame. Every action of the AI drivers is the result of a somewhat long elaboration of all the things happening around them.
About searching for help on the web… You’d be surprised to learn how little to no resources are there about AI in racing games. And due to how ChatGPT is trained, if Internet doesn’t know, then neither ChatGPT really does. Of course there are a bunch of tutorials on the base concepts of making a car drive along a path using waypoints or splines, but it basically ends there. There are no in-depth articles about making AI react to the physics of the car, avoiding opponents gracefully, simulating the behaviour of a race… If I have to give my two cents, this scarcity of information is caused by two main reasons: first, different racing games have substantial differences in car physics and gameplay requirements, so there is no general AI solution that works everywhere. Second, simcade and full-sim racing games have always been a prerogative of the AA/AAA industry, and how the AI works is basically a trade secret.
I want to end the AI topic with something I deeply realised and understood while working on it: the simpler, the better. In the case of opponents AI, one might be tempted to add as many different behaviours as possible, trying to cover all the possibilities and unique cases that come up during a race. But in my experience, I’ve had the best results with short math formulas instead of long chains of if-then and elaborated state machines. Of course, the entire AI code is complex by definition. But in its complexity, I believe it’s quite straightforward and elegant, and accomplishes good racing with just the right amount of code. To conclude, here’s a somewhat related quote which summarises perfectly my feelings about this topic, as it took more time to create something simple and elegant, than something complex and intricate:
“If I had more time, I would have written a shorter letter.”
Blaise Pascal (“Lettres Provinciales”, 1657)
Performance struggles
Let’s now talk about the shadow lurking on DTC while I was trying to make opponents drive decently. Don’t worry, this will be just one section!
As soon as I had the first iteration of AI opponents, I wanted to test how DTC was running on Quest 2 in a worst case scenario: a race where I start in last position and try to keep all the opponents in sight, while recording the gameplay, so to put as much pressure on the GPU as possible. Well, to my dismay, the game was running quite bad. So imagine how I felt after being already depressed by the AI problems, now also knowing the game performance just wasn’t there, despite all the optimisation work I did in the months before, and reducing the number of maximum cars on track from 12 to 8 (that’s 33% less cars, it’s a lot)!
To be fair I knew for long time there was something weird going on with performance, but I decided to wait and concentrate on one issue at a time, prioritising AI first. But I couldn’t stop my brain from thinking about a solution and so by the time I completed the AI, I already had multiple ideas for the performance bottlenecks. It took little time to apply the changes I thought of: remove all usage of MaterialPropertyBlocks; simplifying and merging redundant operations in shaders; reducing and streamlining the use of shader keywords; simplifying the rendering logic and shaders for mirrors and realtime reflections.
After those changes I felt confident and decided to test directly by restoring the maximum of 12 cars on track and trying again the worst case scenario… And lo and behold, solid 72 fps on Quest 2 with still some GPU and CPU room to spare! Those few changes were enough to recover a ton of performance, and didn’t even affect the game look in any meaningful way. That was a big victory for me and for the project, I was the most positive about DTC I have been in a long time. And yes, this means there will be 12 cars on the grid just like in V-Speedway.
Not long until the release
With the biggest issues out of the way, I can finally move to the final phase of development which should be relatively short. There is not much left to do now! The upcoming tasks are more of a routine work rather than problems needing research, so I don’t expect to find roadblocks anymore and should move forward quickly. I’ve already assigned the specs for the Sport version of the cars and added opponent drivers in the cockpits. Then it’s the turn for driver animations, difficulty settings, and crating events for the campaign. The artist is already at work on the next three cars, which you might have seen on our socials- can’t wait to add them in game after the launch!
I will publish the store page for DTC as soon as I can, so you can wishlist and preorder it. Please remember it will launch in Early Access! To me it means the game will launch small, and will be expanded for free for some time before reaching the “final” version. For me Early Access is not about launching a broken game and fixing it while players are on it, so I’ll do my best to launch it already well working.
On a final note, DTC will launch first on Meta Quest, then soon after on the other platforms- after all, the overwhelming majority of the players is on Quest. Regardless of that, I greatly prefer finishing the game first and spending time for the ports later, rather than working on the ports first and losing valuable time with the result of having to push the release again and again. I can’t do everything simultaneously after all, I’m just one person. But I’m doing my best to keep my codebase flexible, so if the SDKs of the other platforms are up to standard and work without issues, there won’t be much work needed for the ports anyways.
Wrapping up
That was a long diary, right? If you arrived here without skipping, well, props to you! Now you know better how daunting solo game development is. Are you thinking about creating a game? Why do you want to hurt yourself so bad? Don’t do that, look for another hobby! …Nah, I’m joking. Unless………
Ok ok, I’ll stop it. But for real, if you want to work on a project solo, please keep it simple for your mental health. It’s very easy to get carried away and realise the project grew too large for your size only when it’s too late. This stuff becomes overwhelming very quickly and you can’t exactly predict how it’s going to affect you and those around you. But it’s a beautiful job when done in a sane way.
See you soon with news!
Danjel Ricci “SkyArcher”