Dev Diaries - Building a JRPG - Part 10

Life Updates

It is now June, 2020. I've been having fun in the last couple of days juggling with taking care of my daughter, keeping up with family things, continuing my graduate school studies, and doing some development whenever I can.

It's not easy at all, but doable. Progress on almost everything I do is just a tad slower. For example, what used to take me only 2 hours to accomplish now takes me about a whole day. Sometimes, things just fall into the next day. It is mainly the case because I find myself working on things on and off, and not for long periods of time. Essentially there is less time for deep focus. That's okay, and expected. After all, priorities have shifted. 😊

I do notice that it does take me less time to start working, and I goof off less. Anyway, despite all these life changes, I try to keep up with everything by constantly tweaking my day. This is the benefit on being on paternity leave.

I did manage to make some progress within the week from the last discussion in implementing the basic menu system, so that's a sign that I'm still somewhat functional after having a lack of sleep these past few weeks.

By now, I am so, so glad that my company offers paid paternity leave. Taking leave at this time in 2020 is quite scary. The economy isn't doing well, and a lot of companies have been laying off staff due to losing business from COVID-19. Everyday, I wonder, since I'm not at work, will I become more useless day by day? I think this is called job insecurity.

I try to mitigate this feeling everyday by reminding myself that my work environment, culture, people, and management are great (they really are) -- and that there is nothing to fear. I also try to tell myself that I can at least navigate around a computer, and type on the keyboard, so there has to be some sort of job out there for me should the worst happen!

Hey! my workout routine is great! Prior to taking paternity leave, I weighed 149.2 lbs. Since starting Starting Strength, and 5 weeks later, I'm at 151.0 lbs. Here's a chart of my progress

DescriptionBeforeCurrent
Bodyweight149.2 lbs151.0 lbs
Squat150 lbs175 lbs
Bench Press135 lbs150 lbs
Deadlift175 lbs225 lbs

I start my workout with my wife in our home gym around 3:30 AM, about 5 times a week. I'm so glad we've been able to make it work so far.

Game Dev

We spent a lot of time together last time building out the framework called Jrpg.MenuSystem, for creating menus for our demo, and got far enough to write a couple of unit tests to validate the menu system.

Today, we'll actually start in creating the dialogs which the game will render using Jrpg.MenuSystem! How exciting, right? We're going to go beyond the world of Debug.Log statements, and have text be rendered onto our game. Best of all, we finally get to see the character stats of Cloud!

We will have two Menu dialogs we will be creating today:

  • Character Status - Create a character statistics dialog which can be rendered showing the face of the hero, along with the name, HP, and MP statistics of the hero
  • Debug Log- Create a menu dialog to output content that may be sourced from debugging logs, or from some adventure journal, similar to the one we find in Castle of the Winds.

COTW

I love the basic design of the Character Status screen on the right, and Adventure Log on the left of this game. For such simple dialogs, they convey a good amount of information. Using this game as my inspiration, here's what we will be creating today:

Complete

I personally think that looks pretty sweet! I can't wait to get started.

Jrpg.MenuSystem Should Not Render Content!

First thing's first, we will need to make several modifications to the Jrpg.MenuSystem project.

The first modification we need to address is that we should no longer have the framework be concerned about rendering the menus in the game. Instead, the code-base should act more of a data model in how menus should be represented for a particular JRPG-style game.

It should be the responsibility of the engine to be concerned about how the menus should be rendered. Jrpg.MenuSystem should only provide the hints via contents, and properties on how things should be displayed in specific games.

To be specific, Jrpg.MenuSystem would have been too tightly coupled with UnityEngine to render menus. In order to render menus, our implementation relied on the passing of MonoBehaviour type objects as parameters. This would have made things more complicated than it should.

  1. Dependency on the Unity game engine -- leaving Jrpg.MenuSystem to only work with Unity games.
  2. We would also need to depend on whatever game engine API needed within rendering code of Jrpg.MenuSystem should it be responsible for rendering. This results in less portability.
  3. When changing how a MenuContent is displayed by a Menu, a code change would be required within Jrpg.MenuSystem just to output different representation of the data when the data itself has not changed.

The last point is very important, and logically makes sense. Let's say we have a Menu which is composed of a MenuContentToken type. It is a simple menu that just displays the current HP of a character:

|----------------|
| HP: $CURR_HP$  |
|----------------|

Since $CURR_HP$ here is our token, we typically want to just replace it with the value of the character's current HP. If the rendering logic is to be placed within the responsibilities of the MenuContentToken itself, then the game developer would be limited to only being able to display the character's current HP.

Well, suppose a game developer now wants to do more than that, and wants to make the labels a specific sprite font. The result would be that she would need to go back into the Jrpg.MenuSystem code base, and modify the MenuContentToken class to tweak the rendering logic.

All this means that simple rendering changes, or using the framework for multiple games requires too much modification in Jrpg.MenuSystem, and so it is not scalable.

