Skip to content

Create a SadConsole game: Part 3 - Input

In this part of the tutorial, you’ll explore how input works in SadConsole. You’ll learn how to create a basic map with a player that you can use the mouse and keyboard to interact with.

Input consists of both keyboard and mouse processing. SadConsole doesn’t provide any input processing for gamepads. However, MonoGame, which SadConsole is built on, provides gamepad and joystick processing.

Previous articles in this tutorial:

To start this part of the tutorial, you need a SadConsole project created by following the previous tutorials in this series.

First, create a new root object. This object represents a game scene that includes the map and a status pane. The player character will move around the map. Add a new class to your project (check your IDE’s documentation if needed) named RootScreen.cs. Replace the content with the following:

namespace SadConsoleGame;
internal class RootScreen: ScreenObject
{
public RootScreen()
{
}
}

Next, modify the startup code of the game to indicate that the RootScreen type is the starting object. This simplifies setup and removes the requirement of the Startup method you used in the previous tutorial. In the Program.cs file, change the code to the following:

using SadConsole.Configuration;
using SadConsoleGame;
Settings.WindowTitle = "My SadConsole Game";
Builder
.GetBuilder()
.SetWindowSizeInCells(120, 38)
.ConfigureFonts(true)
.SetStartingScreen<RootScreen>()
.IsStartingScreenFocused(true)
.Run();

Notice the differences in this code from the previous startup code:

  • The project namespace was imported with using SadConsoleGame;.

  • The screen size was changed to 120, 38.

  • .SetStartingScreen<RootScreen>() replaces .OnStart(Startup), which used the Startup method as the “starting” code.

  • IsStartingScreenFocused(true) is called, which automatically sets the starting screen to focused.

  • The Startup() method was removed.

The SetStartingScreen configuration method designates an object as the starting object. It automatically assigns a new instance of that object to the Game.Instance.Screen property. This simplifies game setup. In this case, the newly created RootScreen class is the container for the current game screen. All the startup code goes into that object.

Next, add a basic map surface to the screen.

  1. Open the RootScreen.cs file.

  2. Add a new ScreenSurface field to the class named _map.

    internal class RootScreen: ScreenObject
    {
    private ScreenSurface _map;
    public RootScreen()
    {
    }
    }

    This surface represents the map data.

  3. Modify the constructor of RootScreen to create an instance of the map:

    internal class RootScreen: ScreenObject
    {
    private ScreenSurface _map;
    public RootScreen()
    {
    _map = new ScreenSurface(Game.Instance.ScreenCellsX, Game.Instance.ScreenCellsY - 5);
    _map.UseMouse = false;
    Children.Add(_map);
    }
    }

    Notice two things about the _map variable:

    • It’s a ScreenSurface and not a Console.

      The map isn’t going to use a cursor object to print and collect input from the user, so you don’t need a full console object. Most of the time when you create objects in SadConsole that don’t need a cursor you’ll probably just use a ScreenSurface.

    • The width and height of the object is set to the Game.Instance.ScreenCellsX and Game.Instance.ScreenCellsY variables, respectively.

      ScreenCellsX/Y represent how many default-font-sized cells fit on the screen. These are from the width and height values passed to the Game.Create method at the start of the game. This makes it easy to create a surface that fills the window. For the Y height though, you’re trimming off 5 from the bottom. This leaves some space at the bottom of the screen to add a status console later.

    Next, add some background to the map. Instead of using the FillWithRandomGarbage method from the previous tutorial, draw a gradient. The background doesn’t really represent anything, but it makes it easier to see your objects and demonstrate some key concepts.

  4. Add the FillBackground method to the class to fill the map surface with a gradient:

    private void FillBackground()
    {
    Color[] colors = new[] { Color.LightGreen, Color.Coral, Color.CornflowerBlue, Color.DarkGreen };
    float[] colorStops = new[] { 0f, 0.35f, 0.75f, 1f };
    Algorithms.GradientFill(_map.FontSize,
    _map.Surface.Area.Center,
    _map.Surface.Width / 3,
    45,
    _map.Surface.Area,
    new Gradient(colors, colorStops),
    (x, y, color) => _map.Surface[x, y].Background = color);
    }

    The gradient algorithm calculates each X,Y of an area and provides a color that maps to it. What you do with that information is up to you. In this example, the code colors the background of each cell based on the calculated value. The number of colors in the gradient and the number of steps must match for the gradient to work. The following diagram might help you understand how this is put together, but it’s not really important at this point:

    The FontSize used helps stretch the gradient evenly over the surface. The default font used by SadConsole has a 2:1 ratio in width to height. The following image illustrates how the gradient is laid out, it uses a smaller window for the purpose of the illustration:

    SadConsole gradient diagram

  5. Call the FillBackground method from the RootScreen constructor:

    public RootScreen()
    {
    _map = new ScreenSurface(Game.Instance.ScreenCellsX, Game.Instance.ScreenCellsY - 5);
    _map.UseMouse = false;
    FillBackground();
    Children.Add(_map);
    }

