캐릭터 애니메이션 로직 변경

This commit is contained in:
NTG 2025-09-02 15:12:31 +09:00
parent a3c295285f
commit 1fbbb44f02
15 changed files with 334 additions and 115 deletions

View File

@ -46,6 +46,7 @@ GameObject:
- component: {fileID: 3365694194251356714}
- component: {fileID: 8736963048629680089}
- component: {fileID: 127430239903465757}
- component: {fileID: 6612028984666579384}
- component: {fileID: 3095965496140440094}
- component: {fileID: 7606279200344222219}
- component: {fileID: 3805557225565208309}
@ -158,6 +159,18 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 38cb67223546879468e9c0655893e025, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &6612028984666579384
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5259510642736920361}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3af7fa0aab2d45b19f78c34b028732b3, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &3095965496140440094
MonoBehaviour:
m_ObjectHideFlags: 0

View File

@ -2,6 +2,7 @@ namespace DDD.Restaurant
{
public interface IMovementConstraint
{
public bool IsBlockingMovement();
public float GetMovementSpeedMultiplier();
}
}

View File

@ -1,3 +1,4 @@
using DDD.Restaurant;
using UnityEngine;
namespace DDD

View File

@ -1,6 +1,6 @@
using UnityEngine;
namespace DDD
namespace DDD.Restaurant
{
[RequireComponent(typeof(SpineController))]
public class CharacterAnimation : MonoBehaviour
@ -15,6 +15,11 @@ protected virtual void Awake()
protected virtual void Start()
{
}
protected virtual void Update()
{
}
protected virtual void OnDestroy()

View File

@ -1,6 +1,6 @@
using UnityEngine;
namespace DDD
namespace DDD.Restaurant
{
public enum CarriableType
{

View File

@ -5,7 +5,14 @@
namespace DDD.Restaurant
{
public class CharacterInteraction : MonoBehaviour, IInteractor, IEventHandler<RestaurantInteractionEvent>
public interface IInteractionStateProvider
{
bool IsInteracting();
float GetMovementSpeedMultiplier();
bool IsMovementBlocked();
}
public class CharacterInteraction : MonoBehaviour, IInteractor, IEventHandler<RestaurantInteractionEvent>, IInteractionStateProvider
{
[EnumToggleButtons, SerializeField] protected InteractionType _availableInteractions;
[SerializeField, ReadOnly] protected Collider[] _nearColliders = new Collider[10];
@ -142,5 +149,9 @@ protected void InitializeInteractionSolvers(Dictionary<InteractionType, Type> ty
}
}
}
public virtual bool IsInteracting() => _interactingTarget != null;
public virtual float GetMovementSpeedMultiplier() => 1f;
public virtual bool IsMovementBlocked() => false;
}
}

View File

@ -1,13 +1,12 @@
using UnityEngine;
namespace DDD
namespace DDD.Restaurant
{
public class CharacterMovement : MonoBehaviour
{
private CharacterMovementConstraint _constraint;
protected virtual void Awake()
{
_constraint = gameObject.AddComponent<CharacterMovementConstraint>();
}
public virtual bool CanMove()
@ -24,5 +23,19 @@ public virtual bool CanMove()
}
return true;
}
public virtual float GetMovementSpeedMultiplier()
{
var constraints = GetComponents<IMovementConstraint>();
float minMultiplier = 1f;
foreach (var constraint in constraints)
{
float multiplier = constraint.GetMovementSpeedMultiplier();
minMultiplier = Mathf.Min(minMultiplier, multiplier);
}
return minMultiplier;
}
}
}

View File

