2026-05-18 애니메이션 설계 수정
This commit is contained in:
BIN
Assets/01_Scenes/GameScene.unity
LFS
BIN
Assets/01_Scenes/GameScene.unity
LFS
Binary file not shown.
@@ -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;
|
||||
|
||||
@@ -2700,6 +2700,32 @@ AnimatorState:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
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
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
@@ -3092,6 +3118,9 @@ AnimatorStateMachine:
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: 1592270058049249243}
|
||||
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_AnyStateTransitions: []
|
||||
m_EntryTransitions: []
|
||||
|
||||
BIN
Assets/03_Character/WhiteMan/Animations/BackDash.anim
LFS
Normal file
BIN
Assets/03_Character/WhiteMan/Animations/BackDash.anim
LFS
Normal file
Binary file not shown.
@@ -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.
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.
BIN
Assets/05_Data/Combo/Combo_BackDash.asset
LFS
Normal file
BIN
Assets/05_Data/Combo/Combo_BackDash.asset
LFS
Normal file
Binary file not shown.
8
Assets/05_Data/Combo/Combo_BackDash.asset.meta
Normal file
8
Assets/05_Data/Combo/Combo_BackDash.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: deeb30709bba78346aa208023b6e394e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/05_Data/Motion/BackDash.asset
LFS
Normal file
BIN
Assets/05_Data/Motion/BackDash.asset
LFS
Normal file
Binary file not shown.
8
Assets/05_Data/Motion/BackDash.asset.meta
Normal file
8
Assets/05_Data/Motion/BackDash.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4565c3aff6145d340ae707850c3d844d
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -59,6 +59,15 @@
|
||||
"processors": "",
|
||||
"interactions": "",
|
||||
"initialStateCheck": false
|
||||
},
|
||||
{
|
||||
"name": "BackDash",
|
||||
"type": "Button",
|
||||
"id": "673f5cb8-598b-48d3-bef8-0092cf7f4ba6",
|
||||
"expectedControlType": "",
|
||||
"processors": "",
|
||||
"interactions": "",
|
||||
"initialStateCheck": false
|
||||
}
|
||||
],
|
||||
"bindings": [
|
||||
@@ -164,13 +173,24 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user