Search Unity

[RELEASED] Cradle - play Twine stories in Unity

Discussion in 'Assets and Asset Store' started by daterre, Jun 15, 2015.

  1. daterre

    daterre

    Joined:
    Jul 30, 2012
    Posts:
    41


    Cradle

    Twine and Twine-like stories in Unity.

    Project home: http://github.com/daterre/cradle
    Asset store: https://www.assetstore.unity3d.com/en/#!/content/93606

    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.
     
    Last edited: Jul 18, 2017
    theANMATOR2b and Kellyrayj like this.
  2. hugo-tguerra

    hugo-tguerra

    Joined:
    Jul 7, 2013
    Posts:
    13
    This seems pretty promising! Any documentation available? Trying to figure out to use it.
     
  3. daterre

    daterre

    Joined:
    Jul 30, 2012
    Posts:
    41
  4. daterre

    daterre

    Joined:
    Jul 30, 2012
    Posts:
    41
    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.
     
    TheWanderingBen likes this.
  5. TheWanderingBen

    TheWanderingBen

    Joined:
    Nov 3, 2015
    Posts:
    98
    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!
     
  6. TheWanderingBen

    TheWanderingBen

    Joined:
    Nov 3, 2015
    Posts:
    98
    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:

    Code (CSharp):
    1. //The declaration of the delegate
    2. public delegate void PassageCallback(string passageName);
    Code (CSharp):
    1. //The function called by any object wishing to be informed of an OnEnter call
    2. public void RegisterOnEnterCallback(PassageCallback callbackFunction) {
    3.         onEnterCallbacks.Add(callbackFunction);
    4. }
    Code (CSharp):
    1. //An example of a class registering a function to later use for animating on a specific passage
    2.  
    3. public string passage;
    4.  
    5. //...
    6.  
    7. void Start () {
    8.     storyPlayer.RegisterOnEnterCallback(Enter);
    9. }
    10.  
    11. void Enter (string passage) {
    12.     if (this.passage == passage) {
    13.         animator.SetTrigger(triggerName);
    14.     }
    15. }
    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.
     
  7. daterre

    daterre

    Joined:
    Jul 30, 2012
    Posts:
    41
    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):

    Code (CSharp):
    1. public TwineStory story;
    2. public string passage;
    3.  
    4. void Start()
    5. {
    6.     story.OnOutput += Story_OnOutput;
    7. }
    8.  
    9. void OnDestroy()
    10. {
    11.     story.OnOutput -= Story_OnOutput;
    12. }
    13.  
    14. void Story_OnOutput(TwineOutput output)
    15. {
    16.     if (output is TwinePassage && output.Name == this.passage)
    17.     {
    18.         // Do your thing here
    19.     }
    20. }
    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
     
  8. TheWanderingBen

    TheWanderingBen

    Joined:
    Nov 3, 2015
    Posts:
    98
    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.
     
    daterre likes this.
  9. TheWanderingBen

    TheWanderingBen

    Joined:
    Nov 3, 2015
    Posts:
    98
    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:
    Code (CSharp):
    1.  
    2. for (int i = 0; i < story.Links.Count; ++i) {
    3.             TwineLink link = story.Links[i];
    4.             //...
    5.                 TwinePassage passage = story.Passages[link.PassageName];
    6.                 if (passage.Tags.Length > 0 ) {
    7.                     if (passage.Tags[0] == "Map") {
    8.                         //if we have a link to the map, show the travel button...
    9.                         travelButton.gameObject.SetActive(true);
    10.                         travelButton.onClick.AddListener (() => story.Advance (link));
    11.                     } else if (passage.Tags[0] == "Search") {
    12.                         //if we can search, show the search button...
    13.                         searchButton.gameObject.SetActive(true);
    14.                         searchButton.onClick.AddListener (() => story.Advance (link));
    15.                     } else if (passage.Tags[0] == "Conversation") {
    16.                         //if we can converse, show the converse button...
    17.                         converseButton.gameObject.SetActive(true);
    18.                     } else if (passage.Tags[0] == "Notes") {
    19.                         //if we can view notes, show the notes button...
    20.                         viewNotesButton.gameObject.SetActive(true);
    21.                     }
    22.                 }
    23.             //...
    24.         }
    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 :)
     
  10. TheWanderingBen

    TheWanderingBen

    Joined:
    Nov 3, 2015
    Posts:
    98
    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:
    Code (CSharp):
    1. //line 26
    2. static Regex rx_Comment; //begin_benwander_edit
    3.  
    Defining a Twine comment:
    Code (CSharp):
    1. //line 47
    2. //begin_benwander_edit
    3. rx_Comment = new Regex(
    4.                 @"/%(.*?)%/",
    5.                 RegexOptions.Singleline |RegexOptions.Multiline| RegexOptions.ExplicitCapture| RegexOptions.IgnoreCase
    6. );
    7. //end_benwander_edit
    Replacing comments with an empty string:
    Code (CSharp):
    1. //line 227
    2. //begin_benwander_edit
    3. // Comments
    4. output = rx_Comment.Replace(output, match=>{
    5.                 return "";
    6. });
    7. //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.
     

    Attached Files:

  11. TheWanderingBen

    TheWanderingBen

    Joined:
    Nov 3, 2015
    Posts:
    98
    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:

    Code (CSharp):
    1. public int ToInt()
    2. {
    3.     if (this.value is int) {
    4.         return (int) this.value;
    5.     }
    6.     //begin_edit_benwander - converting to double via .NET
    7.     else if (this.value is double) {
    8.         return Convert.ToInt32((double)this.value);
    9.     }
    10.     //end_edit_benwander
    11.     else if (this.value is string) {
    12.         int parsed;
    13.         if (int.TryParse((string)this.value, out parsed))
    14.             return parsed;
    15.         else
    16.             return 0;
    17.     }
    18.     else if (this.value is bool ) {
    19.         return ((bool)this.value) ? 1 : 0;
    20.     }
    21.     else
    22.         return 0;
    23. }
    Again, please let me know if anything here isn't kosher and I'll fix it on my end too!
     
  12. konistehrad2

    konistehrad2

    Joined:
    Feb 11, 2013
    Posts:
    2
    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!
     
    zyzyx likes this.
  13. daterre

    daterre

    Joined:
    Jul 30, 2012
    Posts:
    41
    @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.
     
    konistehrad2 and zyzyx like this.
  14. jrbourne

    jrbourne

    Joined:
    May 8, 2015
    Posts:
    8
    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
     
  15. the_joe_nash

    the_joe_nash

    Joined:
    Mar 22, 2016
    Posts:
    3
    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
     
  16. daterre

    daterre

    Joined:
    Jul 30, 2012
    Posts:
    41
    @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...
     
  17. the_joe_nash

    the_joe_nash

    Joined:
    Mar 22, 2016
    Posts:
    3
  18. daterre

    daterre

    Joined:
    Jul 30, 2012
    Posts:
    41
    @the_joe_nash Is it not appearing in the .twee file? Then it might be a problem with Twine 1 itself...
     
    Last edited: Mar 29, 2016
  19. the_joe_nash

    the_joe_nash

    Joined:
    Mar 22, 2016
    Posts:
    3
    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.
     
  20. daterre

    daterre

    Joined:
    Jul 30, 2012
    Posts:
    41
    Okay got it, opening an issue, will take care of it for upcoming release.
     
  21. Roy1337

    Roy1337

    Joined:
    Aug 18, 2015
    Posts:
    7
    Hi @daterre
    I would love to use this in a project I am working on with some friends. I am having trouble getting it installed.
    Could you please explain how I can get this to install? Here is what I am doing.
    1. Downloaded the files from Github
    2. Open my project in Unity.
    3. Drag the files from GitHub into Unity.
    I then get errors.

    If I open a new empty project, I can drag the files from GitHub into Unity without errors. But if I drag an html file generated from Twine I get this error.
    Win32Exception: ApplicationName='C:/Users/Christian/Documents/Unity Projects/Test/Assets/Cradle/Editor/ThirdParty/PhantomJS/bin/win/phantomjs.exe', CommandLine='"phantom.js_" "file:///C:/Users/Christian/Documents/Unity%20Projects/Test/Assets/Test.html" "C:/Users/Christian/Documents/Unity Projects/Test/Assets/Cradle/Editor/js/StoryFormats/Harlowe/harlowe.bridge.js_"', CurrentDirectory='C:/Users/Christian/Documents/Unity
     
    Last edited: Dec 21, 2016
  22. anathemort

    anathemort

    Joined:
    Jan 6, 2017
    Posts:
    1
    @Roy1337 We just went through that issue too and determined that the Cradle needs to be both in the default Asset directory, and in a path with no spaces. I think your "Unity Projects" path is causing the problem!
     
    Roy1337 likes this.
  23. Roy1337

    Roy1337

    Joined:
    Aug 18, 2015
    Posts:
    7
    Thank you so much! I am still getting errors with the import but hopefully, I can get through them. Do you need all the assets in the assets folder or just the Cradle folder?
     
  24. The-Oracle

    The-Oracle

    Joined:
    Feb 26, 2015
    Posts:
    2
    Hi daterre!

    Thank you very much for your hard work. It's just amazing.
     
  25. Fibroluminique

    Fibroluminique

    Joined:
    Apr 10, 2017
    Posts:
    1
    Hello! I finally could play after fixing some errors in my script (which interferred with the generated C#). Now when I press PLay in Unity, the screen just goes black. Even if I name a random passage "Start", it will show the passage, but I have no way of going to the next passage. No buttons appear, just the text in blue. Even if i click over the blue text, nothing happens. In the screenshot you can see that the elegible text is false (even if my mouse is on the blue text). Thank you!
     

    Attached Files:

  26. daterre

    daterre

    Joined:
    Jul 30, 2012
    Posts:
    41
    Happy to say that Cradle (formerly UnityTwine) has finally been released on the asset store! Get it here: https://www.assetstore.unity3d.com/en/#!/content/93606

    See the title post for info on new features. 2 years of updates, beta testing and use by many indie and commercial titles mean that the plugin is very stable and mature. Questions welcome on this thread or on the GitHub issue page.
     
    Braza and zyzyx like this.
  27. mimatos

    mimatos

    Joined:
    Oct 17, 2018
    Posts:
    1
    I there any way of loading stories from Twine externally at runtime through Cradle to then process? Say from like the Resources folder? This way someone could possible make edits to the dialogue after the game has been built?
     
  28. jesventura36

    jesventura36

    Joined:
    Mar 17, 2021
    Posts:
    1
    I've been trying to import Cradle to Unity for hours. The one from Github but because I could not make it show up in the Assets folder, I just download the Cradle asset from unity itself.

    My next problem is to import Cradle tools in Unity so I can start importing Twine projects in Unity.

    What I already did:
    I have been trying to get Cradle to work for hours and watched tutorials and read blogs but none of their ways work.

    What I couldn't do yet:
    I downloaded story formats too because the tutorial said I need to convert it to json/txt files to copy-paste into Unity but I can't move to that level yet because the Cradle tool/editor does not appear yet.

    The issue:
    Cradle appears in the assets now but in the Tools only Fungus' tools appear even after I've imported it.
    In one of the tutorials I watched, they had a different tool that represents for Cradle but here even after opening and closing the new empty project I don't see it. There is also no editor file in the Cradle folder for me to use,

    Someone told me to just give up since I am a noob, but I want to make this work.