Tutorial Part 5: Actors and Tiles

In the last tutorial we placed a single room where out @ character could walk around, but you’ll remember it ignored the existence of walls. We haven’t written game logic that distinguishes between our player and the floor/walls. Before we can do that, we need to generalize our player character into its own class: Actor.

Actor Class

You’re probably noticing by now that the GameLoop class is becoming bloated with all kinds of code – keyboard input, room building, player creation – very little of it having much to do with the actual game loop. So let’s start moving some of that code out of there, into their own distinct classes. An easy, and necessary, one to start with is dealing with the player.

I’m of the school of thought that inheritance – in moderation – saves work and time when building a Roguelike game. I imagine that most Roguelikes have a variety of interactable, movable characters such as players, non-player characters, creatures, enemies… and that many of them share common behaviours. They can move, and they can interact with the environment or one another. So we could call all of these types of characters “actors” – they’re all capable of acting on the world or one another in some way.

Create a new class called Actor. This will serve as the prototype for all actors in the game, and do some of the heavy lifting for us when those actors need to do something like move or open a door. Since we’ll never be instantiating “actor” objects, it is an abstract base class, hence the keyword in the class definition.

You’ll also notice that we’ve inherited SadConsole’s Entity class as the base class for Actor. This step is critical because it will let us take advantage of SadConsole’s EntityManager which will save us a lot of finicky graphics/positioning work at some later date. For now, it is enough to know that our player was originally defined as an Entity in our GameLoop, and we’re now defining all Actors as a kind of Entity.

using System;
using Microsoft.Xna.Framework;

namespace SadConsoleRLTutorial
{
    public abstract class Actor : SadConsole.Entities.Entity
    {
        private int _health; //current health
        private int _maxHealth; //maximum possible health

        public int Health { get { return _health; } set { _health = value; }} // public getter for current health
        public int MaxHealth { get { return _maxHealth; } set { _maxHealth = value; }} // public setter for current health

        protected Actor(Color foreground, Color background, int glyph, int width=1, int height=1) : base(width, height)
        {
            Animation.CurrentFrame[0].Foreground = foreground;
            Animation.CurrentFrame[0].Background = background;
            Animation.CurrentFrame[0].Glyph = glyph;
        }
    }
}
	

This calls the Entity’s base constructor to build us an Entity that is 1×1 in size. We’re also setting some important properties using various parts of Entity.Animation: .Foreground, .Background and .Glyph

Why can’t we just modify Foreground, Background and Glyph directly, without referencing Animation.CurrentFrame? That’s because all Entities are treated as animations, and we have to tell it which frame of the animation we’re modifying.

Setting Animation.CurrentFrame[0] tells SadConsole that we’re making changes to the first frame of the Entity’s animation. Why the first frame? Because when simple entities are created that have only one frame of animation (e.g. the @ symbol), this is where we can change its visual appearance. If your game will have multiple frames of animation per character, you’ll want to set each frame of animation accordingly. For now, we’re just going to pass the colour and glyph parameters to the object.

Have you thought about what kind of properties that you want all Actors to have in your game? It’s easy to add properties later on, so let’s keep things simple for the time being. I’m going to add two public properties to every Actor: Health and MaxHealth. You can add whatever properties you think will be common to most, if not all, actors for your particular game. The only reason I created these properties was an excuse to explain public getters and setters (aka. accessors).

Data encapsulation or “data hiding” is the concept that only the class has access to its own data and methods, and all other classes can only access information from the object through a public method. Think of it like a bank: you want access to the cash inside the vault, but your only means to getting at it is through the teller. The teller acts as your setter (for deposits) and your getter (for withdrawals). Direct access to the cash is deemed illegal, and breaking into the vault will earn you five wanted levels from the Object-Oriented Police.

You probably noticed that there are two definitions of variables:

private int _health; //current health

public int Health { 
                     get { 
                             return _health; 
                     } 
                     set { 
                             _health = value; 
                     }
}