Now that there’s a background you can place objects on, create and place a player object.

The game will have a player-controlled object, along with other non-player characters (NPC) on the map. Next, create an object named GameObject to represent these objects.

  1. Add a new class to your project named GameObject.cs.

  2. Paste the following code into the file:

    namespace SadConsoleGame;
    internal class GameObject
    {
    public Point Position { get; private set; }
    public ColoredGlyph Appearance { get; set; }
    public GameObject(ColoredGlyph appearance, Point position, IScreenSurface hostingSurface)
    {
    Appearance = appearance;
    Position = position;
    DrawGameObject(hostingSurface);
    }
    private void DrawGameObject(IScreenSurface screenSurface)
    {
    Appearance.CopyAppearanceTo(screenSurface.Surface[Position]);
    screenSurface.IsDirty = true;
    }
    }

Examine this code. The class has two properties:

  • Position

    This is a Point type from the SadRogue.Primitives namespace. It represents a location, and in this case, the position of the game object on the map.

  • Appearance

    The appearance of the game object is a ColoredGlyph, which represents a foreground color, background color, and glyph character combination.

The constructor of the object requires the appearance, position, and a surface. Why provide the surface? To draw the object! When the object is created, the code instantly draws it to the surface by calling the DrawGameObject method from the constructor. DrawGameObject uses the Appearance.CopyAppearanceTo method to draw the object. This method copies all the settings in the appearance to a specific cell on the surface. Lastly, after copying the appearance of the object, the surface is marked as dirty so that it’s redrawn.

Most of the time when you draw on a surface using methods like SetGlyph, SetForeground, or Print, those methods mark the surface as dirty for you. In the preceding code, you’re editing a cell of the surface directly by using the CopyAppearanceTo method, which doesn’t automatically mark the surface as dirty.

Add another method to the GameObject class named Move. This method handles repositioning the object and then drawing it on the surface.

public void Move(Point newPosition, IScreenSurface screenSurface)
{
Position = newPosition;
DrawGameObject(screenSurface);
}

