I've also done some more rendering optimisations, but the game seems mostly fill rate limited at the moment on weaker GPUs. This is mainly because the game renders strictly back to front(for correct alpha blending) and does no culling or depth testing at all. I definitely need to come up with a better solution to alpha blending than this, but it might have to be enough for the demo. The game still runs acceptably on decent GPUs. I also need to decide how I'm going to handle variable frame rates, as I've mostly been testing the game at a fixed 60fps so far.
So basically there's not much to talk about this month. Instead of going over a massive list of bugs that I've fixed(half of which are probably content bugs and not programming), I thought I'd talk about how the world is built and stored in Nirion.
Overview and World Areas
Nirion has one large interconnected world with no loading screens or "cut" transitions of any kind. Because of this, I've decided that the world should be streamed in and out based on the player's(and camera's) position. The main "unit" of the world is an entity called a world area. A world area can contain many transient and non-transient entities. One type of entity that it can hold is a tilemap. Tilemaps make up most of what can be seen visually in Nirion. Besides tilemaps, any other type of entity can be placed inside a world area to complete the area, such as enemies, destructibles(barrels, crates) and doors. World areas are always rectangular.
The purple outlines show the bounds of a world area in the editor.
When the game first starts the one and only "map" asset is loaded and all world areas contained in it are spawned. By itself the world area is just a single entity that stores it's bounds and state. A world area can be in one of three states; unloaded, loaded, and active. Right now whether or not a world area is loaded or unloaded is based entirely on if the world area's center is within a given radius to the camera. This tends to load way more than is actually needed, and also loads unnecessary entities. I plan on changing this to a connection based solution eventually, where world areas will load/unload based on if they're adjacent to an active world area or not. When a world area becomes loaded, it will spawn all it's non transient entities.
A world area can be in an active state if the camera's position is contained within the world area's bounds. When a world area is active, it will spawn all of it's transient entities. An active world area must be loaded already, and when the camera leaves the world area's bounds, it is no longer active. Because world areas can overlap, they are assigned a priority value where areas with higher values will be favored if the camera's position is contained in multiple world area bounds.
Entities are split into transient and non transient categories so that world areas are easier to reset. When a world area is loaded it will load two lists of entity delta values into memory, one for transient entities and one for non transient. It then immediately spawns non transient entities from the delta values in memory. This usually contains entity types such as doors, lamp posts, and lights. When the player enters the world area, it will spawn transient entities from the delta values in memory, and this contains entity types like enemies, destructibles, gear pickups, and energy pickups. When the player leaves the area, transient entities are destroyed, but the delta values still remain in memory. The delta values are finally destroyed once the world area is completely unloaded.
Before the player enters the area. No transient entites are spawned.
After the player enters the area. Transient entities such as the wasp enemies and rocks are spawned.
Tilemaps
World areas can contain an important entity called a tilemap. As the name suggests it's a pretty traditional 2D grid of tiles that can be rendered efficiently. In Nirion, a tile is 8x8 pixels in size, and can can be a static tile, auto tile("baked" down to a static tile at build time) or an animated tile. A tilemap stores a variable amount of tilemap layers, which can either be above the layer that entities exist on, or more commonly, below the entity layer. It also stores one layer of data that describes things like flags(is the tile lava, should the player fall through the ground at this point), what type of collision should be used for that tile, and what "height" the tile is in the game.
I have created a tilemap editor to build the tilemaps in Nirion. This tool includes functionality for painting static, auto or animated tiles, painting other data, and creating/removing layers. Tiles are picked from larger "tilesets" which can be selected by this tool as well. Layers can also be marked as emissive for rendering purposes.
A screeshot of the tilemap editor.
I think Nirion tends to look more diverse due to how it uses layers to composite a relative small amount of tiles. A pretty excessive amount of layers are usually used. For example we can see that the transition between the lighter and darker ground is created by layering a rock auto tile on top of them both.
Another example is how the "lava crack" tile is layered on top of the ground. The lava is also on it's own layer, so we can mark it as emissive without affecting the rest of the ground. This makes it stand out more.
Another feature which I think greatly contributes to the game looking more diverse is auto tiles. Auto tiles are just regular static tiles which will change based on what tiles it's adjacent to. They are mainly drawn in the following sections, inner corners, outer corners and a center:
A grass auto tile
The format is then described in an auto tile asset, which is just a text file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | AutoTile { bitmap = CaveTilemap.png; brushSize = (2, 2); center { (64, 160, 2, 2, 1.0) } right = (80, 160, 1, 2); left = (56, 160, 1, 2); up = (64, 176, 2, 1); down = (64, 152, 2, 1); outBL = (56, 152, 1, 1); outBR = (80, 152, 1, 1); outTL = (56, 176, 1, 1); outTR = (80, 176, 1, 1); inBL = (0, 144, 1, 1); inBR = (8, 144, 1, 1); inTL = (0, 152, 1, 1); inTR = (8, 152, 1, 1); } |
This just tells the editor where each tile will go in the auto tile. The auto tile can then be selected in the editor and draw freely:
When an auto tile is placed, it just does a brute force search of all the tiles around it and decides which static tile it should draw based on the asset shown above. This saves me tons of time and makes the environments look much more diverse and interesting. Auto tiles can also have variations which are randomly picked when drawn.
Bitmap animation tiles can also be drawn if a given animation asset is marked as "tile". This animation can then be selected from the tilemap editor and drawn like any other type of tile. When rendering, all animation frame data for a used animation is uploaded to the GPU and evaluated in the vertex shader. This means we don't have to update vertex buffers for a tilemap every frame just because it's animation is updating.
Altogether tilemaps make up most of the world in Nirion. They define environment collisions and most of the visuals in the world. Once they are drawn, they are attached to an entity and placed in the world. Placing multiple tilemaps together gives the illusion of one large world.
Tilemap collision and data
One thing I didn't mention about tilemaps is that they don't just draw static tiles directly. When a static tile is drawn, a special tilemap called a "tileset" is referenced. A tileset is drawn on, just like a normal tilemap, except instead of drawing graphics on it, information about a specific tile is drawn. This includes data like collision and jump direction(used to compute where the player should land when they jump off a wall).
This uses the tilemap editor as well.
Painting jump direction on the tileset
When you paint a tile on a normal tilemap, it copies this data over. This adds a level of indirection so that I only have to paint collision and jump direction etc for a tileset(all of the Mine area's tiles in this case) instead of on every individual tilemap. This obviously saves me a ton of time and avoids mistakes.
Some other data is computed when the tilemap is saved, such as the height at that tile:
Automatically generated heights.
Entities and layers
The last thing that makes up the world is entities. Entities are used when an object needs additional functionality that a tilemap can't provide. For example, doors are entities because they can be opened and closed based on events in the game. Other entities include enemies, destructible objects, triggers, lights and upgrade pickups.
When entities are placed in the world they can be assigned a layer value. This is an integer value that determines how "high" in the world the entity is placed. Entities with different layer values do not interact physically, and rendering will be altered based on an entities layer relative to the camera's layer. Layers below the camera will be dimmed and scaled down, and layers above the camera will be scaled up and eventually faded out.
Conclusion
That basically sums up how I create the world in Nirion. The way this works has been constantly changing all throughout development and is hopefully stabilizing now. Some things will probably have to change as the game scales up(such as the world area loading criteria and how entities are rendered on top of tilemaps), but hopefully most of this will remain the same. I hope this has been interesting, and maybe next time I'll finally be done with a demo.
Let me know if you have any questions!