Lesson 3.7: Displaying Game Messages

As the player interacts with the game world (including combat), we need to display messages in the game screen: combat messages, damage done to creatures, healing, and more. We are going to do this by having the GameSession view model expose the Messages property. The Messages property is then data bound to the UI.

DisplayMessage Model Class

We want to display a list of rich textual information, so we are going to define the DisplayMessage class to hold that text data, in the SimpleRPG.Game.Engine and Models folder.

using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.Models
{
    public class DisplayMessage
    {
        public DisplayMessage(string title, IList<string> messages)
        {
            Title = title;
            Messages = messages;
        }

        public string Title { get; } = string.Empty;

        public IList<string> Messages { get; }
    }
}

The DisplayMessage has a title and a list of strings. We will build a nice UI to show these messages and let the player know what’s happening in the game.

Now, we need to expose a list of these display messages. First, add the Messages property to the IGameSession.

using SimpleRPG.Game.Engine.Models;
using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.ViewModels
{
    public interface IGameSession
    {
        Player CurrentPlayer { get; }

        Location CurrentLocation { get; }

        Monster? CurrentMonster { get; }

        bool HasMonster { get; }

        MovementUnit Movement { get; }

        IList<DisplayMessage> Messages { get; }

        void OnLocationChanged(Location newLocation);
    }
}

Next, we will update the GameSession class:

using SimpleRPG.Game.Engine.Factories;
using SimpleRPG.Game.Engine.Models;
using System.Collections.Generic;
using System.Linq;

namespace SimpleRPG.Game.Engine.ViewModels
{
    public class GameSession : IGameSession
    {
        private readonly World _currentWorld;
        private readonly int _maximumMessagesCount = 100;

        public Player CurrentPlayer { get; private set; }

        public Location CurrentLocation { get; private set; }

        public Monster? CurrentMonster { get; private set; }

        public bool HasMonster => CurrentMonster != null;

        public MovementUnit Movement { get; private set; }

        public IList<DisplayMessage> Messages { get; } = new List<DisplayMessage>();

        public GameSession(int maxMessageCount)
            : this()
        {
            _maximumMessagesCount = maxMessageCount;
        }

        public GameSession()
        {
            CurrentPlayer = new Player
            {
                Name = "DarthPedro",
                CharacterClass = "Fighter",
                CurrentHitPoints = 10,
                MaximumHitPoints = 10,
                Gold = 1000,
                ExperiencePoints = 0,
                Level = 1
            };

            _currentWorld = WorldFactory.CreateWorld();

            Movement = new MovementUnit(_currentWorld);
            CurrentLocation = Movement.CurrentLocation;
            GetMonsterAtCurrentLocation();

            CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItem(1001));
        }

        public void OnLocationChanged(Location newLocation)
        {
            CurrentLocation = newLocation;
            GetMonsterAtCurrentLocation();
        }

        private void GetMonsterAtCurrentLocation()
        {
            CurrentMonster = CurrentLocation.HasMonster() ? CurrentLocation.GetMonster() : null;

            if (CurrentMonster != null)
            {
                AddDisplayMessage("Monster Encountered:", $"You see a {CurrentMonster.Name} here!");
            }
        }

        private void AddDisplayMessage(string title, string message) =>
            AddDisplayMessage(title, new List<string> { message });

        private void AddDisplayMessage(string title, IList<string> messages)
        {
            var message = new DisplayMessage(title, messages);
            this.Messages.Insert(0, message);

            if (Messages.Count > _maximumMessagesCount)
            {
                Messages.Remove(Messages.Last());
            }
        }
    }
}

The GameSession class:

  1. Implements the Messages property as a List of DisplayMessages (line 23).
  2. Rather than directly adding messages to the list, we created a couple of AddDisplayMessage methods (line 72):
    • The method creates the DisplayMessage.
    • We insert the message to the top of the list so that it is displayed at the top of the UI as well.
    • It also limits how long the list can grow because we don’t want the list to grow unlimitedly.
  3. We also have an overloaded constructor that allows callers to set the list limit (line 25).
  4. Update the GetMonsterAtCurrentLocation method to display a message if there is a Monster at the current location (line 63).

After these changes when we navigate to a location that contains a monster, the following message is created:

Monster Encountered:
You see a Snake here!

