Merge remote-tracking branch 'origin/feat/inport-props' into feat/craft-discovery
# 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
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user