idanarye / bevy-tnua

A floating character controller for Bevy
https://crates.io/crates/bevy-tnua
Apache License 2.0
214 stars 14 forks source link

Allow the character to be affected by the external physical world even when the acceleration is high #30

Open idanarye opened 10 months ago

idanarye commented 10 months ago

If the character is pushed, Tnua will apply the maximum acceleration it can to reset its velocity. If we want the controls to be snappy, the acceleration is usually pretty high - which means that Tnua can easily negate these external forces.

I need to figure out a way to let the character be manipulated by such forces, without compromising on having a high acceleration for regular movement.

Rockson commented 2 months ago

Hi any workaround? I think I have the same problem when I want to push back the character after getting hit

idanarye commented 2 months ago

Not yet. I have direction for an idea how to implement this, but I don't have all the details ironed out and I want to create a demo environment first so that I can create it (which I plan to do after #59). Either way it wouldn't be a workaround - my idea can only be done as a proper feature.

I'll write the idea here (probably should have wrote it down long ago, but there's no time like the present):

Basically, I want to maintain a perceived velocity. Unlike the true velocity, which is read from physics backend, the perceived velocity will be updated by Tnua every frame based on:

  1. The motor (add the boost plus the acceleration times the frame duration)
  2. Expected external forces. For now this just means the gravity, but it might include other forces in the future.
    • It does not include the forces this ticket is about. That's the whole point.
  3. Exponential moving average with the true velocity.

As long as there are no external forces (other than the expected ones, aka gravity) the perceived velocity should be the same as the true velocity, so the third factor will not affect it and it'll behave the same as now. Once there is an external force - e.g. an impact from an attack - there will be a difference between the two. If the EMA was set with $\alpha = 1$, the perceived velocity will immediately match the true velocity - so again no issue. But if it's smaller than 1 - it'll take time, and during that time the character will not be able to react as strongly to the external force (because it would not have perceived that its velocity was changed - or at least won't perceive it in full force) which should produce the desired effect.

idanarye commented 2 months ago

Still need to decide if this should be controlled by the basis or by the controller directly.

idanarye commented 1 month ago

Okay, I have a demo level for testing this: https://idanarye.github.io/bevy-tnua/demos/platformer_3d-avian?level=Pushback

Now to work on the actual feature...

idanarye commented 1 month ago

So... I have it in the feature/pushover branch. But I don't really like how it turned out:

https://github.com/user-attachments/assets/014dc10d-e53d-4e35-b059-5b8996bb6ba4

My main issues:

  1. I couldn't do the prediction of the new velocity sans external forces properly. Maybe it's because for some reason I don't get the same Res<Time> the physics backend uses (it did work better when I used the fixed schedule), maybe it's a rounding problem (it did work better with f64) or maybe I just don't understand various things that happen in the physics simulation. I'm not sure how to fix this.
    • A result of this is that the update factor of the predicted velocity has to be very high. As seen in the video, when I set it to "only" 0.9 it gets weird results. This is not an issue by itself (lots of configurable numbers in Tnua have a "reasonable range") but I don't like it that the EMA is the only thing that prevents my inadequate prediction of velocity from being a total disaster.
  2. When I run toward the bullets it just doesn't feel right. Especially when I stop running after getting hit by a bullet. I don't like it that I can get pushed farther back when I stop after the bullet already applied its impulse.

I'll keep that branch for now, but I'm going to try and think a different solution for this problem.

idanarye commented 1 month ago

New approach - "boundary pushing"

The idea is to maintain a "boundary" in the velocity space. When the character gets pushed and due to that has a velocity of - let's say - 20 units west, then the boundary is at -20.0 * Vector3::X and a direction of Dir3::NEG_X. Once there is a boundary, the rules are:

  1. If the basis (this one is going to have to be implemented in the basis) wants to cross that boundary (to its negative side) it'll have to use a much lower acceleration (can be configured separately for grounded and airborne? Should it be a factor over the regular accelerations? That's a basis configuration issue which I'll have to deal with, but also an implementation detail)
  2. If the character's velocity does cross the boundary - the boundary gets updated to where the velocity is.
  3. Once the boundary is close enough to Vector3::ZERO (on the axis of it's direction) it gets removed (or just gets ignored?)

The big question here is - how to create the boundary in the first place? Maybe something similar to the pushover mechanism but without the EMA? Instead, I'll always "predict" the new boundary location (maybe do it in the basis itself? TnuaBuiltinWalk does calculate a running_velocity) and if they are too far away create a boundary? The direction of the difference can be used to define the direction of the boundary.

idanarye commented 1 month ago

I'd also like to somehow automatically "dissolve" boundaries over time.

If the character runs in the same direction it was pushed the boundary will not get pushed back - or at least it won't get pushed back all the way to zero. Say the character's speed is 10 and it gets pushed to a speed of 20. It'll slow down (with the boundary pushing acceleration, which is lower than its regular acceleration) from 20 to 10 because of the boundary, and then continue to run west at that speed. As long as it runs, the speed will not fall below 10 - which means that the boundary will not be pushed below 10.

That would mean that if the character keeps running for a while, the barrier will still be there when it finally brakes. We don't want that - if the character keeps running, it should eventually (a word which sounds like a lot of time, but in practice it'll usually be less than 10 seconds - maybe even less than 5) become a normal run that uses the normal brake acceleration.

