Programming Big Maps

First, an obligatory line about how it has been a while since I’ve posted an update and how it’s because a lot of things are happening that we’re very excited about and can’t talk about yet, but rest assured the game is moving along nicely.

We’ve got a video coming out next week that is going to dive into some of the modding capabilities of Cantata, but before that I wanted to write up a little technical post on how we handle the maps from a programming perspective. This post is going to be geared towards people that understand some Computer Science topics, so feel free to get off the bus at this stop before we go on. Rest assured the next thing we post will be much more palatable.

Cantata’s maps can be big. Really big. Like, 500x500 tiles big.“500”, typed out on a screen seems a bit small, but consider that a standard Advance Wars map is about 20x20 tiles. That’s 25 times bigger than a normal tactics game. We’re talking bigger-than-War in the East big — War in the East’s total hex count on the “Immense” size is 2,600 hexes. Cantata can get up to 250,000, nearly 100x that.


Cantata’s Max Map vs. Other Games


War in the East (Note full map for WITE not actually playable for a single scenario)

BUT! Just because we have the power doesn’t mean we’ll use it. For the main campaigns in Cantata we’re targeting much more reasonably sized maps so players can get through a given campaign chapter in a few hours. But if you want to make a world-sized tactics game, Cantata is here for you. I’ve even got some wacky mod ideas post-Cantata that I want to try to use the huge map for. The idea is that it can be a canvas for anything you can think of and hopefully open up new opportunities in tactics games as of yet undiscovered.

But how? Unity, Cantata’s game engine, functions largely off of people placing GameObjects in a given scene. GameObjects can have functionality and behavior attached to them, so it’s pretty common in tactics games made in Unity to work off of a “one GameObject per tile” model. This means each individual tile is its own object and contains the relevant interaction data, sprite, mesh, etc. This is mostly fine if your maps are tactics-game small, like 8x8, but you can quickly get into territory where even a moderately sized map, like 20x20, means you’re spawning 400 GameObjects into Unity. This is generally bad as GameObjects are “heavy”, so you want to minimize how many you have. Cantata zoomed out, on any map size, has a camera view of about 65x35 (2,275) tiles view, so having a GameObject per-tile just wasn’t feasible, especially once you add in UI, Units, Buildings, Supply Lines, etc. A game’s no good if it’s just map, we need all the other parts of the game too!

What I opted to do instead was to use a technique that leverages the GPU to handle the map drawing, instead of using the CPU-intensive GameObject case. Starting off with a great tutorial by Quill18, the general idea is that, instead of having one GameObject per tile (which also means one mesh per tile), you create your tile mesh independently of any given tile. The “tiles” themselves are just textures anyways, and if you essentially copy their color data to the right location on a single mesh, your one mesh can represent a large number of tiles! Great!

Cantata goes a fair bit beyond Quill’s tutorial in a few places, especially because Cantata supports modded terrains. The key here is that, if I make a mod and you make a mod, and we want a map to be able to show terrains from both of our mods, we need to combine those tilemap tilesheets together somehow on the map itself. Just assigning a single mesh to a single texture doesn’t work, even if we tried to stitch all those disparatepngs together into a single texture (you quickly run into limits on individual texture sizes).

So instead we use Unity/HLSL’s ability to leverage texture arrays! Cantata basically grabs all the terrain spritesheets it has for a given map, and then puts them all into separate arrays based on their size — so 64x64 sheets get their own array, 256x256 sheets get their own, etc.

At this point we’ve got a mesh we’re making in code (Cantata’s maps are all generated at runtime), and a few arrays of textures to put on that mesh. Now is the final part where we’ve got to make the two come together! To do this in a high-performance way, we do some old school shader-trickery where we pass in a separate png to the map shader where the color of the pixels correspond to data about a tile. Here’s a snippet of actual code:

Without context on the whole file it may be hard to understand, but you can see at the top how I’m setting a value r to be a certain value based on the width of the spritesheet. Then later you can see I’m assigning a value in the TerrainIndexColors to be a Color, with that r value as the color’s Red.

I then look for that value in the shader, and use it to assign the right texture array:

The idea is that the color of a given pixel acts as instructions for the shader, telling it what array to pick from, what index of sprite in a given array, if the tile is in Fog or not, and so on. Add all this together and we’re properly setting the tiles! Cantata does other some other things here to allow for larger maps, like “chunking” tiles to be seperate meshes (with each “chunk” being 64x64 tiles), but what I presented above represents a a high level overview of the whole pipeline!

The best part this is that it’s also really fast. To the game, the giant map looks as computationally-intense as a few standard game models hanging around in a scene, but nowhere near as intense as the 250,000 GameObjects we would have been dealing with. In fact in Cantata, with the above system, a 500x500 (actually 512x512) map is only 64 total GameObjects! So it’s probably possible to do maps that are even bigger than 500x500 but I’ll have to save that for later.

It’s hard to really show what a 500x500 map would look like in Cantata, but here’s a screenshot of a randomly generated one. You can get a sense a bit of the scale if you look in the left corner — that small, barely visible thick black line down there is the UI that frames the tiles in the screen!

Hope you all enjoyed this little technical deep dive on some of the technology behind Cantata! Feel free to ask any questions about how we do stuff or if you want me to go into more detail on something I outlined above!

Thanks for reading, and look forward to the new dev log coming something next week!