using UnityEngine; namespace Ashwild.Player { /// /// Drives a humanoid Animator from the local player's PlayerEvents bus so a visible rig /// reflects what the player is doing. It is a pure consumer of the bus — it never simulates /// anything itself — which lets the exact same component drive two independent rigs from one /// shared source of truth: the owner's first-person arms (seen only by the owner, never /// networked) and his third-person body (replicated to other players by a sibling FishNet /// NetworkAnimator). Because both rigs read the same events at the same frame, the arms the /// owner sees and the body everyone else sees stay in lockstep by construction. /// This is owner-only local simulation: add every instance to /// PlayerNetworkController.ownerOnlyBehaviours so remote puppets never run it — a remote body /// is animated by its replicated NetworkAnimator instead. Every parameter name is optional; /// leave a field blank to skip that parameter, so a minimal arms rig can ignore the full /// locomotion blend that the third-person body uses. /// [DisallowMultipleComponent] public class PlayerAnimatorDriver : MonoBehaviour { #region Serialized Fields [Header("Target")] [SerializeField] private Animator animator; [Tooltip("Transform whose facing defines local move direction. Defaults to the Animator's transform when left empty.")] [SerializeField] private Transform orientation; [Header("Float parameters (leave blank to skip)")] [SerializeField] private string speedParam = "Speed"; [SerializeField] private string moveXParam = "MoveX"; [SerializeField] private string moveYParam = "MoveY"; [Tooltip("Planar speed (m/s) that maps to 1 in the blend tree. 0 feeds raw speed unscaled.")] [SerializeField] private float normalizeSpeed = 6f; [Tooltip("Damping applied to float parameters for smooth blends.")] [SerializeField] private float floatDampTime = 0.1f; [Header("Bool parameters (leave blank to skip)")] [SerializeField] private string groundedParam = "Grounded"; [SerializeField] private string crouchingParam = "Crouching"; [SerializeField] private string sprintingParam = "Sprinting"; [Header("Trigger parameters (leave blank to skip)")] [SerializeField] private string jumpParam = "Jump"; [SerializeField] private string attackParam = "Attack"; [SerializeField] private string landParam = "Land"; #endregion #region State private Vector3 planarVelocity; private int speedId, moveXId, moveYId; private int groundedId, crouchingId, sprintingId; private int jumpId, attackId, landId; #endregion #region Unity Lifecycle /// /// Caches parameter hashes (0 for blank names, which are then skipped) and defaults the /// orientation reference so local move direction works without extra wiring. /// private void Awake() { speedId = Hash(speedParam); moveXId = Hash(moveXParam); moveYId = Hash(moveYParam); groundedId = Hash(groundedParam); crouchingId = Hash(crouchingParam); sprintingId = Hash(sprintingParam); jumpId = Hash(jumpParam); attackId = Hash(attackParam); landId = Hash(landParam); if (orientation == null && animator != null) orientation = animator.transform; } /// /// Subscribes to the locomotion/tool events that map onto animation, and seeds the bool /// parameters from the bus's current state so a freshly enabled rig starts in the right pose. /// private void OnEnable() { PlayerEvents.VelocityChanged += HandleVelocityChanged; PlayerEvents.GroundedChanged += HandleGroundedChanged; PlayerEvents.CrouchChanged += HandleCrouchChanged; PlayerEvents.SprintChanged += HandleSprintChanged; PlayerEvents.Jumped += HandleJumped; PlayerEvents.AttackSwung += HandleAttackSwung; PlayerEvents.LandingStunStarted += HandleLandingStunStarted; SetBool(groundedId, PlayerEvents.IsGrounded); SetBool(crouchingId, PlayerEvents.IsCrouching); SetBool(sprintingId, PlayerEvents.IsSprinting); } /// /// Unsubscribes — mirrors OnEnable exactly. /// private void OnDisable() { PlayerEvents.VelocityChanged -= HandleVelocityChanged; PlayerEvents.GroundedChanged -= HandleGroundedChanged; PlayerEvents.CrouchChanged -= HandleCrouchChanged; PlayerEvents.SprintChanged -= HandleSprintChanged; PlayerEvents.Jumped -= HandleJumped; PlayerEvents.AttackSwung -= HandleAttackSwung; PlayerEvents.LandingStunStarted -= HandleLandingStunStarted; } /// /// Pushes the smoothed movement floats every frame so blend trees ease between poses /// instead of snapping when velocity changes. /// private void Update() { if (animator == null) return; float scale = normalizeSpeed > 0f ? 1f / normalizeSpeed : 1f; SetFloat(speedId, planarVelocity.magnitude * scale); if (moveXId != 0 || moveYId != 0) { Vector3 local = orientation != null ? orientation.InverseTransformDirection(planarVelocity) : planarVelocity; SetFloat(moveXId, local.x * scale); SetFloat(moveYId, local.z * scale); } } #endregion #region Event Handlers /// /// Caches the planar (horizontal) velocity that Update turns into blend-tree floats. /// private void HandleVelocityChanged(Vector3 velocity) { planarVelocity = new Vector3(velocity.x, 0f, velocity.z); } private void HandleGroundedChanged(bool grounded, float impactSpeed) => SetBool(groundedId, grounded); private void HandleCrouchChanged(bool crouching) => SetBool(crouchingId, crouching); private void HandleSprintChanged(bool sprinting) => SetBool(sprintingId, sprinting); private void HandleJumped() => SetTrigger(jumpId); private void HandleAttackSwung() => SetTrigger(attackId); /// /// A landing stun begins on a hard touchdown — the cue for a landing animation. /// private void HandleLandingStunStarted(float duration) => SetTrigger(landId); #endregion #region Internal Helpers /// /// Hashes a parameter name, returning 0 for a blank name so the setters can skip it. /// private int Hash(string paramName) => string.IsNullOrEmpty(paramName) ? 0 : Animator.StringToHash(paramName); private void SetFloat(int id, float value) { if (id != 0) animator.SetFloat(id, value, floatDampTime, Time.deltaTime); } private void SetBool(int id, bool value) { if (id != 0 && animator != null) animator.SetBool(id, value); } private void SetTrigger(int id) { if (id != 0 && animator != null) animator.SetTrigger(id); } #endregion } }