I can think of two possible solutions:

  1. A timer
  2. Slowly push the boundary even if it doesn't get pushed.
  3. Make the barrier weak (less effect) without moving its position.

I kind of like the third option, but it would mean the barrier will have to be act differently than just acceleration limiter (or maybe the acceleration limit should slowly progress toward the regular acceleration?). Also, if I do that, I probably should do it with a linear dissolution?

Another option - detect when the barrier is not being pushed, and set a (relatively short) timer on that.

idanarye commented 1 month ago

Instead of removing the barrier when it gets to Vector3::ZERO, I'm thinking maybe it'd be better to remove it when it gets pushed after the original predicted velocity? That way I could deal with negative barriers - e.g. the characgter runs, gets pushed but not enough to completely negate its velocity, and continues to run. As long as it continues the run, it'll never get the boundary to zero - because it pushes it down (pushing is always "down" - as in the opposite direction of the boundary) and it's already in the negative.

It's true that with the timer it'll eventually stop pushing, but I don't think it'd feel right to trust the timer on this.

On the other hand, if the character gets pushed west while running east and continues to run. It makes no sense if it has to push the barrier all the way back to its original position - that would mean that after passing the zero it'll have to keep pushing the barrier, causing a slower acceleration that if it'd start from rest. So maybe if the actual and predicted velocities are on the different sides of the zero, I'll terminate the barrier once it passes the zero?

I'm not sure which option is better. But then again - Tnua's philosophy is that every game should be able to configure how it feel, so maybe I should make an enum for choosing between these options?

Rockson commented 1 month ago

I feel sorry I can’t help you much with it since I don’t know the domain. Anyway I think that letting the developer decide is better. Anyway have you considered nullifying the moving velocity while being hit? Then restore it after a timer? It probably wouldn’t allow some use cases but it could be an option

idanarye commented 1 month ago

Not sure I like how it turned out:

https://github.com/user-attachments/assets/df49aa15-9b36-44ee-8543-24be86093a21

Maybe if I get the boundary's strength to fade over time? (or over velocity recovered?)

idanarye commented 1 month ago

I've added a diminishing factor. At first it was linear by percentage of "boundary penetration" (actually - how much velocity has been recovered) which didn't feel very good, but when I made it a power function it became much better:

https://github.com/user-attachments/assets/8a7d696e-d09c-496d-a9d1-0b15aaf0ef7e

I still don't think it's good enough though. I'm not sure if it's because I haven't tweaked enough with the numbers, because I didn't add a dedicated animation, or because something in my math is lacking.

idanarye commented 1 month ago

Tweaking with the numbers improved quite a lot. Changing Pushover: Barrier Strenght Diminishing from 0.2 to 2.0 (to get the barrier strength diminishing function to generally match with this suggestion I got on Reddit greatly improved things:

https://github.com/user-attachments/assets/b0f6ca27-0b3b-46f5-b909-c4e0d98d2607

When I increase the bullet strength the change becomes more apparent:

https://github.com/user-attachments/assets/95c44ca6-5850-44c5-ae05-b5e2b42fe28f

idanarye commented 1 month ago

I'm trying a logarithmic function:

https://github.com/user-attachments/assets/af08d753-0f4f-4036-9e11-d1c491a230c4

I'm not sure if I like it more or less...

idanarye commented 1 month ago

I'll think I go with the logarithmic function

idanarye commented 1 month ago

Or maybe not. I'm getting weird results because using $ln$ moves the number out of the $0..1$ range...

I'll stick with the old function.

idanarye commented 3 weeks ago

And of course I wouldn't think to try it in the regular level before merging it to main...

https://github.com/user-attachments/assets/da754754-c383-44b4-8e78-cff28da70233

In retrospect, I should have seen it coming. Hitting a wall, after all, is also an impulse...

I'll need to figure out how to distinguish between these impulses. I may end up having to make the user code tell the controller about the impulse instead of detecting it automatically...

idanarye commented 3 weeks ago

Since user code will have to manually trigger it anyway (otherwise I can't differentiate between "hitting an obstacle" and "getting hit by an attack"), I'm thinking of maybe just making a TnuaBuiltinKnockback action.

This does mean I'll have to devise a way to prioritize actions so that it won't get overwritten by a user action in the same frame. We don't want to create a glitch that allows cancelling a knockback with a perfectly timed crouch...

idanarye commented 3 days ago

TnuaBuiltinKnockback is the way to go. I've started a branch to work on it.