2026-05-18 애니메이션 설계 수정

This commit is contained in:
2026-05-18 12:44:09 +09:00
parent adcd69c537
commit f2445c30c4
30 changed files with 476 additions and 56 deletions

Binary file not shown.

View File

@@ -10,7 +10,6 @@ public class ActionData : ScriptableObject
public string AnimationState; public string AnimationState;
public float AnimationSpeed = 1f; public float AnimationSpeed = 1f;
public AnimationCurve AnimationSpeedCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f); public AnimationCurve AnimationSpeedCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
public bool ReturnToIdleOnAnimationComplete;
public float Cooldown = 0.3f; public float Cooldown = 0.3f;
public float ComboWindow = 0.25f; public float ComboWindow = 0.25f;
@@ -36,5 +35,9 @@ public class ActionData : ScriptableObject
[Header("Hit Reaction")] [Header("Hit Reaction")]
public Vector2 HitVelocity = Vector2.zero; public Vector2 HitVelocity = Vector2.zero;
public bool UseHitPositionCorrection;
public Vector2 HitTargetOffset = new Vector2(0.8f, 0f);
public float HitPositionCorrectionDuration = 0.08f;
public bool CorrectHitTargetY;
public string HitReactionAnimationState; public string HitReactionAnimationState;
} }

View File

@@ -7,6 +7,12 @@ public class AttackHitbox : MonoBehaviour
private CircleCollider2D _collider; private CircleCollider2D _collider;
private int _damage; private int _damage;
private Vector2 _hitVelocity; private Vector2 _hitVelocity;
private Vector2 _hitSourcePosition;
private Vector2? _hitTargetPosition;
private float _hitPositionMinDistance;
private bool _correctHitTargetY;
private int _hitPositionSolidMask;
private float _hitPositionCorrectionDuration;
private string _hitReactionState; private string _hitReactionState;
private LayerMask _targetLayer; private LayerMask _targetLayer;
private readonly HashSet<IDamageable> _alreadyHit = new(); private readonly HashSet<IDamageable> _alreadyHit = new();
@@ -19,12 +25,18 @@ private void Awake()
_collider.enabled = false; _collider.enabled = false;
} }
public void Activate(ActionData data, Vector2 localPosition, Vector2 hitVelocity, LayerMask targetLayer) public void Activate(ActionData data, Vector2 localPosition, Vector2 hitVelocity, Vector2 sourcePosition, Vector2? hitTargetPosition, bool correctHitTargetY, int hitPositionSolidMask, LayerMask targetLayer)
{ {
transform.localPosition = localPosition; transform.localPosition = localPosition;
_collider.radius = data.Radius; _collider.radius = data.Radius;
_damage = data.Damage; _damage = data.Damage;
_hitVelocity = hitVelocity; _hitVelocity = hitVelocity;
_hitSourcePosition = sourcePosition;
_hitTargetPosition = hitTargetPosition;
_hitPositionMinDistance = Mathf.Abs(data.HitTargetOffset.x);
_correctHitTargetY = correctHitTargetY;
_hitPositionSolidMask = hitPositionSolidMask;
_hitPositionCorrectionDuration = data.HitPositionCorrectionDuration;
_hitReactionState = data.HitReactionAnimationState; _hitReactionState = data.HitReactionAnimationState;
_targetLayer = targetLayer; _targetLayer = targetLayer;
_alreadyHit.Clear(); _alreadyHit.Clear();
@@ -61,6 +73,20 @@ private void TryDamage(Collider2D other)
if (_alreadyHit.Contains(target)) return; if (_alreadyHit.Contains(target)) return;
_alreadyHit.Add(target); _alreadyHit.Add(target);
target.TakeDamage(_damage, _hitVelocity, _hitReactionState); Vector2? targetPosition = GetCorrectionTargetPosition(other);
target.TakeDamage(_damage, _hitVelocity, _hitReactionState, targetPosition, _correctHitTargetY, _hitPositionSolidMask, _hitPositionCorrectionDuration);
}
private Vector2? GetCorrectionTargetPosition(Collider2D other)
{
if (!_hitTargetPosition.HasValue) return null;
if (_hitPositionMinDistance <= 0.001f) return null;
Vector2 currentPosition = other.attachedRigidbody != null
? other.attachedRigidbody.position
: (Vector2)other.transform.root.position;
float currentDistance = Mathf.Abs(currentPosition.x - _hitSourcePosition.x);
return currentDistance < _hitPositionMinDistance ? _hitTargetPosition : null;
} }
} }

