Collaborative movement behaviors

For developers using the Construct 2 Javascript SDK

Post » Sun Jan 25, 2015 7:21 pm

TL;DR
Want to write a behavior that is supposed to play along with other existing movement behaviors? Is it sensitive to single-frame-disturbance? Copy the code below, see if it works for you. Might save you some headscratching.
/TR;DR

Ok, I was pondering a problem this weekend.
Here it is.

Context (the X of the XY Problem)
I want to build a behavior that moves an object.
It does so, by having the objects speed as internal state. (Im thinking of it as 'momentum'.)
And I want to be able to have multiple instances of it on the same object.
So basically my behavior should be able to tolerate other people moving its object as well.
(This should be good modular design, right?)
So, my behavior needs to track the movement other behaviors do and factor it in.
(The reason beeing, that i want to preserve momentum, to provide a nice smooth movement feel.)

Plan
Remember the last location this behavior set the obejct to and compare it with the location it finds itself in at the next invocation to measure speed.

Done so far
So to attribute for other collaborators moving my object, what i do every tick is roughly:
  1. Compare difference 'delta' of instance position now and 'lastPosition'
  2. Compute 'externalSpeed' applied by collaborators: 'delta' / dt
  3. Set my internal speed to the average of internal speed and 'externalSpeed'
  4. Do the actual behavior specifc stuff to set a new speed + position
  5. Remember that new position as 'lastPosition'

This works pretty well most of the time.

Problem
Now you might or might not have already spotted it, but this only works as long as the frame rate is stable.
Here is why.
In the most simplified scenario I have 4 steps:
  1. Drawing of the new frame begins. Time since the last frame is measured: dt
  2. Behavior A is triggered. It moves the object according to its internal 'speed' and dt
  3. Behavior B is triggered. It detects the movement of A, correctly estimates A's speed and accounts for it. B also moves the object.
  4. Other stuff is happening and more time is passing. Say dt_2.

Now, thats all fine and working. The problem starts when this is repeated the next frame:
A is triggered. It gets handed dt_2. It also sees that its object has been moved.
If it tries to estimate the internal speed of B and dt_2 is noticable different from dt it will get a wrong estimate.
(B-speed -> movement by dt -> new object position -> converted back to speed via dt_2 -> wrong)

So putting us in the perspective of a behavior on an object that tries to measure its objects 'speed' from other sources there is no easy way around this.
The (hypothetical) interval over which dt is measured is different from the one we can measure the location 'delta' and we cannot know by how far we are off.

Why is this relevant?
First off: dt is just the inverse of the fps. And the fps are far from stable to begin with.
Someone might be resizing his browser or beeing notified by his phone about a call - we just cannot assume too much about it.
Then, in my current build r195, there are a few artifacts which had a notable effect on my behavior.
  1. The first 1-2 frames (at least when started from C2 directly) come back with a dt = 0.
    This is not so bad, but it put my object into NaN the first time.
  2. When using the 'Debug' Mode to Pause/Resume the first 1-3 frames may come back with a dt of ~ 5ms.
    This is significantly less then the average 60 FPS 16ms.
    This can also happen when switching between my browser tabs and back to the C2 app.
  3. When switching windows away from the browser and back to it (like with detached browser-developper tools) the engine sometimes comes back with exactly 100ms for dt. Looks a bit like a timeout state for a frame, but yeah.
    Again far off from the usual 16ms 'good' case.

What to do?
That - of course - depends heavily on the behavior you want to write.

I tried a few things, skipping frames, averaging dts. But since i'm after smooth movement I figured it best to use a 5 sample median to ignore the 1-2 frame offs in 2. and 3. as much as possible. Which comes at the minor cost of sorting a 5 element list each tick.

Concretely, here is what I do:
  • OnCreate()
    Code: Select all
            this.dx = 0;
            this.dy = 0;
            this.lastX = this.inst.x;
            this.lastY = this.inst.y;
            this.medianDt = 0.016; // assume 60 FPS
            this.lastDts = [0.016, 0.016, 0.016, 0.16, 0.16];
  • tick()
    Code: Select all
            this.getLast5MedianDt();
            this.pickupExternalImpulse();
            if (this.enabled)
            {
                this.actualBehaviorStuff();
            }
            this.lastX = this.inst.x;
            this.lastY = this.inst.y;

    with
    Code: Select all
        behinstProto.getLast5MedianDt = function ()
        {
            var dt = this.runtime.getDt(this.inst);
            this.lastDts.pop();
            this.lastDts.unshift(dt);
            var sample = this.lastDts.slice().sort(function(a,b) {return a-b});
            this.medianDt = sample[2];
        }

        behinstProto.pickupExternalImpulse = function ()
        {
            if (this.lastX !== this.inst.x || this.lastY !== this.inst.y)
            {
                var deltaX = this.inst.x - this.lastX,
                    deltaY = this.inst.y - this.lastY;
                this.dx = (this.dx + deltaX/this.medianDt)/2; // merge own momentum with external one in equal shares
                this.dy = (this.dy + deltaY/this.medianDt)/2;
            }
        }
  • plus all the other stuff, like saving all 6 variables to JSON and handing out 'Debug' Mode infos...
(above code may be considered CC0 licensed)

Any comments on how to better/properly do it are welcome. :-) Or any comments by @scirra on design decisions. ;-)
B
9
S
2
G
2
Posts: 15
Reputation: 886

Return to Javascript SDK

Who is online

Users browsing this forum: No registered users and 0 guests