A solution is to create a decorator class that wraps around the MenuContentToken instance, and implement some logic to display the Content differently after a token has been replaced. This can be cumbersome as we would need to create string manipulation logic, and unique rendering logic on top of MenuContentToken to "shape" the data to how we want it rendered.

A better solution is to decouple the logic for rendering, and handling of the data. We need to modify the Jrpg.MenuSystem components so that they are no longer responsible for rendering the menus, and dialogs. This will force games to be responsible for rendering this content themselves.

We will need to remove the Render methods in all the classes that define it. Menu, MenuContent, MenuContentText, etc.

MenuStack Modifications

Now, let's add more functionality to the MenuStack class.It will no longer be a traditional stack data structure, but a mix of a stack, and another data structure which can allow entries to be replaced in a safe manner.

Let's define the Replace method that takes in 2 parameters: a string parameter called key which corresponds to the key of the Menu to replace, and a Menu , m which corresponds to the new object to replace the existing with.

void Replace(string key, Menu m);

You will notice that containing a simple Stack collection within MenuStack no longer gives us all the functionality we need. Instead, we need a new data structure to contain all the data internally. Since we are leveraging keys to identify menus, we can perform fast replacements of the Menu objects by keys by wrapping a Dictionary<string, Menu> to deal with those types of operations. However, we still want Stack functionality, we still maintain a Stack, but not with Menu objects. Instead we only need to track string objects which correspond to the key. We can rename our existing stack to KeyStack.

The new implementation of our MenuStack now looks like this:

using System.Collections.Generic;
using System.Linq;

namespace Jrpg.MenuSystem
{
    public class MenuStack
    {
        private Dictionary<string, Menu> Menus;
        private Stack<string> KeyStack;

        public MenuStack()
        {
            Menus = new Dictionary<string, Menu>();
            KeyStack = new Stack<string>();
        }

        public Menu Peek()
        {
            return Menus[KeyStack.Peek()];
        }

        public void Push(Menu m)
        {
            Menus.Add(m.Key, m);

            KeyStack.Push(m.Key);
        }

        public Menu Pop()
        {
            var key = KeyStack.Pop();

            var menu = Menus[key];

            Menus.Remove(key);

            return menu;
        }

        public void Replace(string key, Menu m)
        {
            Menus[key] = m;
        }

        public void Clear()
        {
            Menus.Clear();
            KeyStack.Clear();
        }

        public List<string> Keys()
        {
            return Menus.Select(m => m.Key).ToList();
        }

        public Menu Get(string key)
        {
            return Menus[key];
        }

        public int Count()
        {
            return KeyStack.Count;
        }
    }
}

Whenever we update the internal Menus dictionary, we must take care in updating KeyStack too. So, the Push, and Pop methods will modify both structures. We assume that KeyStack, and Menus are always in some synchronized state, so we assume that the number of Menu objects in MenuStack is the number of keys available in KeyStack.

We can do better down the line by adding error checking for duplicate keys on Push, but for now, we can assume that the MenuStack API is used correctly.

Now let's try to implement both Menus individually, and see what other modifications are needed!

The Debate: Unity UI vs Custom

It was originally suggested to me by Scott that I look into using Unity UI to implement rendering of the Menus. However, after looking through the documentation, I decided to forego using the framework, and roll with my own.

The main reasoning behind this decision is mainly because I want to have fun learning how to build a game, the framework, and other technical reasons which Unity UI does not fit my purpose:

  • It seems like we cannot create UI controls dynamically in Unity UI, as it relies on specified fields within the script to be instantiated for the UI controls to be displayed. The fields would need to be declared public, assigned, then instantiated at some point. This would make achieving my requirements in having menus appear in various different places, at different, and occasions a bit more difficult in that I would have to anticipate which scene would contain a menu.
  • I am also considering the fact that JRPGs are dialog heavy, and having tons of UI objects in a scene would make it a bit difficult to manage. My hope is that Jrpg.MenuSystem will also be able to render out dialogs for the game.
  • I did not need anything heavy, or as complicated as Unity UI for what I would need in my JRPG games I would developing in the future. At least, that's what I think.
  • Avoiding the heavy reliance of Unity UI would also allow me to develop the menu system in such a manner that allow its content to be rendered on any game engine.

There are downsides to this though. The main one being that I am yet avoiding another instance where I can learn more about the Unity ecosystem. It's one thing that actually holds me back! So one good argument is that designing a menu system to be rendered by Unity UI presents a really good situation to actually learn the library, since it is a good use-case. However for this project, I think it may not be the "perfect-fit".

Main Menu Grid

The code to manage all menus using Jrpg.MenuSystem, and to render will be contained within a root Game game object. Since this game object already contains a script Game as the game component (I know, confusing...), we will add another one which will just handle menus.

We can create MenuController.cs in our game under Assets, and assign it to the Game game object in the scene.

Let's also create a root game object to contain all our menus, called BackgroundMenu. This is now the main management class for our game to manipulate all things menu-related!

