Tutorial Part 9: Message Log (SadConsole v8)
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:
Afterwards, modify the GameLoop class namespace definitions so it knows the namespace that the UIManager is now located:
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:
//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.ScrollingConsole _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.WindowTheme.FillStyle.Background = DefaultBackground;
_lines = new Queue<string>();
CanDrag = true;
Title = title.Align(HorizontalAlignment.Center, Width);
// add the message console, reposition, and add it to the window
_messageConsole = new SadConsole.ScrollingConsole(width - 1, height - 1);
_messageConsole.Position = new Point(1, 1);
First, we’re defining MessageLogWindow as a type of SadConsole.Window. This allows it to automagically inherit wonderful properties like .CanDrag 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)
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)
// when exceeding the max number of lines remove the oldest one
if (_lines.Count > _maxLines)
// Move the cursor to the last line and print the message.
_messageConsole.Cursor.Position = new Point(1, _lines.Count);
_messageConsole.Cursor.Print(message + "\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");
MessageLog.Position = new Point(0, GameLoop.GameHeight / 2);
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, 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:
- _messageConsole (displays the message data)
- _messageScrollBar (handles the scrolling)
So: one console for message data, one scrollbar for moving it. First modify MessageLogWindow’s class definition by adding a few lines to it:
//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.ScrollingConsole(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 = new SadConsole.Controls.ScrollBar(SadConsole.Orientation.Vertical, height - _windowBorderThickness);
_messageScrollBar.Position = new Point(_messageConsole.Width + 1, _messageConsole.Position.X);
_messageScrollBar.IsEnabled = false;
_messageScrollBar.ValueChanged += MessageScrollBar_ValueChanged;
// enable mouse input
UseMouse = true;
// Add the child consoles to the window
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. SadConsole has a handy Orientation enum that lets us position the scrollbar on the window, which we pass as the first parameter.
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!
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. And finally, we add _messageConsole as a child of the Window, otherwise it won’t display.
Now let’s add some code to process the scrollbar behaviour when it’s triggered. Create a new method called MessageScrollBar_ValueChanged:
// 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:
// Custom Update method which allows for a vertical scrollbar
public override void Update(TimeSpan 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;
// Determines the scrollbar's max vertical position
// Thanks @Kaev for simplifying this math!
_messageScrollBar.Maximum = _scrollBarCurrentPosition - _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 February 27, 2019