using System; using System.Collections.Generic; using Sirenix.OdinInspector; using Spine; using Spine.Unity; using UnityEngine; using AnimationState = Spine.AnimationState; using Random = UnityEngine.Random; namespace DDD.Players { public class SpineController : MonoBehaviour { // Variables #region Variables // Components [field: SerializeField] public SkeletonAnimation SkeletonAnimation { get; private set; } [SerializeField] private Material _originalMaterial; [SerializeField] private Material _replacementMaterial; private AnimationState _animationState; // Variables [SerializeField] private bool _isSkinSet = true; [SerializeField, ShowIf("@_isSkinSet && !_isRandomSkin")] private string _initialSkinName = "default"; [SerializeField, ShowIf("@_isSkinSet")] private bool _isRandomSkin; [SerializeField, ShowIf("@_isSkinSet && _isRandomSkin")] private bool _isRandomRange; [SerializeField, ShowIf("@_isSkinSet && _isRandomSkin && _isRandomRange"), Tooltip("x <= 값 < y")] private Vector2 _randomRange; [SerializeField, ShowIf("@_isSkinSet && _isRandomSkin && !_isRandomRange")] private List _randomStrings; private bool _previousEnabled; #endregion // Unity events #region Unity events private void Awake() { InitializeComponents(); if (!_isSkinSet) return; if (_isRandomSkin) { if (_isRandomRange) { SetRandomSkin(); } else { SetRandomStringListSkin(); } } else { SetSkin(_initialSkinName); } } #endregion // Initialize methods #region Initialize methods [Button("셋팅 초기화")] public virtual void InitializeComponents() { SkeletonAnimation = transform.GetComponentInChildren(); if (!_originalMaterial) { _originalMaterial = SkeletonAnimation.SkeletonDataAsset.atlasAssets[0].PrimaryMaterial; } _animationState = SkeletonAnimation.AnimationState; } #endregion // Methods #region Methods /// 스파인 애니메이션 이름 /// 반복 여부 /// 애니메이션 속도 양수값 /// true인 경우 자동으로 speed에 음수값을 넣음 /// public TrackEntry PlayAnimation(string animationName, bool isLoopActive, float speed = 1f, bool isReverse = false, int trackIndex = 0) { if (!SkeletonAnimation || _animationState == null) return null; if (string.IsNullOrEmpty(animationName)) { Debug.LogError($"{animationName}의 애니메이션은 존재하지 않습니다."); return null; } // 중복 체크 var currentTrackEntry = _animationState.GetCurrent(trackIndex); if (currentTrackEntry != null && currentTrackEntry.Animation.Name == animationName) { return currentTrackEntry; } _animationState.TimeScale = isReverse ? -Mathf.Abs(speed) : Mathf.Abs(speed); var trackEntry = _animationState.SetAnimation(trackIndex, animationName, isLoopActive); if (isReverse) { trackEntry.TrackTime = trackEntry.AnimationEnd; } return trackEntry; } public TrackEntry PlayAnimationDuration(string animationName, bool isLoopActive, float duration, bool isReverse = false, int trackIndex = 0) { if (!SkeletonAnimation || _animationState == null) return null; if (string.IsNullOrEmpty(animationName)) { Debug.LogError($"{animationName}의 애니메이션은 존재하지 않습니다."); return null; } // 중복 체크 var currentTrackEntry = _animationState.GetCurrent(trackIndex); if (currentTrackEntry != null && currentTrackEntry.Animation.Name == animationName) { return currentTrackEntry; } var findAnimation = SkeletonAnimation.Skeleton.Data.FindAnimation(animationName); if (findAnimation == null) { Debug.LogError($"{animationName} 애니메이션을 찾을 수 없습니다."); return null; } var speed = findAnimation.Duration / duration; _animationState.TimeScale = isReverse ? -Mathf.Abs(speed) : Mathf.Abs(speed); var trackEntry = _animationState.SetAnimation(trackIndex, animationName, isLoopActive); if (isReverse) { trackEntry.TrackTime = trackEntry.AnimationEnd; } return trackEntry; } public TrackEntry AddAnimation(string animationName, bool isLoopActive, int trackIndex = 0) { if (!SkeletonAnimation || _animationState == null) return null; if (string.IsNullOrEmpty(animationName)) { Debug.LogError($"{animationName} 애니메이션은 존재하지 않습니다."); return null; } var trackEntry = _animationState.AddAnimation(trackIndex, animationName, isLoopActive, 0); return trackEntry; } public string GetCurrentSkin() { if (SkeletonAnimation == null) return null; return SkeletonAnimation.Skeleton.Skin.ToString(); } public void SetSkin(string skinName) { if (SkeletonAnimation == null && _animationState == null) return; if (string.IsNullOrEmpty(skinName)) { Debug.LogError($"{skinName}의 스킨 이름은 존재하지 않습니다."); return; } SkeletonAnimation.Skeleton.SetSkin(skinName); SkeletonAnimation.Skeleton.SetSlotsToSetupPose(); _animationState.Apply(SkeletonAnimation.Skeleton); } public void SetRandomSkin() { if (SkeletonAnimation == null || SkeletonAnimation.Skeleton == null) return; var skins = SkeletonAnimation.skeleton.Data.Skins; var randomSkin = Random.Range((int)_randomRange.x, (int)_randomRange.y); var randomSkinName = skins.Items[randomSkin].Name; SetSkin(randomSkinName); } public void SetRandomStringListSkin() { if (SkeletonAnimation == null || SkeletonAnimation.Skeleton == null) return; if (_randomStrings == null || _randomStrings.Count <= 0) { Debug.LogError("_randomStrings 설정 오류"); return; } var randomSkin = Random.Range(0, _randomStrings.Count); var randomSkinName = _randomStrings[randomSkin]; SetSkin(randomSkinName); } public void EnableCustomMaterial() { if (_previousEnabled) return; SkeletonAnimation.CustomMaterialOverride[_originalMaterial] = _replacementMaterial; _previousEnabled = true; } public void DisableCustomMaterial() { if (!_previousEnabled) return; SkeletonAnimation.CustomMaterialOverride.Remove(_originalMaterial); _previousEnabled = false; } public bool IsAnimationComplete(int trackIndex = 0) { if (!SkeletonAnimation || _animationState == null) return false; var currentTrackEntry = _animationState.GetCurrent(trackIndex); if (currentTrackEntry == null) { Debug.LogWarning($"트랙 {trackIndex}에서 재생 중인 애니메이션이 없습니다."); return false; } return currentTrackEntry.IsComplete; } #endregion } }