Cavewar Notes


-ยง-

I made a mini RTS maze-building game called Cavewar, using Three.js and no engine, just rawdogging JavaScript. This time I used two other libraries, navcat and three-mesh-bvh. I'm unsure if I want to make a whole campaign, preferring to leave it as a tech demo, for now.

Fun fact: while coming up with the name, I found that there was a 1996 RTS game called "Cavewars". Eternal recurrence or some shit.

Game design

I really like Red Alert 2, Yuri's Revenge, and a fan-made mod, Mental Omega. The old school RTS games were simple, I wanted to keep the controls familiar to these games, but with modern additions like attack move.

Due to various skill issues, I realized that if I wanted to add vehicles, I'd have to create a separate nav mesh to account for the additional radius, and it'd be tricky to create different scales of vehicles, so I scrapped the idea early on. The way that most RTS games handle this is by cheating a bit: each unit occupies a unit size, but infantry units can be smaller.

I wanted to add a resource gathering mechanic, but couldn't quite figure out how. Initially, I thought that civilians would be farmed for resources, but making a harvesting unit would be challenging. Instead, I just made build time the limiting factor, and added a supply cap.

Since there's not really resource management, nor a tech tree, it didn't make much much sense to add other buildings. Instead, I added maze-building mechanics, partly to show off the pathfinding, and partly because I wanted to make a tower defense. I ended up making decisions that were pretty far from what I had in mind initially.

Visual effects

There were a bunch of post-processing effects I tried with this game. At first I defaulted to dithering, because it makes low-res stuff look good, but then I decided on toon outlines + cross-hatching pass. The other problem with dithering is that it removes a lot of detail, and I wanted this game to be easily readable.

A lot of things were implemented with shaders, even the ground texture for example, blends multiple textures together. The texture of the building blocks were implemented with a shader to make them look contiguous and not just a texture per block. A vertex shader was added to make structures look more makeshift. The blood particle effects use a shader for the positions, since there's so many of them. Many of the meshes are procedurally generated, such as shields, selection rings, muzzle flashes, explosions, trees, and grass.

Each of the weapons are separate meshes, so it would be possible to mix and match. They copy positions from bones in the skinned mesh, and this is a pretty expensive operation since it has to be done on the CPU.

Setting

Something post-apocalyptic, where the people are at war with governments. Most of the population are economically worthless and have nothing to lose but their lives. The resistance fighters have ancient technology that somehow works.

Wars are usually won or lost before a single bullet is fired. I initially wanted to make the combat more based on surveillance, where armaments would be impossible to procure by the civilians as long as they were surveilled, but I couldn't figure out how to translate this to gameplay. I wanted to borrow as much as I could from 5th-generation warfare.

There is the obvious Gnostic allegory of the cave, in which the material world is a projection of the cave's shadows. The civilians occupy the cave, and the government forces represent the archons, keeping them in the cave.

Performance

The decision to stick with JS was purely iteration speed. If I had picked C++/Rust and compiled to WASM, I don't think I'd have gotten much done. There are huge limitations, such as having to avoid garbage collection as much as possible by allocating as little as possible, and being mostly limited to a single thread. Yes I know there's Web Workers, but there are very poor sync primitives, I'd have to bring my own Mutex on SharedArrayBuffers, it's not worth the complexity so I just tried to focus on making things have acceptable time complexity.

I wanted each unit to have line-of-sight, something that is often ignored in most RTS because the map design doesn't have true walls or terrain. There are a bunch of things done to make things fast: raycasting is done using three-mesh-bvh library. The mesh it checks against is a combined occlusion mesh of all the terrain and structures, so that it doesn't have to iterate through each structure.

Each unit is placed on a spatial grid, and what would be exponential time complexity to raycast each unit to another is cut down dramatically by using a spatial query to only get nearby units. Also, unit updates are done in phases, this can degrade somewhat gracefully if there's an absurd number of units by being slower to update. The low visual update rate for the skinned meshes was also done on purpose for optimization, but also has a cartoony aesthetic.

Most of repetitive meshes are using THREE.InstancedMesh, the interesting thing is that the bone updates are stored in a data texture. I couldn't use InstancedBufferAttribute which is what I tried initially, because it only works with WebGPU, and I wanted it to be able to fallback to WebGL.