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 { #region Initialize protected override void SelfInitialize() { unitAnimatorCallbacks = parent.GetComponentInChildren(); unitAnimatorCallbacks.OnAnimationEvent += HandleAnimationEvent; unitAnimatorCallbacks.OnApplyRootMotion += HandleRootMotion; animator = parent.GetComponentInChildren(); 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 } }