2026-05-18 부자연스러운 모션

This commit is contained in:
2026-05-18 14:36:34 +09:00
parent f2445c30c4
commit 271991d32f
17 changed files with 407 additions and 25 deletions

Binary file not shown.

View File

@@ -40,4 +40,13 @@ public class ActionData : ScriptableObject
public float HitPositionCorrectionDuration = 0.08f; public float HitPositionCorrectionDuration = 0.08f;
public bool CorrectHitTargetY; public bool CorrectHitTargetY;
public string HitReactionAnimationState; public string HitReactionAnimationState;
[Header("Grab")]
public bool IsGrab;
public Vector2 GrabOffset = new Vector2(0.6f, 0f);
public AnimationCurve GrabOffsetXCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
public AnimationCurve GrabOffsetYCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
public string GrabbedAnimationState;
public float GrabSearchRadius = 2f;
public float GrabRange = 0.5f;
} }

View File

@@ -4,6 +4,8 @@
[RequireComponent(typeof(CircleCollider2D))] [RequireComponent(typeof(CircleCollider2D))]
public class AttackHitbox : MonoBehaviour public class AttackHitbox : MonoBehaviour
{ {
public event System.Action<IDamageable> OnHit;
private CircleCollider2D _collider; private CircleCollider2D _collider;
private int _damage; private int _damage;
private Vector2 _hitVelocity; private Vector2 _hitVelocity;
@@ -75,6 +77,7 @@ private void TryDamage(Collider2D other)
_alreadyHit.Add(target); _alreadyHit.Add(target);
Vector2? targetPosition = GetCorrectionTargetPosition(other); Vector2? targetPosition = GetCorrectionTargetPosition(other);
target.TakeDamage(_damage, _hitVelocity, _hitReactionState, targetPosition, _correctHitTargetY, _hitPositionSolidMask, _hitPositionCorrectionDuration); target.TakeDamage(_damage, _hitVelocity, _hitReactionState, targetPosition, _correctHitTargetY, _hitPositionSolidMask, _hitPositionCorrectionDuration);
OnHit?.Invoke(target);
} }
private Vector2? GetCorrectionTargetPosition(Collider2D other) private Vector2? GetCorrectionTargetPosition(Collider2D other)

View File

@@ -37,6 +37,9 @@ public class Enemy : MonoBehaviour, IDamageable
private float _hitPositionCorrectionDuration; private float _hitPositionCorrectionDuration;
private Vector2 _hitPositionCorrectionStart; private Vector2 _hitPositionCorrectionStart;
private Vector2 _hitPositionCorrectionTarget; private Vector2 _hitPositionCorrectionTarget;
private bool _isGrabbed;
private Vector2 _grabTargetPosition;
private int _grabSolidMask;
private void Awake() private void Awake()
{ {
@@ -66,6 +69,14 @@ private void FixedUpdate()
{ {
if (_rb != null) if (_rb != null)
{ {
if (_isGrabbed)
{
_rb.linearVelocity = Vector2.zero;
_rb.MovePosition(_grabTargetPosition);
_lastVelocity = Vector2.zero;
return;
}
ApplySmoothHitPositionCorrection(); ApplySmoothHitPositionCorrection();
_lastVelocity = _rb.linearVelocity; _lastVelocity = _rb.linearVelocity;
} }
@@ -75,6 +86,8 @@ public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReac
{ {
if (_currentHealth <= 0) return; if (_currentHealth <= 0) return;
_isGrabbed = false;
_currentHealth -= amount; _currentHealth -= amount;
Debug.Log($"{name} 피격: -{amount} (HP: {_currentHealth}/{_maxHealth})"); Debug.Log($"{name} 피격: -{amount} (HP: {_currentHealth}/{_maxHealth})");
@@ -109,6 +122,39 @@ public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReac
Die(); Die();
} }
public void BeginGrab(string grabbedAnimationState, int solidMask)
{
if (_currentHealth <= 0) return;
_isGrabbed = true;
_grabSolidMask = solidMask;
_isHitPositionCorrecting = false;
_hitReactionTimer = 0f;
_lastVelocity = Vector2.zero;
if (_rb != null)
{
_rb.linearVelocity = Vector2.zero;
_grabTargetPosition = _rb.position;
}
if (_anim != null && !string.IsNullOrEmpty(grabbedAnimationState))
_anim.Play(grabbedAnimationState);
}
public void UpdateGrabPosition(Vector2 position)
{
if (!_isGrabbed || _rb == null) return;
_rb.linearVelocity = Vector2.zero;
_grabTargetPosition = GetSafeHitTargetPosition(position, _grabSolidMask);
}
public void EndGrab()
{
_isGrabbed = false;
}
private void OnCollisionEnter2D(Collision2D collision) private void OnCollisionEnter2D(Collision2D collision)
{ {
UpdateGroundedState(collision); UpdateGroundedState(collision);

View File

@@ -154,6 +154,15 @@ public @GameInput()
""processors"": """", ""processors"": """",
""interactions"": """", ""interactions"": """",
""initialStateCheck"": false ""initialStateCheck"": false
},
{
""name"": ""GrabSmash"",
""type"": ""Button"",
""id"": ""c42f3219-3aca-4678-8a86-b74565af3747"",
""expectedControlType"": """",
""processors"": """",
""interactions"": """",
""initialStateCheck"": false
} }
], ],
""bindings"": [ ""bindings"": [
@@ -277,6 +286,17 @@ public @GameInput()
""action"": ""BackDash"", ""action"": ""BackDash"",
""isComposite"": false, ""isComposite"": false,
""isPartOfComposite"": false ""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""8635811d-29ac-4047-a3f7-d7a72f7e362e"",
""path"": ""<Keyboard>/g"",
""interactions"": """",
""processors"": """",
""groups"": """",
""action"": ""GrabSmash"",
""isComposite"": false,
""isPartOfComposite"": false
} }
] ]
} }
@@ -292,6 +312,7 @@ public @GameInput()
m_Player_Dash = m_Player.FindAction("Dash", throwIfNotFound: true); m_Player_Dash = m_Player.FindAction("Dash", throwIfNotFound: true);
m_Player_Roll = m_Player.FindAction("Roll", throwIfNotFound: true); m_Player_Roll = m_Player.FindAction("Roll", throwIfNotFound: true);
m_Player_BackDash = m_Player.FindAction("BackDash", throwIfNotFound: true); m_Player_BackDash = m_Player.FindAction("BackDash", throwIfNotFound: true);
m_Player_GrabSmash = m_Player.FindAction("GrabSmash", throwIfNotFound: true);
} }
~@GameInput() ~@GameInput()
@@ -379,6 +400,7 @@ public int FindBinding(InputBinding bindingMask, out InputAction action)
private readonly InputAction m_Player_Dash; private readonly InputAction m_Player_Dash;
private readonly InputAction m_Player_Roll; private readonly InputAction m_Player_Roll;
private readonly InputAction m_Player_BackDash; private readonly InputAction m_Player_BackDash;
private readonly InputAction m_Player_GrabSmash;
/// <summary> /// <summary>
/// Provides access to input actions defined in input action map "Player". /// Provides access to input actions defined in input action map "Player".
/// </summary> /// </summary>
@@ -419,6 +441,10 @@ public struct PlayerActions
/// </summary> /// </summary>
public InputAction @BackDash => m_Wrapper.m_Player_BackDash; public InputAction @BackDash => m_Wrapper.m_Player_BackDash;
/// <summary> /// <summary>
/// Provides access to the underlying input action "Player/GrabSmash".
/// </summary>
public InputAction @GrabSmash => m_Wrapper.m_Player_GrabSmash;
/// <summary>
/// Provides access to the underlying input action map instance. /// Provides access to the underlying input action map instance.
/// </summary> /// </summary>
public InputActionMap Get() { return m_Wrapper.m_Player; } public InputActionMap Get() { return m_Wrapper.m_Player; }
@@ -465,6 +491,9 @@ public void AddCallbacks(IPlayerActions instance)
@BackDash.started += instance.OnBackDash; @BackDash.started += instance.OnBackDash;
@BackDash.performed += instance.OnBackDash; @BackDash.performed += instance.OnBackDash;
@BackDash.canceled += instance.OnBackDash; @BackDash.canceled += instance.OnBackDash;
@GrabSmash.started += instance.OnGrabSmash;
@GrabSmash.performed += instance.OnGrabSmash;
@GrabSmash.canceled += instance.OnGrabSmash;
} }
/// <summary> /// <summary>
@@ -497,6 +526,9 @@ private void UnregisterCallbacks(IPlayerActions instance)
@BackDash.started -= instance.OnBackDash; @BackDash.started -= instance.OnBackDash;
@BackDash.performed -= instance.OnBackDash; @BackDash.performed -= instance.OnBackDash;
@BackDash.canceled -= instance.OnBackDash; @BackDash.canceled -= instance.OnBackDash;
@GrabSmash.started -= instance.OnGrabSmash;
@GrabSmash.performed -= instance.OnGrabSmash;
@GrabSmash.canceled -= instance.OnGrabSmash;
} }
/// <summary> /// <summary>
@@ -586,5 +618,12 @@ public interface IPlayerActions
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" /> /// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" /> /// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
void OnBackDash(InputAction.CallbackContext context); void OnBackDash(InputAction.CallbackContext context);
/// <summary>
/// Method invoked when associated input action "GrabSmash" is either <see cref="UnityEngine.InputSystem.InputAction.started" />, <see cref="UnityEngine.InputSystem.InputAction.performed" /> or <see cref="UnityEngine.InputSystem.InputAction.canceled" />.
/// </summary>
/// <seealso cref="UnityEngine.InputSystem.InputAction.started" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
void OnGrabSmash(InputAction.CallbackContext context);
} }
} }

