Files
2026-07-09 21:33:33 +02:00

391 lines
18 KiB
C#

using System;
using System.Linq;
using UnityEngine;
using VOID.Generic.Dict;
using VOID.Generic.Objects.Abstract.Events;
using VOID.Generic.Objects.Unit.Animations;
using VOID.Generic.Objects.Unit.Events;
using VOID.Generic.Objects.Weapon;
using VOID.Generic.Objects.Wearable.Events;
using VOID.Generic.Settings;
using VOID.ScriptableObjects.Animations;
namespace VOID.Generic.Objects.Unit
{
[Serializable]
public class UnitObjectAnimator : ObjectComponent<UnitObject>
{
#region Initialize
protected override void SelfInitialize()
{
unitAnimatorCallbacks = parent.GetComponentInChildren<UnitAnimatorCallbacks>();
unitAnimatorCallbacks.OnAnimationEvent += HandleAnimationEvent;
unitAnimatorCallbacks.OnApplyRootMotion += HandleRootMotion;
animator = parent.GetComponentInChildren<Animator>();
animatorOverrideController = new AnimatorOverrideController(animator.runtimeAnimatorController);
animator.runtimeAnimatorController = animatorOverrideController;
}
protected override void EventInitialize()
{
parent.basicEvents.onFixedUpdate.AddListener(UpdateAnimatorVelocity);
parent.basicEvents.onDeathAfter.AddListener(OnDeath);
parent.unitEvents.onEquipAfter.AddListener(OnEquip);
parent.unitEvents.onUnEquipAfter.AddListener(OnUnEquip);
parent.basicEvents.onTurnQueueEnter.AddListener(OnTurnQueueEnter);
parent.basicEvents.onTurnQueueLeave.AddListener(OnTurnQueueLeave);
parent.basicEvents.onRevive.AddListener(OnRevive);
parent.unitEvents.onMoveStart.AddListener(OnMoveStart);
}
protected override void Initialize()
{
var humanoidAnimationSettings = SettingsManager.humanoidAnimation;
SetLoopAnimation(AnimationType.Traverse, humanoidAnimationSettings.defaultLadderUpAnimation);
SetLoopAnimation(AnimationType.Death, humanoidAnimationSettings.defaultDeathAnimation);
UpdateStanceAnimationOnce();
}
#endregion
#region Animation Hashes
// Layers
private static readonly int StanceLayerIndex = 0;
private static readonly int ActionLayerIndex = 1;
// Movement
private static readonly int VelocityHash = Animator.StringToHash("VELOCITY");
private static readonly int VelocityVerticalNormalizedHash = Animator.StringToHash("VELOCITY_VERTICAL");
private static readonly int VelocityHorizontalNormalizedHash = Animator.StringToHash("VELOCITY_HORIZONTAL");
// Events inside animations
private static readonly int TriggerEndLoopHash = Animator.StringToHash("TRIGGER_LOOP_END");
private static readonly int IsLoopHash = Animator.StringToHash("IS_LOOP");
private static readonly int HaveLoopStartHash = Animator.StringToHash("HAVE_LOOP_START");
private static readonly int HaveLoopEndHash = Animator.StringToHash("HAVE_LOOP_END");
// Animations
private static readonly int TriggerCastingHash = Animator.StringToHash("TRIGGER_CASTING");
private static readonly int TriggerAbilityHash = Animator.StringToHash("TRIGGER_ABILITY");
private static readonly int TriggerTraverseHash = Animator.StringToHash("TRIGGER_TRAVERSE");
private static readonly int TriggerDeathHash = Animator.StringToHash("TRIGGER_DEATH");
// Stances
private static readonly int TriggerStanceRelaxHash = Animator.StringToHash("TRIGGER_STANCE_RELAX");
private static readonly int TriggerStanceUnarmedHash = Animator.StringToHash("TRIGGER_STANCE_UNARMED");
private static readonly int TriggerStance1Hash = Animator.StringToHash("TRIGGER_STANCE_1");
private static readonly int TriggerStance1WithShieldHash = Animator.StringToHash("TRIGGER_STANCE_1_WITH_SHIELD");
private static readonly int TriggerStance2Hash = Animator.StringToHash("TRIGGER_STANCE_2");
private static readonly int TriggerStance2SwordHash = Animator.StringToHash("TRIGGER_STANCE_2_SWORD");
private static readonly int TriggerStance2BowHash = Animator.StringToHash("TRIGGER_STANCE_2_BOW");
private static readonly int TriggerStance2CrossbowHash = Animator.StringToHash("TRIGGER_STANCE_2_CROSSBOW");
private static readonly int TriggerStance2StaffHash = Animator.StringToHash("TRIGGER_STANCE_2_STAFF");
// Actions - States
private static readonly int CastingStartLoopHash = Animator.StringToHash("CASTING - START");
private static readonly int CastingLoopHash = Animator.StringToHash("CASTING - LOOP");
private static readonly int CastingEndLoopHash = Animator.StringToHash("CASTING - END");
private static readonly int AbilitySimpleHash = Animator.StringToHash("ABILITY - SIMPLE");
private static readonly int AbilityStartLoopHash = Animator.StringToHash("ABILITY - START");
private static readonly int AbilityLoopHash = Animator.StringToHash("ABILITY - LOOP");
private static readonly int AbilityEndLoopHash = Animator.StringToHash("ABILITY - END");
private static readonly int TraverseStartLoopHash = Animator.StringToHash("TRAVERSE - START");
private static readonly int TraverseLoopHash = Animator.StringToHash("TRAVERSE - LOOP");
private static readonly int TraverseEndLoopHash = Animator.StringToHash("TRAVERSE - END");
private static readonly int DeathStartLoopHash = Animator.StringToHash("DEATH - START");
private static readonly int DeathLoopHash = Animator.StringToHash("DEATH - LOOP");
#endregion
protected UnitAnimatorCallbacks unitAnimatorCallbacks;
protected Animator animator;
protected AnimatorOverrideController animatorOverrideController;
#region RootMotion
public bool applyRootMotion;
private void HandleRootMotion()
{
if (applyRootMotion) parent.transform.position += animator.deltaPosition;
}
#endregion
#region AnimationEvents
private AnimationGenericEvent _currentAnimationEvent;
private int[] _currentExpectedStates;
private void HandleAnimationEvent(AnimationEvent animationEvent)
{
// LITTLE FIX: Avoid executing events multiple times from different animation states
var currentState = animationEvent.animatorStateInfo.shortNameHash;
if (_currentExpectedStates.All(state => state != currentState)) return;
switch (animationEvent.functionName)
{
case "OnStart": OnStartAnimationEvent(); break;
case "OnLoopStart": OnLoopStartAnimationEvent(); break;
case "OnPerform": OnPerformAnimationEvent(); break;
case "OnLoopEnd": OnLoopEndAnimationEvent(); break;
case "OnEnd": OnEndAnimationEvent(); break;
}
}
private AnimationGenericEventSet animSet => parent.unitEvents.onAnimation[_currentAnimationEvent.animationType];
private void OnStartAnimationEvent() => animSet.onStart.Invoke(_currentAnimationEvent);
private void OnLoopStartAnimationEvent() => animSet.onLoopStart.Invoke(_currentAnimationEvent);
private void OnPerformAnimationEvent() => animSet.onPerform.Invoke(_currentAnimationEvent);
private void OnLoopEndAnimationEvent() => animSet.onLoopEnd.Invoke(_currentAnimationEvent);
private void OnEndAnimationEvent() => animSet.onEnd.Invoke(_currentAnimationEvent);
#endregion
#region Events
private void OnDeath(DeathEvent deathEvent)
{
var animationEvent = new AnimationGenericEvent();
animationEvent.unit = deathEvent.obj as UnitObject;
animationEvent.animationType = AnimationType.Death;
StartLoopAnimation(animationEvent);
}
private void OnEquip(EquipEvent equipEvent)
{
if (equipEvent.wearable is WeaponObject)
{
parent.basicEvents.onFixedUpdate.RemoveListener(UpdateStanceAnimationOnce);
parent.basicEvents.onFixedUpdate.AddListener(UpdateStanceAnimationOnce);
}
}
private void OnUnEquip(UnEquipEvent unEquipEvent)
{
if (unEquipEvent.wearable is WeaponObject)
{
parent.basicEvents.onFixedUpdate.RemoveListener(UpdateStanceAnimationOnce);
parent.basicEvents.onFixedUpdate.AddListener(UpdateStanceAnimationOnce);
}
}
private void OnTurnQueueEnter(TurnEvent turnEvent)
{
parent.basicEvents.onFixedUpdate.RemoveListener(UpdateStanceAnimationOnce);
parent.basicEvents.onFixedUpdate.AddListener(UpdateStanceAnimationOnce);
}
private void OnTurnQueueLeave(TurnEvent turnEvent)
{
parent.basicEvents.onFixedUpdate.RemoveListener(UpdateStanceAnimationOnce);
parent.basicEvents.onFixedUpdate.AddListener(UpdateStanceAnimationOnce);
}
private void OnRevive(ReviveEvent reviveEvent)
{
// Forcefully end DEATH animation
animator.CrossFadeInFixedTime("IDLE", 0.1f, ActionLayerIndex);
}
private void OnMoveStart(MoveEvent moveEvent)
{
// Forcefully end (skipping exiting time) current action animation
animator.CrossFadeInFixedTime("IDLE", 0.1f, ActionLayerIndex);
}
#endregion
#region Animations
private void UpdateAnimatorVelocity()
{
var localVelocity = parent.unitNavAgent.localVelocity;
animator.SetFloat(VelocityHash, parent.unitState.isMoving ? parent.unitNavAgent.moveSpeed : 0f);
animator.SetFloat(VelocityVerticalNormalizedHash, localVelocity.normalized.z);
animator.SetFloat(VelocityHorizontalNormalizedHash, localVelocity.normalized.x);
}
public void SetAnimation(AnimationType animationType, ActionAnimationList animationList)
{
var baseClip = GetBaseAnimation(animationType).clip;
animatorOverrideController[baseClip] = animationList ? animationList.list.clip : null;
}
public void SetLoopAnimation(AnimationType animationType, ActionLoopAnimationList animationList)
{
SetLoopAnimation(animationType, animationList ? animationList.list : null);
}
private void SetLoopAnimation(AnimationType animationType, ActionLoopAnimation animations)
{
var baseAnimations = GetBaseLoopAnimation(animationType);
animatorOverrideController[baseAnimations.startClip] = animations?.startClip;
animatorOverrideController[baseAnimations.loopClip] = animations?.loopClip;
animatorOverrideController[baseAnimations.endClip] = animations?.endClip;
}
private void UpdateStanceAnimationOnce()
{
// Prevent running this more than once per frame
parent.basicEvents.onFixedUpdate.RemoveListener(UpdateStanceAnimationOnce);
if (!parent.basicState.turnManager)
{
// Disable (hide) weapons in hands
parent.unitData.leftHandPosition.gameObject.SetActive(false);
parent.unitData.rightHandPosition.gameObject.SetActive(false);
// Out of combat idle and move animations
animator.SetTrigger(TriggerStanceRelaxHash);
return;
}
// Stance depends on currently equipped weapons
var mainHand = parent.unitEquipment.GetEquipped(EquipmentSlotType.MainHand) as WeaponObject;
var offHand = parent.unitEquipment.GetEquipped(EquipmentSlotType.OffHand) as WeaponObject;
var stanceHash = GetCombatStanceAnimationHash(mainHand, offHand);
// TODO: pomyśleć o lepszym ogarnięciu animacji postawy poza i w trakcie walki dla każdej broni
animator.SetTrigger(stanceHash);
// Activate (show) weapons in hands
parent.unitData.leftHandPosition.gameObject.SetActive(true);
parent.unitData.rightHandPosition.gameObject.SetActive(true);
}
public void StartAnimation(AnimationGenericEvent tEvent)
{
// Send event info to callback class so it can be used when calling its events
_currentAnimationEvent = tEvent;
_currentExpectedStates = GetExpectedStates(_currentAnimationEvent.animationType);
animator.SetBool(IsLoopHash, false);
animator.SetTrigger(GetAnimationHash(tEvent.animationType));
}
public void StartLoopAnimation(AnimationGenericEvent tEvent)
{
// Make sure that loop ending trigger is off (from previous loop)
animator.ResetTrigger(TriggerEndLoopHash);
// Send event info to callback class so it can be used when calling its events
_currentAnimationEvent = tEvent;
_currentExpectedStates = GetExpectedStates(_currentAnimationEvent.animationType);
// Run start and end loop animation if present, else skip them
var baseLoopClips = GetBaseLoopAnimation(tEvent.animationType);
animator.SetBool(HaveLoopStartHash, baseLoopClips.startClip);
animator.SetBool(HaveLoopEndHash, baseLoopClips.endClip);
// There is no animation for loop starting - we need to execute events manually
if (animator.GetBool(HaveLoopStartHash) == false)
{
OnStartAnimationEvent();
OnLoopStartAnimationEvent();
OnPerformAnimationEvent();
}
animator.SetBool(IsLoopHash, true);
animator.SetTrigger(GetAnimationHash(tEvent.animationType));
}
public void EndLoopAnimation()
{
// IMPORTANT! This prevent STACKOVERFLOW!
// Only looped animation should proceed here or else below code can make endless loop,
// all thanks to AbilityAction that started non-looped animation
if (animator.GetBool(IsLoopHash) == false) return;
// There is no animation for loop ending - we need to execute events manually
if (animator.GetBool(HaveLoopEndHash) == false)
{
OnLoopEndAnimationEvent();
OnEndAnimationEvent();
}
animator.SetTrigger(TriggerEndLoopHash);
}
#endregion
#region Helpers
private int[] GetExpectedStates(AnimationType animationType)
{
return animationType switch
{
AnimationType.Casting => new[] { CastingStartLoopHash, CastingLoopHash, CastingEndLoopHash },
AnimationType.Ability => new[] { AbilitySimpleHash, AbilityStartLoopHash, AbilityLoopHash, AbilityEndLoopHash },
AnimationType.Traverse => new[] { TraverseStartLoopHash, TraverseLoopHash, TraverseEndLoopHash },
AnimationType.Death => new[] { DeathStartLoopHash, DeathLoopHash },
_ => throw new ArgumentOutOfRangeException()
};
}
private int GetCombatStanceAnimationHash(WeaponObject mainHand, WeaponObject offHand)
{
// UNARMED
if (!mainHand && !offHand) return TriggerStanceUnarmedHash;
// SHIELD
if (offHand && offHand.weaponData.weaponType is WeaponType.Shield) return TriggerStance1WithShieldHash;
// ONE-HANDED
if (!mainHand || (mainHand && mainHand.weaponData.weaponCarryType is not WeaponCarryType.TwoHand)) return TriggerStance1Hash;
// BOW
if (mainHand.weaponData.weaponType is WeaponType.Bow) return TriggerStance2BowHash;
// CROSSBOW
if (mainHand.weaponData.weaponType is WeaponType.Crossbow) return TriggerStance2CrossbowHash;
// STAFF
if (mainHand.weaponData.weaponType is WeaponType.Staff) return TriggerStance2StaffHash;
// SWORD
if (mainHand.weaponData.weaponType is WeaponType.Sword) return TriggerStance2SwordHash;
// TWO-HANDED basic - AXE or MACE
return TriggerStance2Hash;
}
private ActionAnimation GetBaseAnimation(AnimationType animationType)
{
var humanoidAnimationSettings = SettingsManager.humanoidAnimation;
return animationType switch
{
AnimationType.Ability => humanoidAnimationSettings.baseAbility,
_ => throw new Exception($"Base animation don't exists for: {animationType}")
};
}
private ActionLoopAnimation GetBaseLoopAnimation(AnimationType animationType)
{
var humanoidAnimationSettings = SettingsManager.humanoidAnimation;
return animationType switch
{
AnimationType.Casting => humanoidAnimationSettings.baseCastingLoop,
AnimationType.Ability => humanoidAnimationSettings.baseAbilityLoop,
AnimationType.Traverse => humanoidAnimationSettings.baseTraverseLoop,
AnimationType.Death => humanoidAnimationSettings.baseDeath,
_ => throw new Exception($"Loop base animation don't exists for: {animationType}")
};
}
private int GetAnimationHash(AnimationType animationType)
{
return animationType switch
{
AnimationType.Casting => TriggerCastingHash,
AnimationType.Ability => TriggerAbilityHash,
AnimationType.Traverse => TriggerTraverseHash,
AnimationType.Death => TriggerDeathHash,
_ => throw new ArgumentOutOfRangeException(nameof(animationType), animationType, null)
};
}
#endregion
}
}