Dev Diaries - Building a JRPG - Part 9

Dad Life

Being a new dad has really made life busy! There is really no book, article, or YouTube video that can serve as a silver-bullet on how to care for your new baby. Seriously. The only resource is... yourself, your partner, and the baby.

Newborns don't do very much aside from pooping, peeing, eating, and sleeping, so it's easy to forget that they are people too. Your newborn, and my newborn are both different in having their own needs, and wants. So, it's just unfair to care for them in a way that's templated through some How-To. It's been a month for me, and wow it has been rough. You really cannot prepare for children. You can buy all these supplies, and trinkets for your baby, but nothing can really prepare you, and your partner for the baby.

Expect to just feel like a shitty parent, and learn from it. It happens in this weird endless loop -- day after day. Just when you feel like you've been able to stay a step ahead of your kid, they throw you a curve ball by changing the way they sleep, or eat. It does get better though, and it has for me over the past couple of days.

Anyway, here are some things I found out that was specific to our daughter, Riley that we would have never caught had we not taken a step back to rethink what the hell we were doing:

  • First, get a baby monitor, and just trust it. You'll maintain your sanity this way.

  • Riley had been having lots of fits, and diarrhea due to the specific formula we were giving her. Lots of loud crying, and hissing would come from her whenever it was "bathroom" time for her. It was terrible. We were using a primarily milk-based formula, but switched to a non milk-based formula, and has been behaving much better since then. We only get 1 big poop diaper a day now (as opposed to 5)!

  • Building onto the previous point, our daughter just absolutely hates wetting herself. A single drop of pee onto her diaper causes her to throw a rage-fit. As a result, she had not been able to sleep well. Only about 1 to 1.5 hours at a time... Which not only caused her to lose sleep, but for my wife and I too... We did two things here to minimize it after some experimentation:

    • My wife started loosening the diapers. Having a snug diaper for our little one just made it more noticeable that she went to pee. We started applying a thin layer of Aquaphor to mask the wetness of the diaper whenever urination happened. The little one has been sleeping better since then.
  • If your newborn starts crying randomly and has just been fed, don't try to feed immediately again.. Just.. check the diaper. If your newborn starts crying, and there's no bad diaper, and has taken a nap, then feed her!

  • We're trying to not abuse the use of a pacifier. It's bad for the teeth, and we don't want to go through a potential long process in weening her off.

  • Some will say to buy a white noise machine, we're not sure if this helps yet, but we've been using it "in case"... but Riley seems fine sleeping with, or without one.

  • Swaddling? Not too important -- unless your little one kicks herself awake randomly.

Newborns aren't as fragile as you think. Riley and I sometimes play football while I gently run to the end zone of the living room, guess who's the football? She loves this.

My daughter also loves Super Junior. I can't sing in Korean to calm her down, unfortunately, but I can definitely sing a Disney tune. It's been fun!

Baby dance

Get a standing desk to stay productive. You can carry your newborn in one arm while writing/typing on the other. It's easier to do than sitting. My wife, and I both now have standing desks because of this. A happy newborn tends to just stay still while staring into space, anyway.

The standing desk we bought is from FlexiSpot, and is the adjustable 29-48" model. Here's a link to the exact one: https://flexispot.com/height-adjustable-desks/electric-height-adjustable-desk-en1-48

Since I'm also in school, I tend to just read aloud to Riley, and it helps me reinforce what I'm learning. For her, it's a good way to be lulled to sleep. Who the heck would be THAT INTERESTED in topics like Euler integration, anyway?

Also, for some reason, even though my daughter wails, growls, and kicks me whenever she gets upset due to being impatient about something, for some reason, I always forget about it, and love her the same as always. It's crazy -- this is what unconditional love ❤ is, people.

Game Dev!

The scope of our session today is to get started in writing some of the code reflecting the design which I had outlined in the previous article. The goal is to get as far as filling in some of the gaps I had intentionally missed while designing the first version of the Menu System, and to write a couple of unit tests which demonstrate basic functionality.

I'm not sure how many sessions this part of the series in developing a Menu System will expand into, but I want to keep each session bite-sized, as Riley has taken up (rightfully so!) a lot of my focus time. Who knows, maybe she'll start writing her own video games soon!

