Tutorial Part 10: Command Manager

It’s time to do some work on our basic tunnelling generation algorithm. It’s not much fun just running around the maze without things we can interact with. Let’s start by adding a basic command system manager (again – modelled on the RogueSharp tutorials and a Command programming pattern), so in our next tutorial we can integrate some lockable doors that the player can bump into for fun and profit.

A command system/manager allows us to do at least a couple of things:

Command Manager

Let’s build out a basic CommandManager. Create a new subfolder in your project called Commands and create a new CommandManager class within it:

using System;
using Microsoft.Xna.Framework;

namespace SadConsoleRLTutorial.Commands
{
    // Contains all generic actions performed on entities and tiles
    // including combat, movement, and so on.
    public class CommandManager
    {
        public CommandManager()
        {

        }

        // Move the actor BY +/- X&Y coordinates
        // returns true if the move was successful
        // and false if unable to move there
        public bool MoveActorBy(Actor actor, Point position)
        {
            return actor.MoveBy(position);
        }
    }
}

Now we can decouple the keyboard keys from the player’s move method by using the CommandSystem’s more generic call. Modify the UIManager’s CheckKeyboard method:

            // 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))
            {
                GameLoop.CommandManager.MoveActorBy(GameLoop.World.Player, new Point(0, -1));
                CenterOnActor(GameLoop.World.Player);
            }

            // 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))
            {
                GameLoop.CommandManager.MoveActorBy(GameLoop.World.Player, new Point(0, 1));
                CenterOnActor(GameLoop.World.Player);
            }

            // 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))
            {
                GameLoop.CommandManager.MoveActorBy(GameLoop.World.Player, new Point(-1, 0));
                CenterOnActor(GameLoop.World.Player);
            }

            // 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))
            {
                GameLoop.CommandManager.MoveActorBy(GameLoop.World.Player, new Point(1, 0));
                CenterOnActor(GameLoop.World.Player);
            }

Now we’re going to instance the CommandManager in the GameLoop:

// Managers
...
public static CommandManager CommandManager;
...

private static void Init()
{
...
            //Instantiate a new CommandManager
            CommandManager = new CommandManager();
...
}

Give your project a quick run. The player should move around just like before, but under the hood we have a much more generic – and flexible! – method for moving them. Now we can populate the Command System with more actions that allow for more diverse gameplay.

Undo and Redo

I like the idea of storing Undo and Redo information that allows a place to take back (or repeat) an action with a single keypress. Fortunately the Command Manager makes this fairly straightforward to do.

We’re going to store some action information in the Command Manager. It will keep track of what our last action was, so we can repeat it or undo it. Modify CommandManager:

    public class CommandManager
    {
        //stores the actor's last move action
        private Point _lastMoveActorPoint;
        private Actor _lastMoveActor;
...
}
        public bool MoveActorBy(Actor actor, Point position)
        {
            // store the actor's last move state
            _lastMoveActor = actor;
            _lastMoveActorPoint = position;

            return actor.MoveBy(position);
        }

Now let’s use that stored action information by creating Undo and Redo methods:

        // Redo last actor move
        public bool RedoMoveActorBy()
        {
// Make sure there is an actor available to redo first!
            if (_lastMoveActor != null)
            {
                return _lastMoveActor.MoveBy(_lastMoveActorPoint);
            }
            else
                return false;
        }

        // Undo last actor move
        // then clear the undo so it cannot be repeated
        public bool UndoMoveActorBy()
        {
            // Make sure there is an actor available to undo first!
            if (_lastMoveActor != null)
            {
                // reverse the directions of the last move
                _lastMoveActorPoint = new Point(-_lastMoveActorPoint.X, -_lastMoveActorPoint.Y);

                if (_lastMoveActor.MoveBy(_lastMoveActorPoint))
                {
                    _lastMoveActorPoint = new Point(0, 0);
                    return true;
                }
                else
                {
                    _lastMoveActorPoint = new Point(0, 0);
                    return false;
                }
            }
            return false;
        }

The RedoMoveActorBy method simply repeats the last actor’s move. It only checks if there is an actor stored to move to prevent the inevitable situation where there is (null) stored actor data.

The UndoMoveActorBy method checks for an available actor, then reverses the direction of the move using the – operator on the X and Y coordinates of the last move. The – operator should be read as -1 * _lastMoveActorPoint.X.

The if statement at the end of the method allows us to return the success (or failure) of the actor’s undo. This allows for the situation where the actor cannot return to their last position because a tile is no longer there, or creature has moved into that spot. We then clear the actor data from the undo storage, preventing the player from repeating the same undo multiple times.

Now implement these as keyboard commands in the UIManager:

       private void CheckKeyboard()
       {
       ...
            // Undo last command: Z
            if (SadConsole.Global.KeyboardState.IsKeyReleased(Microsoft.Xna.Framework.Input.Keys.Z))
            {
                GameLoop.CommandManager.UndoMoveActorBy();
                CenterOnActor(GameLoop.World.Player);
            }

            // Repeat last command: X
            if (SadConsole.Global.KeyboardState.IsKeyPressed(Microsoft.Xna.Framework.Input.Keys.X))
            {
                GameLoop.CommandManager.RedoMoveActorBy();
                CenterOnActor(GameLoop.World.Player);
            }
       ...
       }

Okay I should have done this ages ago. I’m incredibly sick of my @ player spawning smack dab in the middle of a dungeon wall. (If you’ve already fixed this problem, kudos to you – you’re now promoted to the rank of honorary tutorial writer. I cede my throne to you, your majesty.) Modify the World class’ CreatePlayer method:

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

            // Place the player on the first non-movement-blocking tile on the map
            for (int i = 0; i < CurrentMap.Tiles.Length; i++)
            {
                if (!CurrentMap.Tiles[i].IsBlockingMove)
                {
                    // Set the player's position to the index of the current map position
                    Player.Position = SadConsole.Helpers.GetPointFromIndex(i, CurrentMap.Width);
                }
            }

            // add the player to the global EntityManager's collection of Entities
            GameLoop.EntityManager.Entities.Add(Player);
        }

And finally a quick edit to the UIManager’s Init method to refocus the camera:

            // Start the game with the camera focused on the player
            CenterOnActor(GameLoop.World.Player);

Done and done. Let’s put this tutorial in the can and get thinking on doors, locks and keys for the next one.

Download the final source for tutorial 10 here.

Published on November 28, 2018