View File

@@ -2,5 +2,5 @@
public interface IDamageable public interface IDamageable
{ {
void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null); void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0f);
} }

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
[RequireComponent(typeof(Collider2D))] [RequireComponent(typeof(Collider2D))]
@@ -27,6 +28,15 @@ public class Enemy : MonoBehaviour, IDamageable
private float _hitReactionTimer; private float _hitReactionTimer;
private bool _isGrounded; private bool _isGrounded;
private Vector2 _lastVelocity; private Vector2 _lastVelocity;
private Collider2D[] _bodyColliders;
private readonly List<RaycastHit2D> _castResults = new();
private const float HitPositionSkinWidth = 0.02f;
private bool _isHitPositionCorrecting;
private bool _correctHitPositionY;
private float _hitPositionCorrectionTimer;
private float _hitPositionCorrectionDuration;
private Vector2 _hitPositionCorrectionStart;
private Vector2 _hitPositionCorrectionTarget;
private void Awake() private void Awake()
{ {
@@ -34,6 +44,7 @@ private void Awake()
_rb = GetComponent<Rigidbody2D>(); _rb = GetComponent<Rigidbody2D>();
_anim = GetComponentInChildren<Animator>(); _anim = GetComponentInChildren<Animator>();
_spriteRenderer = GetComponentInChildren<SpriteRenderer>(); _spriteRenderer = GetComponentInChildren<SpriteRenderer>();
_bodyColliders = GetComponentsInChildren<Collider2D>();
if (_spriteRenderer != null) if (_spriteRenderer != null)
_originalColor = _spriteRenderer.color; _originalColor = _spriteRenderer.color;
} }
@@ -54,10 +65,13 @@ private void Update()
private void FixedUpdate() private void FixedUpdate()
{ {
if (_rb != null) if (_rb != null)
{
ApplySmoothHitPositionCorrection();
_lastVelocity = _rb.linearVelocity; _lastVelocity = _rb.linearVelocity;
}
} }
public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null) public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0f)
{ {
if (_currentHealth <= 0) return; if (_currentHealth <= 0) return;
@@ -80,6 +94,8 @@ public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReac
_rb.linearVelocity = Vector2.zero; _rb.linearVelocity = Vector2.zero;
_lastVelocity = Vector2.zero; _lastVelocity = Vector2.zero;
BeginHitTargetPositionCorrection(hitTargetPosition, correctHitTargetY, hitPositionSolidMask, hitPositionCorrectionDuration);
Vector2 nextVelocity = GetHitReactionVelocity(hitVelocity); Vector2 nextVelocity = GetHitReactionVelocity(hitVelocity);
if (nextVelocity != Vector2.zero) if (nextVelocity != Vector2.zero)
{ {
@@ -130,6 +146,98 @@ private Vector2 GetHitReactionVelocity(Vector2 hitVelocity)
return nextVelocity; return nextVelocity;
} }
private void BeginHitTargetPositionCorrection(Vector2? hitTargetPosition, bool correctHitTargetY, int solidMask, float correctionDuration)
{
_isHitPositionCorrecting = false;
if (!hitTargetPosition.HasValue || _rb == null) return;
Vector2 targetPosition = hitTargetPosition.Value;
if (!correctHitTargetY)
targetPosition.y = _rb.position.y;
targetPosition = GetSafeHitTargetPosition(targetPosition, solidMask);
if ((targetPosition - _rb.position).sqrMagnitude <= 0.0001f) return;
if (correctionDuration <= 0f)
{
_rb.position = targetPosition;
return;
}
_isHitPositionCorrecting = true;
_correctHitPositionY = correctHitTargetY;
_hitPositionCorrectionTimer = 0f;
_hitPositionCorrectionDuration = correctionDuration;
_hitPositionCorrectionStart = _rb.position;
_hitPositionCorrectionTarget = targetPosition;
}
private void ApplySmoothHitPositionCorrection()
{
if (!_isHitPositionCorrecting || _rb == null) return;
_hitPositionCorrectionTimer += Time.fixedDeltaTime;
float normalizedTime = Mathf.Clamp01(_hitPositionCorrectionTimer / Mathf.Max(_hitPositionCorrectionDuration, Time.fixedDeltaTime));
float easedTime = Mathf.SmoothStep(0f, 1f, normalizedTime);
Vector2 nextPosition = _rb.position;
nextPosition.x = Mathf.Lerp(_hitPositionCorrectionStart.x, _hitPositionCorrectionTarget.x, easedTime);
if (_correctHitPositionY)
nextPosition.y = Mathf.Lerp(_hitPositionCorrectionStart.y, _hitPositionCorrectionTarget.y, easedTime);
_rb.MovePosition(nextPosition);
if (normalizedTime >= 1f)
_isHitPositionCorrecting = false;
}
private Vector2 GetSafeHitTargetPosition(Vector2 targetPosition, int solidMask)
{
if (solidMask == 0) return targetPosition;
Vector2 startPosition = _rb.position;
Vector2 moveDelta = targetPosition - startPosition;
float distance = moveDelta.magnitude;
if (distance <= 0.001f) return targetPosition;
Vector2 direction = moveDelta / distance;
float closestDistance = GetClosestBodyCastDistance(direction, distance + HitPositionSkinWidth, solidMask);
if (closestDistance >= distance + HitPositionSkinWidth)
return targetPosition;
float allowedDistance = Mathf.Max(closestDistance - HitPositionSkinWidth, 0f);
return startPosition + direction * allowedDistance;
}
private float GetClosestBodyCastDistance(Vector2 direction, float distance, int solidMask)
{
if (_bodyColliders == null || _bodyColliders.Length == 0)
_bodyColliders = GetComponentsInChildren<Collider2D>();
ContactFilter2D filter = new ContactFilter2D
{
useLayerMask = true,
layerMask = solidMask,
useTriggers = false
};
float closest = float.PositiveInfinity;
for (int i = 0; i < _bodyColliders.Length; i++)
{
Collider2D bodyCollider = _bodyColliders[i];
if (bodyCollider == null || bodyCollider.isTrigger) continue;
_castResults.Clear();
int hitCount = bodyCollider.Cast(direction, filter, _castResults, distance);
for (int j = 0; j < hitCount; j++)
{
if (_castResults[j].distance < closest)
closest = _castResults[j].distance;
}
}
return closest;
}
private void UpdateGroundedState(Collision2D collision) private void UpdateGroundedState(Collision2D collision)
{ {
for (int i = 0; i < collision.contactCount; i++) for (int i = 0; i < collision.contactCount; i++)

View File

@@ -145,6 +145,15 @@ public @GameInput()
""processors"": """", ""processors"": """",
""interactions"": """", ""interactions"": """",
""initialStateCheck"": false ""initialStateCheck"": false
},
{
""name"": ""BackDash"",
""type"": ""Button"",
""id"": ""673f5cb8-598b-48d3-bef8-0092cf7f4ba6"",
""expectedControlType"": """",
""processors"": """",
""interactions"": """",
""initialStateCheck"": false
} }
], ],
""bindings"": [ ""bindings"": [
@@ -250,13 +259,24 @@ public @GameInput()
{ {
""name"": """", ""name"": """",
""id"": ""df4d72ec-012c-413a-862d-1a36d2c5b69a"", ""id"": ""df4d72ec-012c-413a-862d-1a36d2c5b69a"",
""path"": ""<Keyboard>/space"", ""path"": ""<Keyboard>/leftCtrl"",
""interactions"": """", ""interactions"": """",
""processors"": """", ""processors"": """",
""groups"": """", ""groups"": """",
""action"": ""Roll"", ""action"": ""Roll"",
""isComposite"": false, ""isComposite"": false,
""isPartOfComposite"": false ""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""4c51b02e-aa8f-4242-81e4-ab1c72a512df"",
""path"": ""<Keyboard>/space"",
""interactions"": """",
""processors"": """",
""groups"": """",
""action"": ""BackDash"",
""isComposite"": false,
""isPartOfComposite"": false
} }
] ]
} }
@@ -271,6 +291,7 @@ public @GameInput()
m_Player_Kick = m_Player.FindAction("Kick", throwIfNotFound: true); m_Player_Kick = m_Player.FindAction("Kick", throwIfNotFound: true);
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);
} }
~@GameInput() ~@GameInput()
@@ -357,6 +378,7 @@ public int FindBinding(InputBinding bindingMask, out InputAction action)
private readonly InputAction m_Player_Kick; private readonly InputAction m_Player_Kick;
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;
/// <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>
@@ -393,6 +415,10 @@ public struct PlayerActions
/// </summary> /// </summary>
public InputAction @Roll => m_Wrapper.m_Player_Roll; public InputAction @Roll => m_Wrapper.m_Player_Roll;
/// <summary> /// <summary>
/// Provides access to the underlying input action "Player/BackDash".
/// </summary>
public InputAction @BackDash => m_Wrapper.m_Player_BackDash;
/// <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; }
@@ -436,6 +462,9 @@ public void AddCallbacks(IPlayerActions instance)
@Roll.started += instance.OnRoll; @Roll.started += instance.OnRoll;
@Roll.performed += instance.OnRoll; @Roll.performed += instance.OnRoll;
@Roll.canceled += instance.OnRoll; @Roll.canceled += instance.OnRoll;
@BackDash.started += instance.OnBackDash;
@BackDash.performed += instance.OnBackDash;
@BackDash.canceled += instance.OnBackDash;
} }
/// <summary> /// <summary>
@@ -465,6 +494,9 @@ private void UnregisterCallbacks(IPlayerActions instance)
@Roll.started -= instance.OnRoll; @Roll.started -= instance.OnRoll;
@Roll.performed -= instance.OnRoll; @Roll.performed -= instance.OnRoll;
@Roll.canceled -= instance.OnRoll; @Roll.canceled -= instance.OnRoll;
@BackDash.started -= instance.OnBackDash;
@BackDash.performed -= instance.OnBackDash;
@BackDash.canceled -= instance.OnBackDash;
} }
/// <summary> /// <summary>
@@ -547,5 +579,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 OnRoll(InputAction.CallbackContext context); void OnRoll(InputAction.CallbackContext context);
/// <summary>
/// Method invoked when associated input action "BackDash" 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 OnBackDash(InputAction.CallbackContext context);
} }
} }