Jrpg.MenuSystem

Today we'll be creating a new project, Jrpg.MenuSystem within the framework. Of course, as usual, since Unity doesn't support anything newer than .NET Standard 2.0, this is what the framework the project will based on.

We'll create the files which were outlined in the previous post:

Cursor.cs
Menu.cs
MenuContent.cs
MenuContentImage.cs
MenuContentMemory.cs
MenuContentOption.cs
MenuContentOptionHandler.cs
MenuContentText.cs
MenuContentToken.cs
MenuContentTokenReplacer.cs
MenuContentType.cs
MenuSize.cs
MenuStack.cs
TilePoint.cs

I'll go through each one, in order and note any changes which may have been made, and if there are any noteworthy things that I may have missed from last time.

Cursor.cs

using System.Collections.Generic;

namespace Jrpg.MenuSystem
{
    public class Cursor
    {
        private Stack<MenuContentMemory> Memory;
        public bool Visible { get; set; }

        public Cursor()
        {
            Memory = new Stack<MenuContentMemory>();
        }

        public void Execute() 
        {
            return;
        }

        public MenuContentMemory Peek()
        {
            return Memory.Peek();
        }

        public void Push(MenuContentMemory mcm)
        {
            Memory.Push(mcm);
        }

        public MenuContentMemory Pop()
        {
            return Memory.Pop();
        }
    }
}

The design of this is left relatively unchanged. Again, it's quite simple, and just performs basic stack operations on MenuContentMemory objects.

Menu.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace Jrpg.MenuSystem
{
    public class Menu
    {
        private Dictionary<string, MenuContent> Contents;

        public string Key { get; set; }
        public TilePoint Location { get; set; }
        public MenuSize Size { get; set; }

        public Menu()
        {
            Contents = new Dictionary<string, MenuContent>();
        }

        public void AddContent(MenuContent mc)
        {
            Contents[mc.Key] = mc;
        }

        public MenuContent RemoveContent(string key)
        {
            var content = Contents[key];

            Contents.Remove(key);

            return content;
        }

        public MenuContent GetContent(string key)
        {
            return Contents[key];
        }

        public string DebugRender()
        {
            StringBuilder sb = new StringBuilder();
            int prevY = 0;
            foreach(var mc in Contents.Values)
            {
                for (var i = 0; i < mc.Location.Y - prevY; i++)
                {
                    sb.AppendLine();
                }
                prevY = mc.Location.Y;

                for (var i = 0; i < mc.Location.X; i++)
                {
                    sb.Append(" ");
                }
                switch (mc.Type)
                {
                    case MenuContentType.Text:
                        sb.Append(((MenuContentText)mc).Content);
                        break;
                    case MenuContentType.Option:
                        sb.Append(((MenuContentOption)mc).Content);
                        break;
                    case MenuContentType.Token:
                        ((MenuContentToken)mc).Replace();
                        sb.Append(((MenuContentToken)mc).Content);
                        break;
                    case MenuContentType.Image:
                        sb.Append(((MenuContentImage)mc).Content);
                        break;
                    default:
                        break;
                }
            }
            return sb.ToString();
        }

        public void Render()
        {
            Console.WriteLine($"Rendering {Key}");
        }
    }
}

At its core, Menu is a dictionary-based data structure housing MenuContent type objects. Most of it is relatively straightforward, and we're not going to touch the Render method today, but one method which I find handy, which should be discussed is the DebugRender method.

The DebugRender method will loop through all the active MenuContent instances within the current MenuContext and invoke custom rendering logic which will output to the console as text.

This is a useful method if we need to debug some individual Menu object in the future. Within the loop, an individual MenuContent object is cast into a more specific type, and custom logic is used to display it through to the console.

You can see that the logic for displaying MenuContentType.Token is different from MenuContentType.Text , for example.

An example output of DebugRender is demonstrated here, with a prompt, and two possible choices:

 Hello! Terry Token, is Pie good?
    Yes
    No

MenuContent.cs

using Jrpg.System;
using UnityEngine;

namespace Jrpg.MenuSystem
{
    public abstract class MenuContent
    {
        protected GameStore gameStore;

        public MenuContent(GameStore g)
        {
            gameStore = g;
        }

