Skip to content Skip to sidebar Skip to footer

How To Ensure A Recursive Function Isn't Called Again Before It Returns

I'm making an RPG in JavaScript and HTML5 canvas. For the dialog, I added a 'typing' effect to appear as if NPC's dialog is being typed out automatically (letter by letter). This

Solution 1:

The problem is that setTimeout returns immediately and then your code continues.

You need a function that calls itself after a timeout and checks to see if the button should be enabled.

Here's a demo: http://cdpn.io/Idypu

Solution 2:

As has been mentioned, the part that's biting you is the combination of event-reaction, asynchronicity and a recursive function which might not kick in how and when you'd expect. It goes a little further, but that's enough to illustrate where things go wrong, without getting into the meta-programming between the cracks of JS and the browser-engine powering it.

Now that the other answers have shown you why it doesn't work the way you think, I'd suggest that the root of the problem is that you're trying to manage something which should happen exactly once (toggling the button) inside of something that could happen a hundred times, where if you wrote a little bit more code and turned it inside out, it would be a lot easier to visualize.

Perhaps a refactor that looks a little something like this, might make things simpler:

var button = {
    el : $("#responseButton"), // cache your references
    setDisabled : function (state) { button.el.prop("disabled", state); },
    enable  : function () { button.setDisabled(false); },
    disable : function () { button.setDisabled(true ); },
    handleClick : function () {
        button.disable();
        Utilities.typeEffect(0, function () { button.enable(); });
    }
},

dialogBox = {
    // if you're going to call this 100x in a row, cache it
    el     : $("#npc_dialog"),
    append : function (text) { dialogBox.el.append(text); }
},

sec = 1000,
textSpeed = {
    fast   : 1/25 * sec, // 25x /sec
    medium : 1/10 * sec, // 10x /sec
    slow   : 1/ 5 * sec  //  5x /sec
};

button.el.on("click", button.handleClick);



Utilities.typeEffect = function (index, done) {
    if (index >= game.data.NPCdialog.length) {
        game.data.enableCycle = true;
        returndone();
    } else {
        var character = game.data.NPCdialog[index];
        dialogBox.append(character);
        setTimeout(function () {
            Utilities.typeEffect(index+1, done);
        }, textSpeed.fast);
    }
};

You should notice that I changed very, very little inside of the actual .typeEffect. What I did do was build a quick little button abstraction that holds a cached reference to the element (don't make jQuery look for it, on your page, 25x per second), adds some quick'n'dirty helper methods... ...and then I gave the button control of its event-handling, including knowing how to clean up, when the time comes.

If you'll notice, I gave .typeEffect one more parameter, done, and the argument I passed it is a function that is defined by button, which knows how to clean itself up, when everything is finished. The function isn't any bigger, and yet, I'm now performing the input-blocking/unblocking exactly once, by passing a callback.

Better yet, I can guarantee that it's blocked right before the start of the recursion, and it's unblocked right after the last recursion occurs, without any ill-defined state to get in the way.

I also cached the dialogue box, to again speed the process along; jQuery does wonders, these days, but this should still be cached.

If I were to take it further, I'd probably make typeEffect a plugin for a dialog-writing component, so that several different types of effects could be dropped in. I'd probably also start passing the recursing function the other arguments it cares about (text-speed, a reference to the dialogue box, et cetera), so that they're easier to swap, and the function becomes more reusable (and easier to see that it works).

Note that this isn't really the answer. It's a solution, which should not only solve the problem, but illustrate how to prevent it, in the future. If it works, use it, if it doesn't, don't. If the concept works, but it's not your style, then change it (though, avoid using a string in setTimeout... that should have stopped being taught years and years ago; performance-killer, safety-hazard if you're mashing in user-created strings, easy to make non-strict eval-context mistakes... ...all of the good stuff).

Solution 3:

Javascript is single-threaded. Unless you deliberately relinquish control, no other Javascript function can be called.

EDIT

Norgard writes:

surely you see the setTimeout, there

I didn't see the setTimeout and stop calling me Shirley.

And I think his plan would work, except that several events are queued up before the handler is fired.

Growler, I think the problem you are having is that you assume that the handler will be fired, and the button disabled, after the very first click and therefore no subsequent events could be generated. This is not, I believe, correct.

Instead, try this:

typeEffect : function() {
  var enabled = true;

  var process = function(index) {
    if (index >= game.data.NPCdialog.length) {
      game.data.enableCycle = true;
      enabled = true;
      $("#responseButton").prop('disabled', false);
    }
    else {
      $("#npc_dialog").append(game.data.NPCdialog.charAt(index));
      setTimeout(function() {
        process(index+1);
      }, 40);
    }
  };

  returnfunction(index) {     
    if (enabled) {
      $("#responseButton").prop('disabled', true);
      process(index);
    }
  };
}(),

Post a Comment for "How To Ensure A Recursive Function Isn't Called Again Before It Returns"