Metroidvania Pacing Chart in Ink

Learn how to ink-ify exported designs from the Metroidvania Pacing Chart tool

Intro

Metroidvania Pacing Chart (shortened from this point on as MVPC) is a nifty little browser app for designing Metroidvania game worlds. The tool can be found here.

Metroidvania is a portmanteau of Metroid and Castlevania and is used to describe games with a huge map of connected rooms that are mostly inaccessible. Exploring initially available rooms affords keys (typically player abilities) that enable access to more rooms.

The MVPC tool is a nice way of mapping out the overall journey of a Metroidvania game. You can define areas, roads, events, currencies and items and prototype a Metroidvania playthrough very quickly. Definitely read this devlog and load up the SuperMetroid.json file to play around with the tool.

In the past, I used MVPC to prototype an ambitious game called Organic. It was never finished but I experimented with writing Organic in Ink using the JSON data exported from MVPC. The experiment was successful and it is indeed possible to “port” data from MVPC to Ink.

Screenshot of Metroidvania Pacing Chart with Organic loaded

Organic is too complex for tutorialization so I made a new chart called King Castle for this purpose. The exported JSON looks like this after I prettified it for better reading.

{
    "areas": [
        {
            "description": "Start area",
            "interactions": [
                {
                    "locks": [],
                    "type": "Road",
                    "value": "Arena"
                },
                {
                    "locks": ["Crown"],
                    "type": "Road",
                    "value": "Castle"
                }
            ],
            "name": "Home"
        },
        {
            "description": "Fighting Place",
            "interactions": [
                {
                    "locks": [],
                    "type": "Road",
                    "value": "Home"
                },
                {
                    "locks": [],
                    "type": "Event",
                    "value": "ConqueredAll"
                },
                {
                    "locks": ["ConqueredAll"],
                    "type": "Item",
                    "value": "Crown"
                }
            ],
            "name": "Arena"
        },
        {
            "description": "Crowns needed!",
            "interactions": [
                {
                    "locks": [],
                    "type": "Road",
                    "value": "Home"
                }
            ],
            "name": "Castle"
        }
    ],
    "currencies": [],
    "events": ["ConqueredAll"],
    "items": ["Crown"]
}

The graph generated by the above JSON data looks like this. Note that area.description data is not currently usable. I edited it after the fact for descriptive sugar.

Screenshot of Metroidvania Pacing Chart with King Castle loaded

King Castle is extremely trivial: you can’t get to the Castle because you have no Crown. So you go to the Arena, conquer everyone and get a Crown. Now you can go to the Castle. Not much to it but it has everything we need to get started with the porting process. The only element missing is Currency but Currencies are just Items with numeric values. Think integers versus bools.

Processing MVPC to Ink

It’s important to understand how to map MVPC’s concepts to Ink’s concepts. We’ll focus on the structures first in order of importance and focus on relationships after.

🟦Areas

Areas in MVPC are rooms or levels. They are supposed to be critical to the player journey. To keep MVPC graphs simple, non-essential rooms should be ignored. Areas contain Interactions which is how we setup relationships. To create the equivalent of an Area in Ink, we can use Knots.

It is possible to use Regular Expressions to convert different parts of JSON data directly to Ink which is what I had to do for Organic. It may also be possible to create a script that does the same. In this case, we can just pull up Inky and write the following to define all Areas in the chart.

=== Home
-> DONE

=== Arena
-> DONE

=== Castle
-> DONE

Note that while MVPC supports spaces in its Area names, Ink does not support spaces in Knot names. Underscores are officially recommended instead of spaces but you could also remove the spaces for PascalCase.

🗝️Items

Items (and Currencies) can be expressed using Ink variables. You could define a variable for each item or even make use of Ink Lists. However, while writing Organic in Ink, it became apparent that using just variables was cumbersome because of complex item retrieval. For example, if you could find the same item or currency in multiple places, you would need to re-write similar logic in multiple knots. Therefore, I determined that variables are not enough and instead Tunnels should be used. The tunnel will allow us to get items from any knot, potentially performing extra action and eventually return to the original knot. Another advantage of using Tunnels is that if an Item is permanent (i.e. it cannot be lost once gained which is supposed to be the normal use case), we can skip using a unique variable and use the tunnel’s knot address for condition checking.

// Normal Method
// VAR crown = false
// === get_crown
// You have earned the Crown!
// ~ crown = true 
// ->->

//Shortcut Technique (no variables)
=== get_crown
You have earned the Crown!
->->

So using this method, each item will have its own knot. For a currency, it may not hurt to use two for gaining and losing value. The following is from Organic.

VAR credit = 0
// . . .
===GetCredit(amount)
~ credit = credit + amount
->->

