Cradle (formerly UnityTwine) is an open-source plugin for Unity that imports Twine stories, plays them and makes it easy to add custom interactivity via scripting.
Twine is an extremely simple and flexible tool for writing interactive stories. It can be used to create adventure games, branching narratives, dialog trees and much, much more.
Cradle allows writers to independently design and test their stories as they would a normal Twine story, while programmers and artists can develop the interaction and presentation without worrying about flow control. When imported to Unity, Cradle kicks in and bridges the gap.
Features
Imports stories created in Twine 1 and 2
Includes a text player prefab for quick script-free, drag-and-drop story setup
Control how the text is displayed and the links are āclickedā via a simple story API
Powerful cue system for easily triggering scripts on passage enter, exit and update
High performance - stories parsed at import time, not at runtime, and compiled as standard C#
UPDATED! Version 2.0.1
Renamed the project to Cradle
Support for published HTML files, including Twine 2
Story formats: added Harlowe, improved Sugarcane/Sugarcube
Complete rewrite of the asset importer:
Extensible, supports multiple story formats
Error checking and graceful failures that donāt break the project
Complete rewrite of the variable system to allow support for many different types (arrays, datasets, etc.)
Added support for passage fragments to allow deferred code execution and output generation (Harlowe makes extensive use of this feature)
Source code now compiles to standalone assemblies
Documentation
The GitHub project page includes detailed documentation on installation, setup and scripting with many code snippet examples.
Examples Snoozing is a short interactive story created with Cradle. The entire source code is available here.
Clockwork by Aaron Steed, included in the Examples folder of the plugin, is provided courtesy of the author.
I just released an updated version 1.1 that adds wider support for Twine features. I think it is almost ready for the asset store!
Parser now supports any valid Twine passage name
Added extensible macro system
Shorthand display syntax support (with parameters)
Visited, visitedTag, turns, parameter functions
Tags are now simple string arrays like the original Twine
Simplified hook setup
The importer is still sensitive to bad Twine syntax (will cause invalid C# to be generated), so my next focus is to make more robust error detection and reporting and avoid generating scripts that donāt compile.
This is fantastic! I recently left my professional development job to strike out independently on my own adventure game, using Unity as my engine, with Twine as my conversation editor. I had searched for such a plugin and failed, planning to write my own and post it on GitHub myself. Thankfully I tried searching again just before starting to write the codeā¦ and woah, what?! I stumbled upon this! Youāve saved me a lot of work!
During development, if I add any features or improvements to your plugin, Iāll be sure to post them to you.
So far, fantastic plugin! One suggestion of a (very minor) feature that Iāve added and am finding useful.
In my project, Iāve added delegates to my story so that any object can register for every passageās Enter, Idle, and Exit. Using your well-documented story states, itās easy to create these delegates, but I was surprised to not find them in the plugin itself, considering there is support for the much more intricate Hook system, which calls dynamic functions based on a passage name.
I guess Iām just using it slightly differently than you are. I have a generic script that I attach to every element I want to animate, then wait for the specific passage to be entered:
//The declaration of the delegate
public delegate void PassageCallback(string passageName);
//The function called by any object wishing to be informed of an OnEnter call
public void RegisterOnEnterCallback(PassageCallback callbackFunction) {
onEnterCallbacks.Add(callbackFunction);
}
//An example of a class registering a function to later use for animating on a specific passage
public string passage;
//...
void Start () {
storyPlayer.RegisterOnEnterCallback(Enter);
}
void Enter (string passage) {
if (this.passage == passage) {
animator.SetTrigger(triggerName);
}
}
Again, similar to the current Hooks system, but I like this because I can create just one script and use it for every animatable, providing the animation name through Unityās inspector. However, this system wouldnāt work with the current Hook system, since my function would need a specific name to be called, but I donāt know that name before compiling.
I can see why you wouldnāt necessarily want to include these delegates in the plugin ā again, fairly straightforward to add anyway ā but Iām finding it very useful and could see this easily integrated.
Hi Ben, sorry I missed this post, I thought I was supposed to be getting notifications on this thread
Seems like an excellent idea, there is something similar already implemented though with C# events (i.e. language-level callbacks):
public TwineStory story;
public string passage;
void Start()
{
story.OnOutput += Story_OnOutput;
}
void OnDestroy()
{
story.OnOutput -= Story_OnOutput;
}
void Story_OnOutput(TwineOutput output)
{
if (output is TwinePassage && output.Name == this.passage)
{
// Do your thing here
}
}
The OnOutput event you need is called before the āEnterā hooks. More OnOutput events are triggered with the content of the passage, but since all you need is Enter in this case, you should only respond when the āoutputā parameter is a passage. Per your suggestion, maybe to simplify matters Iāll add events for OnEnter, OnExit etc. to match the hook system.
Thanks for using the plugin, and Iām always happy to get more suggestions!
Hey, sorry, I missed this reply! Youāre completely right with the OnOutput events, I didnāt even think of that (though, I guess I should have just looked back at the ReadMe for a great example! Whoops!). Thanks for the reply, Iāll remove my addition and just use your solution ā much cleaner.
I came back to share a scary problem that had a very easy solution. This isnāt any change to the UnityTwine plugin, but relates to using Twine for large stories, which anyone using UnityTwine is likely to have:
Twine is terrible at displaying a large amount of Passage nodes in a single Story ā a giant spaghetti monster if you have 50+ nodes. Thankfully, there is a (terribly obscure!) solution: StoryIncludes. StoryIncludes is a feature of Twine 1.4 that allows writers to link multiple Story files together ā share Variables and Links between as many files as you want! This also allows multiple writers to work on the story at the same time ā no merge conflicts!
To import via UnityTwine, Iām just exporting each of these files individually, then concatenating them with a script into a CompleteStory.twee file. I drop that into Unity, UnityTwine does its magic, and presto, I have a whole (large!) story in Unity that I can still preview through in Twine for quick iteration.
I still canāt describe my gratitude for this plugin ā Iām basically using Twine as my Model and using Unity as just View and Controller. Working fantastic, thanks again!
One question: a TwineStory contains a Dictionary named Passages that maps Twine Passage names to their appropriate TwinePassage object. Why is this dictionary protected?
I have recently exposed this Dictionary so that I can detect Tags in any Passage that I have the name for ā itās working really well to preview what type of Passage node a Link references.
Hereās an example where I display certain buttons if any linked Passage has the appropriate tags:
for (int i = 0; i < story.Links.Count; ++i) {
TwineLink link = story.Links[i];
//...
TwinePassage passage = story.Passages[link.PassageName];
if (passage.Tags.Length > 0 ) {
if (passage.Tags[0] == "Map") {
//if we have a link to the map, show the travel button...
travelButton.gameObject.SetActive(true);
travelButton.onClick.AddListener (() => story.Advance (link));
} else if (passage.Tags[0] == "Search") {
//if we can search, show the search button...
searchButton.gameObject.SetActive(true);
searchButton.onClick.AddListener (() => story.Advance (link));
} else if (passage.Tags[0] == "Conversation") {
//if we can converse, show the converse button...
converseButton.gameObject.SetActive(true);
} else if (passage.Tags[0] == "Notes") {
//if we can view notes, show the notes button...
viewNotesButton.gameObject.SetActive(true);
}
}
//...
}
Iām basically wondering if Iāve missed a good reason for hiding that Dictionary. This is working very well for me now, but I could be shooting my future self in the foot!
As always, answer at your leisure. Thanks again for the great plugin!
EDIT: I just saw the GetPassages function inside TwineStory, which adds protection for missing PassageNames, but itās also hidden. I should probably expose that function instead of the Dictionary itself, but the question stands
Iāve edited the TweeParser to recognize /%Twine Comments%/. Only a few small additions, but I canāt keep Twine organized without comments, so the change was a requirement for me.
See below for the individual code snippets, though Iāve also attached the full TweeParser.cs file:
I tried to keep the same format as the rest of the parser by using Regex with a regular expression ā seemed simple enough, but I have never used the .net Regex class and I have very little experience with regular expressions in general, so if something is off please do let me know!
One annoying thing: this replaces every comment with an empty string ā which translates to an empty line in the TwineStory. Itās okay for me, since I ignore empty lines in all of my formatting (it does make it slightly less efficient at run-time, but barely), but if you needed to keep your empty lines, this solution wouldnāt work. I tried returning null in my lambda function, but the same empty lines appear. Maybe thereās a better way to replace without needing to output a blank string? I donāt know.
Iāve made a very minor edit to the TwineVar.cs file:
The ToInt function in the TwineVar class was crashing with an InvalidCast error when converting doubles to integers. Iāve changed the function to explicitly convert a double via the .NET built-in function:
public int ToInt()
{
if (this.value is int) {
return (int) this.value;
}
//begin_edit_benwander - converting to double via .NET
else if (this.value is double) {
return Convert.ToInt32((double)this.value);
}
//end_edit_benwander
else if (this.value is string) {
int parsed;
if (int.TryParse((string)this.value, out parsed))
return parsed;
else
return 0;
}
else if (this.value is bool ) {
return ((bool)this.value) ? 1 : 0;
}
else
return 0;
}
Again, please let me know if anything here isnāt kosher and Iāll fix it on my end too!
Awesome work on this plugin; Iām really enjoying tooling around with it!
That said, I noticed it was licensed under the GPL. Given that, am I correct in understanding that creating a game with this library would require me to release my gameās source code? I checked to ensure it wasnāt LGPL, which would allow linking against a separate DLL package, but it doesnāt appear to be.
Thanks again, and looking forward to digging in even further on this lib!
@konistehrad2 - Youāre right, I thought Iād keep it GPL till I release but thatās kind of pointless. Iāll change it to something more permissive.
@TheWanderingBen , your input is invaluable, thanks! I am (slowlyā¦) working on a new version that greatly improves the feature set and the workflow, so these fixes will help move that along.
Iām really excited about what you have done. However, as a relative newbie Iām having a bit of trouble following your directions. Here is what I did:
Downloaded the files from Github
Opened a new project/scene
dragged UnityTwine over to my project
went to twine. (I am using Twine 2)
got an old story, checked the format box to be Entweddle
pressed play and then copied the resulting text (the story) into a text editor and saved it as teststory.twee
I then dragged the textplayer prefab into the scene
When I dragged the teststory.twee file into my project listing ā it simply came over as the text file - it was not converted to a .cs file (as you had for your example).
Hence, I was stumped since I didnāt have a story file created such as your snoozingstory.cs. How can I convert my teststory.twee (version 2 of twine) into teststory.cs? and then insert it into my textplayer?
Probably confused a bit about all this - I tried to follow directions, but apparently need a bit more detail about how the twee file is converted to a .cs fileā¦
Hi daterre!
Thanks for your plugin, its working great. I have only one problem. I canāt get the scandinavian characters showing in the generated c# / .twee -file. Is there anyway to force the twineTexts and twineLinks to show Scandinavian characters (ƤĆĆ¶Ć etc.)
Thank you,
the_joe_nash
Hi! @daterre
Twee file shows all the characters. But once the c# is compiled in Unity all the scandinavian characters appear as question mark kind of symbols. I tried to look in to those Regex:s but couldnāt get it to work.