Dev Diaries - Building a JRPG - Part 8

A Menu System

I want to switch gears for a moment, and think about implementing a menu system. It has been on my TODO list for some time now, and with the remaining features we need to implement to "finish" our Demo, it has become unavoidable that we need some sort of menu system for dialogue, and action selection for battles.

Going forward, the demo needs some sort of mechanism to communicate events which have occurred through something other than a Debug.log statement. It makes sense that at this point, we need to work on a Menu system!

Wow, I think I lucked out on this tone! Fortunately, menus in classic JRPGs tend to have limited variations in size, orientation, and location when displayed on the screen. Given that the typical 16-bit SNES JRPG was on a screen resolution of 256x224, or a 32-bit PlayStation JRPG on a logical 320x240, it wouldn't really make sense to have that many menu dialogs with lots of complexity in content being displayed at once on a 20" CRT TV.

For the rest of this design document, I'm going to use the menu system in Final Fantasy 7 as an example. I found most of the images, and screenshots off of Google image search and modified them here for this discussion.

An Existing Example

Party menu

The party menu is the most familiar to players of the game. Excluding the tutorial dialog in purple being shown in the above figure, there are 4 menu dialogs which are being displayed at once. I've mapped them out here:

Mapped Party Menu

The obvious is observed:

  • Menu dialogs can be stacked on top of one another
  • Menu dialogs can be arranged in various locations on screen. The above shows dialogs 2, 3, and 4 stacked on top of 1, and arranged to the right-most area of the screen.
  • Although dialogs can be of various sizes, they are predictable. For example, the option dialog, 2, in the Party menu has relatively the same width as the item actions dialog from the Items menu.

Item Menu

Orientation

The last point from the previous section is very important to consider since it also has to do with orientation of a menu. To reiterate, two menu dialogs may have the same height, but different widths, or the same width, but different heights. Now, excuse the crappy drawings.

The following shows a menu dialog that looks to be either in landscape, or portrait orientation. Notice though that both dialogs have the same area of being 2 units. The landscape oriented dialog is a 2x1, while the portrait dialog is 1x2.

A2

Creating a bigger dialog, we have two dialogs of the same area of 6 units, but in different orientations. Again, the only difference here is the arrangement of dimensions.

A3

Since most JRPG dialogs are always vertical (rotated by 90°), or horizontal (no rotation at all), we can then imply menu orientation through the size in dimensions of the dialog.

If a larger width as opposed to height implies a dialog in landscape orientation, then a larger height opposed to the width implies a portrait orientation.

Location

Dialogs can be placed in different locations. Looking at the Final Fantasy party arrangement screen, we see that the dialogs are laid out nicely, but in a deliberate manner.

Party Select Screen

A menu system should have relative control over to how its dialogs are laid out. However, it need not be in absolute locations.

Screen

In order to make sense of "locations" within the screen. We need to abstract the screen of the game into a grid. The location of each menu won't be the absolute pixel coordinate on the screen, but rather a tile coordinate. The tile coordinate refers to where the top-left most tile of the menu dialog should be placed at.

The main challenge is how we should divide the screen to render out a number of tiles. The NES and SNES rendered the screen in units of tiles. Although the SNES rendered frames at a resolution of 256x224, the screen was actually split into a number of 8x8 tiles with the effective tile resolution being 32x30 tiles in with and height.

We can do something similar. Except, the challenge here is that the screen resolution of our game can vary. If we choose to have a fixed tile size like the NES/SNES, then we cannot accurately depict a menu which can be legible across all screen resolutions.

For example, if we choose tiles to be 16x16, then splitting a screen to units of 16x16 will result in different number of tile dimensions at a resolution of 1024x768 (64x48), compared to 1920x1200 (120x75). If we assume our dialogs are to be placed in tile coordinates, then they will be placed differently on different screen resolutions.

What we can do is scale our tile units based on the screen resolution. Instead of just having a single 16x16 tile, we can scale the tile unit once we have exceeded a certain screen resolution. To make things easy, we can scale the tile unit by the width of the screen.