        public MenuContentType Type { get; set; }
        public string Key { get; set; }
        public MenuSize Size { get; set; }

        public TilePoint Location { get; set; }

        /* Must Override */
        public abstract void Render(MonoBehaviour mono);
    }
}

MenuContent serves as a base class for more specific MenuContent types. The only method here which needs to be implemented is the Render method.

Again, I'm not going to go through rendering today as I haven't thought too much about it, but I expect it will need some sort of Unity game object passed to it, so let's take that into account for now, and pivot away from it if needed in the future.

MenuContentImage.cs

using Jrpg.System;
using System;
using UnityEngine;

namespace Jrpg.MenuSystem
{
    public class MenuContentImage : MenuContent
    {
        public string Content { get; set; }

        public MenuContentImage(GameStore g) : base(g) { }

        public override void Render(MonoBehaviour mono)
        {
            throw new NotImplementedException();
        }
    }
}

This is a simple class which doesn't do much for now as displaying, and handling images within a Menu is going to be saved for a separate discussion another day. 😊

MenuContentOption.cs

using NetStandardSystem = System;
using System.Collections.Generic;
using System.Text;
using Jrpg.System;
using UnityEngine;

namespace Jrpg.MenuSystem
{
    public class MenuContentOption : MenuContent
    {
        public string Content { get; set; }
        public string Handler { get; set; }
        public int Index { get; set; }

        public MenuContentOption(GameStore g) : base(g) 
        {
            Type = MenuContentType.Option;
        }

        public void Handle()
        {
            MenuContentOptionHandler handler =
                (MenuContentOptionHandler)NetStandardSystem.Activator.CreateInstance(
                    NetStandardSystem.Type.GetType(Handler), 
                    new object[] { }
                );

            // Now, handle
            handler.Handle(this.gameStore);
        }

        public override void Render(MonoBehaviour mono)
        {
            throw new NetStandardSystem.NotImplementedException();
        }
    }
}

Okay, so this class changed just slightly with a couple of additions when compared to what we originally designed.

First, in order to logically represent ordering for menu options we will add an Index property.

Then, we need to implement logic to run the Handle method. If you're familiar with respect to how I've been doing things for this entire framework, you'll easily pick up the usage of .NET Reflection here.

The handler object which is created draws from the Handler property which is expected to contain the class to be instantiated to handle the selection of the option at runtime. This logic is custom, and is intended to be very flexible.

MenuContentOptionHandler.cs

using Jrpg.System;

namespace Jrpg.MenuSystem
{
    public interface MenuContentOptionHandler
    {
        void Handle(GameStore gs);
    }
}

All option handlers should implement this particular interface.

MenuContentText.cs

using System;
using Jrpg.System;
using UnityEngine;

namespace Jrpg.MenuSystem
{
    public class MenuContentText : MenuContent
    {
        public string Content { get; set; }

        public MenuContentText(GameStore g) : base(g)
        {
            Type = MenuContentType.Text;
        }

        public override void Render(MonoBehaviour mono)
        {
            throw new NotImplementedException();
        }
    }
}

Probably the most simple of all MenuContent sub-types, the MenuContentText object just contains the text content of what is to be displayed for a specific region.

MenuContentToken.cs

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

namespace Jrpg.MenuSystem
{
    public class MenuContentToken : MenuContent
    {
        public MenuContentToken(GameStore g) : base(g)
        {
            Type = MenuContentType.Token;
        }
        public string Content { get; set; }
        public List<string> Replacers { get; set; }

        public void Replace()
        {
            foreach(var replacer in Replacers)
            {
                MenuContentTokenReplacer replaceHandler =
                    (MenuContentTokenReplacer)NetStandardSystem.Activator.CreateInstance(
                        NetStandardSystem.Type.GetType(replacer),
                        new object[] { }
                    );

                Content = Content.Replace(replaceHandler.Token, replaceHandler.Replace(this.gameStore));
            }
        }

        public override void Render(MonoBehaviour mono)
        {
            throw new NetStandardSystem.NotImplementedException();
        }
    }
}

Similar to MenuContentOptionHandler.cs, MenuContentTokenReplacer.cs implements a base abstract class which will be extended by other sub-types handling the chosen options.

Every MenuContentTokenReplacer object has a Token property that will be searched for in the Content property of the MenuContentToken object.

