Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Objects lose their member functions across passages

Hi, I'm a new Twine user with a programming background, which is getting me in trouble  ;). I'm having a problem with JavaScript objects in Twine. I suspect I am brutally abusing Twine in trying to do this, but I'm not really sure why it isn't working. My problem may have something to do with an aspect of JavaScript that's obfuscated by the fact that I don't know the Twine back-end. I've searched through the forum and known issues list, and I don't think this behavior is described anywhere else.

When I create an object like this:
<br />::Start<br />&lt;&lt;init&gt;&gt;<br />&lt;&lt;print $MyObject.name&gt;&gt; &lt;&lt;print $MyObject.sayHello&gt;&gt;<br /><br />::Init<br />&lt;&lt;set $MyObject = {<br />&nbsp; &nbsp; name: &quot;I am a JavaScript Object!&quot;,<br />&nbsp; &nbsp; sayHello: function() { return &quot;Hello world!&quot;; }<br />}&gt;&gt;<br />
I will get output like: I am a JavaScript Object! Hello world!

And if I create a new passage [[DebugMyObject]] as follows:

<br />::DebugMyObject<br />&lt;&lt;print $MyObject.name&gt;&gt; &lt;&lt;print $MyObject.sayHello&gt;&gt;<br />

and display <<DebugMyObject>> in the Start passage, I get the same output.

BUT, if I add a link [[DebugMyObject]] and follow it, I get:
I am a JavaScript Object! <<print>> bad expression: Property 'sayHi' of object #<Object> is not a function

I'm baffled by the fact that the name property retains its value, while sayHi stops working. I suspect that for some reason, the value is actually still there, but something Twine does in between passages makes JavaScript forget it's a function.

I could make all my functions global (I guess this would mean attaching them to window, which I haven't tried but I just saw someone suggest in another forum thread) so you'd have a function sayHi(obj) instead of $MyObject.sayHi(). That approach has some issues, though, so I'd prefer to avoid it if possible.

Am I doing something wrong or is this a bug or am I just trying to do something Twine is really not intended to be able to handle?

Thanks in advance to anyone who can help me out. I know this is a bit outside the common usage of Twine variables.

I've attached the tweecode export of my short sample story.

Comments

  • This is just a guess, but variables are stored in an array called state.history (in a property called variables). When you visit a new page, that page gets added to the front of the state.history array, and presumably the variables property is copied over. I would suggest it only copies the properties not the methods of each variable.

    You might try attaching variables to state directly, as that may mean you are using the same variables each page, rather than a copy. That will impact on what happens if the player goes back I would guess.
  • Thank you very much Pixie, that makes sense. I'm not sure exactly how function properties are stored in JS - it's entirely likely that they might not be copied the same way as data properties.

    I take it that part of the point of copying the variables property from one passage to the next in the <b>state.history</b> array is to allow true undo with reversion of variable states. In that case it sounds as if I need to decide on a per-object basis between having member functions and Twine's undo/back feature working properly.

    Is <b>state</b> the right place to attach globals? I read something in the Google group that suggested global functions should be attached to <b>page</b>, but if <b>state</b> represents the story state, story-related globals would reasonably go there. I'm not sure what the pros and cons of each approach would be. I'm thinking that the best option is to put a hash of variables somewhere, to minimize the possibility of accidentally giving something the same name as an existing property!
  • I am no expert, but...

    JavaScript is quite different to other OO languages I am familiar with, with prototypes, rather than inheritance, for example, so this is why I suspect methods are not copied.

    I am only guessing about state. I have checked that variables on state will get saved and loaded properly (I am using SugarCube though, no guarantee other formats will be the same). I have not come across a page object; do you mean passage? That will change when the player goes to a new page.

    Global functions should be attached to the window object. They can then be called like other functions

    In a script passage
    window.sayHi = function() { return &quot;Hello world!&quot;; }

    In a normal passage:
    &lt;&lt;print sayHi()&gt;&gt;
  • Hmm, I thought I'd bookmarked the google groups thread I referred to, but I guess I didn't. Oh well...

    You've answered my question, I believe. I'll have to mock up a new version of the test story and make sure it works the way I think it should, but it seems that if I create a hash on the state and create a retrieval function on the window I can have a set of global passage-independent objects that can have methods but won't revert properly on undo; for objects that need undo functionality, I'll have to make them "dumb" data objects. Or I suppose I could have functions that restore their methods at the beginning of each new passage, but that's probably not necessary.

    Thank you for your help! This gets me unstuck on a number of things :)
  • What you have run into is one of the differences between the Sugarcane and SugarCube story formats.

    If I modify your example slightly to use the long version of the 'display' macro and build it using SugarCude (SugarCube does not support the short version) you will see that it will retain your function because when the variables are cloned between passages (both formats do this) the SugarCube format also clones the function pointers.

    NOTE: I did not need to change the other passages within your example.

    :: Start
    <<display "Init">>
    <<print $MyObject.name>> <<print $MyObject.sayHello>>

    [[DebugMyObject]]
    NOTE:
    1. SugarCube has a StoryInit special passage which is automatically called when your story starts, and this would replace your 'Init' passage.
    2. Function pointers are NOT stored in 'saves', so need to be re-added to the object prototypes via the 'after load' feature.
  • I could probably fix this in 1.4.2. Give me a couple hours...
  • Greyelf, thanks! I hadn't looked at story formats beyond the ones that are packaged with the Twine 1.4.1 build (mostly because it looks like 1.4.1 has some backwards compatibility issues). I had noticed that most people (at least in this forum and the blogs I've been reading) seem to be using Sugarcube though. Depending on what I'm trying to do, switching to Sugarcube might be a good idea.

    L, you're awesome! (And I managed to miss until just now that you put out a new beta version of 1.4.2 yesterday, which I will go ahead and start using instead of 1.4.1 - I hadn't downloaded beta 2 as it looked to be a couple months old and I figured I'd wait for the next one)
  • greyelf wrote:

    1. SugarCube has a StoryInit special passage which is automatically called when your story starts, and this would replace your 'Init' passage.


    The 1.4.2 beta supports StoryInit in its included story formats as well.
  • I came up with a workaround for people still using 1.4.1 or the Mac beta (though since this is fairly recent, I'm guessing the Windows beta build doesn't have it incorporated yet either). It's clunkity clunk to the clunx, but you can use postrender to add the function member back to your variable every time the page is loaded. I had to do that for the translation stuff I'm working on.
Sign In or Register to comment.