int _health has a leading underscore and is lowercase – following stylistic guidelines – to indicate that it is a private variable. No, the police won’t show up at your door because you didn’t use lowercase to indicate a private variable, but Visual Studio will gently remind you if you forgot.

For public int Health, notice that the leading character is uppercase: this tells other coders (or yourself) that it is a public method. I’ve unwrapped the braces in public int Health to help you understand what is happening side of its methods:

When you set some variable equal to Actor.Health, it’s actually running a method called “get” – then returning the information stored in _health. (“Return” is basic to all methods. It outputs a certain value when the method finishes running.) When you update Actor.Health, it’s running the “set” method. This pushes whatever value you updated it with into the _health variable. Note: “value” is a keyword that the compiler recognizes, so you must use the word value or your setter won’t work!

Why use getters and setters to access private data? Aside from encapsulation, it allows you to do fantastic stuff like modify the data you’re sending and receiving from the object. Imagine that even though the actor’s health and maxHealth are stored as an integers – say, 45 out of 57… I can take those and convert them into percentages in the getters and setters.

public int Health { 
                     get { 
                            return ((_health / _maxHealth) * 100); // returns current health as a percentage of maxHealth
                     } 
                     set { 
                            _health = value; 
                     }
}

Think about how awesome that is:

That means you can use getters and setters (within reason) to do some data processing before storing it, or before sending it to other objects. This may seem like a small point, but this helps to enforce data encapsulation and make your life easier.

Pro tip: you can create a read-only property by creating the get method but leaving out the set method.

What if I don’t need to do any kind of data processing ever, and I just want a public variable? You can do that too, using “auto-implemented” properties. This is a nice C# feature:

public int Health { get; set; }

Notice that there is no private int _health in this code frag. C# takes care of creating and setting the private variables behind the scenes for you. You get a public property with no fuss.

I tend to use auto-implemented properties in cases where I know I’ll never need to modify the data in any way when sending it to other objects. Honestly, 90% of the time I just create private _variables and their public properties because it’s good OOP training and suffering builds character.

Back to the Actor class. We agreed that all Actors probably can move, so let’s add a couple of movement methods that we can use to push the actor around the screen with.

        // Moves the Actor BY positionChange tiles in any X/Y direction
        // returns true if actor was able to move, false if failed to move
        public bool MoveBy(Point positionChange)
        {
            Position += positionChange;
            return true;
        }

        // Moves the Actor TO newPosition location
        // returns true if actor was able to move, false if failed to move
        public bool MoveTo(Point newPosition)
        {
            Position = newPosition;
            return true;
        }

One method uses the addition assignment operator to nudge the Actor over by positionChange tiles in X/Y directions. The other sets the Actor’s position to the X/Y coordinates you feed it. Notice that there is no logic checking whether this location is a legal placement. We’ll get on that soon.

Player Class

Now that we’ve got an Actor, we can finally start building classes that inherit from. A Player class seems like a reasonable thing, even if we are only creating a singleton. Why? Because the player will eventually be getting properties and methods that no other actors will have.

using System;
using Microsoft.Xna.Framework;

namespace SadConsoleRLTutorial
{
    // Creates a new player
    // Default glyph is @
    public class Player : Actor
    {
        public Player(Color foreground, Color background) : base(foreground, background, '@')
        {
            
        }
    }
}

GameLoop Cleanup

Finally, let’s knit together our new classes by cleaning up the GameLoop. At the top of GameLoop redefine player as a Player instead of a SadConsole.Entities.Entity:

        private static Player player;

Let’s clean up the CreatePlayer method and make use of our new Player class:

        // Create a player using the Player class
        // and set its starting position
        private static void CreatePlayer()
        {
            player = new Player(Color.Yellow, Color.Transparent);
            player.Position = new Point(20, 10);
        }

