Visual Novel System
A visual novel package built to read data from a JSON file. Supports scalable event handling as well as conversation branching.
After completing my work on Lythe the Forgetful Witch, I was left with one, big question:
"How could I make this dialogue system more scalable?"
This package is my solution.
01
Script Entry Struct
This system starts with VNScriptEntry, a struct that stores all of the data we plan to pull from the JSON file. For each variable used in the JSON file, we have an associated member variable.
At the current stage in the project, my struct tracks three fields:
-
The name of the character currently speaking
-
The text being spoken
-
The name of the event you wish to call, formatted like a method call
-
An array that holds all events. We'll be generating the events later using the event name we get from our JSON file

Here is an example of the format needed for the JSON files. VNScriptEntry objects are nested within an array. When the file is read, groups of entries are saved together inside an array stored in a separate ScriptEntryHolder class. These holders are then referenced by their IDs when the conversation branches, allowing different player choices to result in different conversations.

This is the VNScriptEntry struct. When the JSON file is read, each object is reformatted as an instance of this struct.
02
VN Event Class
The VNEvent class is the parent class that all events must derive from. Though never directly implemented, the ScriptEntry class holds an array of these events. Using polymorphism, all events can be kept together in an array. Additionally, all events can be triggered using their inherited OnHandleEvent() method.

Though there are several methods in the VNEvent class, there are two key players. First is the OnHandleEvent() method we discussed above. Next is the InitializeEvent() method, which is used to assign any references unique to the event subclass. In example, if we use an event to spawn a background image, the name of the file we wish to spawn will be passed into the initialization method as a string. Its file is then found using Unity's addressables system inside the subclass. The same process is used for other event types, such as playing audio cues or loading different JSON scripts.
03
Script Reader Class
The VNScriptReader class is used to parse the JSON text into instances of the VNScriptEntry class. This is done used the Newtonsoft.Json package. If an entry's _eventName field is not null, then we parse the event name and its parameters from the _eventName string and generate an instance of the appropriate VNEvent subclass.

The CreateVNEvents() method takes the string event names and parameters from our script entry, then converts them into an array of VNEvent objects. These events are created through a series of steps.
First, the ParseEventData method creates an array all all event strings that were passed to us. This makes it so that we can call multiple events on a single script entry.
Next, the GetEventType() method returns a substring of the event name from the event string. This is used to remove parenthesis and parameters from our event string. Then, we create an instance of the associated event type.
Finally, the GetEventParameter() method separates the parameters from the event string. This works very similarly to the GetEventType() method.
After we've created an instance of the correct VNEvent subclass, we can pass the subclass the parameter substring (which in most cases will be a file name) through its InitializeEvent() method.
04
So how does it scale?
So, how does this system address my scalability concerns? Well, while it is still in development, in its current state I am able to create any number of events without impacting the underlying structure. Any event I want to create needs to meet only two criteria:
-
It must inherit from the VNEvent class
-
It must follow the "VN____Event" naming system.
Because I am using polymorphism to create instances of my event classes, my system will not work if the event does not inherit from the VNEvent class. Additionally, because I am creating my type instances by their string names, failing to use the correct naming convention will result in a failure to create a valid type.
Using this solution, the scalability of this system completely floors any of my prior attempts at this type of dialogue/narrative centric system.

Here is an example of a custom VNEvent. This event is used to spawn background images. Its InitializeEvent() method overrides the base class to parse the parameter string for a background image file name. Then, assigns its _bgSprite field to the associated sprite.
In my current project, I've used this pattern to spawn background images, spawn CG images, play audio cues, change scripts, trigger player choice prompts, and swap conversation paths after receiving input during player choices.