[RELEASED] Cradle - play Twine stories in Unity

Cradle

Twine and Twine-like stories in Unity.

Project home: GitHub - daterre/Cradle: Play Twine stories in Unity.
Asset store: Unity Asset Store - The Best Assets for Game Making

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.

2 Likes

This seems pretty promising! Any documentation available? Trying to figure out to use it.

Yes, all the documentation is on the GitHub project page (scroll down).

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.

1 Like

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.

Thanks again!

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 :confused:

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!

Cheers

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.

1 Like

Hello again daterre,

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 :slight_smile:

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:

Adding the new Regex:

//line 26
static Regex rx_Comment; //begin_benwander_edit

Defining a Twine comment:

//line 47
//begin_benwander_edit
rx_Comment = new Regex(
                @"/%(.*?)%/",
                RegexOptions.Singleline |RegexOptions.Multiline| RegexOptions.ExplicitCapture| RegexOptions.IgnoreCase
);
//end_benwander_edit

Replacing comments with an empty string:

//line 227
//begin_benwander_edit
// Comments
output = rx_Comment.Replace(output, match=>{
                return "";
});
//end_benwander_edit

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.

2439709ā€“167259ā€“TweeParser.cs (7.98 KB)

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!

1 Like

@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.

2 Likes

Hi daterre:

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:

  1. Downloaded the files from Github
  2. Opened a new project/scene
  3. dragged UnityTwine over to my project
  4. went to twine. (I am using Twine 2)
  5. got an old story, checked the format box to be Entweddle
  6. pressed play and then copied the resulting text (the story) into a text editor and saved it as teststory.twee
  7. I then dragged the textplayer prefab into the scene
  8. 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).
  9. 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ā€¦

Thanks,

JRBOURNE

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

@the_joe_nash
Iā€™ll look into it!

@jrbourne - Iā€™m going to release a brand-new Twine 2 compatible version of UnityTwine in a few days, so please bear with me till thenā€¦

@daterre
Thanks for your reply :slight_smile:

@the_joe_nash Is it not appearing in the .twee file? Then it might be a problem with Twine 1 itselfā€¦

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.

Okay got it, opening an issue, will take care of it for upcoming release.