The first task is to build our main grid for which all Menu objects will be rendered over. This is needed because the locations, and size of Menuobjects are within context to this main grid, not the screen.

Initially, building this out was more of a struggle than I initially assumed. My original intention was to add a bunch of SpriteRenderer game components into the BackgroundMenu game object, and have each SpriteRenderer render a single tile within the 64x36 grid.

After much struggling, I had found that a Unity GameObjectcannot contain more than one of the same GameComponenttype. This was easily confirmed by Scott, who tried adding multiple SpriteRenderercomponents to a Unity GameObject through the Unity editor. Duh!

The solution which I came up with was to create many GameObject instances under the main BackgroundMenu object. Each of these game objects would then represent a single tile with a single SpriteRenderer object.

/**
 * Builds the main menu grid for all menus to be laid on
 */
private void BuildMenuGrid()
{
    Game game = GameObject.Find("Game").GetComponent<Game>();
    Sprite sprite = game.LoadSpriteFromResource(GetTileAsset());
    GameObject bgMenu = GameObject.Find("BackgroundMenu");

    bgMenu.GetComponent<RectTransform>().position = TileToWorldPoint(0, 0);
    bgMenu.GetComponent<RectTransform>().sizeDelta = new Vector2(Screen.width, Screen.height);

    for (var y = 0; y < GridRows; y++)
    {
        for (var x = 0; x < GridColumns; x++)
        {
            GameObject g = new GameObject();
            g.transform.parent = bgMenu.transform;
            g.name = $"bgtile-{x}-{y}";

            SpriteRenderer s = g.AddComponent<SpriteRenderer>();
            s.sprite = sprite;
            s.sortingOrder = 1;
            s.sortingLayerName = SORTING_LAYER;

            // We want to keep the background transparent for now.
            s.color = new Color(0f, 0f, 0f, 0f);
           
            g.transform.position = TileToWorldPoint(x, y);
        }
    }

    BgMenu = bgMenu;
}

The code above isn't scary at all and is just a simple loop iterating through 36 times vertically, and 64 times horizontally. Each iteration creates a game object which then has a SpriteRenderer attached to it.

In Unity, we can assign a GameObject to some parent object through an assignment like this:

childGameObject.transform.parent = parentGameObject.transform;

Another handy method I have written is to translate the tile to world point. This first takes an x, y coordinate representing a tile location, then translates it to screen coordinates by doing basic multiplication to determine offsets.

From the previous post, we know that the offset width, and the offset height are determined through the following calculations:

private int OffsetWidth()
{
    return (Screen.width - GetTileSize() * GridColumns) / 2;
}

private int OffsetHeight()
{
    return (Screen.height - GetTileSize() * GridRows) / 2;
}

Then, we just need to get the size of the tile based on the screen resolution width, and multiply that with the tile offset of x, and y.

private int GetTileSize()
{
    if (Screen.width <= 1280)
        return 16;
    if (Screen.width <= 1920)
        return 24;
    if (Screen.width <= 2560)
        return 32;
    if (Screen.width <= 3840)
        return 48;

    return 48;
}

The full conversion of the tile, to screen, to world coordinates is then implemented with the calculation, and converted to a Vector3.

private Vector3 TileToWorldPoint(int x, int y)
{
    var c = GameObject.Find("Main Camera").GetComponent<Camera>();
    var v = c.ScreenToWorldPoint(new Vector3(
        OffsetWidth() + (x * GetTileSize()),
        OffsetHeight() + (y * GetTileSize()),
        0));

    return v;
}

Now to just display our grid, we can build it in Awake, and then render it on Update.

void Awake()
{
    Menus = new MenuStack();

    // Default to showing the menus for now.
    ToggleShowMenu();

    BuildMenuGrid();
}

// Update is called once per frame
void Update()
{
    if (GameStore.GetInstance().Get<bool>("ShowMenu"))
        DisplayMenu();
}

In this grid building routine, I have temporarily made the background color transparent, but the following screenshot shows the area in which the grid is covering in the screen:

Menu Grid

This looks pretty good!

Looking at the Unity Editor, we also see many game objects being nested under BackgroundMenu.

Background Menu Tiles

You'll notice though, as we run the game, we potentially end up creating many game objects which causes the tiles to lay over one another. Even worse, we are creating more tiles unnecessarily to lay out the background on each Update call by the engine. Wow! That eats up a lot of memory.

We'll circle back to this issue by introducing a cache to store built menus, with the ability to remove items from the cache to refresh/update on render.

Rendering Text

The next challenge is to actually build some menu on top of this grid! We'll want to get our menu looking something like this:

Test

The above screenshot shows 2 dialog boxes on top of the menu grid. We can see that each dialog has its own container of text. The text in this case isn't really "text" but rather tiles rendering sprites which represent a subset of characters. -- Sprite fonts, basically.

Sprite Font

Each tile in the sprite font sheet is 16x16 in pixel width by height. Slicing them individually, and creating the game objects with SpriteRenderers similar to building the menu grid is an approach we can take to render text.

