jump ani fixing and transition animations

This commit is contained in:
Nicholas Butzke
2026-08-10 12:50:00 -04:00
parent 833e936bcd
commit 0ce626ad42
13 changed files with 359 additions and 35 deletions
+32 -4
View File
@@ -250,7 +250,12 @@ func _apply_color(col: Color) -> void:
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
var previous_speed := movement_speed
var slowdown := previous_speed - speed
if state in ["ground", "idle"]:
if state == "stop" and current_state != "stop":
_brake_left = BRAKE_DURATION
_brake_strength = clampf(maxf(previous_speed, speed) / 10.0, 0.55, 1.0)
if absf(_locomotion_fwd) > 0.15:
_brake_fwd = signf(_locomotion_fwd)
elif state in ["ground", "idle"]:
if speed > 4.5 and slowdown < 0.05:
_brake_armed = true
if _brake_armed and previous_speed > 4.5 and slowdown > 0.12:
@@ -259,7 +264,7 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
_brake_armed = false
if absf(_locomotion_fwd) > 0.15:
_brake_fwd = signf(_locomotion_fwd)
elif state not in ["ground", "idle"]:
elif state not in ["ground", "idle", "stop"]:
_brake_left = 0.0
_brake_armed = true
current_state = state
@@ -378,7 +383,7 @@ func _set_shadows_recursive(node: Node) -> void:
func _process(delta: float) -> void:
var brake_target := 0.0
if _brake_left > 0.0 and current_state in ["ground", "idle"]:
if _brake_left > 0.0 and current_state in ["ground", "idle", "stop"]:
_brake_left = maxf(0.0, _brake_left - delta)
var progress := 1.0 - _brake_left / BRAKE_DURATION
var envelope := smoothstep(0.0, 1.0, progress / 0.30) \
@@ -457,6 +462,15 @@ func _process(delta: float) -> void:
t_upper_arm_r_rot.x = -0.5
t_lower_arm_l_rot.x = -1.2
t_lower_arm_r_rot.x = -1.2
"stop":
# Neutral base under the catching-step overlay below. Keeping the run
# cycle out of this state prevents residual speed from accelerating it.
t_root_rot.x = deg_to_rad(8.0)
t_thigh_r_rot.x = deg_to_rad(-24.0)
t_calf_r_rot.x = deg_to_rad(12.0)
t_thigh_l_rot.x = deg_to_rad(8.0)
t_calf_l_rot.x = deg_to_rad(14.0)
"slide":
t_root_pos.y = -0.6
@@ -485,6 +499,20 @@ func _process(delta: float) -> void:
t_upper_arm_r_rot.z = -0.5
t_lower_arm_l_rot.x = -0.5
t_lower_arm_r_rot.x = -0.5
"air_alt":
# Mirror the simple fallback silhouette to match the alternating
# offline-authored variants used by skinned motion-lab characters.
t_thigh_l_rot.x = 0.2
t_thigh_r_rot.x = -0.4
t_calf_l_rot.x = 0.1
t_calf_r_rot.x = 0.5
t_upper_arm_l_rot.x = -0.8
t_upper_arm_r_rot.x = -0.8
t_upper_arm_l_rot.z = -0.5
t_upper_arm_r_rot.z = 0.5
t_lower_arm_l_rot.x = -0.5
t_lower_arm_r_rot.x = -0.5
"wall_run":
t_root_rot.z = deg_to_rad(-15)
@@ -549,7 +577,7 @@ func _process(delta: float) -> void:
# Blend a single catching step over the locomotion result. It rises quickly
# and releases over the same span as the run-to-idle crossfade, preserving
# momentum without freezing the whole mannequin in an exaggerated plant.
if current_state in ["ground", "idle"] and _brake_weight > 0.001:
if current_state in ["ground", "idle", "stop"] and _brake_weight > 0.001:
var travel_sign := -1.0 if _brake_fwd < 0.0 else 1.0
t_root_pos.y = lerpf(t_root_pos.y, -0.025, _brake_weight)
t_root_rot.x = lerp_angle(
+61 -17
View File
@@ -48,8 +48,11 @@ const CLIP_FALLBACKS := {
"Run": ["Run", "Walk", "Idle"],
"Sprint": ["Sprint", "Run", "Walk", "Idle"],
"Jump": ["Jump", "Fall", "Idle"],
"JumpAlt": ["JumpAlt", "Jump", "Fall", "Idle"],
"Fall": ["Fall", "Jump", "Idle"],
"FallAlt": ["FallAlt", "Fall", "Jump", "Idle"],
"Land": ["Land", "Idle"],
"Stop": ["Stop", "Land", "Idle"],
"Crouch": ["CrouchIdle", "Crouch", "Idle"],
"CrouchWalk": ["CrouchWalk", "Crouch", "CrouchIdle", "Walk"],
"SlideStart": ["SlideStart", "Slide"],
@@ -85,7 +88,7 @@ const CLIP_FALLBACKS := {
"Throw": ["Throw", "Hit"],
}
const LOOPING_CLIPS := ["Idle", "Walk", "Run", "Sprint", "Fall", "Crouch",
const LOOPING_CLIPS := ["Idle", "Walk", "Run", "Sprint", "Fall", "FallAlt", "Crouch",
"CrouchIdle", "CrouchWalk", "Slide", "WallRunLeft", "WallRunRight",
"WallCling", "Grapple",
"StrafeWalkForward", "StrafeWalkBackward", "StrafeWalkLeft", "StrafeWalkRight",
@@ -99,17 +102,13 @@ const BLEND_TIME := 0.36
## drifts across their thresholds, and that is where hard cuts were most
## visible.
const BLEND_TIMES := {
"Dash": 0.20, "Jump": 0.24, "Hit": 0.14, "Land": 0.28,
# SlideStart has a distinct authored crouch pose; blending into it from
# airborne/standing can send the lower body through a ground-plane flip.
"SlideStart": 0.0, "Slide": 0.30, "Death": 0.34,
"Dash": 0.20, "Jump": 0.12, "JumpAlt": 0.12,
"Fall": 0.12, "FallAlt": 0.12, "Hit": 0.14, "Land": 0.28,
"Stop": 0.12, "SlideStart": 0.12, "Slide": 0.30, "Death": 0.34,
"Throw": 0.20, "PistolReload": 0.30,
"Idle": 0.42, "PistolIdle": 0.42, "Walk": 0.40, "Run": 0.40, "Sprint": 0.40,
"CrouchIdle": 0.40, "CrouchWalk": 0.40, "Fall": 0.32,
# The two authored wall performances start in distinct side poses. A full
# body crossfade from Fall/ground interpolates the hips and thighs through
# an upside-down midpoint, so hand off directly to the authored first pose.
"WallRunLeft": 0.0, "WallRunRight": 0.0,
"CrouchIdle": 0.40, "CrouchWalk": 0.40,
"WallRunLeft": 0.12, "WallRunRight": 0.12,
"WallCling": 0.32, "Grapple": 0.32,
}
const GROUND_STATE_BLEND := 0.30
@@ -135,6 +134,7 @@ var _wall_surface_normal_world := Vector3.ZERO
var _wall_surface_point_world := Vector3.ZERO
var _motion_yaw := 0.0
var _motion_was_wall_run := false
var _stop_entry_speed := 0.0
var _resolved_clips: Dictionary = {} # canonical name -> actual clip name
var _current_clip: String = ""
@@ -234,6 +234,7 @@ func load_model(path: String) -> void:
_wall_surface_point_world = Vector3.ZERO
_motion_yaw = 0.0
_motion_was_wall_run = false
_stop_entry_speed = 0.0
_loco_blend_target = Vector2.ZERO
_loco_blend_visual = Vector2.ZERO
_loco_scale_target = 1.0
@@ -857,7 +858,7 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
_prev_state = state
return
# A heavy landing plays the Land one-shot before locomotion resumes.
if state in ["ground", "idle"] and _prev_state == "air" \
if state in ["ground", "idle"] and _prev_state in ["air", "air_alt"] \
and _vertical_speed() < -12.0 and _resolved_clips.has("Land"):
_oneshot_lock = 0.25
_play_clip("Land")
@@ -866,6 +867,10 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
_prev_state = state
if state != "slide":
_slide_start_remaining = 0.0
if state == "stop" and previous_state != "stop":
_stop_entry_speed = speed
elif state != "stop":
_stop_entry_speed = 0.0
var clip := "Idle"
match state:
@@ -878,6 +883,13 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
"air":
# Rising = jump, falling = the fall loop.
clip = "Jump" if _vertical_speed() > 0.5 else "Fall"
"air_alt":
# Offline-mirrored source clips swap the leading knee and arm while
# preserving the exact Titanfall timing and silhouette.
clip = "JumpAlt" if _vertical_speed() > 0.5 else "FallAlt"
"stop":
clip = "Stop"
_loco_scale_target = 1.0
"slide":
if previous_state != "slide":
_slide_start_remaining = _animation_length_if_distinct(
@@ -910,10 +922,11 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
clip = "Death"
if clip != "":
# A wall jump hands the full body from a side performance to Jump/Fall.
# Interpolating those two poses can put the thighs through the horizontal
# midpoint, which reads as a delayed leg flip on the jump frame.
_play_clip(clip, false, 0.0 if exiting_wall_run else -1.0)
var blend_override := -1.0
if state in ["wall_run", "slide", "air", "air_alt", "stop"] \
or exiting_wall_run:
blend_override = _speed_scaled_transition(speed)
_play_clip(clip, false, blend_override)
if _pose_mod:
_pose_mod.state = state
@@ -927,6 +940,8 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
var scale := 1.0
if clip in ["SlideStart", "Slide"]:
scale = _slide_playback_scale(speed)
elif clip == "Stop":
scale = _stop_playback_scale(_stop_entry_speed)
elif clip == "Grapple" or clip in ["WallRunLeft", "WallRunRight"]:
# Grapple is held, and the wall clips are compact propulsion loops.
# Neither should become a frantic treadmill at high traversal speed.
@@ -1187,6 +1202,20 @@ func _slide_playback_scale(speed: float) -> float:
return clampf(speed / maxf(run_anim_reference_speed, 0.01), 0.75, 1.8)
func _stop_playback_scale(entry_speed: float) -> float:
# Finish the weight-catching step while the physical stopping glide still
# has momentum. Faster entries compress the one-shot without hard-cutting it.
return clampf(entry_speed / maxf(run_anim_reference_speed, 0.01), 1.0, 2.2)
func _speed_scaled_transition(speed: float) -> float:
# High-speed traversal needs a quicker silhouette handoff, but never a
# zero-frame snap. At ordinary run speed this is ~0.12 s; near the movement
# cap it approaches 0.07 s.
var speed_factor := clampf((speed - 3.0) / 15.0, 0.0, 1.0)
return lerpf(0.18, 0.07, speed_factor)
## Pick the cloth solver's detail level from how far the camera is.
##
## Re-checked a few times a second rather than every frame: the answer changes
@@ -1248,8 +1277,23 @@ func _play_ground_locomotion(speed: float, crouched: bool) -> void:
if _current_state_node != state_node:
var exiting_wall_run := _current_state_node in [
"WallRunLeft", "WallRunRight"]
_state_trans.xfade_time = 0.0 if exiting_wall_run \
else GROUND_STATE_BLEND
var exiting_stop: bool = \
_current_state_node == String(_resolved_clips.get("Stop", ""))
if exiting_wall_run:
_state_trans.xfade_time = _speed_scaled_transition(speed)
elif exiting_stop:
# The stop one-shot has already absorbed the residual momentum. Enter
# the Idle point directly under the crossfade; carrying Sprint's stale
# BlendSpace coordinate forward would create a final fast run cycle.
_loco_blend_target = Vector2.ZERO
_loco_blend_visual = Vector2.ZERO
_loco_scale_target = 1.0
_loco_scale_visual = 1.0
_anim_tree.set("parameters/loco_blend/blend_position", Vector2.ZERO)
_anim_tree.set("parameters/loco_scale/scale", 1.0)
_state_trans.xfade_time = 0.18
else:
_state_trans.xfade_time = GROUND_STATE_BLEND
_anim_tree.set("parameters/state/transition_request", state_node)
_current_state_node = state_node