top of page
  • Writer's pictureWilliam Santos

Visual Novel System

Overview

The Visual Novel System is a Unity Package I developed to introduce flexible dialogue and scripted sequences into my game projects. The core of this package is a 3rd-party plugin called Yarnspinner developed by Secret Labs. The framework already comes with basic dialogue and scripting functionality out of the box. My package extends theirs to support features that are typical of visual novels, namely character portraits, animations, background transitions, etc.


Tech Summary

Genre

Visual Novel

Team Size

1 Developer

Engine

Unity 2021

Platform

PC

Development Time

1 Month


 

Content

About Yarnspinner

As mentioned before, Yarnspinner is a free third-party plugin that offers a powerful dialogue system out of the box. To display dialogue, the designer just needs to create a Yarn File. It’s basically a formatted text file.

title: T2_Start
tags: 
colorID: 0
position: 0,-50
---
<<clearAll>> 
<<enter Saoirse Left Neutral>>
This is Level 2
<<setSpeaker Saoirse Happy>>
You have 7 turns to complete the mission. Let's go!
===

As you may have guessed, it can even support functions, and already has two pre-defined functions for you to use.

<<wait .75>> // Dialogue System waits for .75f seconds
<<stop>> // Ends dialogue

And this is where its real strength comes in. I can extend the base functionality of Yarnspinner to support even more functions, effectively allowing me to create entire scripted sequences off of this setup.


Architecture

In keeping with the spirit of Yarnspinner, I wanted my Visual Novel System to be just as flexible such that I could apply it to any number of future game projects in different ways. As such, this project makes full use of abstract classes, especially for character portraits, since I have no idea how my future self may want to use this system.


Implementation Notes

This system was designed using Yarnspinner 1.2.7. It may not work in later version of Yarnspinner. That said, I may approach this project again to build an even stronger system. If you are curious about code, you can find it here on GitHub.


The Actor

I defined portraits displayed in my system as actors. They're broken down into three parts:

  • The Actor Base which acts as the container and processes all UI requests.

  • The Actor Definition where the designer defines all the relevant data regarding the actor (character portraits, animators, names, etc.)

  • The Actor Scriptable Object where the actual data is interfaced with in the Unity Editor.

Now, I had no idea how my future self would implement character portraits for his games. He could have animated portraits or maybe he wanted single static portraits. Rather than worry about every possible implementation, I will delegated the responsibility of figuring that out to him. So all three parts, the Actor Base, Actor Definition, and Actor Scriptable Object were all made to be abstract for him to extend.


ActorBase is the container that is responsible for displaying the Actor to the player. The parts I made abstract were how it enters, exits, and sets the emotions of the actor.

using UnityEngine;
using VN_System.UI;

namespace VN_System.Actors
{
    public abstract class ActorBase : MonoBehaviour
    {
        [SerializeField]
        protected ActorTypeID _actorTypeID = ActorTypeID.Static;
        public ActorTypeID actorTypeID => _actorTypeID;

        [SerializeField]
        protected GameObject _main = null;

        [SerializeField]
        protected Sprite _defaultImage = null;

        protected ActorDefinitionBase _actorDefinition = null;
        public ActorDefinitionBase actorDefinition => _actorDefinition;

        public EmotionID currentEmotion { get; protected set; }

        public abstract void EnterActor(ActorDefinitionBase aD, EmotionID emotionID);

        public abstract void ExitActor();

        public abstract void SetEmotion(EmotionID newEmotion);

        public virtual void Register(ActorSlot slot)
        { }
    }
}

Contained inside the ActorBase is an ActorDefinitionBase. This class is effectively a data container that the Visual Novel System can parse to display the correct character portraits.

using UnityEngine;
using VN_System.UI;

namespace VN_System.Actors
{
    public abstract class ActorDefinitionBase
    {
        [SerializeField, Header("Template")]
        private ActorPrefab _actorPrefab = new ActorPrefab();

        [Header("Speaker Name")]
        public string firstName = "";
        public string lastName = "";
        public string fullName => firstName + " " + lastName;
        
        public ActorTypeID GetActorType()
        {
            return _actorPrefab.actorTypeID;
        }

        public GameObject GetPrefab()
        {
            return _actorPrefab.prefab;
        }

        public abstract BasePortrait GetPortrait(EmotionID emotionID);
    }
}

In order for the Designer to be able to tweak and edit that data, I created a ScriptableObject wrapper class. This would allow the user to instantiate an ActorDefinitionBase in the Unity Editor and update the portrait data as they need it.

using UnityEngine;

namespace VN_System.Actors
{
    public abstract class ActorSO_Base : ScriptableObject
    {
        public abstract string GetActorID();

        public abstract ActorDefinitionBase GetActorDefinition();
    }
}

Linking the Functionality to Yarnspinner

Adding new functionality to Yarnspinner was extremely simple. The plugin has a component called DialogueRunner. If I use the AddCommandHandler function, I can quickly add new functions that can be called directly from a standard Yarn file. With this, I created a helper class whose sole purpose was to store all the commands inside the DialogueRunner at runtime before any dialogue is even run.


using System;
using UnityEngine;
using VN_System.Actors;
using VN_System.UI;
using VN_System.Media;
using VN_System.Transitions;
using TMPro;
using Yarn.Unity;

namespace VN_System.Core
{
    public class VN_CommandLibrary : MonoBehaviour
    {
        ...

