Story Nodes & Open Roads [Dev Log #4]
Story Nodes & Open Roads
I am trying to get into the habit of making consistent dev logs, and I think the best way to do that is going to be to feature some of the design process, rather than just trying to showcase actual gameplay. I linked my Dev Log #4 video, but wanted to make a text post as well.
In this post, I wanted to break down how I am programming my story structure - the actual system that allows for a linear story that the character can follow and progress in. Originally, I had a Dialogue System & Story System separately, but eventually decided it made sense to have StoryNodes consist of dialogue, and have any other one-off dialogue in separate places. So here is the basic layout of my story system.
- StoryNode.cs
- This is the atomic unit for a "piece" of story. A StoryNode object consists of the following:
- storyNodeName - a unique identifier so I can reference a given StoryNode in other scripts, or in nextStoryNode
- DialogueBlock - this is a custom class that contains all the dialogue for a given StoryNode. It has multiple pieces that allow for full conversations between multiple characters, as well as a list of choices presented at the end for the player to choose between. Not every DialogueBlock has choices, as not every conversation needs to trigger a decision from the It includes two fields:
- A list of DialogueEntry objects - a custom class in of itself that acts as the atomic unit of a conversation. It includes:
- speakerName - which character is speaking
- dialogueString - the actual line of dialogue. This can be multiple sentences/panels, just one given "chunk" of dialogue for a single character
- A list of StoryChoice objects - another custom class that allows dialogue to trigger an external function. In action, a button is displayed for each "choice", and whichever the player chooses is executed. It includes:
- choiceText - the text shown in the button, i.e. the "choice" being made
- resultText - optional field that displays a "reaction" when the button is pressed
- actionToTrigger - the name of a function to run if this choice is selected
- A list of DialogueEntry objects - a custom class in of itself that acts as the atomic unit of a conversation. It includes:
- triggerNPC - if a StoryNode is triggered by talking to a specific NPC, we can designate that NPC here
- nodeDescriptor - this is a line of text that displays when the player hits "H" and acts as a "hint" for the story. This "hint" helps the player figure out how to advance the story.
- nextStoryNode - the name of the next node. This allows us to link StoryNodes together in a linear fashion. Note there is some wiggle room for jumping between nodes non-linearly, but generally we will progress from node -> node as defined by this field
- nodeComplete - just what it says, a boolean field indicating whether the node is complete or not.
- CompleteNode() - a function that completes the node. It does three things:
- Prints debug logs to indicate the node was completed, as well as a warning if the node was already marked complete
- Sets nodeComplete = true
- Runs the AdvanceToNextNode() function in StoryController - see below
- This is the atomic unit for a "piece" of story. A StoryNode object consists of the following:
- StoryController.cs
- This is the most important function for managing the story. It is where we store .json data into actual StoryNode objects, and host a series of functions to interact with the StoryNodes. Includes the following:
- allStoryNodes - List of StoryNodes, containing (as expected) all story nodes. Loaded during the Awake() function
- currentNode - Stores the current StoryNode
- Awake() - Has several error checks and debug logs, but most importantly runs the LoadStoryData() function from the StoryLoader class on each of the .json story files we have. In effect, it loads our story into StoryNode objects.
- RetrieveStoryNodeByName() - Allows us to return a StoryNode by its name
- RetrieveCurrentStoryNode() - returns currentNode, which is a private field
- StoreCurrentStoryNode() - stores a node out-of-order. Useful for non-linear sequences, such as death cycles, where we can't simply move to the nextStoryNode
- AdvanceToNextNode - grabs the nextStoryNode field from the current node, and sets it as the new currentNode field
- LoadStoryNodes() - Used for the save/load system; goes into the save file and looks for any completed nodes, and marks them as "completed" in the current game state. Then sets the currentNode as the first/earliest StoryNode that is not yet marked "complete"
- This is the most important function for managing the story. It is where we store .json data into actual StoryNode objects, and host a series of functions to interact with the StoryNodes. Includes the following:
- StoryLoader.cs
- This is where the actual functionality for loading from .json files is stored. In short, we load the raw .json text and parse it into a list of StoryNodes, which is then utilized in the Awake() function in StoryController.
- StoryTrigger.cs
- This is a highly-manual, not-particularly-modular class that defines a ton of logic related to how a story node is triggered - i.e. it is a prerequisite to a StoryNode being run. There are a few ways in which a StoryNode is triggered
- Interacting with a character (launching dialogue)
- Changing scenes (triggering a cutscene)
- Hitting a "trigger object" (invisible GameObject that triggers a cutscene/dialogue)
- This script also contains one-off logic such as "TriggerBlockers", which are objects that block a player from accessing certain locations until prerequisite StoryNodes have been completed. For instance, in Act 1, there are two locations where lifeforms can be found - call them Location A and Location B. Location B is locked behind a blocker until all lifeforms in Location A have been discovered. If a player tries to access Location B before the prereqs are completed, a line of dialogue is triggered, and they are physically prevented from going forward. If they try to access Location B after the prereqs are completed, they proceed through as if there is no blocker at all.
- This is also where most non-linear logic is contained. For instance, when you are introduced to the Community Center, you can explore the three rooms in any order. Once you have explored all three rooms, you unlock a new StoryNode; however, when you have only explored #1 and #2, you don't have access to that new StoryNode, and must continue exploring the Community Center
- This is a highly-manual, not-particularly-modular class that defines a ton of logic related to how a story node is triggered - i.e. it is a prerequisite to a StoryNode being run. There are a few ways in which a StoryNode is triggered
- StoryAction.cs
- This is a somewhat-more-modular class that contains logic relating to actions selected during a StoryNode - so when a player is presented with a StoryChoice, and chooses one of the "choices" presented, this is where the corresponding "actionToTrigger" is run. In effect, this is a list of named functions with their own unique functionality based on the possible choices presented in a given StoryNode.
- For example, the first StoryNode has two choices - either leave the ship, or wait in the ship. If you choose option A, you run "StartDisembarkSequence", which loads the new scene and completes the StoryNode. If you choose option B, you run "RestartNode", which ends the dialogue sequence and does not complete the StoryNode. Other logic includes triggering cutscenes, launching battles, and triggering camera FX.
- This is a somewhat-more-modular class that contains logic relating to actions selected during a StoryNode - so when a player is presented with a StoryChoice, and chooses one of the "choices" presented, this is where the corresponding "actionToTrigger" is run. In effect, this is a list of named functions with their own unique functionality based on the possible choices presented in a given StoryNode.
This is a general breakdown of the system I have created to tell my story. I will admit, it is highly linear in its construction, but for my purposes it has been working well. It also allows for a pretty straightforward way of creating conversations and triggering actions out of those conversations. Hope this is informative, and make sure to check out the YouTube video breaking it down, as well as try out the demo of my game DISCOvery: Corporate Explorer!!
Get DISCOvery: Corporate Explorer
DISCOvery: Corporate Explorer
Discover new lifeforms on a distant unexplored planet!
Status | In development |
Author | Bearrsona |
Genre | Role Playing |
Tags | 2D, Atmospheric, Indie, No AI, Pixel Art, Turn-Based Combat |
Languages | English |
More posts
- DISCOvery: Corporate Explorer [Demo is live!]27 days ago
Leave a comment
Log in with itch.io to leave a comment.