Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement very basic look ahead functionality #284

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

audeck
Copy link
Contributor

@audeck audeck commented May 1, 2024

Compared to the old look ahead PR (#160), this has the disadvantage of being directly affected by follow_damping (though conceptually, I am completely fine with that). Otherwise, it is very reminiscent of the original, being implemented only for the 2D SIMPLE mode with a follow target (as that is the main use case for me), but the @export values are much friendlier for me to use personally.

Additionally, I've included a potentially better way of defining @export property visibilities. This approach has the advantage of less code distance (i.e. being able to define a property's visibility right where you define it), but it also has the disadvantage of needing an editor reload every time you change when it should be visible. I'm also unsure of how it'd interact with other property.usage values, but they should co-exist peacefully in the _validate_property() function. Note that this is merely a proposal.

Still a few TODOs left at the very least; opening a PR to get input on the aforementioned, how to reconcile the 2 PRs together, and nitpicks of course :^).

@ZenithStar
Copy link
Contributor

(I'm not working on any 2D games at the moment, so I'm not particularly invested in this feature, so take my opinion with a grain of salt.)
I see that you implement the look-ahead offset proportional to velocity, but I'd expect the behavior to be more similar to the framed follow mode, but with the look-ahead offset state snapping to the direction the player moves after passing a threshold:
1710055789822403

@audeck
Copy link
Contributor Author

audeck commented May 2, 2024

I definitely see what you mean. What would make the most sense to me when trying to replicate the camera in question, would be framed follow mode, with the look ahead for it acting to offset the frame as well (and, therefore, give the player some leeway before shifting the camera the other way).

Hollow Knight does something similar for the X axis, just without dead zones:

youtube-video-gif(2)

