Tutorial Part 4: Create a Room

Now that we’ve got a walking @ character, let’s place it inside of a room. Nothing fancy: just a rectangle that we’ll place somewhere on the console that the character can walk inside of, or around. But first, let’s think about what we’re trying to do:

Breaking final game behaviour down into smaller intermediate steps – especially with sub-steps – makes a coding challenge much easier to handle.

Let’s start with the first problem: creating a rectangle composed of walls. Let’s deal with the walls first, because we’re going to create our first new class: TileBase. The TileBase class will be very generic, so we can use it for all kinds of tiles in the game. I’m going to define a Tile as an object that is static on the map (e.g. a wall or floor), as opposed to dynamic objects like walking NPCs and enemies. Why TileBase? This is a naming convention that tells me that the class is a base class, which means that other classes will inherit its properties. We’ll see that when we create the Wall class later.

TileBase Class

Add a new empty Class to your project called TileBase. You’ll notice that VS2017 will automatically populate the class name and constructor for you. The 4 seconds saved here will some day add up to 17 minutes of life extension.

First off, let’s put in a comment that describes what the intended use of this class is. I haven’t talked about commenting at all, hoping that you’ve been elegantly commenting your own project with the grace of a Japanese haiku writer. If you haven’t, start now.

A reply from Snayff, who has been helping to edit and bugfix these tutorials:

<Snayff> @vga256 after your reference to a haiku I was disappointed there wasn’t one in the comment, so I wrote one for you:

// Abstract and basic
// TileBase is the simple form
// Of all the tiles

using System;
using Microsoft.Xna.Framework;
using SadConsole;
namespace SadConsoleRLTutorial
{
    // TileBase is an abstract base class 
    // representing the most basic form of of all Tiles used.
    public abstract class TileBase : Cell
    {
        // Default Constructor
        public TileBase() : base()
        {
        }
    }
}

Notice that I’ve added a few keywords to the class:

using SadConsole tells the compiler that we’ll be using the SadConsole namespace in the class, which is necessary because…

…. we need to base our TileBase class on ANOTHER class: SadConsole.Cell

(Be sure to add the SadConsole and Microsoft.Xna.Framework namespaces to the top of this class, because we’ll be using those today.)

Cells are one of the most basic data types in SadConsole. A cell is a glyph (bitmap character or graphic) that can be displayed on a console. It has a foreground colour and a background colour. It does not have a Position on the screen. That’s important: we don’t set the X/Y coordinates of individual tiles as we might with other kinds of game engines. A cell doesn’t know where it is on the console, but the console knows where every Cell is. We’ll get to that later!

public abstract class TileBase : Cell tells the compiler that TileBase will be based on Cell, and inherit all of its properties and methods. Think of : as the “is a” operator… TileBase is a Cell.

An OOP-related note on the “abstract” keyword: TileBase is technically termed an abstract base class (“ABC”) because it will have no objects of its own. Other classes will be derived from it – like Wall – which will have its own objects however! The abstract keyword here is optional, but I thought I’d put it in to make our design clear.

Finally, you’ll notice that the constructor uses the : is-a operator. This tells the compiler that our constructor should inherit the constructor of its base class. In this case, it means: TileBase will borrow Cell’s constructor and run it, before running its own construction code just below that. We can do some really cool stuff using the base constructor in a bit.

Alright. Now that the skeleton of our class is written, let’s fill in some details so it actually works:

    public abstract class TileBase : Cell
    {

        // Movement and Line of Sight Flags
        protected bool IsBlockingMove;
        protected bool IsBlockingLOS;

        // Tile's name
        protected string Name;

        // TileBase is an abstract base class 
        // representing the most basic form of of all Tiles used.
        // Every TileBase has a Foreground Colour, Background Colour, and Glyph
        // IsBlockingMove and IsBlockingLOS are optional parameters, set to false by default
        public TileBase(Color foreground, Color background, int glyph, bool blockingMove=false, bool blockingLOS=false, String name="") : base(foreground, background, glyph)
        {
            IsBlockingMove = blockingMove;
            IsBlockingLOS = blockingLOS;
            Name = name;
        }
    }

In the above code we set two bools (false/true flags) that control whether the Tile can block character movement and line of sight. The real meat on the bones is in the constructor:

An OOP note on the “protected” keyword: this tells the compiler that only derived classes (such as Walls and Floors – which we’ll create in a little bit) are allowed to access TileBase’s data. This isn’t strictly necessary – we could have use the “public” keyword here instead – but I’m trying to enforce some stricter OOP rules for fun and self-flagellation.

The constructor takes two Colours (foreground and background) and a glyph number as required parameters. It passes these three parameters on to Cell constructor, and creates a new Cell using those colours and glyph. The constructor also accepts three more optional parameters: blockingMove, blockingLOS, and name. If you don’t include these parameters when you create a new Tile, the constructor will automagically plug the defaults in (does not block moves, does not block line of sight, and has no name).

Finally, in the construction code, you’ll notice that we’re setting the public bool IsBlockingMove and IsBlockingLOS to the parameter we’ve supplied. Remember that the public keyword tells the compiler that this property’s data can be read and set by other classes. We do that because we want to be able to check Tile.IsBlockingLOS later on, like by a Map class.

Okay. That’s a usable TileBase class. Let’s create a class that inherits all of it, so we can put some on the screen.

Wall Class

This one’s easy. We’re going to inherit the TileBase class completely without any modifications.

Side discussion: Why bother creating a separate class for Walls? Why not use an Entity-Component System style design where everything is an entity with behavioural components attached to it? You absolutely can, but this tutorial is focused on a traditional OOP inheritance model.