View File

@@ -15,6 +15,7 @@ public class InputManager : MonoBehaviour, GameInput.IPlayerActions
public event Action OnDash_Event; public event Action OnDash_Event;
public event Action OnRoll_Event; public event Action OnRoll_Event;
public event Action OnBackDash_Event; public event Action OnBackDash_Event;
public event Action OnGrabSmash_Event;
private void Awake() private void Awake()
{ {
@@ -77,4 +78,9 @@ public void OnBackDash(InputAction.CallbackContext ctx)
OnBackDash_Event?.Invoke(); OnBackDash_Event?.Invoke();
} }
public void OnGrabSmash(InputAction.CallbackContext ctx)
{
if (ctx.phase == InputActionPhase.Started)
OnGrabSmash_Event?.Invoke();
}
} }

View File

@@ -35,6 +35,7 @@ public class PlayerController : MonoBehaviour
[SerializeField] private ComboNode _dashRootNode; [SerializeField] private ComboNode _dashRootNode;
[SerializeField] private ComboNode _rollRootNode; [SerializeField] private ComboNode _rollRootNode;
[SerializeField] private ComboNode _backDashRootNode; [SerializeField] private ComboNode _backDashRootNode;
[SerializeField] private ComboNode _grabSmashRootNode;
private readonly Dictionary<ActionData, float> _motionCooldownTimers = new(); private readonly Dictionary<ActionData, float> _motionCooldownTimers = new();
private readonly List<ActionData> _motionCooldownKeys = new(); private readonly List<ActionData> _motionCooldownKeys = new();
@@ -59,11 +60,14 @@ public class PlayerController : MonoBehaviour
private CancellationTokenSource _attackCts; private CancellationTokenSource _attackCts;
private CancellationTokenSource _motionCts; private CancellationTokenSource _motionCts;
private CancellationTokenSource _animationSpeedCts; private CancellationTokenSource _animationSpeedCts;
private CancellationTokenSource _actionVelocityCts;
private bool _isMotionActive;
private ActionData _lastAttackGizmoData; private ActionData _lastAttackGizmoData;
[SerializeField] private float _hitGizmoFadeDuration = 0.5f; [SerializeField] private float _hitGizmoFadeDuration = 0.5f;
private ActionData _lastHitData; private ActionData _lastHitData;
private Vector2 _lastHitCenter; private Vector2 _lastHitCenter;
private float _lastHitTime = -1f; private float _lastHitTime = -1f;
private Enemy _lastHitEnemy;
[Header("Debug")] [Header("Debug")]
[SerializeField] private bool _showAttackDebug = true; [SerializeField] private bool _showAttackDebug = true;
@@ -83,6 +87,7 @@ private void Awake()
_spriteRenderer = GetComponentInChildren<SpriteRenderer>(); _spriteRenderer = GetComponentInChildren<SpriteRenderer>();
_bodyColliders = GetComponentsInChildren<Collider2D>(); _bodyColliders = GetComponentsInChildren<Collider2D>();
EnsureAttackHitbox(); EnsureAttackHitbox();
_attackHitbox.OnHit += OnAttackHit;
} }
private void Start() private void Start()
@@ -94,6 +99,7 @@ private void Start()
InputManager.Instance.OnDash_Event += OnDashInput; InputManager.Instance.OnDash_Event += OnDashInput;
InputManager.Instance.OnRoll_Event += OnRollInput; InputManager.Instance.OnRoll_Event += OnRollInput;
InputManager.Instance.OnBackDash_Event += OnBackDashInput; InputManager.Instance.OnBackDash_Event += OnBackDashInput;
InputManager.Instance.OnGrabSmash_Event += OnGrabSmashInput;
} }
private void OnDestroy() private void OnDestroy()
@@ -107,14 +113,16 @@ private void OnDestroy()
InputManager.Instance.OnDash_Event -= OnDashInput; InputManager.Instance.OnDash_Event -= OnDashInput;
InputManager.Instance.OnRoll_Event -= OnRollInput; InputManager.Instance.OnRoll_Event -= OnRollInput;
InputManager.Instance.OnBackDash_Event -= OnBackDashInput; InputManager.Instance.OnBackDash_Event -= OnBackDashInput;
InputManager.Instance.OnGrabSmash_Event -= OnGrabSmashInput;
} }
_attackCts?.Cancel(); CancelAndDispose(ref _attackCts);
_attackCts?.Dispose(); CancelAndDispose(ref _motionCts);
_motionCts?.Cancel(); CancelAndDispose(ref _animationSpeedCts);
_motionCts?.Dispose(); CancelAndDispose(ref _actionVelocityCts);
_animationSpeedCts?.Cancel();
_animationSpeedCts?.Dispose(); if (_attackHitbox != null)
_attackHitbox.OnHit -= OnAttackHit;
} }
private void FixedUpdate() private void FixedUpdate()
@@ -198,9 +206,13 @@ private void OnJumpInput()
private void OnDashInput() => ExecuteMotionNode(_dashRootNode); private void OnDashInput() => ExecuteMotionNode(_dashRootNode);
private void OnRollInput() => ExecuteMotionNode(_rollRootNode); private void OnRollInput() => ExecuteMotionNode(_rollRootNode);
private void OnBackDashInput() => ExecuteMotionNode(_backDashRootNode); private void OnBackDashInput() => ExecuteMotionNode(_backDashRootNode);
private void OnGrabSmashInput() => ExecuteAttackNode(_grabSmashRootNode);
private void HandleComboInput(ComboInputType input) private void HandleComboInput(ComboInputType input)
{ {
if (_isMotionActive && !CanTransitionFromCurrentNode(input))
return;
if (_attackCooldownTimer > 0f) if (_attackCooldownTimer > 0f)
{ {
float elapsed = Time.time - _attackStartTime; float elapsed = Time.time - _attackStartTime;
@@ -257,6 +269,16 @@ private void ExecuteMotionNode(ComboNode root)
_comboWindowTimer = root.ComboWindow; _comboWindowTimer = root.ComboWindow;
} }
private void ExecuteAttackNode(ComboNode root)
{
if (root == null || root.Action == null) return;
if (_attackCooldownTimer > 0f) return;
PerformAttack(root.Action);
_currentNode = root;
_comboWindowTimer = root.ComboWindow;
}
private void UpdateMotionCooldowns() private void UpdateMotionCooldowns()
{ {
if (_motionCooldownTimers.Count == 0) return; if (_motionCooldownTimers.Count == 0) return;
@@ -288,8 +310,8 @@ private void SetMotionCooldown(ActionData data)
private async void PerformAttack(ActionData data, bool preserveHorizontalVelocity = false) private async void PerformAttack(ActionData data, bool preserveHorizontalVelocity = false)
{ {
_attackCts?.Cancel(); CancelMotion();
_attackCts?.Dispose(); CancelAndDispose(ref _attackCts);
_attackCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken); _attackCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
CancellationToken token = _attackCts.Token; CancellationToken token = _attackCts.Token;
@@ -301,14 +323,16 @@ private async void PerformAttack(ActionData data, bool preserveHorizontalVelocit
ClearActionLocks(); ClearActionLocks();
PlayActionAnimation(data); PlayActionAnimation(data);
if (data.HasMotion) PlayActionVelocity(data);
ApplyActionVelocity(data);
LockMovementIfNeeded(data, preserveHorizontalVelocity, true); LockMovementIfNeeded(data, preserveHorizontalVelocity, true);
LockFacingIfNeeded(data); LockFacingIfNeeded(data);
try try
{ {
if (data.IsGrab)
await GrabRoutine(data, token);
else
await HitRoutine(data, token); await HitRoutine(data, token);
} }
catch (System.OperationCanceledException) catch (System.OperationCanceledException)
@@ -323,15 +347,16 @@ private async void PerformMotion(ActionData data)
// 대시/구르기 같은 모션은 공격을 끊고 새로운 콤보 노드가 된다. // 대시/구르기 같은 모션은 공격을 끊고 새로운 콤보 노드가 된다.
CancelAttack(); CancelAttack();
_motionCts?.Cancel(); CancelAndDispose(ref _motionCts);
_motionCts?.Dispose();
_motionCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken); _motionCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
CancellationToken token = _motionCts.Token; CancellationToken token = _motionCts.Token;
SetMotionCooldown(data); SetMotionCooldown(data);
_isMotionActive = true;
ClearActionLocks(); ClearActionLocks();
FaceMotionDirection(data); FaceMotionDirection(data);
PlayActionAnimation(data); PlayActionAnimation(data);
PlayActionVelocity(data);
LockMovementIfNeeded(data); LockMovementIfNeeded(data);
LockFacingIfNeeded(data); LockFacingIfNeeded(data);
@@ -341,11 +366,15 @@ private async void PerformMotion(ActionData data)
await MotionRoutine(data, token); await MotionRoutine(data, token);
completed = true; completed = true;
} }
catch (System.OperationCanceledException) { } catch (System.OperationCanceledException)
{
_isMotionActive = false;
}
if (completed) if (completed)
{ {
StopActionVelocity(data); StopActionVelocity(data);
_isMotionActive = false;
ClearActionLocks(); ClearActionLocks();
PlayIdleAnimation(); PlayIdleAnimation();
} }
@@ -359,9 +388,6 @@ private async Awaitable MotionRoutine(ActionData data, CancellationToken token)
while (ShouldKeepActionPlaying(data, elapsed, duration)) while (ShouldKeepActionPlaying(data, elapsed, duration))
{ {
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
float normalizedTime = GetActionNormalizedTime(data, elapsed, duration);
ApplyActionVelocity(data, normalizedTime);
await Awaitable.NextFrameAsync(token); await Awaitable.NextFrameAsync(token);
elapsed += Time.deltaTime; elapsed += Time.deltaTime;
} }
@@ -369,13 +395,34 @@ private async Awaitable MotionRoutine(ActionData data, CancellationToken token)
private void CancelAttack() private void CancelAttack()
{ {
_attackCts?.Cancel(); CancelAndDispose(ref _attackCts);
CancelAndDispose(ref _actionVelocityCts);
_attackCooldownTimer = 0f; _attackCooldownTimer = 0f;
_pendingInput = null; _pendingInput = null;
_attackHitbox?.Deactivate(); _attackHitbox?.Deactivate();
ClearActionLocks(); ClearActionLocks();
} }
private void CancelMotion()
{
_isMotionActive = false;
CancelAndDispose(ref _motionCts);
CancelAndDispose(ref _actionVelocityCts);
}
private bool CanTransitionFromCurrentNode(ComboInputType input)
{
if (_comboWindowTimer <= 0f || _currentNode == null) return false;
foreach (var transition in _currentNode.Transitions)
{
if (transition.Trigger == input && transition.Next != null && transition.Next.Action != null)
return true;
}
return false;
}
private async Awaitable HitRoutine(ActionData data, CancellationToken token) private async Awaitable HitRoutine(ActionData data, CancellationToken token)
{ {
float attackStartTime = Time.time; float attackStartTime = Time.time;
@@ -402,6 +449,101 @@ private async Awaitable HitRoutine(ActionData data, CancellationToken token)
PlayIdleAnimation(); PlayIdleAnimation();
} }
private async Awaitable GrabRoutine(ActionData data, CancellationToken token)
{
Enemy target = FindGrabTarget(data);
if (target == null)
{
await WaitForActionEnd(data, token);
ClearActionLocks();
PlayIdleAnimation();
return;
}
target.BeginGrab(data.GrabbedAnimationState, _groundLayer.value);
try
{
float elapsed = 0f;
float duration = GetActionDuration(data);
while (ShouldKeepActionPlaying(data, elapsed, duration))
{
token.ThrowIfCancellationRequested();
float normalizedTime = GetActionNormalizedTime(data, elapsed, duration);
target.UpdateGrabPosition(GetGrabWorldPosition(data, normalizedTime));
await Awaitable.NextFrameAsync(token);
elapsed += Time.deltaTime;
}
target.EndGrab();
target.TakeDamage(data.Damage, GetHitVelocity(data.HitVelocity), data.HitReactionAnimationState);
}
catch (System.OperationCanceledException)
{
target.EndGrab();
throw;
}
ClearActionLocks();
PlayIdleAnimation();
}
private Enemy FindGrabTarget(ActionData data)
{
Vector2 playerPosition = _rb != null ? _rb.position : (Vector2)transform.position;
float grabRangeSqr = data.GrabRange * data.GrabRange;
if (_lastHitEnemy != null && _lastHitEnemy.isActiveAndEnabled &&
GetEnemySqrDistance(_lastHitEnemy, playerPosition) <= grabRangeSqr)
{
return _lastHitEnemy;
}
Collider2D[] hits = Physics2D.OverlapCircleAll(transform.position, data.GrabSearchRadius, _enemyLayer);
Enemy closest = null;
float closestSqrDistance = float.PositiveInfinity;
foreach (var hit in hits)
{
Enemy enemy = hit.GetComponentInParent<Enemy>();
if (enemy == null) continue;
float sqrDistance = GetEnemySqrDistance(enemy, playerPosition);
if (sqrDistance > grabRangeSqr) continue;
if (sqrDistance >= closestSqrDistance) continue;
closest = enemy;
closestSqrDistance = sqrDistance;
}
return closest;
}
private float GetEnemySqrDistance(Enemy enemy, Vector2 playerPosition)
{
if (enemy == null) return float.PositiveInfinity;
return ((Vector2)enemy.transform.position - playerPosition).sqrMagnitude;
}
private Vector2 GetGrabWorldPosition(ActionData data, float normalizedTime)
{
float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
Vector2 playerPosition = _rb != null ? _rb.position : (Vector2)transform.position;
Vector2 offset = data.GrabOffset;
float offsetXMultiplier = data.GrabOffsetXCurve != null ? data.GrabOffsetXCurve.Evaluate(normalizedTime) : 1f;
float offsetYMultiplier = data.GrabOffsetYCurve != null ? data.GrabOffsetYCurve.Evaluate(normalizedTime) : 1f;
offset.x *= facing * offsetXMultiplier;
offset.y *= offsetYMultiplier;
return playerPosition + offset;
}
private void OnAttackHit(IDamageable target)
{
if (target is Enemy enemy)
_lastHitEnemy = enemy;
}
private void ActivateAttackHitbox(ActionData data) private void ActivateAttackHitbox(ActionData data)
{ {
Vector2 localPosition = GetAttackLocalPosition(data.Offset); Vector2 localPosition = GetAttackLocalPosition(data.Offset);
@@ -475,7 +617,7 @@ private void LockMovementIfNeeded(ActionData data, bool preserveHorizontalVeloci
if (!forceLock && data.CanMoveDuringAction) return; if (!forceLock && data.CanMoveDuringAction) return;
// 이동 없는 공격은 직전 프레임의 걷기 속도를 이어받지 않게 한다. // 이동 없는 공격은 직전 프레임의 걷기 속도를 이어받지 않게 한다.
if (!preserveHorizontalVelocity && !data.HasMotion) if (!preserveHorizontalVelocity && data.Velocity == Vector2.zero)
_rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y); _rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y);
_inputLockTimer = GetActionLockDuration(data); _inputLockTimer = GetActionLockDuration(data);
@@ -493,7 +635,8 @@ private void PlayIdleAnimation()
{ {
if (_anim == null) return; if (_anim == null) return;
_animationSpeedCts?.Cancel(); CancelAndDispose(ref _animationSpeedCts);
CancelAndDispose(ref _actionVelocityCts);
_anim.speed = 1f; _anim.speed = 1f;
if (!string.IsNullOrEmpty(_idleAnimationState)) if (!string.IsNullOrEmpty(_idleAnimationState))
_anim.Play(_idleAnimationState); _anim.Play(_idleAnimationState);
@@ -503,8 +646,7 @@ private void PlayActionAnimation(ActionData data)
{ {
if (_anim == null) return; if (_anim == null) return;
_animationSpeedCts?.Cancel(); CancelAndDispose(ref _animationSpeedCts);
_animationSpeedCts?.Dispose();
_animationSpeedCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken); _animationSpeedCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
_anim.speed = GetAnimationSpeed(data, 0f); _anim.speed = GetAnimationSpeed(data, 0f);
@@ -517,6 +659,36 @@ private void PlayActionAnimation(ActionData data)
ApplyAnimationSpeedCurve(data, _animationSpeedCts.Token); ApplyAnimationSpeedCurve(data, _animationSpeedCts.Token);
} }
private void PlayActionVelocity(ActionData data)
{
CancelAndDispose(ref _actionVelocityCts);
if (data.Velocity == Vector2.zero) return;
_actionVelocityCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
ApplyActionVelocityCurve(data, _actionVelocityCts.Token);
}
private async void ApplyActionVelocityCurve(ActionData data, CancellationToken token)
{
float elapsed = 0f;
float duration = GetActionDuration(data);
try
{
while (ShouldKeepActionPlaying(data, elapsed, duration))
{
token.ThrowIfCancellationRequested();
float normalizedTime = GetActionNormalizedTime(data, elapsed, duration);
ApplyActionVelocity(data, normalizedTime);
await Awaitable.NextFrameAsync(token);
elapsed += Time.deltaTime;
}
}
catch (System.OperationCanceledException) { }
}
private async void ApplyAnimationSpeedCurve(ActionData data, CancellationToken token) private async void ApplyAnimationSpeedCurve(ActionData data, CancellationToken token)
{ {
float duration = GetActionDuration(data); float duration = GetActionDuration(data);
@@ -702,6 +874,20 @@ private void ClearActionLocks()
_facingLockAction = null; _facingLockAction = null;
} }
private void CancelAndDispose(ref CancellationTokenSource cts)
{
if (cts == null) return;
try
{
cts.Cancel();
}
catch (System.ObjectDisposedException) { }
cts.Dispose();
cts = null;
}
private float GetActionDuration(ActionData data) private float GetActionDuration(ActionData data)
{ {
return Mathf.Max(data.MotionDuration, data.HitTiming + data.HitDuration, 0.01f); return Mathf.Max(data.MotionDuration, data.HitTiming + data.HitDuration, 0.01f);
@@ -722,7 +908,7 @@ private bool ShouldKeepActionPlaying(ActionData data, float elapsed, float durat
private float GetActionNormalizedTime(ActionData data, float elapsed, float duration) private float GetActionNormalizedTime(ActionData data, float elapsed, float duration)
{ {
if (!HasActionAnimation(data)) if (!ShouldUseAnimationLength(data))
return Mathf.Clamp01(elapsed / duration); return Mathf.Clamp01(elapsed / duration);
AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0); AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0);

View File

@@ -702,6 +702,32 @@ AnimatorState:
m_MirrorParameter: m_MirrorParameter:
m_CycleOffsetParameter: m_CycleOffsetParameter:
m_TimeParameter: m_TimeParameter:
--- !u!1102 &-4203153932972611291
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: GrabSmash
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 7c16f79e8404ac7478767cd96faaeb72, type: 2}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &-3985368095300963852 --- !u!1102 &-3985368095300963852
AnimatorState: AnimatorState:
serializedVersion: 6 serializedVersion: 6
@@ -3121,6 +3147,9 @@ AnimatorStateMachine:
- serializedVersion: 1 - serializedVersion: 1
m_State: {fileID: 6644522863449380584} m_State: {fileID: 6644522863449380584}
m_Position: {x: 1140, y: -100, z: 0} m_Position: {x: 1140, y: -100, z: 0}
- serializedVersion: 1
m_State: {fileID: -4203153932972611291}
m_Position: {x: 5280, y: 90, z: 0}
m_ChildStateMachines: [] m_ChildStateMachines: []
m_AnyStateTransitions: [] m_AnyStateTransitions: []
m_EntryTransitions: [] m_EntryTransitions: []

Binary file not shown.

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7c16f79e8404ac7478767cd96faaeb72
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c35c8bd4e0ad2cf41ab1af35fc81bbc0
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e3de0ce9283788a4c9a4a9486a1122e7
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c9a3bb08b50045a4395d19af97aab515
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -68,6 +68,15 @@
"processors": "", "processors": "",
"interactions": "", "interactions": "",
"initialStateCheck": false "initialStateCheck": false
},
{
"name": "GrabSmash",
"type": "Button",
"id": "c42f3219-3aca-4678-8a86-b74565af3747",
"expectedControlType": "",
"processors": "",
"interactions": "",
"initialStateCheck": false
} }
], ],
"bindings": [ "bindings": [
@@ -191,6 +200,17 @@
"action": "BackDash", "action": "BackDash",
"isComposite": false, "isComposite": false,
"isPartOfComposite": false "isPartOfComposite": false
},
{
"name": "",
"id": "8635811d-29ac-4047-a3f7-d7a72f7e362e",
"path": "<Keyboard>/g",
"interactions": "",
"processors": "",
"groups": "",
"action": "GrabSmash",
"isComposite": false,
"isPartOfComposite": false
} }
] ]
} }