Suppose we standardize our screen resolution to be 64x36 tiles -- giving us a 16:9 aspect ratio. This means that based on the tile size, we can generate a different size of the overall menu based on screen resolution. The table here shows how large a full-screen menu would be based on the tile size:

Tile SizeScreen Resolution
16x161024 x 576
24x241536 x 864
32x322048x1152
48x483072 x 1728

However, notice that a full screen menu generates a size that isn't mapped to your traditional "full-screen" resolution. For instance, at 1024 pixels in width, we are used to seeing 768 pixels in corresponding height. Here, we see that 1024 pixels in width generate 576 pixel in height. Another example is that 1536x864 is also an unconventional screen resolution.

We can compensate for this by actually ensuring that our 64x36 "grid" always is centered on the screen.

For example, for a 1280x720 resolution game. We can render 1024x576 menu screen using 16x16 tiles like so:

Full Screen Menu

Yeah, I know another ugly drawing, but stick with me! By centering the menu grid on a screen, we can blacken out the borders and create the illusion of a "full screen menu".

To find the offset for how big the menu grid should be in pixels, all we need to know is the screen resolution:

MENU_WIDTH = (SCREEN_WIDTH - TILE_WIDTH*GRID_TILES_WIDTH) / 2;
MENU_HEIGHT = (SCREEN_HEIGHT - TILE_HEIGHT*GRID_TILES_HEIGHT) / 2;

Given that our screen resolution is 1280x720, we find that our menu grid offset should be 128 pixels from the top, and 72 pixels to the left. This means our menu grid should start rendering at (128, 72).

We can generalize this by providing screen resolution thresholds for a particular tile size

If SCREEN_WIDTH <= 1280
    TILE_SIZE = {W: 16, H: 16}
If SCREEN_WIDTH <= 1920
    TILE_SIZE = {W: 24, H: 24}
If SCREEN_WIDTH <= 2560
    TILE_SIZE = {W: 32, H: 32}
If SCREEN_WIDTH <= 3840
    TILE_SIZE = {W: 48, H: 48}

You can see that there are drawbacks to this. The most obvious being that we do not support screen resolutions above the traditional 4K resolution. This should be handled on a case-by-case basis if you find it to be really important that the game must run above that resolution. (Which you would, of course.)

Content

Our menu should also have some content. How should content be displayed and laid out?

Consider a very complex menu in Final Fantasy 7, like the Materia menu:

Materia

There is a lot going on, but we can see a menu dialog may consist of various content such as colored text and behavior. There is also another consideration to take in that the content in these menu dialogs can be interactive.

A menu can have a list of choices which when selected, can cause an event to be triggered, and handled. More interestingly, a menu can also control the flow of dialogue:

Dialog

Meaning menu dialogs can serve as prompts, so our Menu system must be built to serve these purposes.

We can establish a general type called MenuContent to represent a piece of content in our Menu.

Our MenuContent can be several sub-types. Depending on the sub-type, the MenuContent can take on specific schema. Here are some examples of some individual MenuContent items which are renderable by the system.

{
    type: "TEXT",
    content: "Flower Girl",
    location: { w: 0, h: 0 },
    size: { w: 11, h: 1 },
    key: "xyz-000"
}

The basic MenuContent will have a type, and key property associated with it. The type property is useful for allowing decoding of the MenuContent into its proper sub-typed object. The key property is useful for referencing/reading the specific MenuContent item without having to explicitly check for the content within.

The above example is a representation of a line. It spans 10 units, and displays the content "Flower Girl", and also starts at (0, 0).

The location, and length units are by the current tile size being used to display the menu grid. So a single unit can be a square 16, 24, 32, or 48 pixels. This also means that a single character, including whitespace, it assumed to occupy 1 tile unit.

Now what about a 2x2 image?

{
    type: "IMAGE",
    content: "/path/to/resource.jpg",
    location: { w: 0, h: 0 }
    size: { w: 2, h: 2 },
    key: "xyz-001"
}

Let's try to build 2 dialogs with this. First the recreating the flower girl dialog, and second, part of a party menu entry.

[
    {
        type: "TEXT",
        content: "Flower Girl",
        location: { w: 0, h: 0 },
        size: { w: 11, h: 1 },
        key: "xyz-000"
    },
    {
        type: "TEXT",
        content: "What happened?",
        location: { w: 0, h: 1 },
        size: { w: 14, h: 1 },
        key: "xyz-001"
    },
    {
        type: "OPTION",
        content: "You'd better get out of here"
        location: { w: 3, h: 3 },
        size: { w: 28, h : 1 },
        handler: "Some.Class.To.Handle, Assembly-CSharp",
        key: "abc-123",
        index: 0
    },
    {
        type: "OPTION",
        content: "Nothing... hey...",
        location: { w: 3, h: 4 },
        size: { w: 17, h: 1 },
        handler: "Some.Class.To.Handle, Assembly-CSharp",
        key: "abc-456",
        index: 1
    }
]

Here's a mock-up of the first two lines being rendered which I did in Photoshop. The menu dialog is rendered in a 1024x576 pixels, 64x36 tile grid. Only a portion of this grid is taken up by the specific menu dialog with its text contents.

Example Screen

Okay so how about a party menu?

[
    {
        type: "IMAGE",
        content: "/path/to/resource.jpg",
        location: { w: 1, h: 1 },
        size: { w: 6, h: 6 },
        key: "hij-123"
    },
    {
        type: "TEXT",
        content: "Cloud",
        location: { w: 9, h: 1 },
        size: { w: 5, h: 1 },
        key: "hij-456"
    },
    {
        type: "TEXT",
        content: "LV",
        location: { w: 9, h: 2 },
        size: { w: 2, h: 1},
        key: "jkl-123"
    },
    {
        type: "TOKEN",
        content: "$LEVEL$",
        location: { w: 11, h: 2 },
        size: { w: 3, h: 1 },
        replacer: "Some.Class.To.Replace.Token, Assembly-CSharp",
        key: "jkl-456"
    },
    ...
]

MenuContent Schema Types

A MenuContent object can take on the following sub-types:‌

  • TEXT
  • IMAGE
  • TOKEN
  • OPTION

All are pretty self explanatory, but one sub-type I would like to dive deeper on is one which represents a prompt. Each item in the menu content relating to prompt choices will be of type OPTION. The OPTION type has a handler tied to it. This is an instance of MenuHandler. If the cursor chooses an option presented, then the menu system should invoke this MenuHandler.‌

These MenuHandlers all have a common interface, so it is predictable on how the system can execute the handler to handle the option. We should also allow access to the main GameStore in every instance of the MenuHandler. The GameStore can be passed during object construction of the MenuHandler, or passed as a parameter to the method executing the logic for the handler. The detail is not too important, but the intention of initial construction of the MenuHandler is through using .NET reflection, so we just need to be sure we have all the data we need at that point.

MenuStack

This is a stack-like data structure which can push, and pop active Menu objects on screen which will allow management of multiple dialogs being displayed at once. The cursor can then act upon the top of the MenuStack. For example, selecting the MenuContent.

The screen will always render the MenuStack in a bottom up fashion, like the painter's algorithm.

Cursor

The menu must keep track of the cursor. Or, is it the other way around? The cursor has to keep track of which MenuContent item it has focus on.

Whenever we push a Menu dialog to the stack, the cursor's active menu context is altered with a push of an object which keeps track of the position of the cursor in that context of the dialog.

So, cursor should maintain a stack of "memorized" MenuContent information. Whenever the cursor changes focus on a new menu dialog, we can maintain a MenuContentMemory object in its stack. The MenuContentMemory consists of the current MenuContent item of OPTION type and its key property.

The cursor can also be either visible, or invisible. This will allow us to "hide" the cursor from rendering if the active menu dialog contains no select-able option.

Example

Here we push a menu dialog that is 6x6 into the MenuStack at location (1, 1). The landscape orientation noted down is just a sanity check on how the dialog should appear.

Menu Stack 1

A subsequent menu dialog is then pushed onto the stack.

Menu Stack 2

This time it is a menu dialog that is 2x3, and appears to be in "portrait" position because the width is smaller than the height. We position it to be at (0, 6).

Some actions can trigger a "pop" on the menu stack:

  • Cursor which selects a MenuContent option
  • Dismissal of the current dialog by using the "Cancel" button

DataTypes and API

At this point, I think I've communicated enough in how I intend to implement this basic menu. What would be even better is if I now formally define the data types, and some of the API for some of this stuff!

Let's start from the bottom-up, starting with how menus are located on screen, up to how they are managed by the MenuStack.

TilePoint {
    x: int;
    y: int;
}

The TilePoint is a simple structure that houses the tile coordinates in the context of the grid of the menu system. A single location represents a tile location, not a pixel location.

MenuSize {
    width: int;
    height: int;
}

MenuSize defines the size of the menu dialog, or content which occupying the grid of the menu system in tile units.

MenuContentType {
    TEXT,
    IMAGE,
    TOKEN,
    OPTION
}

MenuContentType is the enum which will be used to help define the specific type so of a MenuContent, so that these items can be parsed correctly.

MenuContent {
    type: MenuContentType;
    key: string;
    size: MenuSize;
    location: TilePoint;
    render: () => void;
}

MenuContent is the most basic type of, and non-displayable type of content which can be used by the menu system. The MenuContent type is more of a basic interface, and the specifics must be implemented in a child class.

MenuContentText {
    content: string;
    render: () => void;
}

MenuContentImage {
    content: string;
    render: () => void;
}

MenuContentToken {
    content: string;
    replacer: string;
    render: () => void;
}

MenuContentOption {
    content: string;
    handler: string;
    index: int
    render: () => void;
}

MenuContentText, MenuContentImage, MenuContentToken, and MenuContentOption concretely define the specifics on how to render content within a menu dialog. The key here is that all of the concrete MenuContent types must explicitly override the render method for the object to be displayed.

  • MenuContentToken expects a replacer class, and so we can just define a simple class that builds dynamic content to be rendered:
MenuContentTokenReplacer {
    replace: () => string;
}
  • MenuContentOption expects a handler class, and just like MenuContentToken, we can keep it simple right now. We assume it also takes our global game state as a parameter so that we can "do stuff" with it.
MenuContentOptionHandler {
    handle: (GameStore g) => void;
}

Next, here's Menu. Right now, we just care about managing, and displaying the MenuContent. The render method will render out all the MenuContent items associated with it.

Menu {
    contents: Dictionary<string, MenuContent>;
    location: TilePoint;
    size: MenuSize;
    addContent: (MenuContent mc) => void;
    removeContent: (string key) => void;
    render: () => void;
}

To manage all our Menu objects, we will need the MenuStack. The MenuStack is just a basic stack class that will store Menu objects in a stack-like structure allowing pushing, and popping of elements. Additionally, the render method will render all the menu dialogs from the bottom-up, resembling execution of the painters algorithm.

MenuStack {
    menus: Stack<Menu>;
    push: (Menu m) => void;
    pop: () => Menu;
    render: () => void;
}

The Cursor implementation is simple. We are only concerned about two things relating to the cursor:

  1. Which MenuContentOption it is currently pointing at.
  2. Is it visible?
Cursor {
    memory: Stack<MenuContentMemory>;
    visible: boolean;
    execute: () => void;
    peek: () => MenuContentMemory;
    push: (MenuContentMemory mcm) => void;
    pop: () => MenuContentMemory;
}

A convenience method, execute is also provided in the Cursor instance to invoke the handler that is currently associated with the MenuContentOption which is pointed by the MenuContentMemory instance at the top of its stack.

MenuContentMemory {
    key: string;
}

For now, we just need a key field to point back to the specific MenuContentOption in MenuContent belonging to the Menu at the top of the stack.

Conclusion

I think we now have enough to start writing some code to test some of our assumptions. Like all system requirements, they will definitely change once issues come to light from implementation! In the next article, we'll see what problems we will encounter, and what will need to be changed about our design!