Creating the player entity + movement

Entities in SadConsole are basically small 1×1 Consoles.
They have their own rendering process, position and surface element.
But they additionally also have mouse and keyboard handling.

I am going to start by creating a basic abstract Actor baseline class that we can use for future entities that we are going to make.
This class will have some shared methods, for now I will add a basic movement method to it, but in the future it will maybe also contains methods for damaging / healing actors.

Lets start with defining our class. I will create a new “Actors” folder in my project root and add a new script into it called Actor.cs and inherit it from SadConsole’s Entity class.

internal abstract class Actor : Entity
{
    protected Actor(Color foreground, Color background, int glyph, int zIndex, int maxHealth) : base(foreground, background, glyph, zIndex)
    {
        MaxHealth = maxHealth;
        Health = MaxHealth;
    }

    public int MaxHealth { get; set; }
    public int Health { get; set; }
    public bool IsAlive => Health > 0;
}

Right now it contains only a mandatory constructor to construct the entity properly, where we have to provide the foreground, background, the glyph and also a zIndex.
The zIndex determines on which layer the entity is rendered. For example if two entities are on top of eachother, the one with the highest zIndex will be rendered first, then the next and so on..

Next lets add two shared methods in the class that our entities will most definitely use, movement methods!

public bool Move(int x, int y)
{
	var tilemap = ScreenContainer.Instance.World.Tilemap;

	if (!IsAlive) return false;

	// If the position is out of bounds, don't allow movement
	if (!tilemap.InBounds(x, y)) return false;

	// Don't allow movement for these cases
	var obstruction = tilemap[x, y].Obstruction;
	switch (obstruction)
	{
		case World.ObstructionType.FullyBlocked:
		case World.ObstructionType.MovementBlocked:
			return false;
	}

	// Set new position
	Position = new Point(x, y);
	return true;
}

public bool Move(Direction direction)
{
	var position = Position + direction;
	return Move(position.X, position.Y);
}

Here we have some checks included to make sure we cannot walk on specific Obstruction type tiles, not if the tile is out of the bounds of the tilemap, we cannot move if the entity is dead and we also probably don’t want actors to overlap eachother and walk on top of eachother, to prevent this we need to track their positions in the world.
For this we’ll add a seperate class called ActorManager.cs in the “Actors” folder.

internal sealed class ActorManager
{
	private readonly Dictionary<Point, Actor> _actors = [];
	public readonly EntityManager EntityComponent;

	public ActorManager()
	{
		EntityComponent = new()
		{
			SkipExistsChecks = true
		};
	}

	public bool Add(Actor actor)
	{
		if (ExistsAt(actor.Position)) return false;
		_actors[actor.Position] = actor;

		EntityComponent.Add(actor);

		return true;
	}

	public bool Remove(Actor actor)
	{
		if (!ExistsAt(actor.Position)) return false;
		_actors.Remove(actor.Position);

		EntityComponent.Remove(actor);

		return true;
	}

	public Actor Get(Point point)
	{
		if (_actors.TryGetValue(point, out Actor actor))
			return actor;
		return null;
	}

	public bool ExistsAt(Point point)
	{
		return _actors.ContainsKey(point);
	}

	public void Clear()
	{
		foreach (var actor in _actors.Values)
		{
			_ = Remove(actor);
		}
	}
}

We’ve added a few helper methods mainly for adding, removing, checking existance and clearing actors from the manager.
And also very important we have exposed an property called “EntityComponent” of type “EntityManager”. This component is very important as it tells the host ScreenSurface how to render the entities.

Before we hook this up, we are missing one important thing.
When an entity moves position, it also needs to be updated here in the ActorManager so we can keep track of their locations. One easy way to do this is by listening to the PositionChanged event of the actors.

private void UpdateActorPositionWithinManager(object sender, ValueChangedEventArgs<Point> e)
{
	if (e.OldValue == e.NewValue) return;
	var actor = (Actor)sender;

	// Remove from previous
	_actors.Remove(e.OldValue);

	// Check if the new position is occupied
	if (ExistsAt(e.NewValue))
	{
		throw new Exception($"Cannot move actor to {e.NewValue} another actor already exists there.");
	}

	_actors.Add(e.NewValue, actor);
}

This method will handle the tracking of the actor in the ActorManager by removing them from their last known location, and adding them back to their current new location.
Lets hook this method up by assigning it to the event listener of the actor when we add / remove actors.

public bool Add(Actor actor)
{
	if (ExistsAt(actor.Position)) return false;
	_actors[actor.Position] = actor;

	actor.PositionChanged += UpdateActorPositionWithinManager;
	EntityComponent.Add(actor);

	return true;
}

public bool Remove(Actor actor)
{
	if (!ExistsAt(actor.Position)) return false;
	_actors.Remove(actor.Position);

	actor.PositionChanged -= UpdateActorPositionWithinManager;
	EntityComponent.Remove(actor);

	return true;
}

