78bfdf2828
# Conflicts: # Assets/External/Animated PBR Chest Demo/Materials/WoodChest.mat # Packages/com.distantlands.cozy.core/Content/Integration/Import for BiRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for HDRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for URP.unitypackage.meta
153 lines
6.1 KiB
C#
153 lines
6.1 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using Ashwild.Inventory;
|
|
|
|
namespace Ashwild.Player
|
|
{
|
|
/// <summary>
|
|
/// Keeps a rig's Animator loaded with the clips of whatever item the player is holding. It owns
|
|
/// exactly one concern — *which clips are in the controller right now* — while PlayerAnimatorDriver
|
|
/// owns *which parameters are set*. Splitting them is what lets a new weapon ship without touching
|
|
/// either: the driver keeps firing the same "Attack" trigger, the binder just points that slot at a
|
|
/// different clip.
|
|
///
|
|
/// It never assigns a fresh controller per item. Assigning runtimeAnimatorController rebinds the
|
|
/// Animator, which resets the state machine to Entry and wipes the parameters — the player would
|
|
/// visibly snap to idle every time he scrolled the hotbar. Instead one AnimatorOverrideController is
|
|
/// built in Awake and only its overrides are mutated afterwards, which leaves the current state and
|
|
/// all parameters untouched. That same property is what keeps FishNet's NetworkAnimator working on
|
|
/// the third-person body: an override controller preserves the base controller's parameter list, so
|
|
/// replication never sees the swap.
|
|
/// </summary>
|
|
[DisallowMultipleComponent]
|
|
public class PlayerAnimationSetBinder : MonoBehaviour
|
|
{
|
|
#region Serialized Fields
|
|
|
|
[Header("Target")]
|
|
[SerializeField] private Animator animator;
|
|
[Tooltip("Which rig this binder drives — it picks the matching set off the held ItemData.")]
|
|
[SerializeField] private PlayerAnimationRig rig = PlayerAnimationRig.Arms;
|
|
|
|
[Header("Sets")]
|
|
[Tooltip("Set used with bare hands, and as the fallback for any slot the held item leaves empty.")]
|
|
[SerializeField] private PlayerAnimationSet defaultSet;
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
private AnimatorOverrideController overrideController;
|
|
private AnimationClip[] placeholders;
|
|
private readonly List<KeyValuePair<AnimationClip, AnimationClip>> overrides = new List<KeyValuePair<AnimationClip, AnimationClip>>();
|
|
private PlayerAnimationSet currentSet;
|
|
private bool ready;
|
|
|
|
#endregion
|
|
|
|
#region Unity Lifecycle
|
|
|
|
/// <summary>
|
|
/// Wraps the authored controller in a single override controller and caches its placeholder
|
|
/// clips, whose names are the slot keys every set is matched against. Done in Awake so the
|
|
/// rebind happens once, before any driver seeds a parameter.
|
|
/// </summary>
|
|
private void Awake()
|
|
{
|
|
if (animator == null)
|
|
{
|
|
Debug.LogError($"[PlayerAnimationSetBinder] '{name}' has no Animator assigned — clips will never swap.", this);
|
|
return;
|
|
}
|
|
|
|
RuntimeAnimatorController master = animator.runtimeAnimatorController;
|
|
if (master == null)
|
|
{
|
|
Debug.LogError($"[PlayerAnimationSetBinder] '{name}' — the Animator has no controller assigned.", this);
|
|
return;
|
|
}
|
|
|
|
overrideController = new AnimatorOverrideController(master) { name = $"{master.name} (Runtime)" };
|
|
overrideController.GetOverrides(overrides);
|
|
|
|
placeholders = new AnimationClip[overrides.Count];
|
|
for (int i = 0; i < overrides.Count; i++)
|
|
placeholders[i] = overrides[i].Key;
|
|
|
|
animator.runtimeAnimatorController = overrideController;
|
|
ready = true;
|
|
|
|
ApplySet(defaultSet);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscribes to the held-item bus. HotbarController re-raises HeldItemChanged on its own first
|
|
/// resolve, so a rig enabled after the player already drew an item still lands on the right set.
|
|
/// </summary>
|
|
private void OnEnable()
|
|
{
|
|
PlayerEvents.HeldItemChanged += HandleHeldItemChanged;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unsubscribes — mirrors OnEnable exactly.
|
|
/// </summary>
|
|
private void OnDisable()
|
|
{
|
|
PlayerEvents.HeldItemChanged -= HandleHeldItemChanged;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Event Handlers
|
|
|
|
/// <summary>
|
|
/// Loads the set the newly drawn item authors for this rig (or the bare-hand set when the player
|
|
/// empties his hands).
|
|
/// </summary>
|
|
private void HandleHeldItemChanged(ItemData item)
|
|
{
|
|
ApplySet(item != null ? item.GetAnimationSet(rig) : defaultSet);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Internal Helpers
|
|
|
|
/// <summary>
|
|
/// Rewrites every slot in one ApplyOverrides call rather than one indexer assignment per clip,
|
|
/// because each individual assignment forces the controller to regenerate. Skips redundant work
|
|
/// when the set has not actually changed — several items can legitimately share one set.
|
|
/// </summary>
|
|
private void ApplySet(PlayerAnimationSet set)
|
|
{
|
|
if (!ready) return;
|
|
if (set == currentSet) return;
|
|
|
|
currentSet = set;
|
|
|
|
for (int i = 0; i < placeholders.Length; i++)
|
|
{
|
|
AnimationClip placeholder = placeholders[i];
|
|
overrides[i] = new KeyValuePair<AnimationClip, AnimationClip>(placeholder, Resolve(placeholder.name, set));
|
|
}
|
|
|
|
overrideController.ApplyOverrides(overrides);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Picks the clip for a slot: the held item's set first, then the default set, then null — which
|
|
/// means "no override" and leaves the master controller's own placeholder playing. That last
|
|
/// step is the safety net: a slot nobody authored still animates instead of freezing the rig.
|
|
/// </summary>
|
|
private AnimationClip Resolve(string slotName, PlayerAnimationSet set)
|
|
{
|
|
AnimationClip clip = set != null ? set.GetClip(slotName) : null;
|
|
if (clip == null && defaultSet != null) clip = defaultSet.GetClip(slotName);
|
|
return clip;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|