Building a World with Tiled
In the last month, development of my factory-building, physics puzzling Metroidvania game, Kinematic, shifted gears. The majority of prototype work is now done, and I have a strong sense of how all the mechanics interact. So now it's time for me to work on building the world in which Jem's adventure will take place. In this blog post, I'm going to talk about some of the technical decisions and solutions I've come up with to build effectively. And I will particularly focus on my use of the open-source map editor Tiled, from Thorbjørn Lindeijer (and more than 300 other contributors).
Before I get started, I want to highlight 2 goals for my map editing workflow.
First, and most importantly, I want to make it quick and easy to experiment and iterate. Given the choice between a utility that will produce a more polished first pass slowly vs. a utility that will let me make less polished passes quickly, I will always choose the latter. As I mentioned in my AAA to Indie blog post, "You win every time a human with a skill looks at the state of the game, makes a change, and observes whether the game is better".
Second, I want to make map editing fun. I'm learning a lot about myself over the course of my first indie video game. And in particular I've noticed that I'm self-conscious about level design. For whatever reason, I have a tendency to look for other areas of the game to improve, rather than working on maps. I have to put in extra effort to focus my attention on this very important task. Thus, everything I can do to make the day-to-day operation of this work smoother, less boring, and less tedious pays off. In particular, this means I'm keeping an eye out for tasks that annoy or frustrate me, regardless of how much actual time they take. Usually, if I spend a day optimizing away a total of 3 hours of effort across the project's lifetime, that would seem like a waste. But for level design in particular, if eliminating that 3 hours of effort would make me more likely to want to work on the map, I feel like it's worth it.
The world of Kinematic is built in the "discrete-room" style of the 2D Metroid games, as distinct from the free-flowing "open" style of games like Ori and the Blind Forest. Like Axiom Verge, Kinematic's rooms are exclusively rectangles. Other games in the genre have anywhere from 100-400 rooms, so I'm aiming for a similar number for Kinematic. In my case, the dimensions of the room are an integer multiple of 27 tiles wide by 15 tiles high, with 24x24 pixel tiles. (Yes, 24x24 is a dumb number of pixels for GPU memory; but it makes me happy to produce art at a different resolution than most other pixel games)
The basic functionality of Tiled makes it easy to work on the gameplay space. I organize each room into 2-6 layers:
The most obvious layer is the terrain layer. This is a grid of tiles that define collision (and primary art) for the room. Each tile in the tileset has one of 3 different collision states. Empty space, partial collision, and full collision. What's partial collision? The core conceit of Kinematic is that Jem goes through the world, building large physics-based Rube Goldberg devices that fling resources around. Partial collision means that these flying (gliding, bouncing, clinging) resources can pass through the tile, but the player, enemies, and projectiles cannot. In particular, this allows me to define room boundaries that allow the factory to be connected across them.
The second always-present layer is the object layer. This layer is not tile-based. It instead uses Tiled's template system to allow me to easily lay out gameplay objects like doors, enemies, save points, machines, switches, and cutscene triggers. The template system lets me organize these into categories, associate an in-editor image with them, and specify any mandatory or optional parameters that the game will be expecting each instance to have.
2 other tile-based layers have gameplay effects. These are both optional for a given room. If they don't exist, the game carries on as if they were empty. There is a layer that defines where water exists in the game. This one is pretty self-explanatory. There's also a layer defining where resources exist that can be extracted for use by the factory. Based on which type of resource it is, there are different rules about the geometric position an extracting machine needs to be in to use them.
The other 2 tile-based layers are purely visual. A background layer draws behind everything in the gameplay space, and a foreground layer draws in front of everything. The foreground layer is the only layer that uses art at a size other than 24x24. In order to help flesh out the world, these tiles are 36 pixels tall, extending above the square in which they're placed. This allows things like grass and roots to cross tile boundaries in ways that help believability. And more broadly, having some art that crosses tile boundaries can make the space feel less rigid and more organic. At this point, few rooms have these layers. But when the game is nearing completion, I expect to add these layers to most or all rooms, to polish the visual presentation.
I have a straightforward exporter that packages all of these layers into the json format that my engine is expecting. Since Tiled already has a built-in json exporter, I struggled to register my custom exporter to output json files. I figured out that I could register this exporter with shortName "json2" in order to have it present in the list of exporters.
My prototype tilesets for room creation were completely ad hoc. I drew a dozen or two tiles that seemed visually interesting to me, and then experimented with laying out the world. Particularly compelling to me was the idea of making the transitions between tiles smooth and clean. Thus I had a specific logic for things like columns, which expected to have a top and a bottom tile that I placed by hand.
When preparing for producing 100+ rooms, I realized two major problems with the tilesets I had defined. The first was the difficulty of moving the design of a room from one environment (and thus one tileset) to another. The second was how tedious and frustrating it was to manually apply that transition logic for columns and the like.
For the former problem, I settled on the idea of defining a standard tileset layout that all of the game's tilesets will follow. If each tile slot in that layout has a specific gameplay purpose, and internally consistent art, then I can swap tilesets by simply hand-editing the Tiled source file.
For the latter, I decided to use Wang tiles. Specifically, I'm using 3-edge Wang tiling, for air, partial collision, and complete collision. I defined a basic placeholder tileset with 86 tiles in it. This contains the full 81 tiles for the Wang set, plus an additional variant for full collision with 0 or 1 air edges, as those are the most commonly used tiles. As you can see, I defined a simple visual language to convey what each tile's gameplay purpose is.
The beauty of Wang tiles in Tiled is that I can edit the map by selecting between the 3 gameplay tile types, and then paint the edges of tiles. The editor will then make sure that the tiles on either side of the edge I'm painting correctly blend into each other. It will even randomly select between the variants for the tiles that have them.
86 tiles per tileset a lot of art to make, but I have 2 main tools to keep that under control. The first is that I'm seeing how far I can go with only actually implementing a subset of those. In each game-ready tileset, many (30-40) of the tiles will still have the placeholder image. Then, as I lay out the map, if I ever see those green/brown placeholder images, I immediately know I'm using a tile that hasn't been commonly in use. Thus I can evaluate whether to add that tile, or simply restructure that gameplay space to not use new art. The second tool is that, as I mentioned in 2 previous blog posts, I define my tiles in terms of material and height, rather than hand shading. That makes it much easier to construct transition tiles by chopping and screwing the edges of existing tiles and fudging the center together, letting my Aseprite exporter convert into normal maps, and the engine make it pretty.
The other core workflow Tiled helps me with is laying out the entire game world. There are 3 main purposes of this workflow.
I need to be able to zoom out and give my brain a simplified conceptual view of the game world, so I can think about things like flow/pacing, connectivity between areas, barrier and powerup locations, etc.
I need the game to know what rooms are next to each other, so both Jem and her factory's outputs can get to the right location when they leave a room. To optimize for iteration, I want this to be completely automatic, with no need for me to manually specify adjacency or door targets.
I want to have the room-editing experience show me adjacent rooms so I can do things like line up doors and partial collision zones for cross-room factory building.
Since Kinematic's world is defined on a regular grid of 27x15-tile room blocks, the Tiled editor makes a natural fit for this operation as well. I have created a world map standard in the same way I made a room standard. The grid is 100x60 pixels, which is a nice round number with similar proportions to 27x15, but is much easier to do mental math on. I have 3 layers, 1 of which is exported for use by other workflows. The other 2 layers are purely cosmetic, though still quite useful.
The main layer is the "Rooms" layer. This layer is an object layer, containing a bunch of Text objects. Each of these represents a room, and is an integer multiple of 100x60 pixels. I lay them out snapped to the grid (by holding Ctrl). The are named with a 2-letter acronym for the zone of the game they're contained in, plus a short name for what they contain. This naming convention helps me with conceptualizing the game world. Worth noting that I have a local hack to my Tiled source code that makes these text boxes draw with a solid black background.
The other layers are both tile-based, and help me with visual representation.
One is a room-background layer that has simple pastel colors for helping my brain parse the layout. There are 4 main colors for normal rooms, and 2 special colors. The special colors are used to denote the starting room for each zone, as well as boss fights. I expect to add more special colors as I identify which other room types are useful to track (e.g. save points).
The other is a door layer. This layer is pretty self-explanatory, though a little fiddly to get right. I have a tileset of every possible set of 0, 1, 2, 3, or 4 doors that can exit a give room block. It's kind of annoying to find the right tile in the tileset, but definitely worth it for how much visual clarity it offers. Also, now that I have 113 rooms laid out, most of the door layouts are visible on the map somewhere, so I can right-click a similar room to eyedropper the door layout I need.
World Map Exporting
I have an exporter that outputs (at least) 2 different files from the World Map.
The first file is a json representation that the game expects. This simply consists of a dictionary that maps room names to rectangles in world-space. Any time I need adjacency information, I simply iterate over all the rooms, as that information isn't needed often enough to require an acceleration structure.
The second file is the .world format that Tiled is expecting. This is a simple json file, with the most complicated part being that I need to specify the relative path of each room in the world. My naming convention for rooms allows me to easily map subfolders for each game zone.
Finally, I noticed one particular aspect of creating the world map was frustrating me. When I made a new room, it was tedious to Save As... a similar room and fix up that room's export target. Thus I spent about a day to implement an extra function in my world map exporter to instantiate any rooms that don't exist on disk. It can make them the correct size, with a default tileset, and a basic layout with full collision on all 4 walls. Importantly, I hacked in a script function that allows me to set the export target for these new rooms, so I can open them, edit them, press Ctrl-E, and start playing in-game.
Thank you very much to @bjorn and the whole Tiled community. I'm extremely thankful for this tool. I can't imagine what Kinematic development would be like without it.
Want to learn more about Kinematic? Want to discuss custom game engine development? Jump in the Discord and let's hang out.