Tutorial Part 9: Message Log

Important note: These tutorials were written for SadConsole version 7.x. The version 8.x tutorials are here.

Let’s make some more use out of our UI system by creating a traditional RL Message Log window that will keep track of our player’s actions. We’ll make use of SadConsole’s interface enhancements like scrolling and windowing along the way.

If you’re starting to feel anal about the structure of your project, now is a good time to re-organize your classes a bit. I’ve created a new folder called UI which represents the SadConsoleRLTutorial.UI namespace, and it will house the UIManager and all of its subsidiary windows.

If you’re grouping things my way, that means modifying the UIManager class definition to suit the new namespace it lives in:

namespace SadConsoleRLTutorial.UI

Afterwards, modify the GameLoop class namespace definitions so it knows the namespace that the UIManager is now located:

using SadConsoleRLTutorial.UI;

MessageLogWindow Class

Create a new class called MessageLogWindow in the UI folder. If you’re using Visual Studio, it will auto-populate the namespace in the class definition. Now let’s build out the class:

using System.Collections.Generic;
using SadConsole;
using System;
using Microsoft.Xna.Framework;

namespace SadConsoleRLTutorial.UI
{
    //A scrollable window that displays messages
    //using a FIFO (first-in-first-out) Queue data structure
    public class MessageLogWindow : Window
    {
        //max number of lines to store in message log
        private static readonly int _maxLines = 100;

        // a Queue works using a FIFO structure, where the first line added
        // is the first line removed when we exceed the max number of lines
        private readonly Queue<string> _lines;

        // the messageConsole displays the active messages
        private SadConsole.Console _messageConsole;

        // Create a new window with the title centered
        // the window is draggable by default
        public MessageLogWindow(int width, int height, string title) : base(width, height)
        {
            // Ensure that the window background is the correct colour
            Theme.FillStyle.Background = DefaultBackground;
            _lines = new Queue<string>();
            Dragable = true;
            Title = title.Align(HorizontalAlignment.Center, Width);

            // add the message console, reposition, and add it to the window
            _messageConsole = new SadConsole.Console(width - 1, height - 1);
            _messageConsole.Position = new Point(1, 1);
            Children.Add(_messageConsole);
        }
}
}

First, we’re defining MessageLogWindow as a type of SadConsole.Window. This allows it to automagically inherit wonderful properties like .Dragable and .Title, as well as take care of the underlying UI processing.

The Console _messageConsole will display our actual message data. The window itself acts more like a container. Why are we creating this extra layer of confusion by nesting a console inside of the window? That will become apparent later, when we want to add a scrollbar!

The Theme.FillStyle.Background property allows us to change the background colour of the window. You’ll probably find that the default blue background SadConsole uses is a little harsh on the eyeballs, so you can change it to any colour using a (Microsoft.Xna.Framework) Color. For now, it is set to the default background colour.

Then we’re creating a Queue, which is a wonderful C# data structure that maintains a collection of objects in a “first-in, first-out” (FIFO) relationship. FIFO allows the MessageLog queue to destroy the earliest messages when its size exceeds _maxLines.

The last few steps instantiate the _messageConsole and reposition it on the screen. Note that we’re making the console smaller than the window so it will not overlap with the window borders.

Now we need to make sure that SadConsole knows to draw this window on every draw cycle. This is one of those critical SadConsole-specific things you need to do every time you inherit one of its console-based classes (e.g. SadConsole.Window):

        //Remember to draw the window!
public override void Draw(TimeSpan drawTime)
        {
            base.Draw(drawTime);
        }

And now let’s implement a new (public) Add method that allows us to add messages to the queue:

        //add a line to the queue of messages
        public void Add(string message)
        {
            _lines.Enqueue(message);
            // when exceeding the max number of lines remove the oldest one
            if (_lines.Count > _maxLines)
            {
                _lines.Dequeue();
            }
            Cursor.Print(message + "\r\n");
        }

Cursor.Position and Cursor.Print are specific to Console classes. The Cursor acts like a traditional command line interface cursor, which can be positioned anywhere in the console via its Position property. The Cursor.Print method translates raw string data into readable lines, split line-by-line to prevent words from breaking apart at the ends of lines. It also respects the “\n\r” escape sequences which allow for new lines and carriage returns. In this case, we’re appending a new line after each message is printed.

It’s time to test out our basic MessageWindow. Add a MessageLog to the UIManager’s class definition:

public MessageLogWindow MessageLog;

Modify the UIManager’s Init method with a few lines of test code. Add these lines to the end of the method:

            MessageLog = new MessageLogWindow(GameLoop.GameWidth / 2, GameLoop.GameHeight / 2, "Message Log");
            Children.Add(MessageLog);
            MessageLog.Show();
            MessageLog.Position = new Point(0, GameLoop.GameHeight / 2);

            MessageLog.Add("Testing 123");
            MessageLog.Add("Testing 1224");
            MessageLog.Add("Testing 123");
            MessageLog.Add("Testing 12543");
            MessageLog.Add("Testing 123");
            MessageLog.Add("Testing 1253");
            MessageLog.Add("Testing 1212");
            MessageLog.Add("Testing 1");
            MessageLog.Add("Testing");
            MessageLog.Add("Testing 122");
            MessageLog.Add("Testing 51");
            MessageLog.Add("Testing");
            MessageLog.Add("Testing 162");
            MessageLog.Add("Testing 16");
            MessageLog.Add("Testing Last");

Now you have a working MessageLog. It would be a lot more useful though, if you could scroll back through its buffer to read older messages. Let’s do that now.