Display the Messages

To display our messages, we are going to create a DisplayMessageListView component in the SimpleRPG.Game project and Shared folder.

<h5>Game Messages:</h5>

@if (Messages != null && Messages.Any())
{
    <div class="table-wrapper-scroll-y my-custom-scrollbar" style="height: 53vh">
    @foreach (var msg in Messages)
    {
        <div><strong>@msg.Title</strong></div>
        foreach (var m in msg.Messages)
        {
            <div>@m</div>
        }
        <hr />
    }
    </div>
}

@code {
    [Parameter]
    public IEnumerable<DisplayMessage> Messages { get; set; } = Array.Empty<DisplayMessage>();
}

This component should look very familiar by now. We created similar ones for Location, Monster, and Inventory. Let’s review the details:

  • It takes a Messages parameter to allow callers to provide the list to show (defaults to an empty list).
  • If there are no messages, then only the heading is shown.
  • Then we create a <div> to wrap the list and provide a scrollbar if one is necessary (similar to how we implemented the PlayerTab component in Lesson 3.2).
  • Next for each DisplayMessage in the list, we show the its title, every line in the message (as its own <div>), and a horizontal divider.

This gives us a nice compact UI for the list of messages. We will be processing a lot of messages as the player goes through the game.

Finally, we need to add the DisplayMessageListView to the middle of our game screen by updating the GameScreen.razor file:

@page "/"
@inject IGameSession ViewModel

<Row Margin="Margin.Is0" Style="height: 5vh; min-height: 32px">
    <Column ColumnSize="ColumnSize.Is12" Style="background-color: lightgrey">
        <Heading Size="HeadingSize.Is3">Simple RPG</Heading>
    </Column>
</Row>
<Row Margin="Margin.Is0" Style="height: 60vh">
    <Column ColumnSize="ColumnSize.Is3.OnWidescreen.Is12" Style="background-color: aquamarine">
        <PlayerComponent Player="@ViewModel.CurrentPlayer" />
    </Column>
    <Column ColumnSize="ColumnSize.Is9.OnWidescreen.Is12" Style="background-color: beige">
        <Row Margin="Margin.Is2.OnY">
            <Column ColumnSize="ColumnSize.Is8.OnWidescreen.Is12">
                <DisplayMessageListView Messages="@ViewModel.Messages" />
            </Column>
            <Column ColumnSize="ColumnSize.Is4.OnWidescreen.Is12">
                <LocationComponent Location="@ViewModel.CurrentLocation" />
                <MonsterComponent Monster="@ViewModel.CurrentMonster" />
            </Column>
        </Row>
    </Column>
</Row>
<Row Margin="Margin.Is0" Style="height: 33vh">
    <Column ColumnSize="ColumnSize.Is3.OnWidescreen.Is12" Padding="Padding.Is2.OnY"
            Style="background-color: burlywood">
        <PlayerTabs Player="@ViewModel.CurrentPlayer" />
    </Column>
    <Column ColumnSize="ColumnSize.Is9.OnWidescreen.Is12" Style="background-color: lavender">
        <Row Margin="Margin.Is2.OnY">
            <Column ColumnSize="ColumnSize.Is8.OnWidescreen.Is12">
                Combat Controls
            </Column>
            <Column ColumnSize="ColumnSize.Is4.OnWidescreen.Is12">
                <MovementComponent Movement="@ViewModel.Movement" LocationChanged="@ViewModel.OnLocationChanged" />
            </Column>
        </Row>
    </Column>
</Row>

We do this by adding DisplayMessageListView in the main, central area of the screen. And we data bind the GameSession.Messages property to the component’s Messages parameter. If we build and run the game, we will see a message when we enter a location with a monster in it.

Fig 1 – Game Screen with Messages

In conclusion, we now have the ability to display messages to the player. We started with just a message notifying of the presence of monsters, but we are going to heavily use display messages as part of the combat system.

Enhancing the game engine with model class and view model changes, adding shared UI components to show game engine data, should all be getting predictable by now. We are following the same MVVM patterns, so we can continue to get comfortable with them. And the capabilities in our game are expanding quite nicely with each lesson because we are focusing more on building features now that we have a flexible framework in place.

Next lesson we will look at the more fun parts of the game engine… monster combat.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s