Unity + Ink: Part 4: Tags and Rich Text

Unity + Ink

The narrative scripting language Ink works with the game engine Unity through a plugin that allows for quickly recompiling, testing, and integrating Ink files in a Unity project.


Tags and Rich Text

Working across this series, the Ink Unity Integration plugin was imported, tested, and then used to build a simple button-based interface. However, there is one more tool Ink has that can be used in Unity: tags.

In Ink, tags are extra code that is ignored. They start with the hash character, #, and run to the end of the line or until another tag is found.

Tags are also attached to the text in front of them. For a single line, its tag would go at the end. For multiple lines, the tags would go at the end of all of text.


"What? Have you never seen a dialogue system before?" #Dan

One way to use tags in Ink when working with Unity, then, is as “speech tags.” These can mark who is saying which words during dialogue.

Parsing Tags

One of the properties not previously discussed so far is currentTags. This is also part of the StoryAPI and, like currentChoices, is populated by a call to the Continue() function.

The currentTags property holds a List<string>. If there are no tags, it will be empty. Therefore, its own property Count can be tested. If its “count” is greater than zero, use the first tag. Otherwise, ignore it.


// Get the current tags (if any)
List<string> tags = story.currentTags;

// If there are tags, use the first one.
// Otherwise, just show the text.
if (tags.Count > 0)
{
   newTextObject.text = tags[0] + " - " + text;
}
else
{
   newTextObject.text = text;
}

Now, if there is a speech tag, it will precede any text and show who is speaking in the interface.

Rich Text

The Ink documentation mentions using tags for things like color or other styling information for text. This could be done, but Unity provides an easier way: rich text.

By default, Text objects have rich text enabled. This means that text can be styled in Ink and passed through to Unity. (In fact, Inky supports a smaller subset of styles as well!)


"What? Have you <i>never</i> seen a dialogue system before?" #Dan

To add emphasis on some text, the <i></i> markup can be used. Added to the text, it will be passed through to Unity and show up as emphasized.

Instead of using tags for colors, the rich text code can be added to Ink directly — or even used directly in Unity itself. For example, to style the color of any speech tags (names), it could be added around them.


 // Load the next block and save text (if any)
 string text = getNextStoryBlock();

 // Get the current tags (if any)
 List<string> tags = story.currentTags;

 // If there are tags, use the first one.
 // Otherwise, just show the text.
 if (tags.Count > 0)
 {
    newTextObject.text = "<color=grey>" + tags[0] + "</color> - " + text;
 }
 else
 {
    newTextObject.text = text;
 }

Marking Options

As a small user interface change, each option (what is shown to the user) can be marked with a number matching its choice index. However, as choosing a zero option might be off-putting for some users, this could be increased by one.


foreach (Choice choice in story.currentChoices)
{
   Button choiceButton = Instantiate(buttonPrefab) as Button;
    choiceButton.transform.SetParent(this.transform, false);

   // Gets the text from the button prefab
   Text choiceText = choiceButton.GetComponentInChildren<Text>();
   choiceText.text = " " + (choice.index + 1) + ". " + choice.text;

   // Set listener
   choiceButton.onClick.AddListener(delegate {
      OnClickChoiceButton(choice);
   });

}

Keep-on Continuing

Along with improving the interface, another change (and function) needs to be made. So far, the function Continue() has been used. This is good if the goal is load content line by line. However, with multi-line content, and those potentially using more rich text markup, this is not as useful.

The function ContinueMaximally() will load content until it finds the next set of choices. In this code, it can be used to load blocks of text until it reaches another choice or the end of the story.


// Load and potentially return the next story block
string getNextStoryBlock()
{
   string text = "";

   if (story.canContinue)
   {
      text = story.ContinueMaximally();
   }

   return text;
}

Styled User Interface

Updating the Ink code, it can now include the following as a much more complex example which also includes usages of rich text markup.


"What? Have you <i>never</i> seen a dialogue system before?" #Dan

* ["No. Of course I have!"]
    "That's good. As this is the beginnings of one. 
    
    "It even has <b>multiple</b> lines of dialogue.
    
    "That's neat, right?" #Dan
    -> DONE

InkExample.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Ink.Runtime;
public class InkExample : MonoBehaviour
{
public TextAsset inkJSONAsset;
private Story story;
public Button buttonPrefab;
// Start is called before the first frame update
void Start()
{
// Load the next story block
story = new Story(inkJSONAsset.text);
// Start the refresh cycle
refresh();
}
// Refresh the UI elements
// – Clear any current elements
// – Show any text chunks
// – Iterate through any choices and create listeners on them
void refresh()
{
// Clear the UI
clearUI();
// Create a new GameObject
GameObject newGameObject = new GameObject("TextChunk");
// Set its transform to the Canvas (this)
newGameObject.transform.SetParent(this.transform, false);
// Add a new Text component to the new GameObject
Text newTextObject = newGameObject.AddComponent<Text>();
// Set the fontSize larger
newTextObject.fontSize = 24;
// Load the next block and save text (if any)
string text = getNextStoryBlock();
// Get the current tags (if any)
List<string> tags = story.currentTags;
// If there are tags, use the first one.
// Otherwise, just show the text.
if (tags.Count > 0)
{
newTextObject.text = "<color=grey>" + tags[0] + "</color> – " + text;
}
else
{
newTextObject.text = text;
}
// Load Arial from the built-in resources
newTextObject.font = Resources.GetBuiltinResource(typeof(Font), "Arial.ttf") as Font;
foreach (Choice choice in story.currentChoices)
{
Button choiceButton = Instantiate(buttonPrefab) as Button;
choiceButton.transform.SetParent(this.transform, false);
// Gets the text from the button prefab
Text choiceText = choiceButton.GetComponentInChildren<Text>();
choiceText.text = " " + (choice.index + 1) + ". " + choice.text;
// Set listener
choiceButton.onClick.AddListener(delegate {
OnClickChoiceButton(choice);
});
}
}
// When we click the choice button, tell the story to choose that choice!
void OnClickChoiceButton(Choice choice)
{
story.ChooseChoiceIndex(choice.index);
refresh();
}
// Clear out all of the UI, calling Destory() in reverse
void clearUI()
{
int childCount = this.transform.childCount;
for (int i = childCount 1; i >= 0; i)
{
GameObject.Destroy(this.transform.GetChild(i).gameObject);
}
}
// Load and potentially return the next story block
string getNextStoryBlock()
{
string text = "";
if (story.canContinue)
{
text = story.ContinueMaximally();
}
return text;
}
// Update is called once per frame
void Update()
{
}
}
view raw InkExample.cs hosted with ❤ by GitHub