【ScenarioFlow】Other Features: Merging Multiple Scenario Scripts and Getting Token Codes

ScenarioFlow
Tool versions
  • Unity: 2022.3.35f1
  • UniTask: 2.5.4
  • ScenarioFlow: 1.1.0
  • SFText Extension Pack: 1.1.0
    • Syntax: v1.1.0
    • Formatter: v1.0.0
    • Utility: v1.0.0

Introduction

In this article, we are going to learn about a feature of merging multiple scenario scripts into a single scenario script and a feature of getting token codes specified for asynchronous commands in ScenarioFlow.

These features are not essential for creating dialogue scenes, but they can be useful depending on the situation. Especially, we need to get token codes specified for asynchronous commands when implementing a save function for dialogue scenes.

Set Up the Sample Project

Import other-features.unitypackage below to an Unity project to preapare programs and objects needed for this learning.

The sample project includes the following five scenario scripts, and we will run Story (Composite Scenario Script). Make sure that this script is asigned to the ScenarioScript property of the ScenarioManager object on the scene, and then run the sample scene.

  • Story (Composite Scenario Script)
  • Story-Intro (SFText)
  • Story-Composite (SFText)
  • Story-TokenCode (SFText)
  • Story-End (SFText)
Run the sample scene

The important points of this sample are, (1) a running dialogue scene consists of multiple scenario scripts, and (2) token codes specified for asynchronous commands are shown on the console. Refer to 【ScenarioFlow】How to Write SFText and 【ScenarioFlow】Practical Features: Branching Scenarios and Fast-Forwarding for the details of the commands in the sample project.

Merge Sceanrio Scripts by Composite Script

How to Use Composite Script

We can merge multiple scenario scripts into a single scenario script using composite script.

We create a new composite script by right-clicking on the Project window and selecting Create/ScenarioFlow/Composite Scenario Script.

Create a new composite script

We asign scenario scripts to be merged to the Scenario Script property after creating a new composite script.

The sample project has the composite script named Story, on which the four scenario scripts are registered. Then, the single dialogue scene that consists of the dialogue scenes described in the four registered scenario scripts will be played if we run the Story composite script.

Story composite script. It consists of the four files,Story-Intro, Story-Composite, Story-TokenCode, andStory-End.

Important Points of Composite Script

Reagarding use of composite script, keep the following points in mind.

  • Scenario scripts registered on a composite script are merged in the order of the registration
  • Registered scenario scripts are independent of each other
  • A registered scenario script can refer to labels declared in other registered scenario scripts

First, a composite script generates a single scenario script from multiple scenario scripts, and the generated content differs depending on the registration order. A composite script merges registered scenario scripts from the top to the bottom. Make sure that the Story composite script merges the registered scenario scripts in the order of Story-Intro, Story-Composite, Story-TokenCode, and Story-End initially, but the data content shown on the Inspector window changes if you change the registration order of the scenario scripts.

Second, scenario scripts registered on a composite script are independent of each other. This means, for example, macro scopes declared in a SFText registered on a composite script have no impact on other registered SFTexts.

Finally, a scenario script registered on a composite script can refer to labels declared in other registered scenario scripts. Confirm that the show two selections async command declared in the Story-Intro SFText refers to the composite script lecture label and token code lecture label, which are declared in the Story-Composite SFText and Story-TokenCode SFText, respectively. Also, note that labels that have the same name can’t exist across scenario scripts registered on the same composite script.

A snippet of Story-Intro.sftxt (this script refers to labels in other SFTexts)
Sheena      | So could I ask you which one you want to know?                            | 
            |                                                                           | 
$f-promised | show two selections async                                                 | 
            | {How to merge multiple scenario scripts}                                  | 
            | --- Jump to {composite script lecture}                                    | 
            | {How to retrieve specified token codes}                                   | 
            | --- Jump to {token code lecture}                                          | 
SFText
A snippet of Story-Composite.sftxt
#label    | //============ {composite script lecture} ============//   | 
$parallel | change character image async                               | 
          | Change the icon to {smile}                                 | 
Sheena    | OK! Let's learn how to merge multiple scripts.             | 
SFText
A snippet of Story-TokenCode.sftxt
#label    | //============ {token code lecture} ============//                          | 
$parallel | change character image async                                                | 
          | Change the icon to {smile}                                                  | 
Sheena    | OK! Let's learn how to retrieve the token code specified for a command.     | 
SFText

When to Use Composite Script

We may want to use a composite script when we collaborate with others to make a large dialogue scene. We can make a single dialogue scene while the team members work independently by creating small dialogue scenes and merging them by a composite script.

Get Token Codes Specified for Commands

Get Token Codes through the ITokenCodeGetter Interface

The ScenarioTaskExecutor class implements the TokenCode property, and we can retrieve the token code specified for a command when that command is called by reading that property through the ITokenCodeGetter interface. (The reason why we access the property thorough the interface is not to access other irrelevant members implemented by the class.)

As the value of the TokenCode property changes every time a command is called, we have to take care of the place where the value is read and the timing of reading the value in order to get the value properly.

One of the best ways to read the TokenCode property is to create a decorator of the IScenarioTaskExecutor interface and read the token code value as the first process of the ExecuteAsync method that is a member method of the interface.

