Setting up the tilemap and tile information

Lets start with designing a class for our Tile which we can expand when needed, you can add this Tile.cs inside a new “World” folder in your project root.

Our tile will inherit from the ColoredGlyph class which SadConsole uses to know how to render a tile to the screen.

internal class Tile : ColoredGlyph
{
    public int X { get; }
    public int Y { get; }
    public ObstructionType Obstruction { get; set; }

    public Tile(int x, int y)
    {
        X = x;
        Y = y;
    }
}

The obstruction type will be an enum that tells us how this tile behaves for entities when they are trying to walk or path over it.

public enum ObstructionType
{
    /// <summary>
    /// Can walk through, can see through
    /// </summary>
    Open,

    /// <summary>
    /// Cannot walk through, can see through
    /// </summary>
    MovementBlocked,

    /// <summary>
    /// Can walk through, cannot see through
    /// </summary>
    VisionBlocked,

    /// <summary>
    /// Cannot walk through, Cannot see through
    /// </summary>
    FullyBlocked
}

Now the Tilemap class itself will contain our map how it should be rendered, and where entities get information from if they can see through or navigate tiles.

internal class Tilemap
{
    public readonly int Width;
    public readonly int Height;
    public readonly Tile[] Tiles;

    public Tilemap(int width, int height)
    {
        Width = width;
        Height = height;

        // Initialize base tiles
        Tiles = new Tile[Width * Height];
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                Tiles[Point.ToIndex(x, y, width)] = new Tile(x, y)
                {
                    Obstruction = ObstructionType.FullyBlocked
                };
            }
        }
    }
}

This is a very barebones template, it basically initializes a Tile array the size of our world grid (Width * Height) and then inits a tile in each index with a default obstruction of “FullyBlocked”. The default tile is just a black background, white foreground and character 0. (empty)

We will need a few more tools in this class to make it more useful.
Lets start with adding a method to check if a position is within the bounds of the map.

public bool InBounds(int x, int y)
{
	return x >= 0 && y >= 0 && x < Width && y < Height;
}

Now we’ll add two indexers so we can easily access the tiles.
An indexer is basically a shortcut to access something at a given index for example array[0] the part between [ ] is the indexer, and 0 is the index.

public Tile this[int x, int y, bool throwExceptionWhenOutOfBounds = true]
{
	get => this[Point.ToIndex(x, y, Width), throwExceptionWhenOutOfBounds];
}

public Tile this[int index, bool throwExceptionWhenOutOfBounds = true]
{
	get
	{
		var point = Point.FromIndex(index, Width);
		if (!InBounds(point.X, point.Y))
		{
			if (throwExceptionWhenOutOfBounds)
				throw new Exception($"Position {point} is out of bounds of the tilemap.");
			return null;
		}
		return Tiles[index];
	}
}

These are two indexers that will allow both [x, y] and [Point.ToIndex(x, y, Width)]

The last option we need is a way to reset the tilemap entirely (for example, when generating something completely different after it already had some generation done on it.)

public void Reset()
{
	for (int x = 0; x < Width; x++)
	{
		for (int y = 0; y < Height; y++)
		{
			Tiles[Point.ToIndex(x, y, Width)].Clear();
			Tiles[Point.ToIndex(x, y, Width)].Obstruction = ObstructionType.FullyBlocked;
		}
	}
}

Now that we have a Tile class, ObstructionType enum defined and a Tilemap class.
It is time to connect the tilemap to the world screen, to do this we’ll make our own WorldScreen surface class so we can keep all the code for that seperate from the ScreenContainer.

internal class WorldScreen : ScreenSurface
{
	public readonly Tilemap Tilemap;

	public WorldScreen(int width, int height) : base(width, height)
	{
		// Setup tilemap
		Tilemap = new Tilemap(width, height);

		// Setup a new surface matching with our tiles
		Surface = new CellSurface(width, height, Tilemap.Tiles);
	}

	public void Generate()
	{
		// Moved temporary fill color to the generate method
        this.Fill(background: Color.Blue);
	}
}

Here we are inheriting from ScreenSurface so our WorldScreen has a surface to render for SadConsole. We are also adding our Tilemap here in this class, and we initialize it in the constructor with the same size as the WorldScreen.

And lastly we create a new Surface with the Tilemap’s Tiles (ColoredGlyph) to render what we are having on the tilemap. The tiles are immediately pushed to the screen for rendering when a change happens (this happens because when any property of ColoredGlyph is modified, the IsDirty property is set on the ColoredGlyph and so the Surface knows to re-render a given ColoredGlyph).

You should have ended up with this setup:

Now in the ScreenContainer.cs lets replace our ScreenSurface World with our new WorldSurface:

public WorldScreen World { get; }

And in the constructor:

// World screen
World = new WorldScreen(Game.Instance.ScreenCellsX.PercentageOf(70), Game.Instance.ScreenCellsY);
Children.Add(World);

Lets also remove the .Fill option on the World screen so we can start rendering our dungeon that we will make.

// Temporary for visualization of the surfaces
PlayerStats.Fill(background: Color.Green);
Messages.Fill(background: Color.Yellow);

And last but not least, in our Program.cs in the OnStart callback lets call our new Generate method on our WorldScreen so we can trigger our dungeon generation in the next article.

Builder gameStartup = new Builder()
	.SetScreenSize(60, 40)
	.SetStartingScreen<ScreenContainer>()
	.OnStart(GameStart)
	.IsStartingScreenFocused(true)
	.ConfigureFonts((fontConfig, game) =>
	{
		fontConfig.UseCustomFont(Constants.Font);
	});

Game.Create(gameStartup);
Game.Instance.Run();
Game.Instance.Dispose();
private static void GameStart(object sender, GameHost e)
{
	ScreenContainer.Instance.World.Generate();
}

This should leave you with the same screen visuals as before, but we’ll jump into dungeon generation in the next article!

Checkout the next article to continue with the series!

You can find all the code for this series down in the repository here:
https://github.com/Ven0maus/Code2DTutorials/tree/Tutorials/Roguelike

Leave a comment

Design a site like this with WordPress.com
Get started