Adding a Scrollbar to MessageLogWindow

For a scrolling console, I’m roughly following the directions Thraka wrote here. The idea is to expand _messageConsole’s height property to _maxLines, and then using its ViewPort to selectively view portions of it. That’s how we’ll see the scrollback buffer.

The overall architecture of the Window will become like this, where each of these bullet points is a ‘has a’ relationship:

So: one console for message data, one scrollbar for moving it. First modify MessageLogWindow’s class definition:

        //scrollbar for message console
        private SadConsole.Controls.ScrollBar _messageScrollBar;

        //Track the current position of the scrollbar
        private int _scrollBarCurrentPosition;

        // account for the thickness of the window border to prevent UI element spillover
        private int _windowBorderThickness = 2;

Now modify the MessageLogWindow’s constructor and replace _messageConsole’s instantiation code:

                        // add the message console, reposition, enable the viewport, and add it to the window
            _messageConsole = new SadConsole.Console(width - _windowBorderThickness, _maxLines);
            _messageConsole.Position = new Point(1, 1);
            _messageConsole.ViewPort = new Rectangle(0, 0, width - 1, height - _windowBorderThickness);

            // create a scrollbar and attach it to an event handler, then add it to the Window
            _messageScrollBar = SadConsole.Controls.ScrollBar.Create(SadConsole.Orientation.Vertical, height - _windowBorderThickness);
            _messageScrollBar.Position = new Point(_messageConsole.Width + 1, _messageConsole.Position.X);
            _messageScrollBar.IsEnabled = false;
            _messageScrollBar.ValueChanged += MessageScrollBar_ValueChanged;
            Add(_messageScrollBar);

            // enable mouse input
            UseMouse = true;

Let’s walk through the code dump. We’re resizing the _messageConsole to make room for the scrollbar. Subtracting the _windowBorderThickness from the width makes sure that the _messageConsole is contained within the Window. That’s something you’ll see throughout these projects.

Then we’re creating the _messageScrollBar. Notice that instead of using a constructor, we’re using the Controls.ScrollBar.Create method. I’ve had a brief chat with Thraka about this style of instantiation, and it may change in the future… since it seems to break with our coding conventions in SadConsole.

We reposition _messageScrollBar to the right side of the window, letting it overlap the window edge. You can skooch it inside of the window, but you risk letting it overlap with the _messageConsole. If that happens, the scrollbar won’t accept mouse control (because the _messageConsole will steal the mouse from it).

Until now, we haven’t had to deal with Events. An Event allows a class to notify an object (within the class or in another class) that something interesting has happened. In this case, we’re interested in the ValueChanged EventHandler built into the ScrollBar. The += operator is used to subscribe to events. The right side of the operator describes the method – in this case MessageScrolBar_ValueChanged – that will be called when the event is triggered.

Afterwards, we Add the _messageScrollBar to the Window. Note: we use Add, not Children.Add! 

Finally, we set UseMouse to true, to tell the Window to begin processing Mouse events. That is a critical step, because without it, it won’t trigger any mouse events.

Now let’s add some code to process the scrollbar behaviour when it’s triggered:

        // Controls the position of the messagelog viewport
        // based on the scrollbar position using an event handler
        void MessageScrollBar_ValueChanged(object sender, EventArgs e)
        {
            _messageConsole.ViewPort = new Rectangle(0, _messageScrollBar.Value + _windowBorderThickness, _messageConsole.Width, _messageConsole.ViewPort.Height );
        }

En anglais: every time the scrollbar’s value changes, we update the viewport position.

Now for the biggest code hunk – we’re going to override the Update method so we can do some math on the scrollbar. This code was directly taken from the SadConsole documentation:

        // copied directly from http://sadconsole.com/docs/make-a-scrolling-console.html
        // and modified to suit this class variable names
        public override void Update(TimeSpan time)
        {
            base.Update(time);

            // Ensure that the scrollbar tracks the current position of the _messageConsole.
            if (_messageConsole.TimesShiftedUp != 0 | _messageConsole.Cursor.Position.Y >= _messageConsole.ViewPort.Height + _scrollBarCurrentPosition)
            {
                //enable the scrollbar once the messagelog has filled up with enough text to warrant scrolling
                _messageScrollBar.IsEnabled = true;

                // Make sure we've never scrolled the entire size of the buffer
                if (_scrollBarCurrentPosition < _messageConsole.Height - _messageConsole.ViewPort.Height)
                    // Record how much we've scrolled to enable how far back the bar can see
                    _scrollBarCurrentPosition += _messageConsole.TimesShiftedUp != 0 ? _messageConsole.TimesShiftedUp : 1;

                _messageScrollBar.Maximum = (_messageConsole.Height + _scrollBarCurrentPosition) - _messageConsole.Height - _windowBorderThickness;

                // This will follow the cursor since we move the render area in the event.
                _messageScrollBar.Value = _scrollBarCurrentPosition;

                // Reset the shift amount.
                _messageConsole.TimesShiftedUp = 0;
            }
        }

Lots of stuff going on in this hunk. We perform a base.Update first, to ensure SadConsole updates this window. The first if statement determines whether the console has filled with enough data to warrant scrolling. Afterwards, we do some math to determine the proper position of the scrollbar in relation to the _messageConsole. The TimesShiftedUp is a built-in SadConsole.Console field.

And that’s it. Run the project, and you should have a scrollable window with a bunch of test data in it. We’ll put this window to good use in the future when we track player behaviour.

Download the final version of the source code for tutorial #9 here.

Published on November 22, 2018