I occasionally write a blog about OOP and static analysis, mostly focusing on compiler-related topics. As you might have noticed, game development is not on that list. My professional experience is limited to web development and static analysis.
That said, I have some experience with game development. I started with the Warcraft 3 editor and spent quite a bit of time working on add-ons for Garry’s Mod. Not to mention the many small games I made using Love2D that never saw the light of day. Also I tried participating in the previous jam, but I couldn’t quite handle the learning curve. After working on a puzzle game for a while, I decided to give it another shot and participate in this tech jam.
Still, I wouldn’t call myself particularly competent in this area, which made this adventure all the more eventful.
Every game jam starts with a solid idea. At first, I wanted to create a simplified version of a space simulation engine—something between KSP and Space Engineers. Fortunately, I quickly abandoned that idea due to its complexity. Ironically, someone else ended up submitting a project that looked like a mini Space Engine. Cool.
So, I started thinking about what I could realistically accomplish. Being a fan of a well-known survival game series featuring anomalies and artifact hunting, I came up with a simple idea: create AI bots that could surround the player. In the game, this is usually accompanied by a memorable battle cry:
“Sboku, sboku zahodi!”
Thus, the name SbokuBot was born. To this day, I still wonder if it was a mistake not to call it s&boku bot to take advantage of the obvious pun.
I thought this was a good choice since game AI is often based on graph structures—something much closer to my professional skills. The initial concept was as follows:
The bots should be able to surround the player, actively using cover.
They should hide while reloading and move between cover spots while teammates provide distractions.
At that point, I started figuring out how to implement it. s&box already had a NavMesh with an API for pathfinding. The tricky part was the cover system. How should I determine cover locations? Manually? By scanning and memorizing the environment? Extracting them from geometry?
Initially, I wanted to do this using a weighted NavMesh, but when I first opened the API reference, I was surprised at how minimalistic it was. The only thing I could do was find a path from point A to point B. That meant I had to refine my idea based on a very broad task description.
The next question was: where do I start? Since s&box is very much like Unity in the sense that everything—including the character controller—has to be built from scratch. So, basing my project on an existing system was rather a necessity. Facepunch had made their own shooter, Nicked, which I originally wanted to fork. Thankfully, its source code was open.
However, after reading discussions about Nicked, I decided against it. It was too bulky, and I needed something simpler. That led me to Simple Weapon Base (SWB), a weapon pack with a built-in FFA deathmatch mode—exactly what I needed.
I was thrilled to see that SWB already had dummy bots included, as if everything was ready for me to just add AI. Or so I thought.
While digging through SWB’s internals, I was surprised to find this in the `PlayerBase` class:
[Sync] public bool IsBot { get; set; }
This meant bots weren’t separate entities—they were just flagged with a `bool`. The entire system relied on `if` statements to determine behavior. That’s when I first suspected things would be more complicated than I expected. And indeed, SWB wasn’t designed for bots. In the weapon class, for example, reloading was handled like this:
else if (Input.Down(InputButtonHelper.Reload))
This meant weapon actions were directly tied to human input. The same applied to shooting. The funniest part? If you gave a bot a gun and held down the left mouse button, both you and the bot would start firing—because the weapon component responded to your keyboard inputs.
While I solved the issue with the property instead of the class by creating my own controller that implemented a common interface, this time I had to dive into the project’s code, refactor it, and extend that common interface.
Fixing the bot logic took me almost a week. The sheer amount of new information slowed me down significantly. Even something as simple as rotating the bot’s camera towards a target was daunting, since I’m not perfect with linear algebra. When I first encountered quaternions, I probably gained a few gray hairs.
Nonetheless, after about four attempts, I managed to write a new bot class, and things started moving forward. Just for fun, I decided to use a human avatar as the enemy and let the player control Terry. This turned the game into a battle where the old avatar takes down the new one.
With the basics set up, it was time to (finally) work on AI. s&box provided a `NavMeshAgent` component that could find paths and avoid collisions, but I encountered a funny problem—bots kept sliding around like they were on ice.After searching in Discord for advice, I ended up creating my own agent from scratch, directly interfacing with the NavMesh API. The task was simple: fetch a path using `GetSimplePath` and store it until an update was needed. I had to sacrifice crowd control features, but it worked.
A day later, I had an AI bot that could follow the player and shoot. By then, it was already January 8.One more note—I didn’t dive in headfirst. Instead, I decided to start with something simpler. I thought about mimicking NPC behavior from Half-Life 2—they never seemed particularly smart to me, but they also weren’t just standing around like statues. They’d either move around randomly or take cover. So this became both my first step and my "bare minimum" approach.
A week into development, I finally tackled AI logic. The big question was: how should I implement it? A quick search led me to three possible approaches:
Finite-State Machine (FSM) – The bot switches between different states, each defining a unique behavior.
Behavior Tree.
Decision Tree.
I didn't even describe the last two—because I didn’t understand them :). While FSM resembled a familiar state pattern, the tree-based approaches seemed way more complex. I couldn’t grasp their benefits for my specific use case in the short timeframe. So maybe next time.
FSM, however, had their own challenges. For example, let’s say a bot has the state "moving from point A to B" and another state "attacking the target." What if I wanted the bot to both move and attack at the same time?
That’s where Hierarchical Finite-State Machines (HFSMs) come in. However, I misunderstood them and accidentally invented my own version instead. I suspected this for a while, but I only confirmed it now.
Anyway, here’s what my NPC’s state system looked like:
Movement States: idle, chasing, combat maneuvering (strategic movements, like taking cover).
Combat States: Idle, shooting, reloading.
Clearly, there were two parallel layers of states (movement and combat), so I split them into separate subtypes:Additionally, I introduced conditions (an `ICondition` interface). This prevented redundant checks across multiple states. For instance, if the bot lost its weapon, it shouldn’t try to shoot or reload. Here’s a class structure of condtions:
Their usage was as follows:
The FSM runs every set period of time:
Check conditions – If needed, change states.
Execute logic – Run the behavior of the current state (handled by `Think`).
How I did it
Each state had a straightforward implementation:
Idle – both combat and action states wait for the right conditions to change the state.
Attacking and reloading – If the bot has ammo and a clear shot, it fires (`IsShooting = true`). Otherwise, it reloads.
Chasing – Updates target coordinates and requests a path from NavMesh. The bot continues chasing until it's close enough to enter combat maneuvering.
Combat Maneuvering – Is more interesting. AI finds a nearby cover and moves behind it. If no cover is available, the bot picks a random movement pattern. Below is a flowchart of the algorithm (where "tick" refers to a `Think` call):
Once this work was done, I had an AI bot that resembled a basic Half-Life 2 NPC. Before refining it further, I wanted to showcase its existing features.
A week before the deadline, I had a great idea: create a simple combat mode where I could test the AI and showcase it to others. The concept was simple: a wave-based arena mode where you fight off waves of enemies. Between rounds, you level up, but so do your enemies.
I planned to build it in one day (optimistic, I know). I only needed:
Three menus: character upgrades, weapon selection, and the game over screen.
A spawner for enemy bots.
A round manager to handle progression.
Long story short: I didn’t (in time).
Another one, another one, another one If I had to pinpoint what went wrong the most, it was definitely poor planning. Altogether, I spent about 2–3 full workdays just on the arena. On top of that, the end of the holidays hit my motivation pretty hard.
But the shortcomings in the platform also had an impact. Here’s what went wrong:
Project Naming Bug – When setting up my s&box project, I used uppercase letters in the package ident. This caused issues, I forgot which exactly. Later, when I added a period (".") to the ident, all my components disappeared. It took me an hour to realize the project name was the culprit.
Editor Glitches – I followed the official docs to add an in-editor tool… but nothing happened. After 30 minutes of restarting the editor, the tool suddenly appeared. Also a redundant prefab spawns on the scene each time I open the editor.
NavMesh Broke – A week before submission, the NavMesh suddenly stopped working. After debugging for days, I found out it wasn’t my fault—many people had the same issue. The fix? Regenerate the NavMesh.
Exceptions – As I was refactoring the project and hunting for bugs in the final days, I suddenly discovered that the NRE wasn’t caused by my code, but happened when calling `NavMesh.GetSimplePath` and `GetComponent`. The exception from the former breaks the mode since the bots can no longer find paths.
Networking Bugs – In the final days, duplicate player instances started spawning randomly, leading to such amusing bugs:
Players get extra guns. I noticed it when I fixed a bug in networking, but the problem remained. Co-op mode is still unfinished.
There were more straightforward situations too—like when I was debugging multiplayer and couldn’t figure out why the `IsProxy` property was returning the wrong result. Who would’ve known that you need to call it after `OnStart`, not `OnAwake`, especially when the documentation doesn’t mention it (just a reminder, I’m an amateur in game dev).
Change of concept At this point, I realized it was time to change the idea because all I had ready were just interfaces. And submitting a jam entry with a library that only had this to offer:
public interface ISbokuState
{
public void Think();
public void OnSet();
public void OnUnset();
}
Well, that seemed... questionable. It became clear that I probably wouldn’t be able to pull off that “smart” AI in time, so I decided to focus on two things:
Finish the arena.
Make the existing logic portable to any weapon base.
The second point was necessary because during development, I had glued everything to SWB and I didn’t consider it might backfire.
To fix this, I had to turn `SbokuBase` into an abstract class and detach it from SWB, then inherit `BotAdapter` from it. During development, these were two unrelated components, both nailed to the weapon pack (the first was my implementation of the NavMesh agent, and the second was my implementation of the controller for SWB). On top of that, the states were also tightly bound to SWB. So, I decided to settle on the “minimum” version. With the new plan, I could at least submit something functional. In soviet Russia, deadlines hit you With three days left, I still didn’t have a proper arena (despite my "one-day" plan). It was crunch time. The game mode was almost finished, so there’s not much to say here—I just had to sit down, get it done, test it, and repeat.
Creating the Map Until now, I had been testing on a rough prototype—a bunch of resized cubes. But I needed a real map.Fortunately, I had experience with Hammer Editor (from Counter-Strike: Source map-making). Unfortunately, Source 2’s Hammer is completely different.
Jokes aside, the editor has really leveled up. Now it’s more like Blender. The new Hammer is great, but it has one downside—there's not enough guides online. Learning the new tools took time, but I eventually built a spectacular box with slight verticality. Adding some props and fog made it look halfway decent. And I’m happy with the result. There’s definitely room to grow, though.
Final Steps: Modular AI Library With the game mode finished, I returned to my original goal—creating a reusable AI framework. Overall, I already described my plan in the "Change of Concept" section. And it turned out to be a bit easier than I expected. So, the last day for me was spent in relaxed tweaks and refactoring.
In the end, I submitted my entry on the last day—alongside 99 other participants. Some submissions were really impressive, and some of them are even in the same field as mine.My game mode, Sboku Arena, is available here, and the AI library, available here. Yeah, it's the library that I submitted to the jam. They share the same repo on GitHub. Feel free to improve it if you want—there’s definitely room for enhancement.
And a huge shoutout to the SWB creators—without their work, this game mode wouldn’t have been possible.
This was a fun, albeit chaotic, experience. Jumping into an unfamiliar field like game development was both exciting and frustrating. But I learned a lot, and I’m happy to see my work alongside other great submissions. I especially enjoyed (It’s just my background) a Lua interpreterported from C++. Check out the article about it, it’s awesome.
As for s&box, I have mixed feelings. It has huge potential, and compared to Unity, it feels refreshing. However, the ecosystem is still rough—tons of roadblocks and breaking updates discourage me from using it for now. So I’ll focus on Sboku Arena and postpone the puzzle game I mentioned.
That’s it for now! If you're interested, you can follow me onX. See you next time!
Comments