In order to that that, some simple helper routines are needed to load up the sliced up sprites in Unity, and render the appropriate character.

public List<Sprite> LoadAllSpritesFromResource(string path)
{
    return Resources.LoadAll<Sprite>(path).ToList();
}

Loads the list of sliced sprites based on the name of the main resource.

Suppose our sprite font asset is called font, then we can get the name of the sliced sprite with this simple routine in MenuController.

private string GetFontTileAsset()
{
    return $"font_{GetTileSize()}x{GetTileSize()}";
}

In Unity, our sliced sprites will be numbered in sequentially.

Sequential font sprite slices

We define the alphabet available for use within our dialogs:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!"$%&\()*+-,/

We can write a method to return the sliced sprite based on the offset that was generated by Unity. The logic hard codes some of the base numbers, and will end up being different for every game, but that's okay since this method is game-specific anyway.

private int GetLetterIndex(char t)
{
    char start = 'A';

    if (Char.IsWhiteSpace(t))
        return -1;

    if (Char.IsUpper(t))
        start = 'A';
    else if (Char.IsLower(t))
        start = 'a';
    else if (Char.IsNumber(t))
        start = '0';

    int offset = t - start;

    if (start == 'A')
        return 63 + offset;
    else if (start == 'a')
        return 95 + offset;
    else if (start == '0')
        return 46 + offset;

    switch (t)
    {
        case '!':
            return 31;
        case '"':
            return 32;
        case '#':
            return 33;
        case '$':
            return 34;
        case '%':
            return 35;
        case '&':
            return 36;
        case '\'':
            return 37;
        case '(':
            return 38;
        case ')':
            return 39;
        case '*':
            return 40;
        case '+':
            return 41;
        case '-':
            return 43;
        case '.':
            return 44;
        case '/':
            return 45;
        default: return 234;
    }
}

Now, we can use the index returned by GetLetterIndex, and extract the sprite, and create a list of Sprite objects to be assigned to SpriteRenderers .

private List<Sprite> ToFontSprites(string content)
{
    var game = GameObject.Find("Game").GetComponent<Game>();
    var result = new List<Sprite>();
    foreach (var c in content)
    {
        int index = GetLetterIndex(c);

        if (index == -1)
        {
            result.Add(game.LoadSpriteFromResource(GetMenuTileAsset()));
            continue;
        }

        Sprite letterAsset = game.LoadAllSpritesFromResource(GetFontTileAsset())
            .Find(d => d.name.Equals($"{GetFontTileAsset()}_{index}"));

        result.Add(letterAsset);
    }

    return result;
}

Character Status

What is our requirement to display the character statistics? Right now, we can keep it simple. Here's what the character status screen should display, and update as we are playing the game:

  1. Display the image of Cloud
  2. Display the name, in this case: Cloud
  3. Display the basic stats of Cloud: HP, MP, and current EXP

The character status menu should also be updated dynamically based on events which occur in the game. For example, if the hero takes damage, the character status menu screen should be updated to reflect this.

From the original specification, the Menu is a composition of MenuContent objects. For the character status screen, it will be composed of MenuContentImage, and MenuContentToken.

Layout

From the figure above, we have:

  1. Face - MenuContentImage- A 100x100 JPG resource file
  2. Name - MenuContentToken - With the string: $NAME$
  3. HP - MenuContentToken- With the string: HP: $HP_LABEL$
  4. MP - MenuContentToken- With the string: MP: $MP_LABEL$
  5. EXP - MenuContentToken- With the string EXP: $EXP_LABEL$

MenuContentToken is especially useful for these types of menu screens since the labels change dynamically, and values are not known ahead of time before render. We can the fill these details in programmatically.

It should be easy to create Menu objects. The goal is to be able to create menus using automated tools, or builders. For that to be possible, we need to allow Menu objects to be defined through a definition file. These definition files should have a somewhat-strict schema which mirror the POCO object.

We can use some deserializer such as Newtonsoft.Json to parse the raw string as a JObject in which we can walk through. Before I show you that though, here is an example of such a definition file which constructs the Character Status screen:

{
 "Key": "menu-character-stats",
 "Location": {
     "X": 0,
     "Y": 0
 },
 "Size": {
     "Width": 22,
     "Height": 8
 },
 "Contents": [
     {
        "Key": "stat-face",
        "Type": "IMAGE",
        "Content": "cloud_face",
        "Size": {
            "Width": 6,
            "Height": 6
        },
        "Location": {
            "X": 0,
            "Y": 0
        }
     },
     {
        "Type": "TOKEN",
        "Key": "stat-name",
        "Content": "$NAME$",
        "Replacers": [
            {
                "Token": "$NAME$",
                "Agent": "MenuContentTokenNameReplacer, Assembly-CSharp"
            }
        ],
        "Size": {
            "Width": 16,
            "Height": 1
        },
        "Location": {
            "X": 9,
            "Y": 1
        }
     },
     {
         "Type": "TOKEN",
         "Key": "stat-hp",
         "Content": "HP:    $HP_LABEL$",
         "Replacers": [{
             "Token": "$HP_LABEL$",
             "Agent": "MenuContentTokenHpReplacer, Assembly-CSharp"
         }],
         "Size": {
             "Width": 16,
             "Height": 1
         },
         "Location": {
             "X": 9,
             "Y": 3
         }
     },
     {
        "Type": "TOKEN",
        "Key": "stat-mp",
        "Content": "MP:    $MP_LABEL$",
        "Replacers": [{
            "Token": "$MP_LABEL$",
            "Agent": "MenuContentTokenMpReplacer, Assembly-CSharp"
        }],
        "Size": {
            "Width": 16,
            "Height": 1
        },
        "Location": {
            "X": 9,
            "Y": 4
        }
    },
    {
        "Type": "TOKEN",
        "Key": "stat-xp",
        "Content": "EXP:    $EXP_LABEL$",
        "Replacers": [{
            "Token": "$EXP_LABEL$",
            "Agent": "MenuContentTokenExpReplacer, Assembly-CSharp"
        }],
        "Size": {
            "Width": 16,
            "Height": 1
        },
        "Location": {
            "X": 9,
            "Y": 5
        }
    }
 ]
}

The root object represents the entire Menu object, with the Contents property representing the list of MenuContent objects which compose the entire Menu.

For IMAGE, we parse this as MenuContentImage where the Content field refers to the resource file which is read in by Unity.

TOKEN creates a MenuContentToken with a list of Replacer objects for executing string replacement with a specific logic when the Replace method is called.

The rest of the properties defined should be fairly straightforward.

Our MenuContentTokenReplacer classes are simple. Here is an example of the MenuContentTokenHpReplacer class:

using Jrpg.CharacterSystem;
using Jrpg.MenuSystem;
using Jrpg.System;

public class MenuContentTokenHpReplacer : MenuContentTokenReplacer
{
    public MenuContentTokenHpReplacer(string token) : base(token)
    {
    }
    public override string Replace(GameStore g)
    {
        var cloud = g.MainParty.GetActiveCharacter();
        var currHp = cloud.Statistics[StatisticType.HpCurrent].CurrentValue;
        var maxHp = cloud.Statistics[StatisticType.HpMax].CurrentValue;

        return $"{currHp}/{maxHp}";
    }
}

The token will be replaced by whatever current HP and max HP value the current active character already has. In this case, Cloud is the active character, so it will be his HP.

The parser will be able to parse 3 distinct MenuContent types in addition to Menu: Text, Image, and Token. MenuBuilder.cs parses out the definition files and returns the Menu object. The things worth noting are:

  • The resourceName is the name of the JSON string found within the Resources folder in the Unity project.
  • We use NewtonSoft.Json to parse the string into a JObject, and walk through the tree to obtain the values.
  • For TOKEN type definitions, we perform some additional work by populating the Replacer list with MenuContentTokenReplacementDefinition objects. These objects just have 2 properties: the token to be replaced, and the replacement logic to handle what to replace the token with.
  • For each MenuContent created, we add it to the Menu through the AddContent method call.
using System.Linq;
using Jrpg.System;
using Jrpg.MenuSystem;
using Newtonsoft.Json.Linq;
using UnityEngine;

public class MenuBuilder
{
    public static Menu BuildFromDefinition(string resourceName)
    {
        var game = GameObject.Find("Game").GetComponent<Game>();

        var definition = game.LoadStringFromResource(resourceName);

        JObject o = JObject.Parse(definition);
        JToken menuKeyToken = o.GetValue("Key");
        var menuKey = menuKeyToken.ToString();

        var menuLocationToken = o.GetValue("Location");
        var menuLocationX = menuLocationToken.Value<int>("X");
        var menuLocationY = menuLocationToken.Value<int>("Y");

        var menuSizeToken = o.GetValue("Size");
        var menuSizeWidth = menuSizeToken.Value<int>("Width");
        var menuSizeHeight = menuSizeToken.Value<int>("Height");

        Menu m = new Menu();
        m.Key = menuKey;
        m.Size = new MenuSize(menuSizeWidth, menuSizeHeight);
        m.Location = new TilePoint(menuLocationX, menuLocationY);

        var menuContents = o.GetValue("Contents").Children();
        foreach (var contentToken in menuContents)
        {
            var menuContentType = contentToken.Value<string>("Type");
            var menuContentKey = contentToken.Value<string>("Key");
            var menuContentSizeWidth = contentToken["Size"].Value<int>("Width");
            var menuContentSizeHeight = contentToken["Size"].Value<int>("Height");
            var menuContentLocationX = contentToken["Location"].Value<int>("X");
            var menuContentLocationY = contentToken["Location"].Value<int>("Y");

            if (menuContentType.Equals("TEXT"))
            {
                var menuContentContent = contentToken.Value<string>("Content");

                MenuContentText mcText = new MenuContentText(GameStore.GetInstance());
                mcText.Key = menuContentKey;
                mcText.Content = menuContentContent;
                mcText.Size = new MenuSize(menuContentSizeWidth, menuContentSizeHeight);
                mcText.Location = new TilePoint(menuContentLocationX, menuContentLocationY);

                m.AddContent(mcText);
            }
            else if (menuContentType.Equals("IMAGE"))
            {
                var menuContentContent = contentToken.Value<string>("Content");

                MenuContentImage mcImage = new MenuContentImage(GameStore.GetInstance());
                mcImage.Key = menuContentKey;
                mcImage.Content = menuContentContent;
                mcImage.Size = new MenuSize(menuContentSizeWidth, menuContentSizeHeight);
                mcImage.Location = new TilePoint(menuContentLocationX, menuContentLocationY);

                m.AddContent(mcImage);
            }
            else if (menuContentType.Equals("TOKEN"))
            {
                var menuContentContent = contentToken.Value<string>("Content");

                MenuContentToken mcToken = new MenuContentToken(GameStore.GetInstance());
                mcToken.Key = menuContentKey;
                mcToken.Content = menuContentContent;
                mcToken.Size = new MenuSize(menuContentSizeWidth, menuContentSizeHeight);
                mcToken.Location = new TilePoint(menuContentLocationX, menuContentLocationY);

                var replacerList = (JArray)contentToken["Replacers"];
                foreach (var replacementDef in replacerList.ToList())
                {
                    var def = new MenuContentToken.MenuContentTokenReplacementDefinition(
                        replacementDef["Token"].Value<string>(),
                        replacementDef["Agent"].Value<string>()
                    );

                    mcToken.Replacers.Add(def);
                }


                m.AddContent(mcToken);
            }
        }

        return m;
    }
}

A lot of this code is repetitive, but in general, is kept easy to follow, and understood.

Then we can make the call to build the menu-character-stats menu with just the following call in MenuController:

Menus.Push(MenuBuilder.BuildFromDefinition("menu-character-stats"));

Just a simple, single line!

Rendering though, is not as simple. 😊😊😊

private void RenderMenus()
{
    Game game = GameObject.Find("Game").GetComponent<Game>();
    Sprite sprite = game.LoadSpriteFromResource(GetMenuTileAsset());

    foreach(var menuKey in Menus.Keys())
    {
        Menu peeked = Menus.Get(menuKey);

        if (!MaybeUpdateCache(menuKey))
            continue;

        GameObject dialog = new GameObject();
        dialog.transform.parent = BgMenu.transform;
        dialog.name = DialogCacheKey(menuKey);

        Cache.Add(DialogCacheKey(menuKey), dialog);
        for (var y = peeked.Location.Y; y < peeked.Location.Y + peeked.Size.Height; y++)
        {
            for (var x = peeked.Location.X; x < peeked.Location.X + peeked.Size.Width; x++)
            {
                GameObject g = null;

                g = new GameObject();

                g.transform.parent = dialog.transform;
                g.name = $"menutile-{x}-{y}";

                SpriteRenderer s = g.AddComponent<SpriteRenderer>();
                s.sprite = sprite;
                s.sortingOrder = 2;
                s.sortingLayerName = SORTING_LAYER;

                g.transform.position = TileToWorldPoint(x, y);
            }
        }

        foreach (var mcKey in peeked.Keys())
        {
            var mc = peeked.GetContent(mcKey);

            if (mc.Type == MenuContentType.Text)
            {
                var mcText = (MenuContentText)mc;
                var sprites = ToFontSprites(mcText.Content);

                for (var k = 0; k < sprites.Count; k++)
                {
                    Sprite letterAsset = sprites[k];

                    if (letterAsset.name.StartsWith("menu_tile"))
                        continue;

                    GameObject lg = new GameObject();
                    lg.transform.parent = dialog.transform;
                    lg.name = $"menu-content-text-{k}";

                    SpriteRenderer ls = lg.AddComponent<SpriteRenderer>();
                    ls.sprite = letterAsset;
                    ls.sortingOrder = 3;
                    ls.sortingLayerName = SORTING_LAYER;

                    lg.transform.position = TileToWorldPoint(
                        peeked.Location.X + mcText.Location.X + k,
                        peeked.Location.Y + (peeked.Size.Height - mcText.Location.Y - 1));
                }
            }
            else if(mc.Type == MenuContentType.Image)
            {
                var mcImage = (MenuContentImage)mc;
                var imageSprite = game.LoadSpriteFromResource(mcImage.Content);
                
                GameObject lg = new GameObject();
                lg.transform.parent = dialog.transform;
                lg.name = $"menu-content-image";

                SpriteRenderer ls = lg.AddComponent<SpriteRenderer>();
                ls.sprite = imageSprite;
                ls.sortingOrder = 3;
                ls.sortingLayerName = SORTING_LAYER;

                lg.transform.position = TileToWorldPoint(
                    peeked.Location.X + mcImage.Location.X,
                    peeked.Location.Y + (peeked.Size.Height - mcImage.Location.Y - 1));
            }
            else if(mc.Type == MenuContentType.Token)
            {
                MenuContentToken mcToken = (MenuContentToken)mc;
                mcToken.Replace();

                var sprites = ToFontSprites(mcToken.Content);
                for (var k = 0; k < sprites.Count; k++)
                {
                    Sprite letterAsset = sprites[k];

                    if (letterAsset.name.StartsWith("menu_tile"))
                        continue;

                    GameObject lg = new GameObject();
                    lg.transform.parent = dialog.transform;
                    lg.name = $"menu-content-token-{k}";

                    SpriteRenderer ls = lg.AddComponent<SpriteRenderer>();
                    ls.sprite = letterAsset;
                    ls.sortingOrder = 3;
                    ls.sortingLayerName = SORTING_LAYER;

                    lg.transform.position = TileToWorldPoint(
                        peeked.Location.X + mcToken.Location.X + k,
                        peeked.Location.Y + (peeked.Size.Height - mcToken.Location.Y - 1));
                }
            }
        }
    }
}