Now that the GameObject type has been created, use it to represent the player. Add a player object to the RootScreen class.

  1. Open the RootScreen.cs file.

  2. Add a new field to the class to represent the controlled player object:

    internal class RootScreen: ScreenObject
    {
    private ScreenSurface _map;
    private GameObject _controlledObject;
    // ... other code ...
  3. In the RootScreen constructor, create the object to represent the player:

    public RootScreen()
    {
    _map = new ScreenSurface(Game.Instance.ScreenCellsX, Game.Instance.ScreenCellsY - 5);
    _map.UseMouse = false;
    FillBackground();
    Children.Add(_map);
    _controlledObject = new GameObject(new ColoredGlyph(Color.White, Color.Black, 2), _map.Surface.Area.Center, _map);
    }

    This creates a black and white glyph “2”, the smiley face glyph. It’s placed at the center of the map surface. Run your project and you’ll see the smiley face!

Smiley face glyph 2

Now that you have a player on the screen, you’ll need to get the keyboard working.

The previous tutorial article had you working with the Cursor object. The console had focus, and the cursor accepted input. You typed input onto the screen. While the cursor is great for getting input from the user and typing it onto the screen, you don’t typically use it for game interaction. Instead, you can interpret the keyboard as it’s pressed, and move the game object.

Open the RootScreen.cs file.

SadConsole sends keyboard input to the focused object. To process the keyboard in your scene, you need to focus the object and override the ProcessKeyboard method.

The RootScreen object is already focused because it’s the starting screen. If you want to change which object directly receives keyboard input, focus it. Things like popup windows focus themselves for keyboard input, and then when hidden, restore focus to the previously focused object.

Override the ProcessKeyboard method in the RootScreen class:

  1. Import the SadConsole.Input namespace at the top of the code file:

    using SadConsole.Input;
  2. Add the following code after the FillBackground method:

    public override bool ProcessKeyboard(Keyboard keyboard)
    {
    return base.ProcessKeyboard(keyboard);
    }

    SadConsole calls this method every game frame on the focused object. The base.ProcessKeyboard call allows the RootScreen’s base class to do its normal keyboard processing, which processes any keyboard-based components added to the object. You’ll learn more about components later.

Next, update the keyboard code to move the player-controlled object around. The player object has a Position property. Check if a direction key is pressed, and then change the Position property to move the object.

The keyboard parameter passed to the ProcessKeyboard method contains the current state of the keyboard and any changes to it from the previous game frame. This parameter is used to test if a key is pressed, the state of a key (up or down), or if a key was just released.

Change the ProcessKeyboard method to the following code:

public override bool ProcessKeyboard(Keyboard keyboard)
{
bool handled = false;
if (keyboard.IsKeyPressed(Keys.Up))
{
_controlledObject.Move(_controlledObject.Position + Direction.Up, _map);
handled = true;
}
else if (keyboard.IsKeyPressed(Keys.Down))
{
_controlledObject.Move(_controlledObject.Position + Direction.Down, _map);
handled = true;
}
if (keyboard.IsKeyPressed(Keys.Left))
{
_controlledObject.Move(_controlledObject.Position + Direction.Left, _map);
handled = true;
}
else if (keyboard.IsKeyPressed(Keys.Right))
{
_controlledObject.Move(_controlledObject.Position + Direction.Right, _map);
handled = true;
}
return handled;
}

Here’s a look at this code:

  • The handled variable indicates that the code processed the keyboard, and this value is returned at the end of the method. This value is important when you want to prevent other objects from processing the keyboard after you did.

  • All four directional keys are checked with the keyboard.IsKeyPressed method.

Now run the game and try moving the character around the screen:

Moving the player

Notice that you can move the character around the map, but the character is leaving a trail behind it. When the character moves, the code copies the appearance of the character to the new position on the surface directly, which changes what’s on the surface. It doesn’t restore what used to be on the surface.

You can solve this problem in several ways, but for this tutorial, use a simple approach. When a game object is moved to a position on the map, store the target cell’s appearance in the game object. When the game object is moved to another position, restore previous position’s cell appearance.

For this code, edit the GameObject.cs file.

  1. Open the GameObject.cs file.

  2. Add a private variable named _mapAppearance to the class, below the existing properties. This variable stores the map’s cell appearance before the character moves to that cell.

    internal class GameObject
    {
    private ColoredGlyph _mapAppearance = new ColoredGlyph();
    public Point Position { get; private set; }
    // ... other code ...
  3. In the constructor, the game object is initially placed on the map. Copy the cell of the map before drawing the object:

    public GameObject(ColoredGlyph appearance, Point position, IScreenSurface hostingSurface)
    {
    Appearance = appearance;
    Position = position;
    // Store the map cell
    hostingSurface.Surface[position].CopyAppearanceTo(_mapAppearance);
    // draw the object
    DrawGameObject(hostingSurface);
    }
  4. Modify the Move method to restore the old cell and store the new position cell of the map:

    public void Move(Point newPosition, IScreenSurface screenSurface)
    {
    // Restore the old cell
    _mapAppearance.CopyAppearanceTo(screenSurface.Surface[Position]);
    // Store the map cell of the new position
    screenSurface.Surface[newPosition].CopyAppearanceTo(_mapAppearance);
    Position = newPosition;
    DrawGameObject(screenSurface);
    }

Run the game and you’ll see that there’s no trail left behind the player:

Moving the player

Now you understand a bit more about how input works, and you have a basic map and game object. These are just the basics; there’s a lot more you can do with input, but you’ll explore that when you work with the user interface controls provided by SadConsole. The next part of the tutorial series explores how to create more map objects and display feedback to the user.