flareteam / flare-engine

Free/Libre Action Roleplaying Engine (engine only)
http://flarerpg.org/
GNU General Public License v3.0
1.11k stars 188 forks source link

Animations using millisecond timing #984

Closed clintbellanger closed 10 years ago

clintbellanger commented 10 years ago

I want to take one more look at this for Flare 1.0, just in case we can figure out a better way than what we have.

We started to make this transition, but maybe our approach was off.

I want to express all animation timing in terms of milliseconds. I also want the same data to work at 30fps and 60fps (maybe beyond). Right now if we wanted to change Flare Game from 30fps to 60fps it requires halving all frame and speed variable in all of the data. When really we should be able to flip one variable (the max static FPS) and all the game's content data still works.

The problem I was running into was with Animation timing. Internally we took that desired millisecond timing, compared to the current fixed frame rate, and converted those ms timings into frame timings. But frame timings for animations are using a single framecount duration for all the frames in an animation.

Example. Let's say we create an animation with each frame at 50ms timing. At 60fps this works predictably, with 50ms timing being the same as 3 frames (16.6ms per frame). But if we run that same animation at 30fps, the closes frame counts are 1 frame (round down to 33ms) or 2 frames (round up to 66ms). Changing a 50ms animation to run at 33ms or 66ms feels drastically different, and obviously wrong, which is when I scrapped the idea.

Maybe animations need a more sophisticated internal counter to help get around this. Internally animations could track total milliseconds elapsed, and use that to calculate the nearest frame each frame (instead of once for the whole animation).

E.g. if we have a 4-frame Running animation that we want at 50ms, this is how the frames display at 60fps:

111222333444 (200 total ms)

At 30fps we have two less than ideal options, if we convert that 50ms to a fixed framecount.

1234 (133 total ms, noticeably fast)
11223344 (266 total ms, noticeably slow)

When really, a staggered frame timing would give a better approximation of the desired 50ms, so it may be calculated like:

122344 (200 total ms, correct speed)

Some frames display once, others display twice. But the whole animation loop lasts 200ms -- now it's the same pace at 30fps or 60fps.

I think the basic logic would resemble this (pseudocode):

animation_counter += ms_per_frame; // 33.3 or 16.6 typically
while (animation_counter > desired_ms) { // 50ms in this case
   current_frame++; // wrap if a looping animation
   animation_counter -= desired_ms;
}

This wouldn't be perfect any single frame, but over a few frames it should keep about the right animation speed.

If frames can be skipped entirely, we would need to tweak the "Active Frame" trigger so it still triggers on a skipped frame.

I'd like to hear thoughts on this, whether this makes sense or falls apart in certain cases.

pennomi commented 10 years ago

I think that makes sense, as long as the "closest" interpolation isn't determined per-animation but based on the global clock. Basically I'd want to avoid 122344 always being the solution (we'd probably see weirdness in the animation?) but rather based on what the global timer really is, that same animation could end up 112334 sometimes.

Not sure I really explained what I mean, but hopefully you get the gist.

dorkster commented 10 years ago

Would it make sense to change animation durations to represent the duration of the entire animation, instead of the duration of a single frame? Given a total duration and the total number of frames, we should easily be able to calculate approximate frame duration based on our fps cap. Sure, this can be inaccurate sometimes (a 15 frame animation lasting 500ms would actually be 495ms), but I think the interpolation solution would hit the same issue when frames can't be spaced nicely.

stefanbeller commented 10 years ago

Think about the Bresenham algorithm here, which is an algorithm for drawing lines in a pixelized environment. Now imagine the y axis as the index of the animation and the x axis as the time, so we maybe want to have something like this: bresenham

pennomi commented 10 years ago

@stefanbeller that's exactly what I meant! I guess a picture is worth a thousand words.

stefanbeller commented 10 years ago

Here is the link I forgot, http://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm

dorkster commented 10 years ago

I'm not sure if Bresenham would be the right solution. Given the previously mentioned 200ms, 4-frame animation...

At 30 fps it seems alright:

Duration: 200ms | Frames: 4 | FPS: 30
122334

But then 60 fps isn't optimal, spending too much time on the 2nd and 3rd frames:

Duration: 200ms | Frames: 4 | FPS: 60
112222333344
stefanbeller commented 10 years ago

Maybe we need a 'balancing' bresenham? So to pick up the example with 4 frames and duration 200 milliseconds, at 30 fps we'd have 6 frames in 200ms, but just 4 sprites to render, so the solution was

122334

but this would put too much weight on the 2 and 3 as well? Maybe we'd need to alternate to 112344 instead or rather 112334 122344

screenshot from 2014-05-25 20 00 59

dorkster commented 10 years ago

I'm less concerned about the 30 fps example, since 4 doesn't go evenly into 6. It's acceptable to have some imbalance, and I think we should keep the imbalance consistent instead of alternating it.

I was focusing more on the 60 fps example where 4 goes evenly into 12. Each frame should be 3 ticks. Instead we get two frames with 2 ticks, and two frames with 4 ticks.

stefanbeller commented 10 years ago

Heh, I assumed the bresenham would produce an 'even' line with 4 times 3 ticks for 12. Just tested and you're right, it's likely not a good idea to employ the pure bresenham algorithm.

dorkster commented 10 years ago

I've worked on getting the engine & data to a state where this can be experimented with. The total duration of each animation in flare-game is evenly divisible by the number of frames. I did just that and set the number of ticks per actual frame as duration/frames. I provided the Bresenham algorithm as a fallback, but we don't see its effects with the current game data.

Anyway, here are the branches for engine and data. Please be aware I haven't touched devlab and minicore in the data yet: https://github.com/dorkster/flare-engine/tree/60fps https://github.com/dorkster/flare-game/tree/60fps

Right now, the game seems perfectly playable when switching between 30fps and 60fps without changing any other code. However, here's what still needs to be done: