This site uses cookies to ensure you get the best experience. More info
Got it!

Tilemaps implementation



Emerging issues

Today I’m going to show how I integrated tilemaps into the engine and used them to show a basic map in game.

But before that I need to highlight a few issues that emerged during the process of implementation.

Camera movement

The first issue was related to what we see on screen. The player could move for now, but I had to implement some kind of camera movement so we can explore different areas of any loaded tilemap.

Because I’m developing the engine simultaneously with the game (meanwhile tinkering with dependent libraries) I usually code things according to my current needs. That is why I didn’t implement any camera class or anything like that.
It seems a bit too complex, especially when all I need is simple camera movement. To be honest I don’t really find a place for it, at least for now.

What I did instead, was introducing the ViewPort - as in, the currently visible part of the world on screen - as an instance of Bramble.Core.Rect structure, accessible from a Game instance.

public Vector2D ViewPortOffset
{
    get
    {
        return ViewPort.Position;
    }
}

public Rect ViewPort
{
    get;
    private set;
}

It technically is what the name states - it’s a rectangle of given size placed in some arbitrary position.

So, how would we move the camera now in this kind of implementation?
Well, it’s quite simple - we just have to change the ViewPort so the rectangle is placed in different position.

Let’s take a look how operating on the viewport looks like.

public void CenterViewTo(Vector2D position)
{
    ViewPort = new Rect(new Vector2D(position.X - Terminal.Size.X / 2, position.Y - Terminal.Size.Y / 2), Terminal.Size);
}

public void MoveViewBy(Vector2D distance)
{
    ViewPort = new Rect(ViewPort.Position + distance, Terminal.Size);
}

Here we have 2 methods. One that centers the viewport to a given position (for example a player, so we center the camera on him), which changes our viewport accordingly.
It’s worth to note that the rectangle’s position is placed in upper left corner so we have to subtract half of the terminal size to get it properly.

The other method is used to move our viewport relatively.

This kind of method is useful, when we move our player by X units, and we want the camera to keep on being centered on the player.

So now inside our PlayerMovementHandler component, right after applying the movement vector we also move our viewport.

if(movement != Vector2D.Zero)
{
    GameObject.Position += movement;
    Game.MoveViewBy(movement);
}

But there is another issue related to this.

ViewPort units vs World units

The Terminal accepts viewport units. Which are restricted to be non-negative points within the viewport, and if we now keep moving our player, his position’s X or Y may be bigger than the one restricted by terminal size.

Which means that current Render method of Player class needs to be changed.

public override void Render(Malison.Core.ITerminal terminal)
{
    terminal[Position][TermColor.White, TermColor.Black].Write((int)Glyph.CharacterInversed);
}

As you can see we do not take the game’s viewport into the account. The method would try to render the player’s world position as viewport position.

To fix that, we simply need to subtract the ViewPortOffset from the player’s position, like so:

public override void Render(Malison.Core.ITerminal terminal)
{
    terminal[Position - Game.ViewPortOffset][TermColor.White, TermColor.Black].Write((int)Glyph.CharacterInversed);
}

Excellent, now our rendering works properly!

But this example shows a different problem, related to the current architecture.
We would have to act the same way in all other Render methods of all other game objects. And that’s definitely not convenient. It smells, and violates the DRY principle.

But it’s not the topic of this post so let’s leave it for now like it is and pretend everything is good.

Level

Having resolved the issues at hand, let’s move on to the actual tilemaps integration.

I have decided to leave the Praedium.Engine namespace untouched by the external libraries - such as the TiledSharp which I used to parse the .tmx file structure.

Instead, the engine now contains it’s own abstractions used to represent such data structures, and the first one is the Level class.

public abstract class Level
{
    public SortedList<int, TileLayer> Layers = new SortedList<int,TileLayer>();

    public Game Game
    {
        get;
        set;
    }

    public void Render(Malison.Core.ITerminal terminal)
    {
        foreach (var layer in Layers.Values)
        {
            foreach (var tile in layer.Tiles)
            {
                if(Game.ViewPort.Contains(tile.Position))
                {
                    terminal[tile.Position - Game.ViewPortOffset].Write(tile.Character);
                }
            }
        }
    }

    public void AddGameObject(GameObject obj)
    {
        Game.AddGameObject(obj);
    }

    public void AddTileLayer(TileLayer layer)
    {
        Layers.Add(layer.ZIndex, layer);
    }
    
    public void Load()
    {
        OnLoad();
    }

    public void Unload()
    {
        OnUnload();
    }

    protected abstract void OnLoad();

    protected abstract void OnUnload();
}

The Level is basically a scene that contains multiple layers of tiles, ordered by their Z-Index.

Rendering the level means rendering every tile from every tile layer one by one. Here we handle the rendering and complying with the game’s viewport offset.

TileLayer

The next abstraction is the TileLayer. TileLayer is a structure that has a certain Z-Index, by which it’s rendering order is decided and it also contains a collection of tiles.

public struct TileLayer
{
    public int ZIndex;
    public Tile[] Tiles;

    public TileLayer(int zIndex, Tile[] tiles)
    {
        ZIndex = zIndex;
        Tiles = tiles;
    }
}

Tile

The final and most basic structure is the Tile. A tile is represented by a Character structure, a position in world space units and - for now - a simple boolean flag to handle collisions in order to restrict player movement.

If you don’t remember, the Character structure is a set of a character code in given encoding, a foreground and background, which is all we need to render it on screen.

public struct Tile
{
    public Character Character;
    public Vector2D Position;
    public bool Collideable;

    public Tile(Character character, Vector2D position, bool collideable = false)
    {
        Character = character;
        Position = position;
        Collideable = collideable;
    }
}

Adding level support to Game class

To allow our game to handle the levels, I added a Level property that defines the current level and a LoadLevel method that handles callbacks for the levels and takes care of cleaning up the entities assigned by current level.

Other than that I had to call Render on the level before rendering the game objects.

public Level Level
{
    get;
    private set;
}

public void LoadLevel(Level targetLevel)
{
    if(Level != null)
        Level.Unload();

    entities.Clear();

    targetLevel.Game = this;
    targetLevel.Load();

    Level = targetLevel;
}

Usage example

Having defined all these classes and structures let’s look onto an example.

The example will be a tilemap representing the farm. It’s not a finished map but it’s good enough for a sandbox.

So, how do we use those classes from the engine?

Well, it’s quite simple, all we need to do is inherit from the Level class and within the appropriate callback (OnLoad) we load our map from resources.

The code - thanks to TiledSharp library is quite straightforward.

Firstly, we load the map from the file, which we can access from resources that are copied over to the output directory.

Secondly, we get the tileset which contains all meta-information for specific tiles and then we iterate through every layer to create a new one.

The GetTile helper method is where we process all information needed to create instances of the Tile structure.

After parsing every tile of every layer we take care of tilemap-defined objects.

For now there is only one - a Spawn, which is the place where our character should be instantiated at.

Having created our player, and set his position to the one defined by spawn, we can center our viewport to focus our player.

public class FarmLevel : Level
{
    protected override void OnLoad()
    {
        TmxMap map = new TmxMap("Resources/farm.tmx");

        TmxTileset tileset = map.Tilesets[0];

        foreach (var layer in map.Layers)
        {
            int zIndex = int.Parse(layer.Properties["ZIndex"]);

            Tile[] tiles = layer.Tiles.Select(x => GetTile(x, tileset)).ToArray();

            AddTileLayer(new TileLayer(zIndex, tiles));
        }

        var spawn = map.ObjectGroups[0].Objects["Spawn"];

        Player player = new Player();
        player.Position = new Vector2D((int)(spawn.X / spawn.Width), (int)(spawn.Y / spawn.Height));

        AddGameObject(player);

        Game.CenterViewTo(player.Position);
    }

    private Tile GetTile(TmxLayerTile tile, TmxTileset tileset)
    {
        int code = int.Parse(tileset.Tiles[tile.Gid - 1].Properties["Code"]);
        TermColor foreground = TermColor.Parse(tileset.Tiles[tile.Gid - 1].Properties["Foreground"]);
        TermColor background = TermColor.Parse(tileset.Tiles[tile.Gid - 1].Properties["Background"]);

        Character character = new Character(code, foreground, background);
        Vector2D position = new Vector2D(tile.X, tile.Y);
        bool collideable = bool.Parse(tileset.Tiles[tile.Gid - 1].Properties["Collideable"]);

        return new Tile(character, position, collideable);
    }

    protected override void OnUnload()
    { }
}

After implementing our class for specific level we then need to make changes in the application code that sets up our game inside AppForm.cs.

_game = new Game(Terminal);

_game.LoadLevel(new FarmLevel());

Because of implementing the Level class we do not need to manually instantiate level-specific game objects such as the Player. The level itself takes care of that now.

Working tilemap import Previously mentioned GIF capture of working example.

Wrapping it up

There is still a lot of things I need to cover such as level transitions, keeping certain objects not destroyed between transitioning and so on but it’s a starting point and it works.

I have also mentioned the rendering issue, which I’ll be tackling in the next post by implementing components that handle strictly the rendering code.

And that’s basically it for this post. If you have any suggestions, feel free to drop a comment below.
See you next time!