feat: shooter animation feel — directional lean, slide, composed airborne, weapon hold
Layer a procedural SkeletonModifier3D (ShooterPoseModifier) on top of the base clip so the character reads like a movement-shooter avatar: - Directional lean: banks into the movement direction (right/left/back) and blends smoothly for diagonals, driven by velocity relative to facing. - Slide: leans the torso back and pitches the head up to look forward, instead of the base clip's forward-torso/legs-out "spine break". - Airborne: plays a composed Jump pose (weapon ready) rather than a flailing fall. - Weapon hold: the base clip already keeps the arms down (weapon at the hip); on ADS both arms lift and swing in toward centre-front to aim. ADS is read from the active weapon and synced (synced_is_ads) so remote players raise their weapons too. The controller feeds movement direction + ADS to SkinnedPlayerModel.set_locomotion() each frame (works for local and remote via synced velocity/rotation). All rotations are authored in skeleton space (fwd=+Z, up=+Y, right=-X) and converted per-bone; tuning constants are at the top of ShooterPoseModifier. Verified by rendering the poses (idle/strafe/back/slide/ads) in a real Godot viewport. Smoke test 30/30. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9939e7e524
commit
29fdca3565
@@ -73,6 +73,7 @@ var synced_movement_speed: float = 0.0
|
||||
var synced_is_crouching: bool = false
|
||||
var synced_position: Vector3 = Vector3.ZERO
|
||||
var synced_velocity: Vector3 = Vector3.ZERO
|
||||
var synced_is_ads: bool = false
|
||||
@export var synced_skin_id: String = ""
|
||||
@export var synced_weapon_path: String = ""
|
||||
|
||||
@@ -864,11 +865,17 @@ func _physics_process(_delta: float) -> void:
|
||||
else:
|
||||
grapple_swing_player.volume_db = lerpf(grapple_swing_player.volume_db, -80.0, _delta * 15.0)
|
||||
|
||||
# Local player's aim-down-sights state (drives the model's weapon raise).
|
||||
synced_is_ads = _read_ads()
|
||||
|
||||
# Drive the visual model (skinned GLB or procedural) from local state
|
||||
var visual = get_visual_model()
|
||||
if visual:
|
||||
var h_speed = Vector2(velocity.x, velocity.z).length()
|
||||
visual.update_state(sm.current_state, h_speed, sm.input_crouch)
|
||||
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)
|
||||
|
||||
# Publish state for remote peers
|
||||
synced_movement_state = sm.current_state
|
||||
@@ -878,6 +885,28 @@ func _physics_process(_delta: float) -> void:
|
||||
synced_velocity = velocity
|
||||
|
||||
|
||||
## Movement direction relative to facing: x = strafe (+right), y = forward
|
||||
## (+forward). Derived from velocity so it works for local and remote players.
|
||||
func _local_move_dir() -> Vector2:
|
||||
var hspeed := Vector2(velocity.x, velocity.z).length()
|
||||
if hspeed < 0.5:
|
||||
return Vector2.ZERO
|
||||
var local_vel := global_transform.basis.inverse() * velocity
|
||||
return Vector2(local_vel.x / hspeed, -local_vel.z / hspeed)
|
||||
|
||||
|
||||
## Whether the local player's active weapon is aiming down sights.
|
||||
func _read_ads() -> bool:
|
||||
if not is_instance_valid(camera):
|
||||
return false
|
||||
var wman = camera.get_node_or_null("WeaponManager")
|
||||
if wman and "active_slot" in wman and wman.weapons.has(wman.active_slot):
|
||||
var w = wman.weapons[wman.active_slot]
|
||||
if w and "is_ads" in w:
|
||||
return w.is_ads
|
||||
return false
|
||||
|
||||
|
||||
func _on_movement_event(ev: String, data: Dictionary) -> void:
|
||||
if ev == "chain_updated":
|
||||
chain_updated.emit(data.count, data.bonus)
|
||||
@@ -914,6 +943,9 @@ func _process(delta: float) -> void:
|
||||
var visual = get_visual_model()
|
||||
if visual:
|
||||
visual.update_state(synced_movement_state, synced_movement_speed, synced_is_crouching)
|
||||
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)
|
||||
# Check for weapon changes
|
||||
if synced_weapon_path != "" and synced_weapon_path != visual.get_meta("current_weapon_path", ""):
|
||||
visual.set_weapon(synced_weapon_path)
|
||||
|
||||
Reference in New Issue
Block a user