Planimeter / game-engine-2d

Planimeter Game Engine 2D - LÖVE-based game engine for Lua
https://github.com/Planimeter/game-engine-2d/wiki
MIT License
736 stars 74 forks source link

Implement player movement interpolation #6

Closed andrewmcwatters closed 9 years ago

andrewmcwatters commented 9 years ago

player:move() currently jumps players' positions to pathfinding nodes one at a time, rather than interpolating their positions. Research interpolating players' positions and solve the following questions:

Should Grid implement generic interpolating or easing functions for usage across various subsystems? gui currently has its own ease and animate functionality.

How should Grid provide player or general movement acceleration? Should this be done on a per entity basis?

How should this integrate with players' or all entities' animations if interpolating all entity movement? How should footstep sounds be integrated?

How should players teleporting or players moving instantaneously somewhere else over long distances factor into this?

ghost commented 9 years ago

Should Grid implement generic interpolating or easing functions for usage across various subsystems? gui currently has its own ease and animate functionality.

Definitely generic interpolation. It's needed pretty much everywhere.

How should Grid provide player or general movement acceleration? Should this be done on a per entity basis?

This should probably be done on a per-entity basis. Player movement is usually very different from normal physics object movement.

How should this integrate with players' or all entities' animations if interpolating all entity movement? How should footstep sounds be integrated?

I think the way this is done in most modern engines is with "animation events". You can specify an event to happen at a specific keyframe (like playing a footstep sound when the foot hits the ground).

How should players teleporting or players moving instantaneously somewhere else over long distances factor into this?

Perhaps add a distance check? If the player moves too far in a single frame, don't interpolate.

andrewmcwatters commented 9 years ago

:question: I feel like easing and interpolation at this point should be refactored out of panel (misspoke earlier, it's not in gui) and into its own interface and provide some sort of callback mechanisms for progress, completion, etc, or something. I'll have to look back at the implementation and carefully see what would be the easiest to work it. I'm just not sure about the implementation details in my head, or what it would look like for people to use, which is pretty important to me.

:+1: I agree with the per-entity basis sentiment. Grid's current focus is top-down games, and though it's not impossible to handle side-scrollers right now, it certainly doesn't have to concern itself with things like Box2D, so developers are best handling this themselves as needed.

:+1: Admittedly, I didn't have animation events planned for the upcoming spritesheet format for Grid, which implements named animations so developers can do things like player:setAnimation( "swing" ) or player:setAnimation( "walk" ) and that sort of thing. That being said, animation events are the way to go, and I'll keep that in mind.

:question: I've noticed some pretty big engines, regardless of distance, still interpolate player movement in these cases. I think this is widely a bug in games I've seen, though, because it just doesn't make sense. It might speak to underlying network ramifications, though, which is interesting to me, because networkvars in Grid don't do anything besides transmit changes from the server to clients. (No interpolation, etc.)

Another question at that point is what is the right distance? Perhaps this is something I can ignore for now and come back to later.

Tamschi commented 9 years ago

I don't think interpolation across large distances is a bug as much as correct behaviour in unusual circumstances. If a player really was supposed to teleport, the game would normally send a specific command to that effect.

On another note, you should look into function-of-time vs. driven easing. Since the latter only takes current offset and speed into account it's easy to activate multiple easing functions / drivers at the same time and still get a good result. It also results in slightly different pathing behaviour if the target position changes before it is reached, which may look more natural with the same amount of programmatic complexity.

However, this method blocks you from calculating the overall time consumption of the animation beforehand, which could cause problems with lost time. In my case I can work around it using C#'s Task async feature to carry over split-frame timing corrections implicitly. Since Lua coroutines have a similar API it should be possible that way here too, but the specific approach I used requires a lot of set-up and likely wouldn't work with the language since the direction of continuation references is reversed in C#, leading to an entirely different low-level API.

andrewmcwatters commented 9 years ago

On another note, you should look into function-of-time vs. driven easing. Since the latter only takes current offset and speed into account it's easy to activate multiple easing functions / drivers at the same time and still get a good result. It also results in slightly different pathing behaviour if the target position changes before it is reached, which may look more natural with the same amount of programmatic complexity.

This was something I was concerned with in regards to refactoring panel to use an interpolation library/interface that player could share, alongside anything else in the game. I think about the two in fundamentally different ways, and precisely the two that you mentioned, so I'm glad you mentioned it.

When I think about animating panels, I think about the easing approach I want elements to use (linear, quint), and how long I want them to take to transition. But when I think about player movement, I think instead about things like player acceleration, and incrementally moving players along a directional vector based on their movement speed.

In the end, there might be some sort of basic lerping going on between player positions, but when it comes to driving their acceleration, I think I'd like to somehow separate that from the interpolation that we use in panel, since at any point in time, the player might abruptly stop.

Tamschi commented 9 years ago

This was something I was concerned with in regards to refactoring panel to use an interpolation library/interface that player could share, alongside anything else in the game. I think about the two in fundamentally different ways, and precisely the two that you mentioned, so I'm glad you mentioned it.

When I think about animating panels, I think about the easing approach I want elements to use (linear, quint), and how long I want them to take to transition. But when I think about player movement, I think instead about things like player acceleration, and incrementally moving players along a directional vector based on their movement speed.

In the end, there might be some sort of basic lerping going on between player positions, but when it comes to driving their acceleration, I think I'd like to somehow separate that from the interpolation that we use in panel, since at any point in time, the player might abruptly stop.

There are several GUI situations I can think of where user input may interrupt the animation timeline, for example when a user removes items in a list that is shown horizontally but wrapped around. The best approach would likely be to add the original movement of the transitions so any sequence of actions still completes at the right time after the last input. (I think this is how some Unix file explorers handle removals and insertions.) Implementing this with direct functions likely requires virtualising the animations to some extent so they can operate additively.

The most significant practical difference is likely the trade-off between timing precision and "organic" reactivity, which makes it somewhat less likely for the action to complete precisely in time with the driven approach. For that reason I think it still makes more sense to use (relatively) static timelines to organise sequences of GUI animations.

An ugly side effect of driven easing is that by default it's very sensitive to frame time variation. With constant speed it's barely a problem (unless speed * time-step / offset is large) but if you have varying acceleration you may have to take higher order derivatives into account or approximate them if that's impossible. You'll also have to think about letting animations complete without using up all available time, or there will be some creeping error if you use it for any sequences or game logic that's not turn-based.

andrewmcwatters commented 9 years ago

Implemented in build 747, featured in Facepunch Programming's What Are You Working On? April 2015