The algorithm involves inspecting each Menu in the MenuStack. Since we cannot access Menu objects directly, we can just iterate through the keys and retrieve them this way. A better way in the future to do this is to create an iterator specifically to achieve this.

As we are iterating through the keys, we obtain the Menu object, and create a parent GameObjectto house the entire contents of the Menu. This GameObjectwill contain the contents of the Menu as child GameObjects. The approach to render the Text, Image, and Token types are all generally the same, so I will only discuss the approach to render MenuContentToken types.

Upon retrieving the Menu, we inspect every key corresponding to the MenuContent contained within the Menu. We retrieve the MenuContent, and cast it to its appropriate type, and build GameObjects for the MenuContent to be rendered by Unity.

Notice how after we cast the current MenuContent to a MenuContentToken, we immediate run the Replacemethod to execute all the logic to replace the tokens. The final resulting string after replacement can then be accessed through the Content property.

The list of character sprites for the font is retrieved from ToFontSprites. From here, we iterate through this list and build the SpriteRenderercomponents to attach to the GameObject.

Setting the menu grid to be transparent, and calling the RenderMenus method, we are now able to render our character status menu with just a JSON definition file!

Built Character Status

Caching

If we test the menu by battling, there are two things we see wrong with our current implementation:

  1. We are constantly creating GameObjects to render all the Menu data.
  2. if we trigger a battle, and lose some HP, the character status menu is not updated to reflect the new HP.

To solve (1), we can introduce a cache to store the previously built Menu dialogs, and their contents. For now, it is a simple static property in MenuController:

private static readonly Dictionary<string, GameObject> Cache
    = new Dictionary<string, GameObject>();

Constructed dialogs will be stored in this cache, and if the Menu objects by key exist in the cache, then the RenderMenus method will skip constructing GameObjects to build the Menu.

The cache can also be forced to kick items out through BustCachedMenu, which takes in the cached Menu which is referenced by the key.

public static bool BustCachedMenu(string menuKey)
{
    var builtKey = DialogCacheKey(menuKey);
    var result = false;

    if (Cache.ContainsKey(builtKey))
        result = Cache.Remove(builtKey);

    return result;
}

RenderMenus can call the helper method MaybeUpdateCache to intelligently skip the rendering of the Menu, if cached, or build the Menu from definition, if it is not found in the cache.

private bool MaybeUpdateCache(string menuKey)
{
    if (Cache.ContainsKey(DialogCacheKey(menuKey)))
        return false;

    // We will need to update as there could be no entry,
    // due to being new, or stale from busting.
    Menu newMenu = MenuBuilder.BuildFromDefinition(menuKey);
    Menus.Replace(menuKey, newMenu);

    Destroy(GameObject.Find(DialogCacheKey(menuKey)));

    return true;
}

To make the cached Menu stale during battle, we can add the call in MonsterCollision::FixedUpdate. Whenever the monster performs an attack, we can call:

MenuController.BustCachedMenu("menu-character-stats");

To replace/update the Menu with new values.

Realtime Update

Debug Log Menu

Our current way of reading debugging information through Debug.Log involves reading the Console found in the Unity Editor.

Unity debug console

This is useful, but we can actually output log information in the game. All we need to do is create a new menu screen to display the debugging information. Such a menu screen can also be used to show an adventure log like some of the old school RPG games.

To get started on this, we can define this menu through JSON, and have the code automatically parse, and render the content on screen.