@ -1,18 +1,47 @@
using UnityEngine;
namespace DDD
namespace DDD.Restaurant
{
[RequireComponent(typeof(CharacterAnimation))]
public class CharacterMovementConstraint : MonoBehaviour, IMovementConstraint
{
private CharacterAnimation _characterAnimation;
private IInteractionStateProvider _interactionStateProvider;
private void Awake()
{
_characterAnimation = GetComponent<CharacterAnimation>();
_interactionStateProvider = GetComponent<IInteractionStateProvider>();
}
public bool IsBlockingMovement()
{
if (GetComponent<CharacterAnimation>().IsPlayingAnimation())
if (_characterAnimation.IsPlayingAnimation())
{
return true;
}
if (_interactionStateProvider != null && _interactionStateProvider.IsMovementBlocked())
{
return true;
}
return false;
}
public float GetMovementSpeedMultiplier()
{
if (IsBlockingMovement())
{
return 0f;
}
if (_interactionStateProvider != null)
{
return _interactionStateProvider.GetMovementSpeedMultiplier();
}
return 1f;
}
}
}

View File

@ -1,7 +1,7 @@
using Pathfinding;
using UnityEngine;
namespace DDD
namespace DDD.Restaurant
{
[RequireComponent(typeof(AIPath))]
public class NpcMovement : CharacterMovement, IAiMovement, ICurrentDirection

View File

@ -1,48 +1,186 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
namespace DDD.Restaurant
{
public enum PlayerTaskAnimationState
{
None = 0,
Dashing
}
public enum PlayerActionAnimationState
{
None = 0,
CleaningTable = 1,
}
public enum PlayerDefaultAnimationState
{
None = 0,
Idle = 1,
Moving = 2,
}
[RequireComponent(typeof(PlayerMovement))]
public class PlayerAnimation : CharacterAnimation
{
private PlayerMovement _playerMovement;
private IMovementState _movementState;
private PlayerTaskAnimationState _currentTaskAnimationState = PlayerTaskAnimationState.None;
private PlayerActionAnimationState _currentActionAnimationState = PlayerActionAnimationState.None;
private PlayerDefaultAnimationState _currentDefaultAnimationState = PlayerDefaultAnimationState.None;
private Dictionary<PlayerTaskAnimationState, string> _taskToAnimation = new()
{
{ PlayerTaskAnimationState.Dashing, "Dash" },
};
private Dictionary<PlayerActionAnimationState, string> _actionToAnimation = new()
{
{ PlayerActionAnimationState.CleaningTable, "Cleaning/CleaningTable" },
};
private Dictionary<PlayerDefaultAnimationState, string> _defaultToAnimation = new()
{
{ PlayerDefaultAnimationState.Idle, "Idle" },
{ PlayerDefaultAnimationState.Moving, "RunFast" },
};
protected override void Awake()
{
base.Awake();
_playerMovement = GetComponent<PlayerMovement>();
_movementState = GetComponent<IMovementState>();
}
protected override void Start()
{
base.Start();
_playerMovement.OnMoving += OnMove;
_playerMovement.OnDashing += OnDash;
InitializeTaskAnimations();
}
protected override void Update()
{
UpdateAnimations();
}
protected override void OnDestroy()
{
base.OnDestroy();
if (_playerMovement)
if (_movementState != null)
{
_playerMovement.OnMoving -= OnMove;
_playerMovement.OnDashing -= OnDash;
_movementState.OnDashing -= HandleDashingTask;
}
}
private void OnMove(bool isMoving)
private void InitializeTaskAnimations()
{
string animationName = isMoving ? RestaurantPlayerAnimationType.Walk : RestaurantPlayerAnimationType.Idle;
_spineController.PlayAnimation(animationName, true);
_movementState.OnDashing += HandleDashingTask;
}
private void OnDash(float dashTime)
private void UpdateAnimations()
{
_spineController.PlayAnimationDuration(RestaurantPlayerAnimationType.Dash, false, duration:dashTime);
}
// 1순위: Task 애니메이션이 재생 중이면 다른 애니메이션 처리하지 않음
if (_currentTaskAnimationState != PlayerTaskAnimationState.None)
{
_currentActionAnimationState = PlayerActionAnimationState.None;
_currentDefaultAnimationState = PlayerDefaultAnimationState.None;
return;
}
// 2순위: ActionState 체크
var desiredActionState = GetDesiredActionState();
if (desiredActionState != PlayerActionAnimationState.None)
{
if (_currentActionAnimationState != desiredActionState)
{
_currentDefaultAnimationState = PlayerDefaultAnimationState.None;
_currentActionAnimationState = desiredActionState;
PlayActionAnimation(_currentActionAnimationState);
}
return;
}
// 3순위: 기본 AnimationState 처리
var desiredADefaultState = GetDesiredADefaultState();
if (_currentDefaultAnimationState != desiredADefaultState)
{
_currentDefaultAnimationState = desiredADefaultState;
PlayStateAnimation(_currentDefaultAnimationState);
}
}
private void HandleDashingTask(float duration)
{
_ = PlayTaskAnimation(PlayerTaskAnimationState.Dashing, duration);
}
private async Task PlayTaskAnimation(PlayerTaskAnimationState state, float duration = 0f)
{
_currentTaskAnimationState = state;
if (_taskToAnimation.TryGetValue(state, out string animationName))
{
if (duration > 0f)
{
_spineController.PlayAnimationDuration(animationName, false, duration);
}
else
{
_spineController.PlayAnimation(animationName, false);
}
}
await Awaitable.WaitForSecondsAsync(duration);
_currentTaskAnimationState = PlayerTaskAnimationState.None;
}
private PlayerActionAnimationState GetDesiredActionState()
{
// 여기서 현재 액션 상태를 확인하는 로직 구현
// 예: 상호작용 컴포넌트에서 상태 가져오기
return PlayerActionAnimationState.None;
}
private void PlayActionAnimation(PlayerActionAnimationState actionAnimationState)
{
if (_actionToAnimation.TryGetValue(actionAnimationState, out string animationName))
{
_spineController.PlayAnimation(animationName, true);
}
}
private PlayerDefaultAnimationState GetDesiredADefaultState()
{
if (_movementState.IsMoving())
{
return PlayerDefaultAnimationState.Moving;
}
return PlayerDefaultAnimationState.Idle;
}
private void PlayStateAnimation(PlayerDefaultAnimationState state)
{
if (_defaultToAnimation.TryGetValue(state, out string animationName))
{
switch (state)
{
case PlayerDefaultAnimationState.None:
break;
case PlayerDefaultAnimationState.Idle:
case PlayerDefaultAnimationState.Moving:
_spineController.PlayAnimation(animationName, true);
break;
default:
throw new ArgumentOutOfRangeException(nameof(state), state, null);
}
}
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.InputSystem;
@ -12,6 +11,7 @@ public class PlayerInteraction : CharacterInteraction
private float _interactHeldTime;
private bool _isInteracting;
private float _interactionStartTime = -1f;
protected override void Start()
{
@ -68,7 +68,7 @@ protected virtual void Update()
if (_nearestInteractable != _previousInteractable)
{
_previousInteractable = _nearestInteractable;
OnNearestInteractableChanged(_nearestInteractable);
BroadcastInteractionUi(_nearestInteractable, CanInteractTo(_nearestInteractable), 0f);
}
if (_isInteracting)
@ -90,13 +90,17 @@ protected virtual void Update()
OnInteractionCompleted();
ResetInteractionState();
OnInteractionHoldProgress(0f);
BroadcastInteractionUi(_nearestInteractable, CanInteractTo(_nearestInteractable), 0f);
}
else
{
OnInteractionHoldProgress(ratio);
BroadcastInteractionUi(_nearestInteractable, CanInteractTo(_nearestInteractable), ratio);
}
}
else
{
BroadcastInteractionUi(_nearestInteractable, CanInteractTo(_nearestInteractable), 0f);
}
}
@ -120,28 +124,9 @@ private void OnInteractPerformed(InputAction.CallbackContext context)
private void OnInteractCanceled(InputAction.CallbackContext context)
{
OnInteractionHoldProgress(0f);
BroadcastInteractionUi(_nearestInteractable, CanInteractTo(_nearestInteractable), 0f);
ResetInteractionState();
}
protected void OnNearestInteractableChanged(IInteractable newTarget)
{
if (newTarget != null)
{
BroadcastShowUi(newTarget, CanInteractTo(newTarget), 0f);
}
else
{
EventBus.Broadcast(GameEvents.HideInteractionUiEvent);
}
}
protected void OnInteractionHoldProgress(float ratio)
{
if (_interactingTarget != null)
{
BroadcastShowUi(_interactingTarget, CanInteractTo(_interactingTarget), ratio);
}
_interactionStartTime = -1f;
}
protected override void OnInteractionCompleted()
@ -149,8 +134,14 @@ protected override void OnInteractionCompleted()
}
private void BroadcastShowUi(IInteractable interactable, bool canInteract, float ratio)
private void BroadcastInteractionUi(IInteractable interactable, bool canInteract, float ratio)
{
if (interactable == null)
{
EventBus.Broadcast(GameEvents.HideInteractionUiEvent);
return;
}
var displayParameters = interactable.GetDisplayParameters();
var evt = GameEvents.ShowInteractionUiEvent;
evt.CanInteract = canInteract;
@ -167,7 +158,7 @@ protected void ResetInteractionState()
_isInteracting = false;
_interactingTarget = null;
_interactHeldTime = 0f;
OnInteractionHoldProgress(0f);
BroadcastInteractionUi(_nearestInteractable, CanInteractTo(_nearestInteractable), 0f);
}
protected IInteractable GetNearestInteractable()
@ -206,5 +197,26 @@ public override bool FetchSolverTypeForInteraction(InteractionType type, out Typ
return base.FetchSolverTypeForInteraction(type, out solverType);
}
public override bool IsInteracting() => _isInteracting;
public override float GetMovementSpeedMultiplier()
{
if (_isInteracting == false)
{
_interactionStartTime = -1f;
return 1f;
}
if (_interactionStartTime < 0f)
{
_interactionStartTime = Time.time;
}
float elapsed = Time.time - _interactionStartTime;
float normalizedTime = Mathf.Clamp01(elapsed / _restaurantPlayerDataSo.DecelerationTime);
return Mathf.Clamp01(_restaurantPlayerDataSo.InteractionDecelerationCurve.Evaluate(normalizedTime));
}
}
}

