Monday, May 17, 2021

Once again, but with Templates

I've been wrestling with the system behind visible terrain for the past week. It's been in this awkward state of "I almost like using this" for too long now. I'd like to use the system for real... so let's make a real game.

How about we combine the idea of a tower defense game with a card-system that ties structures to rooms? The player can create defensive areas and manage resources to build new rooms. I'm a big fan of platformers; that should give this game a lot of verticality and make falling traps - both creatures falling and spikes from above - fun to design.

Anything built here can be re-used elsewhere. Alisia Deena Rain can stave off every enemy from her game, use every one of her powers, and have a failure state based on lives. mDiyo (the character) can build swords and armor, 'find' soldiers to give the swords to, and fend off a whole bunch of slimes. As long as the game is fun, anything goes!

Basic Game Stats

Resolution: 640x360 px
Tile size: 16x16
Template size: 8x8 tiles or 128x128 pixels
Room size: Minimum of 1 Template. Maximum of... a lot?
 
Style: 2D sidescroller
Genres: Platformer, Tower Defense
Aesthetics of Play: Challenge, Fantasy, Discovery, Expression

References: Boss Monster, Super Metroid, Starcraft

Resolution
 
640x360 (360p) is the ideal resolution for retro-styled games. Most monitors use a 16x9 resolution with 1920x1080 (1080p) as their base. The scaling works near perfectly:

360p: 1x
720p: 2x
1080p: 3x
1440p: 4x
4K: 6x
5K: 8x

There are a few resolutions where this doesn't scale up as nicely such as 1366x768. In these cases, we can give the player a bit more screen space in whichever direction doesn't match. 

Tile Size
 
Tile sizes were picked by feel, system limitation, and tradition. Unity lets tiles be any resolution at all; you can even mix and match them. Traditional tile sizes are 8x (Sega Genesis), 16x (very common), or 32x (RPG Maker), with a few crazy systems going for larger systems and a few super-constrained systems going for less. 

At 16x, the screen can display tiles 40 wide and 22.5 down. This gives us a nice area to work with that isn't so small we end up fiddling with all the tiny little details, but isn't so large that a good artist needs to make multiple variations on tiles to get them right. This size groups up well in sets of 2, 3, and 4.

Video cards store textures and process information in powers of 2. Unity automatically adds a buffer to the edge of graphics that aren't a power of 2. A lot of weird problems are mostly mitigated, but there are still a number of features that don't work correctly with odd shaped textures due to under-the-hood shenanigans that the engine performs to prevent your computer from exploding.

Templates and Rooms
 
 

This is a full-size mockup of the game. Each light blue square and its border is a set of tiles that we can call a template. Groups of these templates make up a room. Rooms can be as small as 1 template or larger than the entire screen. Templates can be arbitrary shapes, but rectangles should be easiest to work with. 

Making it Work


Mockups aren't too hard to make in-game. The base project has tiles, colliders, characters, health, tools, and anything else needed already set up. Almost all of it is in a state of "this works, but it's ugly/hardcoded/barely useful", also known as "good enough". Let's just grab some basic colors for tiles and...

Perfect. I can work with this. 

Getting the template system working was no easy feat. I had duplicated a lot of code into six separate places... templates were loading differently from the editor's inspector, the game itself, and I wanted to make them load by clicking on the asset. Editing one thing would leave the rest intact; refactoring this mess down was mandatory. The entire workflow needed to be adjusted so that I could spend more than a moment in the editor without short-circuiting my brain with code questions.
 
The code's flavor changed from 300 lines of spaghetti into this:
public void LoadTemplate(Template template)
{
    //Find tilemaps
    Tilemap[] maps = TemplateHelper.FindTilemaps();

    //Let the Template Builder know that we're adjusting it from the outside
    TemplateBuilder builder = GameObject.Find("Template Builder").GetComponent<TemplateBuilder>();
    builder.assetName = template.name;

    //Load the room
    Vector3Int templateCorner = Vector3Int.zero;
    TemplateEditorHelper.LoadRoom(maps, template, builder, templateCorner);
}
A lot of effort was put into making a template system that could save and load rooms that have a background, terrain, and foreground layer. The template needs to keep track of objects like trees, spikes, or special creatures. It also needed a new set of paint.
 
This looks good. Suspiciously good... has it ever looked this good? I don't think so. Why does this feel right and why didn't I spend the time before to make this actually work in a reasonable manner that a designer could understand? It feels like I've spent so much time floundering around in my own head that actually showing this off is a feat in and of itself.
 
Let's grab a few sprites from my unsorted design archives and the Open Pixel Project, a few sounds from royalty free sites, and string everything together in the most basic version of 'reasonable'. Write just a little bit of code to get this whole thing working... 
 
public override void GenerateLevel()
{
    //Terrain
    Tilemap[] maps = RoomTemplateHelper.FindTilemaps();
    Vector3Int roomSize = groundLayer[0].GetBaseSize();
    Vector3Int mapSize = new Vector3Int(roomSize.x * groundSize.x, roomSize.y * groundSize.y, 1);
    TileBase[] tiles = new TileBase[mapSize.x * mapSize.y];
    Debug.Log("Making " + tiles.Length + " tiles betterererer");

    //Wipe the map
    maps[0].SetTilesBlock(new BoundsInt(Vector3Int.zero, mapSize), tiles);
    maps[1].SetTilesBlock(new BoundsInt(Vector3Int.zero, mapSize), tiles);
    maps[2].SetTilesBlock(new BoundsInt(Vector3Int.zero, mapSize), tiles);

    //Generate ground
    int baseCount = groundLayer.Length;
    for (int y = 0; y < groundSize.y - 1; y++)
    {
        for (int x = 0; x < groundSize.x; x++)
        {
            RoomTemplateBase template = groundLayer[rand.Next(0, baseCount)];
            RoomTemplateHelper.LoadRoom(maps, prefabContainer, template, new Vector3Int(x * roomSize.x, y * roomSize.y, 0));
        }
    }

    //Generate top layer
    baseCount = grassLayer.Length;
    int treeCount = grassTrees.Length;
    for (int x = 0; x < groundSize.x; x++)
    {
        if (x % 4 == 2)
            RoomTemplateHelper.LoadRoom(maps, prefabContainer, grassTrees[rand.Next(0, treeCount)], new Vector3Int(x * roomSize.x, (groundSize.y - 1) * roomSize.y, 0));
        else
            RoomTemplateHelper.LoadRoom(maps, prefabContainer, grassLayer[rand.Next(0, baseCount)], new Vector3Int(x * roomSize.x, (groundSize.y - 1) * roomSize.y, 0));
    }
}

The overall result?

We have ground, grass, trees, a ghost template, the character, a multi-part hitpoint and status effect system from another source, and a tool UI from that same other source. The ground is built from rooms and everything works as intended.
 
It just works. Huh.  
 
IT FINALLY WORKS!!

I spent a week getting all of this to work. Most of it was in place already; it was ugly, sabotagetastic, and weird. Now it's something I can be proud of and have enough progress to think about sharing with the world. 
 
I'll work on the other systems in due time as the card system gets fleshed out and the skeleton turns into a fully-fleshed out game. For the first time in the history of the Base Project, a game has been built out of its pieces. This has been a long, long time coming and I'm glad it's finally coming together.

No comments:

Post a Comment

Self Reflection, Avatar Reflection

It started as a joke. One day I decided that my game development was going poorly because I was too attached to my characters. If I messed a...