So for example, if the Content property has First Name: $FNAME$, and Token is $FNAME$, then the Replace method which is implemented by the MenuContentTokenReplacer class will run the specialized logic to replace the token found within the MenuContentToken.Content property with a custom value.

MenuContentType.cs

namespace Jrpg.MenuSystem
{
    public enum MenuContentType
    {
        Text,
        Image,
        Token,
        Option
    }
}

Basic enum definition here! I am sticking to my original design.

MenuSize.cs

namespace Jrpg.MenuSystem
{
    public class MenuSize
    {
        public int Width { get; set; }
        public int Height { get; set; }

        public MenuSize(int w, int h)
        {
            Width = w;
            Height = h;
        }

        public override string ToString()
        {
            return $"{Width} x {Height}";
        }
    }
}

A typical POCO class that abstracts the size of a Menu in the context of tile coordinates. I overrode the ToString method here to make debugging easier.

MenuStack.cs

using System;
using System.Collections.Generic;
using System.Linq;
namespace Jrpg.MenuSystem
{
    public class MenuStack
    {
        private Stack<Menu> Menus;

        public MenuStack()
        {
            Menus = new Stack<Menu>();
        }

        public Menu Peek()
        {
            return Menus.Peek();
        }

        public void Push(Menu m)
        {
            Menus.Push(m);
        }

        public Menu Pop()
        {
            return Menus.Pop();
        }

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

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

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

        public void Render()
        {
            foreach(var m in Menus)
            {
                Console.WriteLine($"Rendering menu {m.Key} at {m.Location}, and size {m.Size}");
                m.Render();
            }
        }
    }
}

My hope is that MenuStack is the main entry point in manipulating menus from the game. The design leads to the developer being only able to use stack-like operations to fully operate on a set of menus.

We'll see if that really is the case, though... 🤣

TilePoint.cs

namespace Jrpg.MenuSystem
{
    public class TilePoint
    {
        public int X { get; set; }
        public int Y { get; set; }

        public TilePoint(int x, int y)
        {
            X = x;
            Y = y;
        }

        public override string ToString()
        {
            return $"({X}, {Y})";
        }
    }
}

Another POCO class! Here, it is a representation of a tile coordinate. TilePoint tells us the exact coordinate of a tile within the grid which contains all menus on screen.\

Unit Tests

The easiest thing to do to test some functionality of our menu system is to write some unit tests now that we have functional code written out.

For the time being, let's just limit ourselves to testing the functionality of MenuStack, and Menu .

Let's create a file called TestMenus.cs under our Jrpg.System.Tests project. Here is a summary of the tests which will validate our implementation:‌

  • TestMenuStack

    • Tests to see if the MenuStack can hold data
  • TestMenuStackPeek

    • Tests to see if we can obtain the top-most Menu without altering the context.
  • TestMenuStackPop

    • Tests if we can remove Menus from the stack, to simulate either moving away from the menu, or the cursor taking some sort of action.
  • TestMenuStackClear

    • Tests to see if we can clear the MenuStack -- leaving the number of items to be 0.
  • TestMenuWithContent

    • Ahhh!! The heart of the test. This actually creates a menu with content! The menu should display a prompt with 2 choices, and each choice taking some sort of action. The rendered menu should look like this:
|--------------------------------------------------|
| Hello! Terry Token, is Pie good?                 |
|    Yes                                           |
|    No                                            |
|--------------------------------------------------|

We will check this using DebugRender.

Setting Up

Within our constructor of the test file, we will set up a working MenuStack instance to be used in the rest of the unit tests. This goes against unit test best practice, but in this situation, we want to see if we can maintain a stable MenuStack structure as we run our tests. We can also always create tests which operate on a different MenuStack instance should we need.