And why not move all of that keyboard input code into its own method? Create a new static method called CheckKeyboard. Then move the contents of the Update method into CheckKeyboard (ctrl-x ctrl-v), and we’ll begin modifying it:

        // Scans the SadConsole's Global KeyboardState and triggers behaviour
        // based on the button pressed.
        private static void CheckKeyboard()
        {
            // As an example, we'll use the F5 key to make the game full screen
            if (SadConsole.Global.KeyboardState.IsKeyReleased(Microsoft.Xna.Framework.Input.Keys.F5))
            {
                SadConsole.Settings.ToggleFullScreen();
            }

            // Keyboard movement for Player character: Up arrow
            // Decrement player's Y coordinate by 1
            if (SadConsole.Global.KeyboardState.IsKeyPressed(Microsoft.Xna.Framework.Input.Keys.Up))
            {
                player.MoveBy(new Point(0, -1));
            }

            // Keyboard movement for Player character: Down arrow
            // Increment player's Y coordinate by 1
            if (SadConsole.Global.KeyboardState.IsKeyPressed(Microsoft.Xna.Framework.Input.Keys.Down))
            {
                player.MoveBy(new Point(0, 1));
            }

            // Keyboard movement for Player character: Left arrow
            // Decrement player's X coordinate by 1
            if (SadConsole.Global.KeyboardState.IsKeyPressed(Microsoft.Xna.Framework.Input.Keys.Left))
            {
                player.MoveBy(new Point(-1, 0));
            }

            // Keyboard movement for Player character: Right arrow
            // Increment player's X coordinate by 1
            if (SadConsole.Global.KeyboardState.IsKeyPressed(Microsoft.Xna.Framework.Input.Keys.Right))
            {
                player.MoveBy(new Point(1, 0));
            }
        }

Notice that we are now using player’s MoveBy method, which it inherited from Actor? Inheritance will allow us to simplify all kinds of code later on.

And finally, add a line to the now empty Update method to call CheckKeyboard:

            CheckKeyboard();

Run and move the player around. Not only is it working exactly the same as it did before, but our code is more organized and easier to understand. As a last step, let’s put in some movement check logic. Add this new method to GameLoop:

        // IsTileWalkable checks
        // to see if the actor has tried
        // to walk off the map or into a non-walkable tile
        // Returns true if the tile location is walkable
        // false if tile location is not walkable or is off-map

        public static bool IsTileWalkable(Point location)
        {
            // first make sure that actor isn't trying to move
            // off the limits of the map
            if (location.X < 0 || location.Y < 0 || location.X >= Width || location.Y >= Height)
                return false;
            // then return whether the tile is walkable
            return !_tiles[location.Y * Width + location.X].IsBlockingMove;
        }

Actor Movement

Now we can trigger this movement check by making a simple modification to our Actor class:

        // Moves the Actor BY positionChange tiles in any X/Y direction
        // returns true if actor was able to move, false if failed to move
        public bool MoveBy(Point positionChange)
        {
            // Check the map if we can move to this new position
            if (SadConsoleRLTutorial.GameLoop.IsTileWalkable(Position + positionChange))
            {
                Position += positionChange;
                return true;
            }
            else
                return false;
        }

Run your project. Is your character stuck in the middle of the field of walls? If so, that’s a good thing. It means it’s banging its cute little head into walls and stopping. If you want to make sure that it’s working properly, move your character into the empty room you created in the GameLoop:

        // Create a player using the Player class
        // and set its starting position
        private static void CreatePlayer()
        {
            player = new Player(Color.Yellow, Color.Transparent);
            player.Position = new Point(5, 5);
        }

Working? Awesome.

There are a few things I’m unhappy with so far, and the major problem is our map system. It’s all happening inside of the GameLoop, as evidenced in our call to SadConsoleRLTutorial.GameLoop.IsTileWalkable(). We need to separate those map methods and data from the GameLoop into their own class. Basic map creation will be the focus of our next tutorial.

Download the final source files for this tutorial here.

Published on November 1, 2018