When adding we register the event, when removing we deregister the event.
This way we can nicely keep track of the positions.

Now all that is left is to hookup the EntityComponent in the WorldScreen and create a player entity! In WorldScreen.cs add following field:

public readonly ActorManager ActorManager;

And in the bottom of the constructor of the WorldScreen add:

// Add the entity component to the world screen, so we can track entities
ActorManager = new ActorManager();
SadComponents.Add(ActorManager.EntityComponent);

Now we can also validate if there is an entity in the spot we are moving, by adjusting the Move method in the Actor.cs class like so:

public bool Move(int x, int y)
{
	var tilemap = ScreenContainer.Instance.World.Tilemap;
	var actorManager = ScreenContainer.Instance.World.ActorManager;

	if (!IsAlive) return false;

	// If the position is out of bounds, don't allow movement
	if (!tilemap.InBounds(x, y)) return false;

	// If another actor already exists at the location, don't allow movement
	if (actorManager.ExistsAt((x, y))) return false;

	// Don't allow movement for these cases
	var obstruction = tilemap[x, y].Obstruction;
	switch (obstruction)
	{
		case World.ObstructionType.FullyBlocked:
		case World.ObstructionType.MovementBlocked:
			return false;
	}

	// Set new position
	Position = new Point(x, y);
	return true;
}

Now lets create our Player.cs class also in the “Actors” folder. This time we inherit from our base class “Actor”

internal class Player : Actor
{
	public Player() : base(Color.White, Color.Transparent, '@', zIndex: int.MaxValue, maxHealth: 100)
	{
		IsFocused = true;
	}

	private readonly Dictionary<Keys, Direction> _playerMovements = new()
	{
		{Keys.W, Direction.Up},
		{Keys.A, Direction.Left},
		{Keys.S, Direction.Down},
		{Keys.D, Direction.Right}
	};

	public override bool ProcessKeyboard(Keyboard keyboard)
	{
		if (!UseKeyboard) return false;
		var moved = false;
		foreach (var kvp in _playerMovements)
		{
			if (keyboard.IsKeyPressed(kvp.Key))
			{
				var moveDirection = kvp.Value;
				moved = Move(moveDirection);
				break;
			}
		}
		return base.ProcessKeyboard(keyboard) || moved;
	}
}

Our player entity has some extra code to handle input, we override HandleKeyboard to retrieve keyboard input when the player entity is focused.

Here we check the default WASD keys and if input is received we translate the direction to move towards by calling the Move method of our base class with the given direction from our WASD mapping.

Our player is initializing with the following

  • Foreground: white
  • Background: transparent
  • Glyph: ‘@’ (a little dwarf in our font)
  • zIndex: int.MaxValue (our player should always render on top of any other entity)
  • MaxHealth: 100

All thats left is to create the player and take the game for a spin!
Lets add the player to the WorldScreen in a seperate method and call that method from our Program.cs GameStart method.

In WorldScreen.cs expose the Player property:

public Player Player { get; private set; }

In order to spawn the player in one of the rooms, we need access to the created rooms.
Thankfully we added an “out” parameter to return those rooms in the Generate method,
lets tweak that a little bit and store it globally in the WorldScreen class.

In the GenerateDungeon.cs in the GenerateMethod rename the List<Rectangle> type to IReadOnlyList<Rectangle> to make it clear we should not modify this list after the out.

public static void Generate(Tilemap tilemap, int maxRooms, int minRoomSize, int maxRoomSize, out IReadOnlyList<Rectangle> dungeonRooms)

And adjust the code a little at the start of the Generate method to make it compatible:

From:

rooms = [];

To:

var rooms = new List<Rectangle>();
dungeonRooms = rooms;

Now in WorldScreen.cs add a global private field to store the rooms:

private IReadOnlyList<Rectangle> _dungeonRooms;

And make sure we out the parameter into this new global private field in our Generate method in the WorldScreen.cs

public void Generate()
{
	DungeonGenerator.Generate(Tilemap, 10, 4, 10, out _dungeonRooms);
	if (_dungeonRooms.Count == 0)
		throw new Exception("Faulty dungeon generation, no rooms!");
}

Next we add our method to create the new player entity in the WorldScreen.cs

public void CreatePlayer()
{
	Player = new Player { Position = _dungeonRooms[0].Center };
	ActorManager.Add(Player);
}

We will just spawn the player in the center of the first room that was created.
It is important that the player is added to the ActorManager, otherwise it will not render!

Now in Program.cs lets call our new method to create the player and give the game a spin.

private static void GameStart(object sender, GameHost e)
{
	ScreenContainer.Instance.World.Generate();
	ScreenContainer.Instance.World.CreatePlayer();
}

Behold our moveable player entity:

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