public TestMenus(ITestOutputHelper output)
{
    this.output = output;

    MenuStack menuStack = new MenuStack();

    Menu m1 = new Menu();
    m1.Key = "menu-party";
    m1.Location = new TilePoint(1, 2);
    m1.Size = new MenuSize(50, 30);

    Menu m2 = new Menu();
    m2.Key = "menu-options";
    m2.Location = new TilePoint(38, 1);
    m2.Size = new MenuSize(10, 20);

    Menu m3 = new Menu();
    m3.Key = "menu-time";
    m3.Location = new TilePoint(38, 25);
    m3.Size = new MenuSize(10, 4);

    Menu m4 = new Menu();
    m4.Key = "menu-location";
    m4.Location = new TilePoint(30, 31);
    m4.Size = new MenuSize(25, 1);

    menuStack.Push(m1);
    menuStack.Push(m2);
    menuStack.Push(m3);
    menuStack.Push(m4);

    this.menuStack = menuStack;
}

Here we create a MenuStack with an overall structure which resembles that of the Final Fantasy 7 Party menu. The exact location and size of each individual menu isn't extremely important for now.

TestMenuStack

[Fact]
public void TestMenuStack()
{
    List<string> keys = new List<string> {
        "menu-party",
        "menu-options",
        "menu-time",
        "menu-location"
    };

    Assert.Equal(keys.Count, menuStack.Count());
    Assert.All(menuStack.Keys(), key => keys.Contains(key));
}

We will check to see if we have all the expected Menu instances we had created within the constructor. The assertion statements simply check to see if we have the expected number, and if all keys match the keys of the Menu instances found in MenuStack.

TestMenuStackPeek

[Fact]
public void TestMenuStackPeek()
{
    var m = menuStack.Peek();

    Assert.Equal("menu-location", m.Key);
    Assert.Equal(4, menuStack.Count());
}

Another small, and easy test to see if the expected top-level Menu matches the one we expect to be (the location menu). Additionally, the Peek method in MenuStack should not alter the context of the menu system, so we should still expect the same number of Menu instances to be in the MenuStack.

TestMenuStackPop

[Fact]
public void TestMenuStackPop()
{
    menuStack.Pop();
    var m = menuStack.Pop();

    Assert.Equal("menu-time", m.Key);
    Assert.Equal(2, menuStack.Count());
}

We remove 2 Menu items from the MenuStack. What is left should just be 2 Menu instances, with the top level being the time menu.

TestMenuStackClear

[Fact]
public void TestMenuStackClear()
{
    menuStack.Clear();

    Assert.Equal(0, menuStack.Count());
}

Pretty self explanatory test here. We just test to see if there is no longer any Menu objects after clearing the MenuStack.

TestMenuWithContent

[Fact]
public void TestMenuWithContent()
{
    var mainCharacterName = "Terry Token";
    var gameStore = GameStore.GetInstance();
    gameStore.Put<string>("MainCharacterName", mainCharacterName);

    Menu m = new Menu();

    var line1 = "Hello! $NAME$, is $FOOD$ good?";
    MenuContentToken mcLine1 = new MenuContentToken(GameStore.GetInstance());
    mcLine1.Key = "line-1";
    mcLine1.Size = new MenuSize(line1.Length, 1);
    mcLine1.Content = line1;
    mcLine1.Location = new TilePoint(1, 1);
    mcLine1.Replacers = new List<string> {
        "Jrpg.SampleGame.Menus.Tokens.MenuTokenReplacerNameTest, Jrpg.SampleGame",
        "Jrpg.SampleGame.Menus.Tokens.MenuTokenReplacerFoodTest, Jrpg.SampleGame"
    };
    mcLine1.Replace();

    var line2 = "Yes";
    MenuContentOption mcLine2 = new MenuContentOption(GameStore.GetInstance());
    mcLine2.Key = "line-2";
    mcLine2.Size = new MenuSize(line2.Length, 1);
    mcLine2.Location = new TilePoint(4, 2);
    mcLine2.Content = line2;
    mcLine2.Handler = "Jrpg.SampleGame.Menus.Options.MenuOptionHandlerYesTest, Jrpg.SampleGame";

    var line3 = "No";
    MenuContentOption mcLine3 = new MenuContentOption(GameStore.GetInstance());
    mcLine3.Key = "line-3";
    mcLine3.Size = new MenuSize(line3.Length, 1);
    mcLine3.Location = new TilePoint(4, 3);
    mcLine3.Content = line3;
    mcLine3.Handler = "Jrpg.SampleGame.Menus.Options.MenuOptionHandlerNoTest, Jrpg.SampleGame";


    m.AddContent(mcLine1);
    m.AddContent(mcLine2);
    m.AddContent(mcLine3);

    var rendered = m.DebugRender();

    this.output.WriteLine(rendered);

    Assert.Contains(mainCharacterName, rendered);

    this.output.WriteLine("Choosing the NO option");

    Assert.Null(gameStore.Get<string>("OptionResult"));

    ((MenuContentOption)m.GetContent("line-3")).Handle();

    Assert.Equal("NO", gameStore.Get<string>("OptionResult"));
}

This test is a little more complex, and requires a bit of explanation. As mentioned earlier, our rendered prompt will look something like this:

|--------------------------------------------------|
| Hello! Terry Token, is Pie good?                 |
|    Yes                                           |
|    No                                            |
|--------------------------------------------------|

Since creating MenuContent objects require a GameStore object, we first retrieve the global instance which we have. If no global instance has yet been instantiated, the Jrpg.System framework will do that automatically for us.

We then set the property MainCharacterName in our GameStore instance to be Terry Test. We will need this later in our test.

We create a Menu and create several MenuContent objects to build our prompt. Our prompt is only three lines, so we'll just need a single MenuContentToken instance to dynamically display the name of our hero, and two MenuContentOption objects to display the Yes, and No choices for this prompt.

For MenuContentToken, our Content string with the token is:

Hello! $NAME$, is $FOOD$ good?

We have 2 tokens which need to be replaced, so we will need to create 2 MenuContentTokenReplacer objects to handle replacement of these tokens.

In Jrpg.SampleGame, we will create:

  • MenuTokenReplacerNameTest.cs
using Jrpg.MenuSystem;
using Jrpg.System;

namespace Jrpg.SampleGame.Menus.Tokens
{
    public class MenuTokenReplacerNameTest : MenuContentTokenReplacer
    {
        public MenuTokenReplacerNameTest() : base()
        {
            Token = "$NAME$";
        }
        public override string Replace(GameStore g)
        {
            return g.Get<string>("MainCharacterName");
        }
    }
}
  • MenuTokenReplacerFoodTest.cs
using Jrpg.MenuSystem;

namespace Jrpg.SampleGame.Menus.Tokens
{
    public class MenuTokenReplacerFoodTest : MenuContentTokenReplacer
    {
        public MenuTokenReplacerFoodTest() : base()
        {
            Token = "$FOOD$";
        }

        public override string Replace(Jrpg.System.GameStore g)
        {
            return "Pie";
        }
    }
}

For both of the above, notice that we have assigned the Token property to what the replacer should look for when executing its replacement logic. Then the Replace method is implemented to return the final string in which we should replace the token by.

I had decided to make MenuTokenReplacerNameTest interesting by having it access the GameStore to return the value which corresponds to the main character name to be used for replacement when MenuContentToken does its replacement logic. This is similar to features in JRPGs where dialogues use the custom character name of a player in place of the original name.

Implementing the MenuContentOptionHandlers to add functionality to the prompt choices is similar to implementing a MenuTokenReplacer object.

The implementations for MenuOptionHandlerYesTest.cs, and MenuOptionHandlerNoTest.cs are similar, and just set a GameStore property of OptionResult to be the corresponding prompt label. This is how our test can check to see if the player had selected a specific prompt.

using Jrpg.MenuSystem;
using Jrpg.System;

namespace Jrpg.SampleGame.Menus.Options
{
    public class MenuOptionHanderYesTest : MenuContentOptionHandler
    {
        public void Handle(GameStore g)
        {
            g.Put<string>("OptionResult", "YES");
        }
    }
}

using Jrpg.MenuSystem;
using Jrpg.System;

namespace Jrpg.SampleGame.Menus.Options
{
    public class MenuOptionHandlerNoTest : MenuContentOptionHandler
    {
        public void Handle(GameStore g)
        {
            g.Put<string>("OptionResult", "NO");
        }
    }
}

Finally, we want to test DebugRender by outputting a representation of the constructed Menu, and assert that the GameStore has the right OptionResult value after selecting the No option in the prompt.

Result

Conclusion

So we've been able to write a some unit tests to validate the basic design of our Menu System. However, we still need to address a few gaps in our system which we have been deferring. The main question is how to render these menus in Unity, and how the various Render methods interact with the engine. This will be explored in the next post!