During the past two weeks I spent the majority of my time programming. One of the features I got working (at least on a basic level) was matinee based digital input replay, which means that matinee can now mimic user input for both analog axes and digital buttons in order to drive GVMachines. This replay of captured user performance is a fundamental feature of Gavit’s machinima support.
The GVMachine class is basically a bridge between an input (a joystick for instance) and a property of an actor (like the location of a box). When the input changes (the joy is pushed forward) the GVMachine translates the data and adjusts the target property accordingly (the box rises).
GVMachines can receive input from one of two sources: either from the user directly or from matinee. The two are handled the exact same way in a GVMachine so it can not tell the difference between them. One can even switch input sources on the fly, interrupt a matinee playback with live input or vice versa. If everything works properly then we can recreate the exact same events later using matinee.
Well, almost the exact same events. Handling analog data is pretty easy because by its nature changes are relatively slow (in computing terms) and gradual so interpolation between keyframes will create a pretty close representation of the original. However digital button events are a bit trickier to handle because they don’t translate well to matinee curves.
Let’s consider the following graph:

This could be an obvious representation of a button being pressed for a while: At the beginning the curve’s value is 0 then suddenly it becomes 1 when the button is pressed, stays 1 while held, until suddenly falling back to 0 on release.
Trouble is that we can only sample the graph every now and then, depending on how fast the game thread ticks. The time between ticks varies in a pseudo random manner which means that although we have a slim chance of learning about an event at the exact time it happens (getting a tick at 0.2 and 0.5 in this example) most of the time we will be late. A typical bad case would be sampling at 0.199: just short of noticing the state change so the next time we come around we already late by almost a full frame.
At this point one could suggest that if I worry about a button press being too late then why not offset the whole curve by a fixed value so events appear in the system a bit earlier thus more or less cancelling out our inherent sampling lag.
This would be a reasonably good solution but there is something to consider: we can record what a GVMachine does, regardless if it was controlled by live user performance or by matinee.
Imagine that I try to perform a series of car stunts, 2 minutes of length. I could try and repeat the whole sequence over and over until I get every bit right. This approach would not be very efficient as a single mistake in the last second would render the whole performance useless.
A better way of doing it is focusing on one short segment at a time, redoing it over and over while recording each session. When I’m happy with my performance then I convert the recorded data into a matinee and let it replay from then on automatically and consistently. That segment is “in the bank” so I can focus on the next bit: as soon the matinee replay ends (or even before) I take over and carry on the stunts. I’ve just seamlessly extended the previously recorded data. Replay, add a new segment, rinse, repeat.
And this “recording the replay of a previous recording” part which will run into trouble if the matinee button events keep happening consistently too early or too late: with each recording cycle the events will float off further and further in one direction.
To solve this I tried to make those events “wider”, easier to hit so I ended up a graph something like this:

Each button event is preceded by a warmup period (0.1 on the picture but 0.02 in the current implementation): if the sampled value is in the 0..1 range then we know that a “push” event is coming up, while if the value is between -1 and 0 then a release event is imminent. Not just that but we also know when something supposed to happen in the future: if we got the value of 0.5 then we know that a push event supposed to occur WarmupTime*0.5 seconds from now.
By keeping track of the average time between ticks we could take an educated guess which one is closer to the real thing: this tick or the next one. In the former case we fire the event now, a bit earlier while the latter case will produce a late event in the next tick. The important thing is that regardless of too soon or too late, we picked the one which is closer to the ideal time.
The somewhat random timing of the sampling ticks will cause a certain event to be fired earlier in one recording cycle and later in another. This won’t stop the floating of the timing but will slow it down, hopefully to an extent where this won’t ever become a practical problem.
The biggest disadvantage of this approach is that events can not be closer than the warmup time, which is 0.02 seconds currently. I did some measurements and the fastest doubleclick I could reliably perform was 0.022+ long so that value seems fine for now. However this also means that the game thread must run at at least 50 Hz in order to guarantee a hit during any given warmup time, but that is a good idea to maintain for other reasons anyway.
Next time I’ll cover the recording part of the system.