SpineController 추가, 애니메이션 이벤트로 관리

FlipVisualLook RestaurantPlayerCharacter에서 관리
This commit is contained in:
NTG_Lenovo 2025-07-14 14:07:15 +09:00
parent 5065d1a738
commit bfb7ccbe4b
5 changed files with 308 additions and 274 deletions

View File

@ -0,0 +1,264 @@
using System.Collections.Generic;
using Sirenix.OdinInspector;
using Spine;
using Spine.Unity;
using UnityEngine;
using AnimationState = Spine.AnimationState;
namespace DDD
{
public class SpineController : MonoBehaviour
{
// Variables
#region Variables
private SkeletonAnimation _skeletonAnimation;
[SerializeField, ReadOnly]
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<string> _randomStrings;
private bool _customMaterialEnabled;
#endregion
// Unity events
#region Unity events
private void Reset()
{
InitializeComponents();
}
private void Awake()
{
InitializeComponents();
if (!_isSkinSet) return;
if (_isRandomSkin)
{
if (_isRandomRange)
{
SetRandomSkin();
}
else
{
SetRandomStringListSkin();
}
}
else
{
SetSkin(_initialSkinName);
}
}
#endregion
// Initialize methods
#region Initialize methods
private void InitializeComponents()
{
_skeletonAnimation = transform.GetComponentInChildren<SkeletonAnimation>();
if (!_originalMaterial)
{
_originalMaterial = _skeletonAnimation.SkeletonDataAsset.atlasAssets[0].PrimaryMaterial;
}
_animationState = _skeletonAnimation.AnimationState;
}
#endregion
// Methods
#region Methods
/// <param name="animationName">스파인 애니메이션 이름</param>
/// <param name="isLoopActive">반복 여부</param>
/// <param name="speed">애니메이션 속도 양수값</param>
/// <param name="isReverse">true인 경우 자동으로 speed에 음수값(-1)을 넣음</param>
/// <param name="trackIndex"></param>
/// <returns></returns>
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 (_customMaterialEnabled) return;
_skeletonAnimation.CustomMaterialOverride[_originalMaterial] = _replacementMaterial;
_customMaterialEnabled = true;
}
public void DisableCustomMaterial()
{
if (!_customMaterialEnabled) return;
_skeletonAnimation.CustomMaterialOverride.Remove(_originalMaterial);
_customMaterialEnabled = 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
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6d7e5480ae1ebf54b8537ad2a08696d2

View File

@ -1,264 +1,27 @@
using System.Collections.Generic;
using Sirenix.OdinInspector;
using Spine;
using Spine.Unity;
using UnityEngine;
using AnimationState = Spine.AnimationState;
namespace DDD
{
public class RestaurantCharacterAnimation : MonoBehaviour
{
// Variables
#region Variables
private SkeletonAnimation _skeletonAnimation;
[SerializeField, ReadOnly]
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<string> _randomStrings;
private bool _customMaterialEnabled;
#endregion
// Unity events
#region Unity events
private void Reset()
{
InitializeComponents();
}
private RestaurantPlayerMovement _restaurantPlayerMovement;
private SpineController _spineController;
private void Awake()
{
InitializeComponents();
if (!_isSkinSet) return;
if (_isRandomSkin)
{
if (_isRandomRange)
{
SetRandomSkin();
}
else
{
SetRandomStringListSkin();
}
}
else
{
SetSkin(_initialSkinName);
}
_restaurantPlayerMovement = GetComponent<RestaurantPlayerMovement>();
_spineController = GetComponent<SpineController>();
}
#endregion
// Initialize methods
#region Initialize methods
private void InitializeComponents()
private void OnMove(bool isMoving)
{
_skeletonAnimation = transform.GetComponentInChildren<SkeletonAnimation>();
if (!_originalMaterial)
{
_originalMaterial = _skeletonAnimation.SkeletonDataAsset.atlasAssets[0].PrimaryMaterial;
}
_animationState = _skeletonAnimation.AnimationState;
string animationName = isMoving ? RestaurantPlayerAnimation.Walk : RestaurantPlayerAnimation.Idle;
_spineController.PlayAnimation(animationName, true);
}
#endregion
// Methods
#region Methods
/// <param name="animationName">스파인 애니메이션 이름</param>
/// <param name="isLoopActive">반복 여부</param>
/// <param name="speed">애니메이션 속도 양수값</param>
/// <param name="isReverse">true인 경우 자동으로 speed에 음수값(-1)을 넣음</param>
/// <param name="trackIndex"></param>
/// <returns></returns>
public TrackEntry PlayAnimation(string animationName, bool isLoopActive, float speed = 1f, bool isReverse = false, int trackIndex = 0)
private void OnDash(float dashTime)
{
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;
_spineController.PlayAnimationDuration(RestaurantPlayerAnimation.Dash, false, duration:dashTime);
}
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 (_customMaterialEnabled) return;
_skeletonAnimation.CustomMaterialOverride[_originalMaterial] = _replacementMaterial;
_customMaterialEnabled = true;
}
public void DisableCustomMaterial()
{
if (!_customMaterialEnabled) return;
_skeletonAnimation.CustomMaterialOverride.Remove(_originalMaterial);
_customMaterialEnabled = 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
}
}

View File

@ -4,6 +4,32 @@ namespace DDD
{
public class RestaurantPlayerCharacter : RestaurantCharacter
{
private RestaurantPlayerMovement _movement;
private Transform _visualLook;
private void Awake()
{
_movement = GetComponent<RestaurantPlayerMovement>();
_visualLook = transform.Find(CommonConstants.VisualLook);
}
private void Update()
{
FlipVisualLook();
}
private void FlipVisualLook()
{
Vector3 localScale = _visualLook.localScale;
localScale.x = _movement.GetCurrentDirection().x switch
{
> 0.01f => -Mathf.Abs(localScale.x),
< -0.01f => Mathf.Abs(localScale.x),
_ => localScale.x
};
_visualLook.localScale = localScale;
}
}
}

View File

@ -8,8 +8,6 @@ namespace DDD
public class RestaurantPlayerMovement : RestaurantCharacterMovement
{
private Rigidbody _rigidbody;
private RestaurantCharacterAnimation _animation;
private Transform _visualLook;
private RestaurantPlayerDataSo _playerData;
@ -20,11 +18,12 @@ public class RestaurantPlayerMovement : RestaurantCharacterMovement
private bool _isDashCooldown;
private bool _isInitialized;
public Action<bool> OnMoving;
public Action<float> OnDashing;
private void Awake()
{
_rigidbody = GetComponent<Rigidbody>();
_animation = GetComponent<RestaurantCharacterAnimation>();
_visualLook = transform.Find(CommonConstants.VisualLook);
}
private async void Start()
@ -45,13 +44,6 @@ private async void Start()
}
}
private void Update()
{
if (_isInitialized == false) return;
FlipVisualLook();
}
private void FixedUpdate()
{
if (_isInitialized == false) return;
@ -78,18 +70,6 @@ public void SetCurrentDirection(Vector3 normalDirection)
_currentDirection = normalDirection;
}
private void FlipVisualLook()
{
Vector3 localScale = _visualLook.localScale;
localScale.x = _currentDirection.x switch
{
> 0.01f => -Mathf.Abs(localScale.x),
< -0.01f => Mathf.Abs(localScale.x),
_ => localScale.x
};
_visualLook.localScale = localScale;
}
private void OnMove(InputAction.CallbackContext context)
{
@ -107,8 +87,7 @@ private void Move()
SetCurrentDirection(_inputDirection);
_isMoving = _inputDirection != Vector3.zero;
string animationName = _isMoving ? RestaurantPlayerAnimation.Walk : RestaurantPlayerAnimation.Idle;
_animation.PlayAnimation(animationName, true);
OnMoving?.Invoke(_isMoving);
Vector3 finalVelocity = _inputDirection * _playerData.MoveSpeed;
_rigidbody.linearVelocity = finalVelocity;
@ -129,12 +108,10 @@ private bool CanDash()
private IEnumerator DashCoroutine()
{
// TODO : ui생기면 연동
_isDashing = true;
_isDashCooldown = true;
_animation.PlayAnimationDuration(RestaurantPlayerAnimation.Dash, false, _playerData.DashTime);
OnDashing?.Invoke(_playerData.DashTime);
Vector3 dashVelocity = _currentDirection.normalized * _playerData.DashSpeed;
_rigidbody.linearVelocity = dashVelocity;
@ -146,5 +123,7 @@ private IEnumerator DashCoroutine()
yield return new WaitForSeconds(_playerData.DashCooldown);
_isDashCooldown = false;
}
public Vector3 GetCurrentDirection() => _currentDirection;
}
}