View File

@ -7,27 +7,32 @@
namespace DDD.Restaurant
{
public interface IMovementState
{
bool IsMoving();
bool IsDashing();
Action<float> OnDashing { get; set; }
}
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(BoxCollider))]
public class PlayerMovement : CharacterMovement, ICurrentDirection
public class PlayerMovement : CharacterMovement, ICurrentDirection, IMovementState
{
#region Fields
private Rigidbody _rigidbody;
private BoxCollider _boxCollider;
private RestaurantPlayerData _playerDataSo;
private Vector3 _inputDirection;
private Vector3 _currentDirection;
private Vector3 _currentVelocity;
private bool _isInputtedMovement;
private bool _isMoving;
private bool _isDashing;
private bool _isDashCooldown;
private bool _isInitialized;
public Action<bool> OnMoving;
public Action<float> OnDashing;
public Action<float> OnDashing { get; set; }
#if UNITY_EDITOR
private MovementDebugVisualizer _debugVisualizer;
@ -44,15 +49,13 @@ protected override void Awake()
InitializeComponents();
}
private async void Start()
private void Start()
{
await InitializePlayerData();
SubscribeToInputEvents();
}
private void FixedUpdate()
{
if (!_isInitialized) return;
HandleMovement();
#if UNITY_EDITOR
@ -69,6 +72,8 @@ private void OnDestroy()
#region Initialization
private RestaurantPlayerData GetPlayerData() => RestaurantData.Instance.PlayerData;
private void InitializeComponents()
{
_rigidbody = GetComponent<Rigidbody>();
@ -79,46 +84,30 @@ private void InitializeComponents()
#endif
}
private System.Threading.Tasks.Task InitializePlayerData()
{
try
{
_playerDataSo = RestaurantData.Instance.PlayerData;
SubscribeToInputEvents();
_isInitialized = true;
}
catch (Exception e)
{
Debug.LogError($"Player data load failed\n{e}");
}
return System.Threading.Tasks.Task.CompletedTask;
}
private void SubscribeToInputEvents()
{
_playerDataSo.MoveAction = InputManager.Instance.GetAction(InputActionMaps.Restaurant, nameof(RestaurantActions.Move));
_playerDataSo.DashAction = InputManager.Instance.GetAction(InputActionMaps.Restaurant, nameof(RestaurantActions.Dash));
GetPlayerData().MoveAction = InputManager.Instance.GetAction(InputActionMaps.Restaurant, nameof(RestaurantActions.Move));
GetPlayerData().DashAction = InputManager.Instance.GetAction(InputActionMaps.Restaurant, nameof(RestaurantActions.Dash));
_playerDataSo.MoveAction.performed += OnMove;
_playerDataSo.MoveAction.canceled += OnMove;
_playerDataSo.DashAction.performed += OnDash;
GetPlayerData().MoveAction.performed += OnMove;
GetPlayerData().MoveAction.canceled += OnMove;
GetPlayerData().DashAction.performed += OnDash;
}
private void UnsubscribeFromInputEvents()
{
if (!_playerDataSo) return;
if (GetPlayerData() == null) return;
_playerDataSo.MoveAction.performed -= OnMove;
_playerDataSo.MoveAction.canceled -= OnMove;
_playerDataSo.DashAction.performed -= OnDash;
GetPlayerData().MoveAction.performed -= OnMove;
GetPlayerData().MoveAction.canceled -= OnMove;
GetPlayerData().DashAction.performed -= OnDash;
}
#endregion
#region Movement
private void HandleMovement()
private void HandleMovement()
{
if (CanMove())
{
@ -128,7 +117,7 @@ private void HandleMovement()
public override bool CanMove()
{
return base.CanMove() && _playerDataSo.IsMoveEnabled && !_isDashing;
return base.CanMove() && GetPlayerData().IsMoveEnabled && !_isDashing;
}
private void Move()
@ -141,23 +130,25 @@ private void Move()
private void UpdateMovementState()
{
_isMoving = _inputDirection != Vector3.zero;
OnMoving?.Invoke(_isMoving);
_isInputtedMovement = _inputDirection != Vector3.zero;
_isMoving = _isInputtedMovement && _currentVelocity != Vector3.zero;
}
private void UpdateVelocity()
{
if (_isMoving)
float speedMultiplier = GetMovementSpeedMultiplier();
if (_isInputtedMovement)
{
Vector3 slideDirection = GetSlideAdjustedDirection(_inputDirection.normalized);
Vector3 targetVelocity = slideDirection * _playerDataSo.MoveSpeed;
Vector3 targetVelocity = slideDirection * (GetPlayerData().MoveSpeed * speedMultiplier);
_currentVelocity = Vector3.MoveTowards(_currentVelocity, targetVelocity,
_playerDataSo.Acceleration * Time.fixedDeltaTime);
GetPlayerData().Acceleration * Time.fixedDeltaTime);
}
else
{
_currentVelocity = Vector3.MoveTowards(_currentVelocity, Vector3.zero,
_playerDataSo.Deceleration * Time.fixedDeltaTime);
GetPlayerData().Deceleration * Time.fixedDeltaTime);
}
}
@ -178,14 +169,14 @@ private void OnDash(InputAction.CallbackContext context)
}
}
private bool CanDash() => _playerDataSo.IsDashEnabled && !_isDashing && !_isDashCooldown;
private bool CanDash() => GetPlayerData().IsDashEnabled && !_isDashing && !_isDashCooldown;
private IEnumerator DashCoroutine()
{
StartDash();
yield return new WaitForSeconds(_playerDataSo.DashTime);
yield return new WaitForSeconds(GetPlayerData().DashTime);
EndDash();
yield return new WaitForSeconds(_playerDataSo.DashCooldown);
yield return new WaitForSeconds(GetPlayerData().DashCooldown);
ResetDashCooldown();
}
@ -193,10 +184,10 @@ private void StartDash()
{
_isDashing = true;
_isDashCooldown = true;
OnDashing?.Invoke(_playerDataSo.DashTime);
OnDashing?.Invoke(GetPlayerData().DashTime);
Vector3 slideDashDirection = GetSlideAdjustedDirection(_currentDirection.normalized);
_rigidbody.linearVelocity = slideDashDirection * _playerDataSo.DashSpeed;
_rigidbody.linearVelocity = slideDashDirection * GetPlayerData().DashSpeed;
}
private void EndDash()
@ -245,7 +236,7 @@ private Vector3 GetSlideAdjustedDirection(Vector3 inputDirection)
Vector3 slide = Vector3.ProjectOnPlane(inputDirection, hit.normal).normalized;
float slideFactor = CalculateSlideFactor(inputDirection, hit.normal);
return slideFactor < _playerDataSo.MinSlideFactorThreshold ? Vector3.zero : slide * slideFactor;
return slideFactor < GetPlayerData().MinSlideFactorThreshold ? Vector3.zero : slide * slideFactor;
}
private bool TryGetCollisionInfo(Vector3 direction, out RaycastHit hit)
@ -253,16 +244,16 @@ private bool TryGetCollisionInfo(Vector3 direction, out RaycastHit hit)
Vector3 origin = _boxCollider.bounds.center;
Vector3 halfExtents = _boxCollider.bounds.extents;
float distance = Mathf.Min(_boxCollider.bounds.size.x, _boxCollider.bounds.size.z);
int layerMask = ~_playerDataSo.IgnoreSlidingLayerMask;
int layerMask = ~GetPlayerData().IgnoreSlidingLayerMask;
return Physics.BoxCast(origin, halfExtents * _playerDataSo.BoxCastExtentScale,
return Physics.BoxCast(origin, halfExtents * GetPlayerData().BoxCastExtentScale,
direction, out hit, transform.rotation, distance, layerMask, QueryTriggerInteraction.Ignore);
}
private float CalculateSlideFactor(Vector3 direction, Vector3 normal)
{
float dot = Vector3.Dot(direction.normalized, normal);
return Mathf.Pow(1f - Mathf.Abs(dot), _playerDataSo.SlidingThreshold);
return Mathf.Pow(1f - Mathf.Abs(dot), GetPlayerData().SlidingThreshold);
}
#endregion
@ -270,13 +261,22 @@ private float CalculateSlideFactor(Vector3 direction, Vector3 normal)
#if UNITY_EDITOR
private void HandleDebugVisualization()
{
if (_playerDataSo.IsDrawLineDebug)
if (GetPlayerData().IsDrawLineDebug)
{
_debugVisualizer.UpdateVisualization(transform.position, _inputDirection,
_currentVelocity, _playerDataSo);
_currentVelocity, GetPlayerData());
}
}
#endif
public bool IsMoving()
{
return _isMoving;
}
public bool IsDashing()
{
return _isDashing;
}
}
#if UNITY_EDITOR

View File

@ -33,6 +33,9 @@ public class RestaurantPlayerData : ScriptableObject
public float InteractionRadius = 1f;
public LayerMask InteractionLayerMask;
public AnimationCurve InteractionDecelerationCurve;
public float DecelerationTime = 0.3f;
// 디버그
public int InputLineSortingOrder = 10;
public int VelocityLineSortingOrder = 9;

View File

@ -10,13 +10,6 @@ public static class CommonConstants
public const string BlockImage = "BlockImage";
}
public static class RestaurantPlayerAnimationType
{
public const string Idle = "Idle";
public const string Walk = "RunFast";
public const string Dash = "Dash";
}
public static class PathConstants
{
public const string RawSpritesPathUpper = "ASSETS/_DDD/_RAW/SPRITES/";