Technically, Hollow Knight's camera's X axis behavior could be replicated by allowing the offset to persist, setting the look_ahead_min_velocity.x to 1 with look_ahead_min_offset and look_ahead_max_offset being identical (albeit the current implementation doesn't handle this correctly).

As for the Mario camera, the same could be done, but with framed follow mode and checking if the frame moved instead of the follow_target.

However, I think you point out a good distinction in the sense that there is a difference between Mario and Hollow Knight's directional look ahead, and this PR's velocity based look ahead. The former is more so a "forward focus", and the latter a "velocity offset".

Might be a good idea to split the look ahead functionality into these two instead, then, as it'd allow to recreate the Mario camera, but also with an additional offset that scales with velocity when going really fast (for a more momentum-based game).

I.e.:

  • Forward focus:
    • forward_focus
    • forward_focus_offset
    • forward_focus_deadzone (? - not sure if this functionality isn't completely superseded by using framed follow mode)
  • Velocity offset:
    • basically what the initial commit already has, but renamed accordingly

@ramokz
Copy link
Owner

ramokz commented May 7, 2024

This is neat!

Please correct me if I misinterpret how the values are being set and applied to the camera position.

The challenge I see with defining min / max velocity and min offset values is that those values will, likely, need to change as the target's velocity speeds up or slows down. Whereas if it relies on time, then the velocity will dynamically change the Lookahead effect. I'm still generally leaning towards the approach from my previous comment in the other PR about having the properties provide relative rather than absolute values. I.e. the properties provide the option for the user to decide “how many seconds ahead should the camera be positioned based on the current velocity” and “how quickly should it accelerate to that”. For me, it feels like a simpler system to understand, use and change during runtime if needed. Clamping the Lookahead offset also, like you've done, definitely feels like a right choice.

The way I imagine it is that you would have the following properties:

  • Lookahead (bool)
    • Toggles the feature on / off.
  • Lookahead Time (Vector2)
    • Determines how many seconds ahead the follow target will be based on its velocity and updates the camera to gradually move towards that position. The Vector2 values represent the X and Y positions, which can be specified independently of one another.
  • Lookahead Speed (Vector2)
    • How fast the camera should be moving to the Lookahead Time position from its current position. Like with Lookahead Time the X and Y values are being applied to each axis.
    • It could potentially be split in two, if there's a desire for a separate deceleration speed.
  • Lookahead Max (Vector2)
    • A clamp to not exceed a certain distance offset, no matter the Lookahead Time or velocity.

I want to be careful about not overcomplicating the feature and being too opinionated about how it works — particularly for a first iteration. Think this is one of those things that can be done in many ways depending on the needs of a project. Can think of cases where a user would want to enable or disable the Lookahead when certain conditions are met. Take the Mario example, if the user exceeds a certain boundary, like a Framed Follow dead zone, then that could temporarily enable the look Lookahead for x number of milliseconds before disabling it again.
The Hollow Knight example is an interesting one. Wonder if that could be achieved by having a Lookahead Deceleration property that would then be set to 0, which is to say, the camera doesn't return to its original follow target position if the follow target stops moving.

About the alternative way of toggling visibility for the @export, I like the thinking of keeping the visibility checkers near the variable declaration, as it's a bit of a pain to scroll back and forth between the at the minute. That said, I am still favouring the _validate_property() approach, since it follows the conventional Godot standard and is, from my view, easier to read, which is more important as it enables others to more easily understand the codebase.

@audeck
Copy link
Contributor Author

audeck commented May 8, 2024

The Hollow Knight example is an interesting one. Wonder if that could be achieved by having a Lookahead Deceleration property that would then be set to 0, which is to say, the camera doesn't return to its original follow target position if the follow target stops moving.

I've been thinking about this one. It's technically just a follow_offset getting updated at runtime, which, in my opinion, should be left up to the games themselves.

I can think of a game where the movement and "looking" are decoupled (one using WASD, the other IJKL, for example). But then there are two options that the developer could want:

  1. Have an offset toward where the character is moving, even if it's looking behind
  2. Have on offset toward where the character is looking, even if it's moving in the other direction

This is also why I think naming this functionality "Lookahead" could be a bit misleading, especially for people who have no knowledge of Cinemachine, as opposed to naming this some kind of "Velocity offset".

One a side note -- in order to achieve this functionality using Framed Follow, something like a started_moving signal would be nice to have to know when to update the follow_offset.

As for time vs. min/max velocity, I like the fact that it's effectively decoupled from the follow target's speed. I've gone with min/max velocity initally because it'd be easy to use different interpolation functions (your where from interpolate(from, to, where) would be simply (max_vel - cur_vel) / (max_vel - min_vel)), which is something I wanted for my project. May be too opinionated, though.

It'd technically be possible to have a "Lookahead min" and "Lookahead max" offset and use those for the interpolation. That is, calculate the offset depending on where the follow target will be in X seconds, and use that for the interpolation calculation if interpolation isn't set to none. I don't know how interpolation would interact with smoothing/damping in general, though; will have to experiment with that.

@audeck
Copy link
Contributor Author

audeck commented May 8, 2024

I've gone with "smoothing" instead of "speed" for controlling the offset. There is a velocity_offset_smoothing_factor, which just gets input into a lerp call. It is getting applied before follow_damping, which means the velocity offset is still affected by it. I'm not sure how Cinemachine does it, nor do I have any experience using it, so additional input is welcome here:

  1. Should the velocity offset be affected by follow_damping?
  2. What would you expect the smoothing/speed implementation to look like in general? Mainly, what values would you expect to use with it (higher values => more smoothing -- just like follow_damping, or the other way around, perhaps)?

@ramokz
Copy link
Owner

ramokz commented May 24, 2024

I've been thinking about this one. It's technically just a follow_offset getting updated at runtime, which, in my opinion, should be left up to the games themselves.

There was a similar disucssion on Unity's forum about the Lookahead feature for Cinemachine. From that conversation, there is a seemingly agreed idea of allowing the Lookahead mechanic to not move back towards its targeted position based on a user defined parameter. Though as that was not included in Cinemachine, at least at this point in time, one can argue that it isn't needed for a first iteration for this feature. Just something to keep in mind if nothing else.

I can think of a game where the movement and "looking" are decoupled (one using WASD, the other IJKL, for example). But then there are two options that the developer could want:

  1. Have an offset toward where the character is moving, even if it's looking behind
  2. Have on offset toward where the character is looking, even if it's moving in the other direction

Think a solution to that could be a Group Follow. Where, e.g., one target being a playable character, and another being either the mouse position on the screen, or another node in the scene. That way, you're moving the camera based on both positions, which should resolve that?

This is also why I think naming this functionality "Lookahead" could be a bit misleading, especially for people who have no knowledge of Cinemachine, as opposed to naming this some kind of "Velocity offset".

I think both terms have merits. “Velocity offset” infers that the camera will change position, or be offset, due to changes in velocity. While “Lookahead” infers that the camera is looking further ahead from its current position. To my mind, the main interpretational difference is that “Lookahead” suggests that the camera is explicitly moving forward, whereas “Velocity Offset” implies that it's just being offset without a particular direction in mind. To your point, “Lookahead” in of itself doesn't necessarily tell the user what it will actually do and works more like an interpretive description rather than a technical one — similar to “damping” vs. “smoothing”. Though do think that's an easy solve with documentation or by the user enabling it themselves and trying it out. There is also an inverse risk of renaming something that people who are familiar with the feature in Cinemachine to something else that works the same way.

One a side note -- in order to achieve this functionality using Framed Follow, something like a started_moving signal would be nice to have to know when to update the follow_offset.

Not entirely sure how it would impact Framed Follow? But think an easier way to detect movement anyway would be to rely on the velocity changes, or a velocity flag if you will, as they would be set at the same time.

As for time vs. min/max velocity, I like the fact that it's effectively decoupled from the follow target's speed. I've gone with min/max velocity initally because it'd be easy to use different interpolation functions (your where from interpolate(from, to, where) would be simply (max_vel - cur_vel) / (max_vel - min_vel)), which is something I wanted for my project. May be too opinionated, though.

Not sure if a min_velocity is needed? If it had a minimum value, then it would always teleport that minimum distance, which I can't imagine would ever be desirable? The max velocity, on the other hand, would mean that the user would have to guess what an ideal max velocity is. As that property is effectively a limit to, likely, avoid the camera flying off screen. That said, max_velocity might be a solid alternative to my proposal of having a Vector2 with lookahead_max. Potentially, one could also offer both options and clamp both the velocity and positional offset respectively.

I've gone with "smoothing" instead of "speed" for controlling the offset. There is a velocity_offset_smoothing_factor, which just gets input into a lerp call. It is getting applied before follow_damping, which means the velocity offset is still affected by it. I'm not sure how Cinemachine does it, nor do I have any experience using it, so additional input is welcome here:

  1. Should the velocity offset be affected by follow_damping?
  2. What would you expect the smoothing/speed implementation to look like in general? Mainly, what values would you expect to use with it (higher values => more smoothing -- just like follow_damping, or the other way around, perhaps)?
  1. Short answer, yes. Think follow_damping and lookahead_smoothing aren't mutually exclusive and should work alongside one another. Ultimately, the lookahead feature just offsets the follow_target position. Where the lookahead_smoothing merely interpolates that offset value. Whereas follow_damping changes the movement velocity of the camera itself.
    If the lookahead doesn't have any lookahead_smoothing applied, but has follow_damping, then the camera will just behave as if the follow_target has teleported that lookahead distance away.
  2. Think keeping it consistent with follow_damping where 0 = no smoothing, and higher values applies more, makes the most sense.

The above was based on some tests in the latest Cinemachine release in Unity 6. It also made me realize that follow_damping and lookahead are technically opposites of one another and yet work in tandem. follow_damping causes the camera to lag behind the target, whereas lookahead causes the camera to move ahead of the target. If both are enabled then Cinemachine will balance out the velocity drivers of both features. I.e. it can in theory be following a spot in-between the lookahead position and the follow_target. Which I have nothing against, as you would then just need to adjust the follow_damping and lookahead values to work well together.

For the smoothing operation itself, instead of the lerp function, would also suggest trying out the internal _smooth_damp function that is used for follow_damping. That should mirror the smoothing behaviour from the follow logic and allow for using a smoothing parameter value where 0 = no smoothing - not to mention feel a bit nicer too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants