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
- Unity 2021.3 LTS
- GitHub Project: github.com/PlayableDesign/unity-procedural-..
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
.
In the Project
window, under Assets
, create a new folder for "Scripts" and another for "Data".
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.
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.
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
Data - Random Words
Add some book conditions to the "BookConditions" asset.
Add book title words to the other four assets.
Data - Book Rules
Let's add some format strings and the random word data to the rules.
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.
** If prompted to import TextMeshPro Essentials, do so
Change the "Text (TMP)" component width and height.
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.
With the BookController game object selected, check and uncheck the active flag to trigger the OnEnable method and generate new content.
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.