View File

@@ -14,6 +14,7 @@ public class InputManager : MonoBehaviour, GameInput.IPlayerActions
public event Action OnKick_Event; public event Action OnKick_Event;
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;
private void Awake() private void Awake()
{ {
@@ -69,4 +70,11 @@ public void OnRoll(InputAction.CallbackContext ctx)
if (ctx.phase == InputActionPhase.Started) if (ctx.phase == InputActionPhase.Started)
OnRoll_Event?.Invoke(); OnRoll_Event?.Invoke();
} }
public void OnBackDash(InputAction.CallbackContext ctx)
{
if (ctx.phase == InputActionPhase.Started)
OnBackDash_Event?.Invoke();
}
} }

View File

@@ -28,10 +28,13 @@ public class PlayerController : MonoBehaviour
private int _wallDirection; private int _wallDirection;
private float _inputLockTimer; private float _inputLockTimer;
private float _facingLockTimer; private float _facingLockTimer;
private ActionData _movementLockAction;
private ActionData _facingLockAction;
[Header("Motion")] [Header("Motion")]
[SerializeField] private ComboNode _dashRootNode; [SerializeField] private ComboNode _dashRootNode;
[SerializeField] private ComboNode _rollRootNode; [SerializeField] private ComboNode _rollRootNode;
[SerializeField] private ComboNode _backDashRootNode;
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();
@@ -90,6 +93,7 @@ private void Start()
InputManager.Instance.OnKick_Event += OnKickInput; InputManager.Instance.OnKick_Event += OnKickInput;
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;
} }
private void OnDestroy() private void OnDestroy()
@@ -102,6 +106,7 @@ private void OnDestroy()
InputManager.Instance.OnKick_Event -= OnKickInput; InputManager.Instance.OnKick_Event -= OnKickInput;
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;
} }
_attackCts?.Cancel(); _attackCts?.Cancel();
@@ -127,14 +132,10 @@ private void FixedUpdate()
ExecuteBufferedInputIfReady(); ExecuteBufferedInputIfReady();
TickComboWindow(); TickComboWindow();
if (_inputLockTimer > 0f) if (!IsMovementLocked())
_inputLockTimer -= Time.fixedDeltaTime;
else
_rb.linearVelocity = new Vector2(_moveInputX * _moveSpeed, _rb.linearVelocity.y); _rb.linearVelocity = new Vector2(_moveInputX * _moveSpeed, _rb.linearVelocity.y);
if (_facingLockTimer > 0f) if (!IsFacingLocked())
_facingLockTimer -= Time.fixedDeltaTime;
else
UpdateFacingFromMoveInput(); UpdateFacingFromMoveInput();
ApplyGravity(); ApplyGravity();
@@ -196,6 +197,7 @@ private void OnJumpInput()
private void OnKickInput() => HandleComboInput(ComboInputType.Kick); private void OnKickInput() => HandleComboInput(ComboInputType.Kick);
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 HandleComboInput(ComboInputType input) private void HandleComboInput(ComboInputType input)
{ {
@@ -296,12 +298,13 @@ private async void PerformAttack(ActionData data, bool preserveHorizontalVelocit
_attackStartTime = Time.time; _attackStartTime = Time.time;
_hitFired = false; _hitFired = false;
ClearActionLocks();
PlayActionAnimation(data); PlayActionAnimation(data);
if (data.HasMotion) if (data.HasMotion)
ApplyActionVelocity(data); ApplyActionVelocity(data);
LockMovementIfNeeded(data, preserveHorizontalVelocity); LockMovementIfNeeded(data, preserveHorizontalVelocity, true);
LockFacingIfNeeded(data); LockFacingIfNeeded(data);
try try
@@ -326,6 +329,7 @@ private async void PerformMotion(ActionData data)
CancellationToken token = _motionCts.Token; CancellationToken token = _motionCts.Token;
SetMotionCooldown(data); SetMotionCooldown(data);
ClearActionLocks();
FaceMotionDirection(data); FaceMotionDirection(data);
PlayActionAnimation(data); PlayActionAnimation(data);
LockMovementIfNeeded(data); LockMovementIfNeeded(data);
@@ -342,6 +346,7 @@ private async void PerformMotion(ActionData data)
if (completed) if (completed)
{ {
StopActionVelocity(data); StopActionVelocity(data);
ClearActionLocks();
PlayIdleAnimation(); PlayIdleAnimation();
} }
} }
@@ -349,17 +354,14 @@ private async void PerformMotion(ActionData data)
private async Awaitable MotionRoutine(ActionData data, CancellationToken token) private async Awaitable MotionRoutine(ActionData data, CancellationToken token)
{ {
float elapsed = 0f; float elapsed = 0f;
float duration = Mathf.Max(data.MotionDuration, 0.01f); float duration = GetActionDuration(data);
while (elapsed < duration) while (ShouldKeepActionPlaying(data, elapsed, duration))
{ {
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
float normalizedTime = Mathf.Clamp01(elapsed / duration); float normalizedTime = GetActionNormalizedTime(data, elapsed, duration);
ApplyActionVelocity(data, normalizedTime); ApplyActionVelocity(data, normalizedTime);
if (data.ReturnToIdleOnAnimationComplete && IsActionAnimationComplete(data))
return;
await Awaitable.NextFrameAsync(token); await Awaitable.NextFrameAsync(token);
elapsed += Time.deltaTime; elapsed += Time.deltaTime;
} }
@@ -371,6 +373,7 @@ private void CancelAttack()
_attackCooldownTimer = 0f; _attackCooldownTimer = 0f;
_pendingInput = null; _pendingInput = null;
_attackHitbox?.Deactivate(); _attackHitbox?.Deactivate();
ClearActionLocks();
} }
private async Awaitable HitRoutine(ActionData data, CancellationToken token) private async Awaitable HitRoutine(ActionData data, CancellationToken token)
@@ -379,8 +382,7 @@ private async Awaitable HitRoutine(ActionData data, CancellationToken token)
if (!data.HasHit) if (!data.HasHit)
{ {
if (data.MotionDuration > 0f) await WaitForActionEnd(data, token);
await Awaitable.WaitForSecondsAsync(data.MotionDuration, token);
return; return;
} }
@@ -394,10 +396,9 @@ private async Awaitable HitRoutine(ActionData data, CancellationToken token)
await Awaitable.WaitForSecondsAsync(activeTime, token); await Awaitable.WaitForSecondsAsync(activeTime, token);
_attackHitbox.Deactivate(); _attackHitbox.Deactivate();
float remaining = data.MotionDuration - (Time.time - attackStartTime); await WaitForActionEnd(data, token, Time.time - attackStartTime);
if (remaining > 0f)
await Awaitable.WaitForSecondsAsync(remaining, token);
ClearActionLocks();
PlayIdleAnimation(); PlayIdleAnimation();
} }
@@ -405,13 +406,15 @@ private void ActivateAttackHitbox(ActionData data)
{ {
Vector2 localPosition = GetAttackLocalPosition(data.Offset); Vector2 localPosition = GetAttackLocalPosition(data.Offset);
Vector2 hitVelocity = GetHitVelocity(data.HitVelocity); Vector2 hitVelocity = GetHitVelocity(data.HitVelocity);
Vector2? hitTargetPosition = GetHitTargetPosition(data);
_lastHitData = data; _lastHitData = data;
_lastHitCenter = transform.TransformPoint(localPosition); _lastHitCenter = transform.TransformPoint(localPosition);
_lastHitTime = Time.time; _lastHitTime = Time.time;
_hitFired = true; _hitFired = true;
_attackHitbox.Activate(data, localPosition, hitVelocity, _enemyLayer); Vector2 sourcePosition = _rb != null ? _rb.position : (Vector2)transform.position;
_attackHitbox.Activate(data, localPosition, hitVelocity, sourcePosition, hitTargetPosition, data.CorrectHitTargetY, _groundLayer.value, _enemyLayer);
} }
private void EnsureAttackHitbox() private void EnsureAttackHitbox()
@@ -467,21 +470,23 @@ private void StopActionVelocity(ActionData data)
_rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y); _rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y);
} }
private void LockMovementIfNeeded(ActionData data, bool preserveHorizontalVelocity = false) private void LockMovementIfNeeded(ActionData data, bool preserveHorizontalVelocity = false, bool forceLock = false)
{ {
if (data.CanMoveDuringAction) return; if (!forceLock && data.CanMoveDuringAction) return;
// 이동 없는 공격은 직전 프레임의 걷기 속도를 이어받지 않게 한다. // 이동 없는 공격은 직전 프레임의 걷기 속도를 이어받지 않게 한다.
if (!preserveHorizontalVelocity && !data.HasMotion) if (!preserveHorizontalVelocity && !data.HasMotion)
_rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y); _rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y);
_inputLockTimer = Mathf.Max(data.MotionDuration, 0.02f); _inputLockTimer = GetActionLockDuration(data);
_movementLockAction = ShouldUseAnimationLength(data) ? data : null;
} }
private void LockFacingIfNeeded(ActionData data) private void LockFacingIfNeeded(ActionData data)
{ {
if (data.CanTurnDuringAction) return; if (data.CanTurnDuringAction) return;
_facingLockTimer = Mathf.Max(data.MotionDuration, 0.02f); _facingLockTimer = GetActionLockDuration(data);
_facingLockAction = ShouldUseAnimationLength(data) ? data : null;
} }
private void PlayIdleAnimation() private void PlayIdleAnimation()
@@ -504,22 +509,25 @@ private void PlayActionAnimation(ActionData data)
_anim.speed = GetAnimationSpeed(data, 0f); _anim.speed = GetAnimationSpeed(data, 0f);
if (!string.IsNullOrEmpty(data.AnimationState)) if (!string.IsNullOrEmpty(data.AnimationState))
{
_anim.Play(data.AnimationState); _anim.Play(data.AnimationState);
_anim.Update(0f);
}
ApplyAnimationSpeedCurve(data, _animationSpeedCts.Token); ApplyAnimationSpeedCurve(data, _animationSpeedCts.Token);
} }
private async void ApplyAnimationSpeedCurve(ActionData data, CancellationToken token) private async void ApplyAnimationSpeedCurve(ActionData data, CancellationToken token)
{ {
float duration = Mathf.Max(data.MotionDuration, data.HitTiming + data.HitDuration, 0.01f); float duration = GetActionDuration(data);
float elapsed = 0f; float elapsed = 0f;
try try
{ {
while (elapsed < duration) while (ShouldKeepActionPlaying(data, elapsed, duration))
{ {
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
float normalizedTime = Mathf.Clamp01(elapsed / duration); float normalizedTime = GetActionNormalizedTime(data, elapsed, duration);
_anim.speed = GetAnimationSpeed(data, normalizedTime); _anim.speed = GetAnimationSpeed(data, normalizedTime);
await Awaitable.NextFrameAsync(token); await Awaitable.NextFrameAsync(token);
@@ -569,6 +577,17 @@ private Vector2 GetAttackLocalPosition(Vector2 offset)
return offset; return offset;
} }
private Vector2? GetHitTargetPosition(ActionData data)
{
if (!data.UseHitPositionCorrection) return null;
float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
Vector2 playerPosition = _rb != null ? _rb.position : (Vector2)transform.position;
Vector2 offset = data.HitTargetOffset;
offset.x *= facing;
return playerPosition + offset;
}
private string GetActionName(ActionData data) private string GetActionName(ActionData data)
{ {
if (data == null) return string.Empty; if (data == null) return string.Empty;
@@ -585,16 +604,151 @@ private void ApplyForwardStep(float distance, float duration)
_rb.linearVelocity = new Vector2(vx, _rb.linearVelocity.y); _rb.linearVelocity = new Vector2(vx, _rb.linearVelocity.y);
_inputLockTimer = safeDuration; _inputLockTimer = safeDuration;
_movementLockAction = null;
} }
private bool IsActionAnimationComplete(ActionData data) private bool IsActionAnimationComplete(ActionData data)
{ {
if (_anim == null || string.IsNullOrEmpty(data.AnimationState)) return false; if (!HasActionAnimation(data)) return false;
AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0); AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0);
return stateInfo.IsName(data.AnimationState) && stateInfo.normalizedTime >= 1f; return stateInfo.IsName(data.AnimationState) && stateInfo.normalizedTime >= 1f;
} }
private bool IsActionAnimationLooping(ActionData data)
{
if (!HasActionAnimation(data)) return false;
AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0);
if (!stateInfo.IsName(data.AnimationState)) return false;
if (stateInfo.loop) return true;
AnimatorClipInfo[] clipInfos = _anim.GetCurrentAnimatorClipInfo(0);
for (int i = 0; i < clipInfos.Length; i++)
{
AnimationClip clip = clipInfos[i].clip;
if (clip != null && clip.isLooping)
return true;
}
return false;
}
private bool HasActionAnimation(ActionData data)
{
return _anim != null && data != null && !string.IsNullOrEmpty(data.AnimationState);
}
private bool ShouldUseAnimationLength(ActionData data)
{
return HasActionAnimation(data) && !IsActionAnimationLooping(data);
}
private bool IsMovementLocked()
{
return IsTimerOrActionLockActive(ref _inputLockTimer, ref _movementLockAction);
}
private bool IsFacingLocked()
{
return IsTimerOrActionLockActive(ref _facingLockTimer, ref _facingLockAction);
}
private bool IsTimerOrActionLockActive(ref float timer, ref ActionData action)
{
if (timer > 0f)
{
timer -= Time.fixedDeltaTime;
return true;
}
if (action == null)
return false;
if (!HasActionAnimation(action))
{
action = null;
return false;
}
if (!IsActionAnimationActive(action))
{
action = null;
return false;
}
if (IsActionAnimationComplete(action))
{
action = null;
return false;
}
return true;
}
private bool IsActionAnimationActive(ActionData data)
{
if (!HasActionAnimation(data)) return false;
AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0);
return stateInfo.IsName(data.AnimationState);
}
private void ClearActionLocks()
{
_inputLockTimer = 0f;
_facingLockTimer = 0f;
_movementLockAction = null;
_facingLockAction = null;
}
private float GetActionDuration(ActionData data)
{
return Mathf.Max(data.MotionDuration, data.HitTiming + data.HitDuration, 0.01f);
}
private float GetActionLockDuration(ActionData data)
{
return ShouldUseAnimationLength(data) ? 0f : Mathf.Max(data.MotionDuration, 0.02f);
}
private bool ShouldKeepActionPlaying(ActionData data, float elapsed, float duration)
{
if (!ShouldUseAnimationLength(data))
return elapsed < duration;
return !IsActionAnimationComplete(data);
}
private float GetActionNormalizedTime(ActionData data, float elapsed, float duration)
{
if (!HasActionAnimation(data))
return Mathf.Clamp01(elapsed / duration);
AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0);
if (!stateInfo.IsName(data.AnimationState))
return 0f;
return Mathf.Clamp01(stateInfo.normalizedTime);
}
private async Awaitable WaitForActionEnd(ActionData data, CancellationToken token, float alreadyElapsed = 0f)
{
if (!ShouldUseAnimationLength(data))
{
float remaining = GetActionDuration(data) - alreadyElapsed;
if (remaining > 0f)
await Awaitable.WaitForSecondsAsync(remaining, token);
return;
}
while (!IsActionAnimationComplete(data))
{
token.ThrowIfCancellationRequested();
await Awaitable.NextFrameAsync(token);
}
}
private void ApplyGravity() private void ApplyGravity()
{ {
float vy = _rb.linearVelocity.y; float vy = _rb.linearVelocity.y;

View File

@@ -2700,6 +2700,32 @@ AnimatorState:
m_MirrorParameter: m_MirrorParameter:
m_CycleOffsetParameter: m_CycleOffsetParameter:
m_TimeParameter: m_TimeParameter:
--- !u!1102 &6644522863449380584
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: BackDash
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: aa9ae735bd685934e953c3ca7b2bed96, type: 2}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &6664549699494865306 --- !u!1102 &6664549699494865306
AnimatorState: AnimatorState:
serializedVersion: 6 serializedVersion: 6
@@ -3092,6 +3118,9 @@ AnimatorStateMachine:
- serializedVersion: 1 - serializedVersion: 1
m_State: {fileID: 1592270058049249243} m_State: {fileID: 1592270058049249243}
m_Position: {x: 2480, y: -200, z: 0} m_Position: {x: 2480, y: -200, z: 0}
- serializedVersion: 1
m_State: {fileID: 6644522863449380584}
m_Position: {x: 1140, y: -100, 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: aa9ae735bd685934e953c3ca7b2bed96
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

Binary file not shown.

View File

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

View File

@@ -59,6 +59,15 @@
"processors": "", "processors": "",
"interactions": "", "interactions": "",
"initialStateCheck": false "initialStateCheck": false
},
{
"name": "BackDash",
"type": "Button",
"id": "673f5cb8-598b-48d3-bef8-0092cf7f4ba6",
"expectedControlType": "",
"processors": "",
"interactions": "",
"initialStateCheck": false
} }
], ],
"bindings": [ "bindings": [
@@ -164,13 +173,24 @@
{ {
"name": "", "name": "",
"id": "df4d72ec-012c-413a-862d-1a36d2c5b69a", "id": "df4d72ec-012c-413a-862d-1a36d2c5b69a",
"path": "<Keyboard>/space", "path": "<Keyboard>/leftCtrl",
"interactions": "", "interactions": "",
"processors": "", "processors": "",
"groups": "", "groups": "",
"action": "Roll", "action": "Roll",
"isComposite": false, "isComposite": false,
"isPartOfComposite": false "isPartOfComposite": false
},
{
"name": "",
"id": "4c51b02e-aa8f-4242-81e4-ab1c72a512df",
"path": "<Keyboard>/space",
"interactions": "",
"processors": "",
"groups": "",
"action": "BackDash",
"isComposite": false,
"isPartOfComposite": false
} }
] ]
} }