        private DialogueSystem _dialogueSystem = null;
        private DialogueRunner _dialogueRunner = null;

        private void Awake()
        {
            _dialogueRunner = GetComponent<DialogueRunner>();
            _dialogueSystem = GetComponent<DialogueSystem>();
            RegisterCommands();
        }

        protected virtual void RegisterCommands()
        {
            _dialogueRunner.AddCommandHandler("show", Show);
            _dialogueRunner.AddCommandHandler("hide", Hide);
            _dialogueRunner.AddCommandHandler("setSpeaker", SetSpeaker);
            _dialogueRunner.AddCommandHandler("enter", EnterActor);
            _dialogueRunner.AddCommandHandler("exit", ExitActor);
            _dialogueRunner.AddCommandHandler("clearAll", ClearAll);
            _dialogueRunner.AddCommandHandler("clearActors", ClearActors);
            _dialogueRunner.AddCommandHandler("say", Say);
            _dialogueRunner.AddCommandHandler("playSFX", PlaySFX);
            _dialogueRunner.AddCommandHandler("playMusic", PlayMusic);
            _dialogueRunner.AddCommandHandler("setVideo", SetVideo);
            _dialogueRunner.AddCommandHandler("transition", Transition);
        }
        
        ...
        
    }
}

AddCommandHandler takes two parameters.

_dialogueRunner.AddCommandHandler("show", Show);

The first parameter is the string which gets referenced in the Yarn File.

<<show>>

The second parameter is a void function that takes an array of string arguments. Depending on the use case, some functions might have no arguments and others might have multiple. The only trick is parsing the string array appropriately.

protected virtual void Show(string[] info)
{
    ShowDialogueContainer();
}

The Visual Novel System in Action

Persona 5 has been a big interest of mine for a while, and the community actually dug up all of the portraits from the game. So to give the Visual Novel System its first test, I created a demo using Persona 5 assets and sounds. To implement the portraits, all I had to do was inherit from ActorBase, ActorDefinitionBase, and ActorSO_Base. Everything else was handled discretely by the Visual Novel System, and this was the final product:


Below is the Yarn File from that entire scene:

title: Start
tags: 
colorID: 0
position: -458,-595
---
<<clearAll>> 

<<transition FadeIn>> 
<<setVideo P5R_OP_a>> 

<<playMusic P5_00>>
<<setForeground leblanc_ext>>
<<setBackground leblanc_ext>> 
<<wait 3>>

<<clearForeground>> 

<<setSpeaker Narrator>>
On a sunny afternoon in a little cafe in Tokyo...

<<transition Wipe>> 
<<setBackground leblanc_int>> 
<<hide>>

<<wait 2>>

<<enter Futaba Left Happy>> <<say Futaba_01263>> 
Makoto! It's been awhile. How have you been?
<<enter Makoto Right Happy>> <<say Makoto_00351>>
Well I've recently been taking drawing lessons from Yusuke.
<<setSpeaker Futaba Surprised>> <<say Futaba_01382>>
Yusuke?
<<setSpeaker Makoto Neutral>> <<say Makoto_00319>>
Yep. Wanna see what I've been working on?
[[ Sure |Sure]]
[[ No |Exit]]
===
title: Exit
tags: 
colorID: 0
position: -182,-466
---
<<setSpeaker Makoto Sad>> <<say Makoto_00347>> 
I understand.
<<setSpeaker Futaba Sad>> <<say Futaba_01289>> 
I'm sorry.
<<setSpeaker Makoto Neutral>> <<say Makoto_00365>> 
Don't be. I'm sure you have other things to do, yes?
<<clearActors>>
<<setSpeaker Narrator>>
Restarting scene...
<<transition FadeOut>>
[[ Start ]]
===
title: Sure
tags: 
colorID: 0
position: -184,-788
---
<<setSpeaker Makoto Happy>> <<say Makoto_00346>> 
GREAT! Wait right there. I'll be right back.
10 minutes. Just 10 minutes.

<<transition Wipe>> 

<<playSFX HighVoice>> <<wait 0.5>>
<<clearActors>> <<hide>>
<<playSFX HighVoice>> <<wait 0.5>>
<<playSFX HighVoice>> <<wait 0.5>>
<<wait 1>>

<<enter Futaba Right Surprised>> <<say Futaba_01283>> 
...
<<setSpeaker Futaba Concerned>> <<say Futaba_01275>> 
...um
<<enter Makoto Left Neutral>> <<say Makoto_00427>> 
Well?
<<setSpeaker Futaba Concerned>> <<say Futaba_01314>> 
Well...it's a really nice um...
->Bird?
    <<setSpeaker Makoto Surprised>> <<say Makoto_00440>> 
    A bird...is that right?
->What is this?
    <<setSpeaker Makoto Sad>> <<say Makoto_00322>> 
    Is it really that bad that you can't tell?

<<clearActors>>
<<setSpeaker Narrator>>
Restarting scene...
<<transition FadeOut>> 
[[ Start ]]
===

Now this might look terrifying in a code editor, but Yarnspinner actually provides its own Editor for editing nodes, so this is what I really saw:

This made editing the story nodes far easier to do.

And if I ever needed to debug a specific problem in the nodes, I could always look at the file directly.


277 views0 comments

Recent Posts

See All

Comments


bottom of page