{
    "Key": "menu-debug",
    "Location": {
        "X": 23,
        "Y": 0
    },
    "Size": {
        "Width": 42,
        "Height": 8
    },
    "Contents": [
        {
            "Type": "TOKEN",
            "Key": "log-line-1",
            "Content": "$LINE_1$",
            "Replacers": [
                {
                    "Token": "$LINE_1$",
                    "Agent": "DebugLineReplacer1, Assembly-CSharp"
                }
            ],
            "Size": {
                "Width": 24,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 1
            }
        },
        {
            "Type": "TOKEN",
            "Key": "log-line-2",
            "Content": "$LINE_2$",
            "Replacers": [
                {
                    "Token": "$LINE_2$",
                    "Agent": "DebugLineReplacer2, Assembly-CSharp"
                }
            ],
            "Size": {
                "Width": 24,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 2
            }
        },
        {
            "Type": "TOKEN",
            "Key": "log-line-3",
            "Content": "$LINE_3$",
            "Replacers": [
                {
                    "Token": "$LINE_3$",
                    "Agent": "DebugLineReplacer3, Assembly-CSharp"
                }
            ],
            "Size": {
                "Width": 24,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 3
            }
        },
        {
            "Type": "TOKEN",
            "Key": "log-line-4",
            "Content": "$LINE_4$",
            "Replacers": [
                {
                    "Token": "$LINE_4$",
                    "Agent": "DebugLineReplacer4, Assembly-CSharp"
                }
            ],
            "Size": {
                "Width": 24,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 4
            }
        },        {
            "Type": "TOKEN",
            "Key": "log-line-5",
            "Content": "$LINE_5$",
            "Replacers": [
                {
                    "Token": "$LINE_5$",
                    "Agent": "DebugLineReplacer5, Assembly-CSharp"
                }
            ],
            "Size": {
                "Width": 24,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 5
            }
        }
    ]
}

You will notice a limitation in our system. Multiple lines are not supported when outputting text. For now that is okay since JRPGs tend to not have scrollable text. Our debugging log is only an internal tool, which will also show the output in full, in Unity -- therefore showing a subset of the log is fine. For this implementation, I have chose 5 lines.

All DebugLineReplacerN files are extended from BaseDebugLineReplacer. Each replacer will specify the line number they are responsible for so it can be read from the GameStore.

using System.Collections.Generic;
using Jrpg.MenuSystem;
using Jrpg.System;

public abstract class BaseDebugLineReplacer : MenuContentTokenReplacer
{
    protected int LineNumber;

    public BaseDebugLineReplacer(string token) : base(token)
    {
    }

    public override string Replace(GameStore g)
    {
        try
        {
            List<string> line = g.Get<List<string>>("DebugLines");

            return line[LineNumber - 1];
        } catch
        {
            return string.Empty;
        }

    }
}

public class DebugLineReplacer1 : BaseDebugLineReplacer
{
    public DebugLineReplacer1(string token) : base(token)
    {
        LineNumber = 1;
    }
}

public class DebugLineReplacer2 : BaseDebugLineReplacer
{
    public DebugLineReplacer2(string token) : base(token)
    {
        LineNumber = 2;
    }
}

public class DebugLineReplacer3 : BaseDebugLineReplacer
{
    public DebugLineReplacer3(string token) : base(token)
    {
        LineNumber = 3;
    }

}

public class DebugLineReplacer4 : BaseDebugLineReplacer
{
    public DebugLineReplacer4(string token) : base(token)
    {
        LineNumber = 4;
    }
}

public class DebugLineReplacer5 : BaseDebugLineReplacer
{
    public DebugLineReplacer5(string token) : base(token)
    {
        LineNumber = 5;
    }
}

Now, we should change the way we log debugging information. Instead of calling Debug.Log directly, we can use a custom helper which will store the log lines in the GameStore for the debug menu to render.

DebugHelper is a static class with a single method, Log taking in the log line. Internally, it will just maintain the line data found in the DebugLines structure stored in GameStore. It will also call Debug.Log so that the Unity Editor can display the full log information.

using Jrpg.System;
using System.Collections.Generic;
using UnityEngine;

public static class DebugHelper
{
    public static void Log(string message)
    {
        var g = GameStore.GetInstance();

        List<string> lines = new List<string>();
        try
        {
            var currentLog = g.Get<List<string>>("DebugLines");
            if (currentLog != null)
            {
                lines = currentLog;
            }
        } catch
        {
            g.Put<List<string>>("DebugLines", lines);
        }

        lines.Add(message);

        if (lines.Count > 5)
            lines = lines.GetRange(1, 5);

        g.Put<List<string>>("DebugLines", lines);

        Debug.Log(message);
    }
}

To make a log call, we can just write statements like this:

DebugHelper.Log("Init BlinkController with active monster");

We also build the menu for rendering in MenuController for our game:

private void BuildGameMenus()
{
    Menus.Push(MenuBuilder.BuildFromDefinition("menu-character-stats"));
    Menus.Push(MenuBuilder.BuildFromDefinition("menu-debug"));
}

Running the game now, we finally see both the menus we have created!

Menus

Conclusion

Whew that was quite the session, and I think it's great to stop right here. I'm just about to doze off on my keyboard now!

We are not quite done yet. We still need to implement a few more things for the menu system. The most obvious being support for MenuContentOption types, and making use of the Cursor in our game.

That's what we will explore next time!