Create a new class called TileWall. Rewrite the class definition so it “is-a” TileBase. Now rewrite the TileWall constructor so it uses TileBase’s base constructor, and passes the right parameters to it. Remember which parameters are required and which are optional?

using System;
using Microsoft.Xna.Framework;
namespace SadConsoleRLTutorial
{
    // TileWall is based on TileBase
    public class TileWall : TileBase
    {
        // Default constructor
        // Walls are set to block movement and line of sight by default
        // and have a light gray foreground and a transparent background
        // represented by the # symbol
        public TileWall(bool blocksMovement=true, bool blocksLOS=true) : base(Color.LightGray, Color.Transparent, '#', blocksMovement, blocksLOS)
        {
            Name = "Wall";
        }
    }
}

The only differences of note here are that we are passing ‘#’ as the glyph for this object. SadConsole will accept either an integer or a char (hence the ‘ ‘ wrapped around the #) for glyph. Save any grumbling about how we just created a class identical to its base class for later.

Floor Class

Create a new class called TileFloor. This class will do much the same as the Wall class we created earlier, except that it will carry different properties.

using System;
using Microsoft.Xna.Framework;
namespace SadConsoleRLTutorial
{
    // TileFloor is based on TileBase
    // Floor tiles to be used in maps.
    public class TileFloor : TileBase
    {
        // Default constructor
        // Floors are set to allow movement and line of sight by default
        // and have a dark gray foreground and a transparent background
        // represented by the . symbol
        public TileFloor(bool blocksMovement = false, bool blocksLOS = false) : base(Color.DarkGray, Color.Transparent, '.', blocksMovement, blocksLOS)
        {
            Name = "Floor";
        }
    }
}

Not much to say here except that we’ve left default parameter values in the constructor, to allow for the possibility that you’d like to create a special floor tile that blocks movement or line of sight. (E.g. Think about a crumbling floor that you wouldn’t want the player to walk across lest they fall in.)

Room Building!

We can finally start building some Rooms composed of floors and walls. Head over to your GameLoop class and add some new variable definitions to the top of the file:

private static TileBase[] _tiles; // an array of TileBase that contains all of the tiles for a map
private const int _roomWidth = 10; // demo room width
private const int _roomHeight = 20; // demo room height

TileBase[] defines the _tiles variable as an empty array of TileBase. Why TileBase? We’ve defined it as the most basic, most generic, form of tiles that our maps will use. We could have defined separate arrays of TileWall or TileFloor, but it’s easier to store all of these as their base class type.

We also define _roomWidth and _roomHeight to set a rough height and width of our first room. Set these to whatever you’d like (although if the room is larger than the console’s width and height, you won’t see it!).

Now create a new method called CreateFloors(). This little method will expand into its own Map generator class in the future.

        // Carve out a rectangular floor using the TileFloors class
        private static void CreateFloors()
        {
            //Carve out a rectangle of floors in the tile array
            for (int x = 0; x < _roomWidth; x++)
            {
                for (int y = 0; y < _roomHeight; y++)
                {
                    // Calculates the appropriate position (index) in the array
                    // based on the y of tile, width of map, and x of tile
                    _tiles[y * Width + x] = new TileFloor();
                }
            }
        }

Each step of these nested for loops adds a single line of wall tiles to the _tiles array. You’ll notice that there is some math happening each time the _tiles array is addressed. The math pinpoints the correct position or “index” in the array to place the new tile. We’ll talk about tile indexes when we dig into maps in more depth later, because being able to get the right index (position) of the map array is critical for drawing our map properly. For the time being, memorize this formula for addressing a one-dimensional array using X/Y coordinates:

// Converts XY coordinates to a single array index
// Where x and y are the horizontal and vertical positions of the tile
// and width is the width of the map
int index = y * width + x;

Now create another method called CreateWalls(). Here’s where we’ll stamp down some walls and replace floor tiles as we do that.

        // Flood the map using the TileWall class
        private static void CreateWalls()
        {
            // Create an empty array of tiles that is equal to the map size
            _tiles = new TileBase[Width * Height];

            //Fill the entire tile array with floors
            for (int i = 0; i < _tiles.Length; i++)
            {
                _tiles[i] = new TileWall();
            }
        }

You’ll notice that we’re finally setting the _tiles array to having a specific size based on the Width and Height of the console. This is a critical step, because when we instantiate the console, we want to use the _tiles array data to fill the cells of the console. If the array is smaller than the console, the console will complain that it found null data in there.

Finally, the method does a flood fill of _tiles[] by creating new walls at every position in the array. We’re filling the entire array with walls because we want to prevent a situation where there are gaps or holes in the array — SadConsole really doesn’t like encountering “null” or missing data when it’s trying to render graphics to the screen. If it does, it will throw an exception complaining about a null object.

Okay, now that you’ve built two new methods, let’s call them in the Init() method. Add these two lines to the top of GameLoop’s Init method:

            // Build the room's walls then carve out some floors
            CreateWalls();
            CreateFloors();

And (thanks to @IgneSapien for catching this typo) you need to modify the startingConsole instantiation to take advantage of our new _tiles array:

Console startingConsole = new Console(Width, Height, Global.FontDefault, new Rectangle(0, 0, Width, Height), _tiles);

 

Run the program — the entire console should be composed of floor tiles, with a rectangular room positioned somewhere on it. Walk your @ character over to it, and notice that it’s able to walk through the walls. Woops. I guess we’ll need to put in some movement/blocking check logic in the next tutorial! 🙂

Download the final source code files for this tutorial.

Published on October 29, 2018