Ascent is a big game. There's more than 270 billion star systems, almost all of which have planets. Planets can be up to the size of Earth, and in May we began allowing players to construct and populate Colonies on those planets.
When we added colonies, I wanted to go somewhere nobody had gone before - city building without hard coded limits. A colony deed is a 100km radius, giving plenty of space, and there is no limit set anywhere for how many buildings you can construct and maintain. If you've got the materials, you can build whatever number you like.
Since that day, the colonial population has grown to 3.4 million, and the largest colony now has 52,474 buildings, or about the same as the central district of most large Earth cities today. Lesson One: If you don't set hard coded limits, someone will play until they find another one - memory, CPU, GPU, you name it.
(Prolapserium, one of the larger colonies in the game, from 100km altitude. The little black dots are actually Environmental Domes, more than a kilometer across)
And we sure ran into limits; after a few hundred buildings, we had to start being really careful about things like particle effects. After a thousand, we really needed lower LOD models (fewer triangles) to display the distant structures to keep the heat off Unity's main thread and avoid strangling lower end GPUs. At a few thousand buildings, a lower level of detail shader made a lot of sense for distant structures too. Why render reflections and bump mapping on an object that's a few pixels across and ten kilometers away?
But beyond more than a few thousand buildings, the conventional way of displaying them just falls apart, and no amount of smart tweaking will save you. Lesson Two: if you don't wan't to hard code limits, you need to work around the limits of your engine.
The conventional way to display something like a building in Unity (and other engines will be very similar in most respects) is to spawn a game object, attach a mesh filter component to hold the building's shape, a mesh renderer to draw it onto the screen, and a mesh collider so you can walk or fly into it and bounce off or explode or what have you.
But with a few thousand such game objects, all of those components have to wake up every frame, do their thing, and go back to sleep. You lose a lot of CPU power in overheads and context switching, and the more buildings you add, the worse it gets. The rendering thread uses more and more CPU and gives back fewer and fewer frames per second. Worse yet, tens of thousands of these objects means a huge memory overhead, meaning we would need to go 64 bit just to continue growing our colonies, alienating some of our player base.
One possible solution is to have say 100 building objects and swap them around, only ever displaying the closest 100 buildings that are within the camera's field of view. This probably works quite well with an FPS or other such ground based game, but for a space game where you might be kilometers up in the air with a huge field of view, it wouldn't work at all.
So whats our scalable long term solution? Well, firstly, we stop treating each building as its own object by default. Instead, a building is just some data. Pretend it's 1978 and we're writing this on a mainframe... instructions and data only.
Whenever a colony is within visual range, a process in a separate thread scans through the list of buildings several times per second, determines which are visible to the camera and sorts them by distance. It then constructs five "super buildings" out of the buildings it can see, sticking the meshes together into five big ones, by combining the mesh's coordinates with an offset for the building's relative location.
The main thread then takes the mesh data and displays just five structures instead of hundreds, or thousands. While each of these individual "super buildings" are very expensive to render as they are a huge and complex shape, five of them together are vastly less expensive to render than hundreds or thousands of tiny ones.
(The same colony from only a few kilometers up. You can see a huge number of structures within the environmental domes, disappearing into the distance. This scene now renders quickly without using much more memory than a tiny colony.)
More importantly, the main thread doesn't have to process thousands of buildings at all. It is concerned with just five. Instead of 52,474 mesh renderers having to wake up, figure if they're visible and then display (or not), five do.
Better still, memory wise, that's 52,469 fewer mesh renderers, mesh filters and potentially mesh colliders to store, meaning a big colony now uses barely any more memory than a small one, as we only need to keep meta data for each building - its status, type etc.
The end result? The largest colony in the game can now render at sixty frames per second, whereas previously, with that number of buildings, you would be happy to see fifteen. As a result, there is no longer any practical limit to the number of buildings our game engine can handle in a single colony. No doubt, this will only lead to a Lesson Three: once you have removed any practical limit, it's only a matter of time before the players find an Impractical one... I'll let you know how that goes.