Procedural Wording in Unity

Procedural Wording in Unity

In this article I am going to cover how to create a procedural system in Unity that creates unique text content at runtime.

In the example project, I am satisfying an actual use case for a game I am working on that populates random books in a store. I need each book to have a unique title and condition.

Requirements

Setup

Create a new Unity project. I am using the default 3D core template.

With the default "SampleScene" opened, choose the "Main Camera" game object and change the Clear Flags from Skybox to Solid Color.

image.png

In the Project window, under Assets, create a new folder for "Scripts" and another for "Data".

image.png

Procedural Books

In this example, I want to generate random books, including the following properties:

  • Fiction or Nonfiction
  • Title
  • Condition

We're going to build a very basic grammar system using one MonoBehaviour and two ScriptableObject scripts.

image.png

Words Asset

First we need a container for random words that will be used in titles and condition descriptions. Create a new script in the Scripts folder for storing random words.

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "WordsAsset", menuName = "Procedural Wording/WordsAsset")]
public class WordsAsset : ScriptableObject
{
    [SerializeField] private List<string> words;

    public string GetRandom()
    {
        var word = words[Random.Range(0, words.Count)];
        return word;
    }
}

Now you can create new assets in the editor to store a list of words at design time and use GetRandom to choose one from the list at runtime.

Let's extend this to also capitalize words based on a design time setting for each asset. I'll use the System.Globalization namespace for this.

using System.Collections.Generic;
using System.Globalization;
using UnityEngine;

[CreateAssetMenu(fileName = "WordsAsset", menuName = "Procedural Wording/WordsAsset")]
public class WordsAsset : ScriptableObject
{
    [SerializeField] private bool capitalize;
    [SerializeField] private List<string> words;

    public string GetRandom()
    {
        var word = words[Random.Range(0, words.Count)];

        if (capitalize)
            word = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(word);

        return word;
    }
}

Rule Asset

Now we need to be able to use one or more word lists to replace tokens in a sentence. To keep things simple, I am going to use the string.Format method to accomplish this.

At design time, you would create a sentence or paragraph that is peppered with numbered tokens that will be replaced by the format method. Each numbered token will correspond to the index of a list of WordAsset instances.


using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "RuleAsset", menuName = "Procedural Wording/RuleAsset")]
public class RuleAsset : ScriptableObject
{

    [TextArea(10, 50)]
    [Tooltip("A valid format string using {n} to denote the position of each word list entry")]
    [SerializeField] private string format;

    [Tooltip("The position of each word list should match it's position in the format string")]
    [SerializeField] private List<WordsAsset> wordlists;

    public string Apply()
    {
        // store the results of each word list
        var words = new List<string>();

        // get a random word from each words asset
        foreach (var list in wordlists)
        {
            words.Add(list.GetRandom());
        }

        // pass the list of random words to the format method
        return string.Format(format, words.ToArray());
    }
}

Data

We have our two new custom assets that derive from ScriptableObject. Let's create the data to create unique books.

Use the new context menu entries to add instances to the project.

image.png

Create the following assets in the "Data" folder for random word lists.

  • BookConditions
  • BookTitlesFiction_1
  • BookTitlesFiction_2
  • BookTitlesNonfiction_1
  • BookTitlesNonfiction_2

Create the following assets in the "Data" folder for rules.

  • ConditionRule
  • FictionTitleRule
  • NonfictionTitleRule

image.png

Data - Random Words

Add some book conditions to the "BookConditions" asset.

image.png

Add book title words to the other four assets.

image.png

image.png

image.png

image.png

Data - Book Rules

Let's add some format strings and the random word data to the rules.

image.png

image.png

image.png

Book Controller

Now we have enough data to generate random books, we need a game object in the scene to orchestrate the procedure and display content using a UI text field.

Create a new script called "BookController" in the Scripts folder.

using TMPro;
using UnityEngine;

public class BookController : MonoBehaviour
{
    [Tooltip("The text field to update at runtime.")]
    [SerializeField] private TMP_Text text;

    [Header("Rules")]
    [Space(10)]

    [SerializeField] private RuleAsset fictionTitleRule;
    [SerializeField] private RuleAsset nonfictionTitleRule;
    [SerializeField] private RuleAsset conditionRule;

    private void OnEnable()
    {
        // Choose fiction or nonfiction book randomnly 
        bool fiction = Random.value > 0.5f;

        if (fiction)
        {
            SetText(fictionTitleRule.Apply(), conditionRule.Apply());
        }
        else
        {
            SetText(nonfictionTitleRule.Apply(), conditionRule.Apply());
        }
    }

    private void SetText(string title, string condition)
    {
        text.text = title + condition;
    }
}

This script will decide at random if the book is fiction or non-fiction and then reference the rule assets created earlier to generate random content.

Add a new, empty game object to the scene called "BookController" and add this script to it. Add the three rule assets to the appropriate fields.

With the "BookController" game object selected in the scene, add a TextMeshPro text element and then reference it in the "Text" field of the controller.

image.png

** If prompted to import TextMeshPro Essentials, do so

Change the "Text (TMP)" component width and height.

image.png

Play!

Now we have implemented our architecture, the BookController has a reference to the text component to display a book, and it references the rules for the title and condition, which then reference our random word lists.

Test it out in play mode.

image.png

With the BookController game object selected, check and uncheck the active flag to trigger the OnEnable method and generate new content.

image.png

image.png

image.png

Summary

In this article I demonstrated a simple way to generate random, unique content. There are many ways to extend this functionality with tokenizers, regex, etc. This solution also works well with the Addressables package, allowing you to update content after distribution - more on that another day. Feel free to contribute and comment.

Did you find this article valuable?

Support Steve Mcilwain by becoming a sponsor. Any amount is appreciated!