The IScenarioTaskExecutor interface is an interface implemented by the ScenarioTaskExecutor class, which has the ExecuteAsync(UniTask, CancellationToken) method as a member method. We give an instance of the ScenarioTaskExecutor class to the constructor of the ScenarioBookReader class basically, but as it is an implementation of the IScenarioTaskExecutor interface that the constructor of the ScenarioBookReader class actually requires, we can include the function of a decrator we create by giving it to that constructor.

The Implementation Example in the Sample Project

Let’s take a look at the example in the sample project. In the sample project, the ScenarioTaskExecutorTokenLoggingDecorator class is implemented as a decorator of the IScenarioTaskExecutor interface.

ScenarioTaskExecutorTokenLoggingDecorator.cs
using Cysharp.Threading.Tasks;
using ScenarioFlow.Tasks;
using System;
using System.Threading;
using UnityEngine;

namespace OtherFeatures
{
	public class ScenarioTaskExecutorTokenLoggingDecorator : IScenarioTaskExecutor
    {
        private readonly IScenarioTaskExecutor scenarioTaskExecutor;
        private readonly ITokenCodeGetter tokenCodeGetter;

        public ScenarioTaskExecutorTokenLoggingDecorator(IScenarioTaskExecutor scenarioTaskExecutor, ITokenCodeGetter tokenCodeGetter)
        {
            this.scenarioTaskExecutor = scenarioTaskExecutor ?? throw new ArgumentNullException(nameof(scenarioTaskExecutor));
            this.tokenCodeGetter = tokenCodeGetter ?? throw new ArgumentNullException(nameof(tokenCodeGetter));
        }

        public UniTask ExecuteAsync(UniTask scenarioTask, CancellationToken cancellationToken)
        {
            // Get a token code
            var tokenCode = tokenCodeGetter.TokenCode;
            Debug.Log(tokenCode);
            return scenarioTaskExecutor.ExecuteAsync(scenarioTask, cancellationToken);
        }
    }
}
C#

The ExecuteAsync method of this decorator gets a token code from an implementation of the ITokenCodeGetter interface as the first process, print it to the console, and finally call the ExecuteAsync method of the implementation of the IScenarioTaskExecutor class stored as a field. The scenarioTask argument is the asynchronous command being called. We must not operate this argument in the method of a decorator because any processes regarding asynchronous commands are performed by the ScenarioTaskExecutor class.

An instance of the ScenarioTaskExecutor class is asigned to both of the scenarioTaskExecutor field and tokenCodeGetter field in the ScenarioManager class. Also, note that in the ScenarioManager class, a decorator (i.e., an instance of the ScenarioTaskExecutorTokenLoggingDecorator class) is given to the constructor of the ScenarioBookReader class but an instance of the ScenarioTaskExecutor class isn’t.

A snippet of ScenarioManager.cs
ScenarioTaskExecutor scenarioTaskExecutor = new(buttonNotifier, buttonNotifier);
ScenarioTaskExecutorTokenLoggingDecorator scenarioTaskExecutorTokenLoggingDecorator = new(scenarioTaskExecutor, scenarioTaskExecutor);
ScenarioBookReader scenarioBookReader = new(scenarioTaskExecutorTokenLoggingDecorator);
C#

With these implementations, we can get the token codes specified for asynchronous commands being called. Run the sample scene to make sure that the corresponding token code is output to the console when a command is called.

Token codes specified for commands being called are output to the console while the sample scene is running

When to Get Specified Token Codes

We may want to get token codes specified for commands, for example, when we implement a save function in dialogue system. The save function in this context means a function that enables the player to save data by unit of a dialogue whenever they want while a dialogue scene is running.

To implement such a save function in ScenarioFlow, we have to capture necessary data on the scene (e.g., character positions, character images, background image, music, etc.) at appropriate times periodically. An appropriate time is a moment no command is running and therefore the environment on the scene doesn’t change. A moment no command is running is, for example, a moment the system waits for the player’s action (e.g., clicking the screen). A moment the system waits for the player’s action is, for example, a moment right after a command with a non-fluent token code (i.e., standard, forced, or promised) finishes.

In conclusion, it can be said that we sometimes have to get token codes specified for commands in order to capture data necessary for a save function at appropriate times.

Actually, we may not have to get any token codes depending on the way to implement a save function. However, the ITokenCodeGetter interface is provided so that we can implement this kind of function in multiple ways.

Summary

In this article, we learned how to merge multiple scenario scripts into a single scenario script and how to get token codes specified for commands.

First, we can register scenario scripts on a composite script to merge them into a single scenario script. The important points are:

  • Scenario scripts registered on a composite script are merged in the order of the registration
  • Registered scenario scripts are independent of each other
  • A registered sceneario script can refer to labels declared in other registered scenario scripts

Second, we can get the token code specified for an asynchronous command being called with the TokenCode property of the ITokenCodeGetter interface. We have to take care of the place where we read the property and the timing of reading it, and one of the best ways is to create a decorator of the IScenarioTaskExecutor interface and read the property as the first process in the ExecuteAsync member method.

We may not have many opportunities to use the features we learned in this article, but we can make use of them in some cases. It is better to keep in mind that these kinds of features exist.

Comments