Twine and Unity

After posting A Conversation With Ophion earlier this year, I've had several people ask for more information about getting Twine and Unity working together, and this time a tutorial was specifically requested, so I figured I should take some time to talk a little more about the code and proccess I used to make this combination work for my March project. All of the code below is in C# but it doesn't use any particularly exotic features so converting it to UnityScript or Boo shouldn't be too bad.

To start with, the key to Twine/Unity integration is the twee format. Twine is build on top of a system that uses text files in what they call "twee" format to generate the final story file. As a text-based source format, it's much, much easier to integrate with Unity than working with the full Twine source file, which is in a custom machine-readable format, or with a compiled Twine story.

I'll address each of these below with more detail, but the basic process is:

  1. Generate your content in Twine.
  2. Export to twee format.
  3. Get your twee source available in Unity.
  4. Parse the twee file.
  5. Display passages in your game.
  6. Navigate between passages.
  7. (Optional) Send custom data from Twine.

It's a long list, but several of these steps are super-simple. So, here we go.

1. Generate Your Content in Twine

Obviously one of the big benefits to Twine-Unity integration is the Twine editor. The format of your Twine file will depend on what exactly you're trying to accomplish, so I can't give much in the way of advice there, but the main thing to consider here is exactly which features of Twine/twee you're going to use, because aside from the simple navigation, Twine allows you to use various macros and inline Javascript features, and of course none of those features will work in Unity unless you implement them.

If all you need is a dialogue tree system, as I did, then just using the navigation features will work nicely.

2. Export to Twee Format

Luckily, this is an easy one. In the Twine UI, File menu -> Export Source Code..., save as a .txt file. The resulting file is in twee format. Just remember to also save your story as a normal .tws file to save all of your Twine-specific settings (like your layout), and you can always re-export the twee file whenever you need to make changes.

3. Get Your Twee Source Available in Unity

Another easy step. Unity will import text files from your project folder as Text Assets, which can then be loaded from a script. In my case, and possibly in yours, it's even easier to just bind the asset to a variable at design time.

			
			// Just declare a public TextAsset member:
			public TextAsset tweeSourceAsset;
			
			// Then drag your twee source file into this member
			// in the inspector to hook it up.
			
			
			// Now, reading your twee source into your script is
			// exactly this difficult:
			
			protected string tweeSource;
			
			protected void Awake() {
				tweeSource = tweeSourceAsset.text;
			}
		

4. Parse the Twee File

This is where most of the work really needs to be done. Getting your twee file loaded into a usable format is what makes this integration possible, and what exactly you need from Twine/twee will vary, so all I can do is describe what I needed and how I handled it.

Twee format is fortunately pretty simple, so it's easy to see what needs to happen. The basic format of a twee file is:

		:: Passage Title [tag1 tag2]
		Passage body
		[[This link leads to another passage|Another Passage]]

		:: Another Passage [tag3]
		More text goes here.
	

In simplest terms, a twee file is a series of passages, each passage consisting of a title, optionally a set of tags, and a body consisting of text and links to other passages. So I just put together a simple little class to represent a twee passage in code, and stuck it in a class that parses a given twee source file into a dictionary of passages using the passage titles as keys. The result looked like this, which is a little clunky in places due to the time frame and me still learning as I went, but it got the job done:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Text;

public class TweeParser : BaseBehavior {

// The passage class, which is just title, tags, and body
[System.Serializable]
public class TweePassage {
	public string title;
	public string[] tags;
	public string body;
}

// The TextAsset resource we'll be parsing
public TextAsset tweeSourceAsset;

// Dictionary to hold the passages, keyed by their titles
public Dictionary<string, TweePassage> passages =
	new Dictionary<string, TweePassage>();

// I found it useful during development to have an inspectable
// array of the titles of the loaded passages.
public string[] titles = new string[0];

// The one big string that will hold the twee source file
protected string tweeSource;


void Awake() {
	// Load the twee source from the asset
	tweeSource = tweeSourceAsset.text;
	
	// Parse it
	Parse();
	
	// Populate the reference array
	titles = new string[passages.Count];
	passages.Keys.CopyTo(titles, 0);
}


// Where the magic happens
private void Parse() {

	// A reference to the passage we're currently building from the source
	TweePassage currentPassage = null;
	
	// Buffer to hold the content of the current passage while we build it
	StringBuilder buffer = new StringBuilder();
	
	// Array that will hold all of the individual lines in the twee source
	string[] lines;	
	
	// Utility array used in various instances where a string needs to be split up
	string[] chunks;
	
	// Split the twee source into lines so we can make sense of it while parsing
	lines = tweeSource.Split(new string[] {"\n"}, System.StringSplitOptions.None);
	
	// Just iterating through the whole file here
	for (long i = 0; i < lines.LongLength; i++) {
		
		// If a line begins with "::" that means a new passage has started
		if (lines[i].StartsWith("::")) {
			
			// If we were already building a passage, that one is done.
			// Wrap it up and add it to the dictionary of passages. 
			if (currentPassage != null) {
				currentPassage.body = buffer.ToString();
				passages.Add(currentPassage.title, currentPassage);					
				buffer = new StringBuilder();
			}
			
			/* I know, I know, a magic number and chained function calls and it's
			 * ugly, but it's not that complicated. A new passage in a twee file
			 * starts with a line like this:
			 *
			 * :: The Passage Begins Here [someTag anotherTag heyThere]
			 *				 
			 * What's happening here is when a new passage starts, we ignore the
			 * :: prefix, strip off the ] at the end of the tags, and split the
			 * line on [ into two strings, one of which will be the passage title
			 * while the other has all of the passage's tags, if any are found.
			 */
			chunks = lines[i].Substring(2).Replace ("]", "").Split ('[');
			
			// We should always have at least a passage title, so we can
			// start a new passage here with that title.
			currentPassage = new TweePassage();
			currentPassage.title = chunks[0].Trim();
			
			// If there was anything after the [, the passage has tags, so just
			// split them up and attach them to the passage.
			if (chunks.Length > 1) {
				currentPassage.tags = chunks[1].Trim().Split(' ');	
			}
			
		} else if (currentPassage != null) {
		
			// If we didn't start a new passage, we're still in the previous one,
			// so just append this line to the current passage's buffer.
			buffer.AppendLine(lines[i]);	
		}
	}
	
	// When we hit the end of the file, we should still have the last passage in
	// the file in the buffer. Wrap it up and end it as well.
	if (currentPassage != null) {			
		currentPassage.body = buffer.ToString();
		passages.Add(currentPassage.title, currentPassage);
	}
}
}
	

In the end, you've got a dictionary of passages that can be found by title, and since basic navigation in Twine/twee is based on passage titles, you've got the information you need to navigate through a twee story.

5. Display Passages in Your Game

This is a another area that will be heavily dependent on what specifically you're trying to achieve. The big trick here is that getting your text to appear in-game in a way that allows you to programmatically determine which word was clicked on is non-trivial, at least by default. There may be UI frameworks that make this easier, but I was working with stock Unity features, and this wasn't as easy as I would have liked.

What I ended up doing was creating a prefab representing each word, which rendered that word as a text mesh, and then creating a process that could display a passage by generating Word prefabs for each word in the passage. If the word was linked to another passage, i.e. it would normally be a clickable word in the Twine story that leads to another passage, the word was generated with a collider so it could easily receive mouse events for hovering and clicking. This was painful - getting the colliders to match up with the approximate visual size of the mesh and getting the placement of the words right was a fiddly process, and if you need my advice to implement it, you don't want to get into this in the first place, because it's not for the faint of heart.

6. Navigate Between Passages

If you can figure out how to get the display logic working in a way that makes sense for your project, including how to handle allowing the user to "click" a passage link or some equivalent interface scheme, the actual navigation isn't too bad. You've already got the logic to render a passage, so you just need to know which one to switch to. My generated words knew what passage title they led to (stored in a string named wordLink), so I just set them up to fire an event upward when clicked:

void OnMouseDown() {
if (isLinked) {
	SendMessageUpwards("OnTweeNavigate", wordLink, 
		SendMessageOptions.DontRequireReceiver);	
}
}

That event bubbled up to this guy:

void OnTweeNavigate(string passageTitle) {
if (!navLocked) {
	if (parser.passages.ContainsKey(passageTitle)) {
		StartCoroutine("ShowPassage", parser.passages[passageTitle]);
	} else {
		Debug.Log("Non-existent twee passage '" + passageTitle + "' requested");	
	}
}				
}

And then the ShowPassage method does all the heavy lifting involved in clearing out the scene and displaying the requested passage.

7. Send Custom Data from Twine

This was an unexpected benefit of Twine/Unity integration. Since all passages can have an arbitrary list of tags, I could use tags to write events into the Twine/twee script itself to be passed along to the game, so when a particular passage was displayed, various in-game actions could be triggered. All I had to do is loop through the associated tags whenever I displayed a passage, look for specifically-formatted tags, and if they were found, take the appropriate action. I'll pull this example directly from my write-up for my game:

:: Example Dialogue Passage [camera:one event:exampleEvent earth:24,topLeft]
This is the text that the "Earth" voice speaks in this passage.
----
*[[One of the Asteroid's speech options.|Another passage]]
*[[Another speech option.|Somewhere else]]

When this passage is displayed, the game:

This way I could script music changes, layout preferences, camera angles, and whatever else I wanted to plan out to coincide with specific passages, without making any code changes if I wanted to change when or how certain events occurred.

So That's It

Using the above setup and logic, I got a perfectly usable Unity/Twine integration working. There's a lot of potential here, and it's one of the things I hope to revisit later as a full-blown dialogue planning system.

Hopefully this was useful to someone - feel free to use any of the above logic or code for any reason anywhere - and if anyone has any questions, my twitter handle and email address are both scattered all over this site, so just shoot me a message.