2026-05-18 애니메이션 설계 수정
This commit is contained in:
@@ -10,7 +10,6 @@ public class ActionData : ScriptableObject
|
||||
public string AnimationState;
|
||||
public float AnimationSpeed = 1f;
|
||||
public AnimationCurve AnimationSpeedCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
|
||||
public bool ReturnToIdleOnAnimationComplete;
|
||||
public float Cooldown = 0.3f;
|
||||
public float ComboWindow = 0.25f;
|
||||
|
||||
@@ -36,5 +35,9 @@ public class ActionData : ScriptableObject
|
||||
|
||||
[Header("Hit Reaction")]
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@ public class AttackHitbox : MonoBehaviour
|
||||
private CircleCollider2D _collider;
|
||||
private int _damage;
|
||||
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 LayerMask _targetLayer;
|
||||
private readonly HashSet<IDamageable> _alreadyHit = new();
|
||||
@@ -19,12 +25,18 @@ private void Awake()
|
||||
_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;
|
||||
_collider.radius = data.Radius;
|
||||
_damage = data.Damage;
|
||||
_hitVelocity = hitVelocity;
|
||||
_hitSourcePosition = sourcePosition;
|
||||
_hitTargetPosition = hitTargetPosition;
|
||||
_hitPositionMinDistance = Mathf.Abs(data.HitTargetOffset.x);
|
||||
_correctHitTargetY = correctHitTargetY;
|
||||
_hitPositionSolidMask = hitPositionSolidMask;
|
||||
_hitPositionCorrectionDuration = data.HitPositionCorrectionDuration;
|
||||
_hitReactionState = data.HitReactionAnimationState;
|
||||
_targetLayer = targetLayer;
|
||||
_alreadyHit.Clear();
|
||||
@@ -61,6 +73,20 @@ private void TryDamage(Collider2D other)
|
||||
if (_alreadyHit.Contains(target)) return;
|
||||
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
@@ -27,6 +28,15 @@ public class Enemy : MonoBehaviour, IDamageable
|
||||
private float _hitReactionTimer;
|
||||
private bool _isGrounded;
|
||||
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()
|
||||
{
|
||||
@@ -34,6 +44,7 @@ private void Awake()
|
||||
_rb = GetComponent<Rigidbody2D>();
|
||||
_anim = GetComponentInChildren<Animator>();
|
||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||
_bodyColliders = GetComponentsInChildren<Collider2D>();
|
||||
if (_spriteRenderer != null)
|
||||
_originalColor = _spriteRenderer.color;
|
||||
}
|
||||
@@ -54,10 +65,13 @@ private void Update()
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (_rb != null)
|
||||
{
|
||||
ApplySmoothHitPositionCorrection();
|
||||
_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;
|
||||
|
||||
@@ -80,6 +94,8 @@ public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReac
|
||||
_rb.linearVelocity = Vector2.zero;
|
||||
_lastVelocity = Vector2.zero;
|
||||
|
||||
BeginHitTargetPositionCorrection(hitTargetPosition, correctHitTargetY, hitPositionSolidMask, hitPositionCorrectionDuration);
|
||||
|
||||
Vector2 nextVelocity = GetHitReactionVelocity(hitVelocity);
|
||||
if (nextVelocity != Vector2.zero)
|
||||
{
|
||||
@@ -130,6 +146,98 @@ private Vector2 GetHitReactionVelocity(Vector2 hitVelocity)
|
||||
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)
|
||||
{
|
||||
for (int i = 0; i < collision.contactCount; i++)
|
||||
|
||||
@@ -145,6 +145,15 @@ public @GameInput()
|
||||
""processors"": """",
|
||||
""interactions"": """",
|
||||
""initialStateCheck"": false
|
||||
},
|
||||
{
|
||||
""name"": ""BackDash"",
|
||||
""type"": ""Button"",
|
||||
""id"": ""673f5cb8-598b-48d3-bef8-0092cf7f4ba6"",
|
||||
""expectedControlType"": """",
|
||||
""processors"": """",
|
||||
""interactions"": """",
|
||||
""initialStateCheck"": false
|
||||
}
|
||||
],
|
||||
""bindings"": [
|
||||
@@ -250,13 +259,24 @@ public @GameInput()
|
||||
{
|
||||
""name"": """",
|
||||
""id"": ""df4d72ec-012c-413a-862d-1a36d2c5b69a"",
|
||||
""path"": ""<Keyboard>/space"",
|
||||
""path"": ""<Keyboard>/leftCtrl"",
|
||||
""interactions"": """",
|
||||
""processors"": """",
|
||||
""groups"": """",
|
||||
""action"": ""Roll"",
|
||||
""isComposite"": 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_Dash = m_Player.FindAction("Dash", throwIfNotFound: true);
|
||||
m_Player_Roll = m_Player.FindAction("Roll", throwIfNotFound: true);
|
||||
m_Player_BackDash = m_Player.FindAction("BackDash", throwIfNotFound: true);
|
||||
}
|
||||
|
||||
~@GameInput()
|
||||
@@ -357,6 +378,7 @@ public int FindBinding(InputBinding bindingMask, out InputAction action)
|
||||
private readonly InputAction m_Player_Kick;
|
||||
private readonly InputAction m_Player_Dash;
|
||||
private readonly InputAction m_Player_Roll;
|
||||
private readonly InputAction m_Player_BackDash;
|
||||
/// <summary>
|
||||
/// Provides access to input actions defined in input action map "Player".
|
||||
/// </summary>
|
||||
@@ -393,6 +415,10 @@ public struct PlayerActions
|
||||
/// </summary>
|
||||
public InputAction @Roll => m_Wrapper.m_Player_Roll;
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public InputActionMap Get() { return m_Wrapper.m_Player; }
|
||||
@@ -436,6 +462,9 @@ public void AddCallbacks(IPlayerActions instance)
|
||||
@Roll.started += instance.OnRoll;
|
||||
@Roll.performed += instance.OnRoll;
|
||||
@Roll.canceled += instance.OnRoll;
|
||||
@BackDash.started += instance.OnBackDash;
|
||||
@BackDash.performed += instance.OnBackDash;
|
||||
@BackDash.canceled += instance.OnBackDash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -465,6 +494,9 @@ private void UnregisterCallbacks(IPlayerActions instance)
|
||||
@Roll.started -= instance.OnRoll;
|
||||
@Roll.performed -= instance.OnRoll;
|
||||
@Roll.canceled -= instance.OnRoll;
|
||||
@BackDash.started -= instance.OnBackDash;
|
||||
@BackDash.performed -= instance.OnBackDash;
|
||||
@BackDash.canceled -= instance.OnBackDash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -547,5 +579,12 @@ public interface IPlayerActions
|
||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public class InputManager : MonoBehaviour, GameInput.IPlayerActions
|
||||
public event Action OnKick_Event;
|
||||
public event Action OnDash_Event;
|
||||
public event Action OnRoll_Event;
|
||||
public event Action OnBackDash_Event;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -69,4 +70,11 @@ public void OnRoll(InputAction.CallbackContext ctx)
|
||||
if (ctx.phase == InputActionPhase.Started)
|
||||
OnRoll_Event?.Invoke();
|
||||
}
|
||||
|
||||
public void OnBackDash(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if (ctx.phase == InputActionPhase.Started)
|
||||
OnBackDash_Event?.Invoke();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,10 +28,13 @@ public class PlayerController : MonoBehaviour
|
||||
private int _wallDirection;
|
||||
private float _inputLockTimer;
|
||||
private float _facingLockTimer;
|
||||
private ActionData _movementLockAction;
|
||||
private ActionData _facingLockAction;
|
||||
|
||||
[Header("Motion")]
|
||||
[SerializeField] private ComboNode _dashRootNode;
|
||||
[SerializeField] private ComboNode _rollRootNode;
|
||||
[SerializeField] private ComboNode _backDashRootNode;
|
||||
private readonly Dictionary<ActionData, float> _motionCooldownTimers = new();
|
||||
private readonly List<ActionData> _motionCooldownKeys = new();
|
||||
|
||||
@@ -90,6 +93,7 @@ private void Start()
|
||||
InputManager.Instance.OnKick_Event += OnKickInput;
|
||||
InputManager.Instance.OnDash_Event += OnDashInput;
|
||||
InputManager.Instance.OnRoll_Event += OnRollInput;
|
||||
InputManager.Instance.OnBackDash_Event += OnBackDashInput;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
@@ -102,6 +106,7 @@ private void OnDestroy()
|
||||
InputManager.Instance.OnKick_Event -= OnKickInput;
|
||||
InputManager.Instance.OnDash_Event -= OnDashInput;
|
||||
InputManager.Instance.OnRoll_Event -= OnRollInput;
|
||||
InputManager.Instance.OnBackDash_Event -= OnBackDashInput;
|
||||
}
|
||||
|
||||
_attackCts?.Cancel();
|
||||
@@ -127,14 +132,10 @@ private void FixedUpdate()
|
||||
ExecuteBufferedInputIfReady();
|
||||
TickComboWindow();
|
||||
|
||||
if (_inputLockTimer > 0f)
|
||||
_inputLockTimer -= Time.fixedDeltaTime;
|
||||
else
|
||||
if (!IsMovementLocked())
|
||||
_rb.linearVelocity = new Vector2(_moveInputX * _moveSpeed, _rb.linearVelocity.y);
|
||||
|
||||
if (_facingLockTimer > 0f)
|
||||
_facingLockTimer -= Time.fixedDeltaTime;
|
||||
else
|
||||
if (!IsFacingLocked())
|
||||
UpdateFacingFromMoveInput();
|
||||
|
||||
ApplyGravity();
|
||||
@@ -196,6 +197,7 @@ private void OnJumpInput()
|
||||
private void OnKickInput() => HandleComboInput(ComboInputType.Kick);
|
||||
private void OnDashInput() => ExecuteMotionNode(_dashRootNode);
|
||||
private void OnRollInput() => ExecuteMotionNode(_rollRootNode);
|
||||
private void OnBackDashInput() => ExecuteMotionNode(_backDashRootNode);
|
||||
|
||||
private void HandleComboInput(ComboInputType input)
|
||||
{
|
||||
@@ -296,12 +298,13 @@ private async void PerformAttack(ActionData data, bool preserveHorizontalVelocit
|
||||
_attackStartTime = Time.time;
|
||||
_hitFired = false;
|
||||
|
||||
ClearActionLocks();
|
||||
PlayActionAnimation(data);
|
||||
|
||||
if (data.HasMotion)
|
||||
ApplyActionVelocity(data);
|
||||
|
||||
LockMovementIfNeeded(data, preserveHorizontalVelocity);
|
||||
LockMovementIfNeeded(data, preserveHorizontalVelocity, true);
|
||||
LockFacingIfNeeded(data);
|
||||
|
||||
try
|
||||
@@ -326,6 +329,7 @@ private async void PerformMotion(ActionData data)
|
||||
CancellationToken token = _motionCts.Token;
|
||||
|
||||
SetMotionCooldown(data);
|
||||
ClearActionLocks();
|
||||
FaceMotionDirection(data);
|
||||
PlayActionAnimation(data);
|
||||
LockMovementIfNeeded(data);
|
||||
@@ -342,6 +346,7 @@ private async void PerformMotion(ActionData data)
|
||||
if (completed)
|
||||
{
|
||||
StopActionVelocity(data);
|
||||
ClearActionLocks();
|
||||
PlayIdleAnimation();
|
||||
}
|
||||
}
|
||||
@@ -349,17 +354,14 @@ private async void PerformMotion(ActionData data)
|
||||
private async Awaitable MotionRoutine(ActionData data, CancellationToken token)
|
||||
{
|
||||
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();
|
||||
float normalizedTime = Mathf.Clamp01(elapsed / duration);
|
||||
float normalizedTime = GetActionNormalizedTime(data, elapsed, duration);
|
||||
ApplyActionVelocity(data, normalizedTime);
|
||||
|
||||
if (data.ReturnToIdleOnAnimationComplete && IsActionAnimationComplete(data))
|
||||
return;
|
||||
|
||||
await Awaitable.NextFrameAsync(token);
|
||||
elapsed += Time.deltaTime;
|
||||
}
|
||||
@@ -371,6 +373,7 @@ private void CancelAttack()
|
||||
_attackCooldownTimer = 0f;
|
||||
_pendingInput = null;
|
||||
_attackHitbox?.Deactivate();
|
||||
ClearActionLocks();
|
||||
}
|
||||
|
||||
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.MotionDuration > 0f)
|
||||
await Awaitable.WaitForSecondsAsync(data.MotionDuration, token);
|
||||
await WaitForActionEnd(data, token);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -394,10 +396,9 @@ private async Awaitable HitRoutine(ActionData data, CancellationToken token)
|
||||
await Awaitable.WaitForSecondsAsync(activeTime, token);
|
||||
_attackHitbox.Deactivate();
|
||||
|
||||
float remaining = data.MotionDuration - (Time.time - attackStartTime);
|
||||
if (remaining > 0f)
|
||||
await Awaitable.WaitForSecondsAsync(remaining, token);
|
||||
await WaitForActionEnd(data, token, Time.time - attackStartTime);
|
||||
|
||||
ClearActionLocks();
|
||||
PlayIdleAnimation();
|
||||
}
|
||||
|
||||
@@ -405,13 +406,15 @@ private void ActivateAttackHitbox(ActionData data)
|
||||
{
|
||||
Vector2 localPosition = GetAttackLocalPosition(data.Offset);
|
||||
Vector2 hitVelocity = GetHitVelocity(data.HitVelocity);
|
||||
Vector2? hitTargetPosition = GetHitTargetPosition(data);
|
||||
|
||||
_lastHitData = data;
|
||||
_lastHitCenter = transform.TransformPoint(localPosition);
|
||||
_lastHitTime = Time.time;
|
||||
_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()
|
||||
@@ -467,21 +470,23 @@ private void StopActionVelocity(ActionData data)
|
||||
_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)
|
||||
_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)
|
||||
{
|
||||
if (data.CanTurnDuringAction) return;
|
||||
_facingLockTimer = Mathf.Max(data.MotionDuration, 0.02f);
|
||||
_facingLockTimer = GetActionLockDuration(data);
|
||||
_facingLockAction = ShouldUseAnimationLength(data) ? data : null;
|
||||
}
|
||||
|
||||
private void PlayIdleAnimation()
|
||||
@@ -504,22 +509,25 @@ private void PlayActionAnimation(ActionData data)
|
||||
|
||||
_anim.speed = GetAnimationSpeed(data, 0f);
|
||||
if (!string.IsNullOrEmpty(data.AnimationState))
|
||||
{
|
||||
_anim.Play(data.AnimationState);
|
||||
_anim.Update(0f);
|
||||
}
|
||||
|
||||
ApplyAnimationSpeedCurve(data, _animationSpeedCts.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;
|
||||
|
||||
try
|
||||
{
|
||||
while (elapsed < duration)
|
||||
while (ShouldKeepActionPlaying(data, elapsed, duration))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
float normalizedTime = Mathf.Clamp01(elapsed / duration);
|
||||
float normalizedTime = GetActionNormalizedTime(data, elapsed, duration);
|
||||
_anim.speed = GetAnimationSpeed(data, normalizedTime);
|
||||
|
||||
await Awaitable.NextFrameAsync(token);
|
||||
@@ -569,6 +577,17 @@ private Vector2 GetAttackLocalPosition(Vector2 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)
|
||||
{
|
||||
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);
|
||||
_inputLockTimer = safeDuration;
|
||||
_movementLockAction = null;
|
||||
}
|
||||
|
||||
private bool IsActionAnimationComplete(ActionData data)
|
||||
{
|
||||
if (_anim == null || string.IsNullOrEmpty(data.AnimationState)) return false;
|
||||
if (!HasActionAnimation(data)) return false;
|
||||
|
||||
AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0);
|
||||
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()
|
||||
{
|
||||
float vy = _rb.linearVelocity.y;
|
||||
|
||||
Reference in New Issue
Block a user