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
+4 -1
View File
@@ -6,7 +6,10 @@ class_name MovementParams
@export var crouch_speed: float = 4.5
@export var ground_friction: float = 10.0
@export var ground_acceleration: float = 60.0
@export var ground_deceleration: float = 45.0
# Releasing movement carries roughly one metre of momentum from full speed.
# The old 45/s response erased half the velocity in a single rendered frame,
# leaving no physical time for an authored stopping step to read.
@export var ground_deceleration: float = 9.0
# ── Jump / Air ────────────────────────────────────────────────────────────────
@export var jump_velocity: float = 8.5
+24 -3
View File
@@ -83,6 +83,9 @@ var grapple_rope: MeshInstance3D
# broadcasts position/velocity; remote peers interpolate toward it (see
# _process). The server stays authoritative for health, shield, and kills.
var synced_movement_state: String = "idle"
## Presentation-only state. Physics/network gameplay still uses
## synced_movement_state; this adds stop and alternating-air variants.
var synced_animation_state: String = "idle"
var synced_movement_speed: float = 0.0
var synced_is_crouching: bool = false
var synced_position: Vector3 = Vector3.ZERO
@@ -91,6 +94,7 @@ var synced_is_ads: bool = false
var synced_wall_side: float = 0.0 # -1 wall left, +1 wall right (wall-glide bank)
var synced_wall_normal: Vector3 = Vector3.ZERO
var synced_wall_contact_point: Vector3 = Vector3.ZERO
var _airborne_alternate := false
var synced_helmet_closed: bool = false
var synced_is_dancing: bool = false # dance emote (B), shown on the model
## Which of the five routines in DanceRoutines is playing. Replicated, so other
@@ -1063,9 +1067,10 @@ func _physics_process(_delta: float) -> void:
synced_is_ads = _read_ads()
# Drive the visual model (skinned GLB or procedural) from local state
var h_speed = Vector2(velocity.x, velocity.z).length()
var animation_state := _visual_animation_state(sm.current_state, h_speed)
var visual = get_visual_model()
if visual:
var h_speed = Vector2(velocity.x, velocity.z).length()
if visual.has_method("set_locomotion"):
var d := _local_move_dir()
visual.set_locomotion(d.x, d.y, 1.0 if synced_is_ads else 0.0)
@@ -1081,7 +1086,7 @@ func _physics_process(_delta: float) -> void:
visual.set_wall_run_motion(velocity)
if visual.has_method("set_helmet_closed"):
visual.set_helmet_closed(synced_helmet_closed)
visual.update_state(sm.current_state, h_speed, sm.input_crouch)
visual.update_state(animation_state, h_speed, sm.input_crouch)
if visual.has_method("set_dancing"):
visual.set_dancing(synced_is_dancing, synced_dance_index)
if visual.has_method("set_grapple_target") and sm.current_state == "grapple":
@@ -1090,6 +1095,7 @@ func _physics_process(_delta: float) -> void:
# Publish state for remote peers
synced_movement_state = sm.current_state
synced_animation_state = animation_state
synced_movement_speed = Vector2(velocity.x, velocity.z).length()
synced_is_crouching = sm.input_crouch
synced_wall_side = sm.wall_side
@@ -1109,6 +1115,17 @@ func _local_move_dir() -> Vector2:
return Vector2(local_vel.x / hspeed, -local_vel.z / hspeed)
func _visual_animation_state(state: String, horizontal_speed: float) -> String:
if state == "air":
return "air_alt" if _airborne_alternate else "air"
if state == "ground" and is_instance_valid(_machine) \
and not _machine.input_crouch \
and _machine.input_dir.length_squared() < 0.01 \
and horizontal_speed > 0.6:
return "stop"
return state
## Whether the local player's active weapon is aiming down sights.
func _read_ads() -> bool:
if not is_instance_valid(camera):
@@ -1130,6 +1147,10 @@ func _on_movement_event(ev: String, data: Dictionary) -> void:
grapple_latch_player.play()
elif ev == "dash":
_speedline_burst = 1.0
elif ev == "jump":
# Alternate the complete authored airborne silhouette on successive
# ground jumps so bunny hops do not always lead with the same knee.
_airborne_alternate = not _airborne_alternate
elif ev == "double_jump":
_trigger_action("double_jump")
if is_instance_valid(_jet_vfx):
@@ -1183,7 +1204,7 @@ func _process(delta: float) -> void:
if visual.has_method("set_helmet_closed"):
visual.set_helmet_closed(synced_helmet_closed)
visual.update_state(
synced_movement_state,
synced_animation_state,
synced_movement_speed,
synced_is_crouching,
)
+10 -5
View File
@@ -52,11 +52,8 @@ func update(delta: float) -> void:
# ── Horizontal movement (exponential interpolation for smoothness) ───
var hvel := Vector3(vel.x, 0.0, vel.z)
var target_vel := wish_dir * effective_speed
if wish_dir.length_squared() > 0.0:
hvel = hvel.lerp(target_vel, 1.0 - exp(-params.ground_acceleration * delta))
else:
hvel = hvel.lerp(Vector3.ZERO, 1.0 - exp(-params.ground_deceleration * delta))
hvel = _next_horizontal_velocity(
hvel, target_vel, wish_dir.length_squared() > 0.0, delta)
vel.x = hvel.x
vel.z = hvel.z
@@ -127,6 +124,14 @@ func update(delta: float) -> void:
return
func _next_horizontal_velocity(current: Vector3, target: Vector3,
has_input: bool, delta: float) -> Vector3:
var response := params.ground_acceleration \
if has_input else params.ground_deceleration
return current.lerp(target if has_input else Vector3.ZERO,
1.0 - exp(-response * delta))
func _try_step_up(player: CharacterBody3D, wish_dir: Vector3, hvel: Vector3) -> void:
for i in range(player.get_slide_collision_count()):
var col = player.get_slide_collision(i)
+20
View File
@@ -41,6 +41,7 @@ func run_all() -> void:
"test_dash_adds_speed_to_forward_momentum",
"test_chain_bonus_soft_caps",
"test_notify_landed_emits_scaled_event",
"test_ground_release_has_short_stopping_glide",
"test_slide_entry_boosts_and_keeps_direction",
"test_grapple_state_registered_by_machine",
]
@@ -239,6 +240,25 @@ func test_notify_landed_emits_scaled_event() -> void:
_expect(sm.current_jump_count == 0, "landing resets jump count")
func test_ground_release_has_short_stopping_glide() -> void:
var ground = sm.states["ground"]
var velocity := Vector3(0.0, 0.0, -params.walk_speed)
var distance := 0.0
var first_frame_speed := 0.0
for frame in 90:
velocity = ground._next_horizontal_velocity(
velocity, Vector3.ZERO, false, 1.0 / 60.0)
if frame == 0:
first_frame_speed = velocity.length()
distance += velocity.length() / 60.0
if velocity.length() <= 0.6:
break
_expect(first_frame_speed > params.walk_speed * 0.80,
"release keeps most momentum on its first frame")
_expect(distance > 0.75 and distance < 1.5,
"release produces a short stopping glide (%.2f m)" % distance)
func test_slide_entry_boosts_and_keeps_direction() -> void:
sm.switch_to("ground")
fake_player.velocity = Vector3(0, 0, -12) # faster than 0.8*walk_speed