===SpendCredit(amount)
~ credit = credit - amount
->->

🕛Events

For similar reasons, Events also need to be tunnels to maintain flexibility. Events can be toggled on and off and can be used to lock or unlock items or even other events! An Event can be forced to toggle once by locking itself too. In King Castle however none of these features are utilized and so Events in this example can just be a simple tunnel with no corresponding variable.

=== Conquered_All
You have defeated everyone.
You can now pick up a reward.
->->

🛣️Roads

MVPC’s Roads are an Interaction that lets you setup a relationship between two Areas. Roads can be one way or bi-directional too. We can use Ink Choices (aka Options) and Diverts to setup Roads. The choices must be sticky to allow back and forth navigation. To test in Inky, you’ll need to go to start area immediately which can be accomplished with an initial divert.

-> Home
=== Home
You are at the start area.
+ [Go to Arena] -> Arena
+ [Go to Castle] -> Castle

=== Arena
You are at the fighting place.
+ [Go to Home] -> Home

=== Castle
You are at the castle.
+ [Go to Home] -> Home

Now with this, you can move between rooms which is like traversing the map. But this is not the default relationship, so it’s time to lock things up.

🔒Locks

Roads, Items and Events can be locked with Ink Conditions. If using a Knot address or zero-comparison for testing, just checking the name is enough. In King Castle, we need to prevent access to the Castle unless the Crown is attained. So the necessary change looks like.

+ {get_crown} [Go to Castle] -> Castle

Similarly, we can lock item pickup or events with conditions but we need to add these actions first by using tunnels!

🎬Actions

Similar to moving along roads, interactions for items and events will use choices. However, these choices will need to be non-sticky so that they can only be done once. It’s also possible to make them sticky and use conditions but this is not necessary in this case. The choices will enter the tunnel and exit back to the Arena. This is so that the choices can be refreshed and reused.

You are at the fighting place.
* [Conquer them All!] -> Conquered_All -> Arena
* {Conquered_All} [Get Reward] -> get_crown -> Arena
+ [Go to Home] -> Home

All JSON data has been reflected in Ink now and this is the final result with comments stripped out.

-> Home
=== Home
You are at the start area.
+ [Go to Arena] -> Arena
+ {get_crown} [Go to Castle] -> Castle

=== Arena
You are at the fighting place.
* [Conquer them All!] -> Conquered_All -> Arena
* {Conquered_All} [Get Reward] -> get_crown -> Arena
+ [Go to Home] -> Home

=== Castle
You are at the castle.
+ [Go to Home] -> Home

=== get_crown
You have earned the Crown!
->->

=== Conquered_All
You have defeated everyone.
You can now pick up a reward.
->->

Here is a sample run-through from Inky

You are at the start area.

You are at the fighting place.

You have defeated everyone.

You can now pick up a reward.

You are at the fighting place.

You have earned the Crown!

You are at the fighting place.

You are at the start area.

You are at the castle.

Extra Credit

Here are some ways to go beyond.

•Consolidating Actions

It is possible consolidate some interactions in Ink that may not be necessary. For example, if we undergo the ConqueredAll event, we can skip offering a choice to the player and just pick up the crown immediately. This can be done by connecting the two tunnels like so.

* [Conquer them All!] -> Conquered_All -> get_crown -> Arena

•Avoiding Repeated Text

Using tunnels means we return to the knot and the initial text is reshown which is odd. If you look at the sample output, “You are at the fighting place” is displayed three times. There are a few ways of solving this issue but I think the simplest one is to use a Stitch. The stitch can be used to separate the Area description from the possible actions. The tunnel can redirect to the stitch instead of the knot.

=== Arena
You are at the fighting place.
-> Menu
=Menu
* [Conquer them All!] -> Conquered_All -> Arena.Menu
* {Conquered_All} [Get Reward] -> get_crown -> Arena.Menu
+ [Go to Home] -> Home

•Grounding the Navigation

Complex maps with multiple routes can get confusing in pure text. It helps to contextualize by tracking the previous Area. One way of doing this is with the TURNS_SINCE which checks for when we last hit up a knot. This can be used to determine if we came from a specific Area. We can use this in a condition cto change some text to help ground where the player is.

+ [Go { TURNS_SINCE(-> Arena) == 1 : back } to Arena] -> Arena

The above will show the word “back” if the last room we visited was the Arena.

Conclusion

Ink is powerful and as you can see, it’s possible to convert JSON exported from MVPC into a playable text-based prototype in Ink. If you were to look at the JSON text and Ink script, side-by-side, you can see the Ink closely resembles the objects in JSON. Top level array elements are knots, interaction array elements are choices and lock array elements are conditions.