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.
@@ -40,4 +40,13 @@ public class ActionData : ScriptableObject
|
|||||||
public float HitPositionCorrectionDuration = 0.08f;
|
public float HitPositionCorrectionDuration = 0.08f;
|
||||||
public bool CorrectHitTargetY;
|
public bool CorrectHitTargetY;
|
||||||
public string HitReactionAnimationState;
|
public string HitReactionAnimationState;
|
||||||
|
|
||||||
|
[Header("Grab")]
|
||||||
|
public bool IsGrab;
|
||||||
|
public Vector2 GrabOffset = new Vector2(0.6f, 0f);
|
||||||
|
public AnimationCurve GrabOffsetXCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
|
||||||
|
public AnimationCurve GrabOffsetYCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
|
||||||
|
public string GrabbedAnimationState;
|
||||||
|
public float GrabSearchRadius = 2f;
|
||||||
|
public float GrabRange = 0.5f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
[RequireComponent(typeof(CircleCollider2D))]
|
[RequireComponent(typeof(CircleCollider2D))]
|
||||||
public class AttackHitbox : MonoBehaviour
|
public class AttackHitbox : MonoBehaviour
|
||||||
{
|
{
|
||||||
|
public event System.Action<IDamageable> OnHit;
|
||||||
|
|
||||||
private CircleCollider2D _collider;
|
private CircleCollider2D _collider;
|
||||||
private int _damage;
|
private int _damage;
|
||||||
private Vector2 _hitVelocity;
|
private Vector2 _hitVelocity;
|
||||||
@@ -75,6 +77,7 @@ private void TryDamage(Collider2D other)
|
|||||||
_alreadyHit.Add(target);
|
_alreadyHit.Add(target);
|
||||||
Vector2? targetPosition = GetCorrectionTargetPosition(other);
|
Vector2? targetPosition = GetCorrectionTargetPosition(other);
|
||||||
target.TakeDamage(_damage, _hitVelocity, _hitReactionState, targetPosition, _correctHitTargetY, _hitPositionSolidMask, _hitPositionCorrectionDuration);
|
target.TakeDamage(_damage, _hitVelocity, _hitReactionState, targetPosition, _correctHitTargetY, _hitPositionSolidMask, _hitPositionCorrectionDuration);
|
||||||
|
OnHit?.Invoke(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector2? GetCorrectionTargetPosition(Collider2D other)
|
private Vector2? GetCorrectionTargetPosition(Collider2D other)
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ public class Enemy : MonoBehaviour, IDamageable
|
|||||||
private float _hitPositionCorrectionDuration;
|
private float _hitPositionCorrectionDuration;
|
||||||
private Vector2 _hitPositionCorrectionStart;
|
private Vector2 _hitPositionCorrectionStart;
|
||||||
private Vector2 _hitPositionCorrectionTarget;
|
private Vector2 _hitPositionCorrectionTarget;
|
||||||
|
private bool _isGrabbed;
|
||||||
|
private Vector2 _grabTargetPosition;
|
||||||
|
private int _grabSolidMask;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
@@ -66,6 +69,14 @@ private void FixedUpdate()
|
|||||||
{
|
{
|
||||||
if (_rb != null)
|
if (_rb != null)
|
||||||
{
|
{
|
||||||
|
if (_isGrabbed)
|
||||||
|
{
|
||||||
|
_rb.linearVelocity = Vector2.zero;
|
||||||
|
_rb.MovePosition(_grabTargetPosition);
|
||||||
|
_lastVelocity = Vector2.zero;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ApplySmoothHitPositionCorrection();
|
ApplySmoothHitPositionCorrection();
|
||||||
_lastVelocity = _rb.linearVelocity;
|
_lastVelocity = _rb.linearVelocity;
|
||||||
}
|
}
|
||||||
@@ -75,6 +86,8 @@ public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReac
|
|||||||
{
|
{
|
||||||
if (_currentHealth <= 0) return;
|
if (_currentHealth <= 0) return;
|
||||||
|
|
||||||
|
_isGrabbed = false;
|
||||||
|
|
||||||
_currentHealth -= amount;
|
_currentHealth -= amount;
|
||||||
Debug.Log($"{name} 피격: -{amount} (HP: {_currentHealth}/{_maxHealth})");
|
Debug.Log($"{name} 피격: -{amount} (HP: {_currentHealth}/{_maxHealth})");
|
||||||
|
|
||||||
@@ -109,6 +122,39 @@ public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReac
|
|||||||
Die();
|
Die();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void BeginGrab(string grabbedAnimationState, int solidMask)
|
||||||
|
{
|
||||||
|
if (_currentHealth <= 0) return;
|
||||||
|
|
||||||
|
_isGrabbed = true;
|
||||||
|
_grabSolidMask = solidMask;
|
||||||
|
_isHitPositionCorrecting = false;
|
||||||
|
_hitReactionTimer = 0f;
|
||||||
|
_lastVelocity = Vector2.zero;
|
||||||
|
|
||||||
|
if (_rb != null)
|
||||||
|
{
|
||||||
|
_rb.linearVelocity = Vector2.zero;
|
||||||
|
_grabTargetPosition = _rb.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_anim != null && !string.IsNullOrEmpty(grabbedAnimationState))
|
||||||
|
_anim.Play(grabbedAnimationState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateGrabPosition(Vector2 position)
|
||||||
|
{
|
||||||
|
if (!_isGrabbed || _rb == null) return;
|
||||||
|
|
||||||
|
_rb.linearVelocity = Vector2.zero;
|
||||||
|
_grabTargetPosition = GetSafeHitTargetPosition(position, _grabSolidMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndGrab()
|
||||||
|
{
|
||||||
|
_isGrabbed = false;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnCollisionEnter2D(Collision2D collision)
|
private void OnCollisionEnter2D(Collision2D collision)
|
||||||
{
|
{
|
||||||
UpdateGroundedState(collision);
|
UpdateGroundedState(collision);
|
||||||
|
|||||||
@@ -154,6 +154,15 @@ public @GameInput()
|
|||||||
""processors"": """",
|
""processors"": """",
|
||||||
""interactions"": """",
|
""interactions"": """",
|
||||||
""initialStateCheck"": false
|
""initialStateCheck"": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""name"": ""GrabSmash"",
|
||||||
|
""type"": ""Button"",
|
||||||
|
""id"": ""c42f3219-3aca-4678-8a86-b74565af3747"",
|
||||||
|
""expectedControlType"": """",
|
||||||
|
""processors"": """",
|
||||||
|
""interactions"": """",
|
||||||
|
""initialStateCheck"": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
""bindings"": [
|
""bindings"": [
|
||||||
@@ -277,6 +286,17 @@ public @GameInput()
|
|||||||
""action"": ""BackDash"",
|
""action"": ""BackDash"",
|
||||||
""isComposite"": false,
|
""isComposite"": false,
|
||||||
""isPartOfComposite"": false
|
""isPartOfComposite"": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""name"": """",
|
||||||
|
""id"": ""8635811d-29ac-4047-a3f7-d7a72f7e362e"",
|
||||||
|
""path"": ""<Keyboard>/g"",
|
||||||
|
""interactions"": """",
|
||||||
|
""processors"": """",
|
||||||
|
""groups"": """",
|
||||||
|
""action"": ""GrabSmash"",
|
||||||
|
""isComposite"": false,
|
||||||
|
""isPartOfComposite"": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -292,6 +312,7 @@ public @GameInput()
|
|||||||
m_Player_Dash = m_Player.FindAction("Dash", throwIfNotFound: true);
|
m_Player_Dash = m_Player.FindAction("Dash", throwIfNotFound: true);
|
||||||
m_Player_Roll = m_Player.FindAction("Roll", throwIfNotFound: true);
|
m_Player_Roll = m_Player.FindAction("Roll", throwIfNotFound: true);
|
||||||
m_Player_BackDash = m_Player.FindAction("BackDash", throwIfNotFound: true);
|
m_Player_BackDash = m_Player.FindAction("BackDash", throwIfNotFound: true);
|
||||||
|
m_Player_GrabSmash = m_Player.FindAction("GrabSmash", throwIfNotFound: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
~@GameInput()
|
~@GameInput()
|
||||||
@@ -379,6 +400,7 @@ public int FindBinding(InputBinding bindingMask, out InputAction action)
|
|||||||
private readonly InputAction m_Player_Dash;
|
private readonly InputAction m_Player_Dash;
|
||||||
private readonly InputAction m_Player_Roll;
|
private readonly InputAction m_Player_Roll;
|
||||||
private readonly InputAction m_Player_BackDash;
|
private readonly InputAction m_Player_BackDash;
|
||||||
|
private readonly InputAction m_Player_GrabSmash;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides access to input actions defined in input action map "Player".
|
/// Provides access to input actions defined in input action map "Player".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -419,6 +441,10 @@ public struct PlayerActions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public InputAction @BackDash => m_Wrapper.m_Player_BackDash;
|
public InputAction @BackDash => m_Wrapper.m_Player_BackDash;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Provides access to the underlying input action "Player/GrabSmash".
|
||||||
|
/// </summary>
|
||||||
|
public InputAction @GrabSmash => m_Wrapper.m_Player_GrabSmash;
|
||||||
|
/// <summary>
|
||||||
/// Provides access to the underlying input action map instance.
|
/// Provides access to the underlying input action map instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public InputActionMap Get() { return m_Wrapper.m_Player; }
|
public InputActionMap Get() { return m_Wrapper.m_Player; }
|
||||||
@@ -465,6 +491,9 @@ public void AddCallbacks(IPlayerActions instance)
|
|||||||
@BackDash.started += instance.OnBackDash;
|
@BackDash.started += instance.OnBackDash;
|
||||||
@BackDash.performed += instance.OnBackDash;
|
@BackDash.performed += instance.OnBackDash;
|
||||||
@BackDash.canceled += instance.OnBackDash;
|
@BackDash.canceled += instance.OnBackDash;
|
||||||
|
@GrabSmash.started += instance.OnGrabSmash;
|
||||||
|
@GrabSmash.performed += instance.OnGrabSmash;
|
||||||
|
@GrabSmash.canceled += instance.OnGrabSmash;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -497,6 +526,9 @@ private void UnregisterCallbacks(IPlayerActions instance)
|
|||||||
@BackDash.started -= instance.OnBackDash;
|
@BackDash.started -= instance.OnBackDash;
|
||||||
@BackDash.performed -= instance.OnBackDash;
|
@BackDash.performed -= instance.OnBackDash;
|
||||||
@BackDash.canceled -= instance.OnBackDash;
|
@BackDash.canceled -= instance.OnBackDash;
|
||||||
|
@GrabSmash.started -= instance.OnGrabSmash;
|
||||||
|
@GrabSmash.performed -= instance.OnGrabSmash;
|
||||||
|
@GrabSmash.canceled -= instance.OnGrabSmash;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -586,5 +618,12 @@ public interface IPlayerActions
|
|||||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
||||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
||||||
void OnBackDash(InputAction.CallbackContext context);
|
void OnBackDash(InputAction.CallbackContext context);
|
||||||
|
/// <summary>
|
||||||
|
/// Method invoked when associated input action "GrabSmash" is either <see cref="UnityEngine.InputSystem.InputAction.started" />, <see cref="UnityEngine.InputSystem.InputAction.performed" /> or <see cref="UnityEngine.InputSystem.InputAction.canceled" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.started" />
|
||||||
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
||||||
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
||||||
|
void OnGrabSmash(InputAction.CallbackContext context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public class InputManager : MonoBehaviour, GameInput.IPlayerActions
|
|||||||
public event Action OnDash_Event;
|
public event Action OnDash_Event;
|
||||||
public event Action OnRoll_Event;
|
public event Action OnRoll_Event;
|
||||||
public event Action OnBackDash_Event;
|
public event Action OnBackDash_Event;
|
||||||
|
public event Action OnGrabSmash_Event;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
@@ -77,4 +78,9 @@ public void OnBackDash(InputAction.CallbackContext ctx)
|
|||||||
OnBackDash_Event?.Invoke();
|
OnBackDash_Event?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnGrabSmash(InputAction.CallbackContext ctx)
|
||||||
|
{
|
||||||
|
if (ctx.phase == InputActionPhase.Started)
|
||||||
|
OnGrabSmash_Event?.Invoke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public class PlayerController : MonoBehaviour
|
|||||||
[SerializeField] private ComboNode _dashRootNode;
|
[SerializeField] private ComboNode _dashRootNode;
|
||||||
[SerializeField] private ComboNode _rollRootNode;
|
[SerializeField] private ComboNode _rollRootNode;
|
||||||
[SerializeField] private ComboNode _backDashRootNode;
|
[SerializeField] private ComboNode _backDashRootNode;
|
||||||
|
[SerializeField] private ComboNode _grabSmashRootNode;
|
||||||
private readonly Dictionary<ActionData, float> _motionCooldownTimers = new();
|
private readonly Dictionary<ActionData, float> _motionCooldownTimers = new();
|
||||||
private readonly List<ActionData> _motionCooldownKeys = new();
|
private readonly List<ActionData> _motionCooldownKeys = new();
|
||||||
|
|
||||||
@@ -59,11 +60,14 @@ public class PlayerController : MonoBehaviour
|
|||||||
private CancellationTokenSource _attackCts;
|
private CancellationTokenSource _attackCts;
|
||||||
private CancellationTokenSource _motionCts;
|
private CancellationTokenSource _motionCts;
|
||||||
private CancellationTokenSource _animationSpeedCts;
|
private CancellationTokenSource _animationSpeedCts;
|
||||||
|
private CancellationTokenSource _actionVelocityCts;
|
||||||
|
private bool _isMotionActive;
|
||||||
private ActionData _lastAttackGizmoData;
|
private ActionData _lastAttackGizmoData;
|
||||||
[SerializeField] private float _hitGizmoFadeDuration = 0.5f;
|
[SerializeField] private float _hitGizmoFadeDuration = 0.5f;
|
||||||
private ActionData _lastHitData;
|
private ActionData _lastHitData;
|
||||||
private Vector2 _lastHitCenter;
|
private Vector2 _lastHitCenter;
|
||||||
private float _lastHitTime = -1f;
|
private float _lastHitTime = -1f;
|
||||||
|
private Enemy _lastHitEnemy;
|
||||||
|
|
||||||
[Header("Debug")]
|
[Header("Debug")]
|
||||||
[SerializeField] private bool _showAttackDebug = true;
|
[SerializeField] private bool _showAttackDebug = true;
|
||||||
@@ -83,6 +87,7 @@ private void Awake()
|
|||||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||||
_bodyColliders = GetComponentsInChildren<Collider2D>();
|
_bodyColliders = GetComponentsInChildren<Collider2D>();
|
||||||
EnsureAttackHitbox();
|
EnsureAttackHitbox();
|
||||||
|
_attackHitbox.OnHit += OnAttackHit;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
@@ -94,6 +99,7 @@ private void Start()
|
|||||||
InputManager.Instance.OnDash_Event += OnDashInput;
|
InputManager.Instance.OnDash_Event += OnDashInput;
|
||||||
InputManager.Instance.OnRoll_Event += OnRollInput;
|
InputManager.Instance.OnRoll_Event += OnRollInput;
|
||||||
InputManager.Instance.OnBackDash_Event += OnBackDashInput;
|
InputManager.Instance.OnBackDash_Event += OnBackDashInput;
|
||||||
|
InputManager.Instance.OnGrabSmash_Event += OnGrabSmashInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
@@ -107,14 +113,16 @@ private void OnDestroy()
|
|||||||
InputManager.Instance.OnDash_Event -= OnDashInput;
|
InputManager.Instance.OnDash_Event -= OnDashInput;
|
||||||
InputManager.Instance.OnRoll_Event -= OnRollInput;
|
InputManager.Instance.OnRoll_Event -= OnRollInput;
|
||||||
InputManager.Instance.OnBackDash_Event -= OnBackDashInput;
|
InputManager.Instance.OnBackDash_Event -= OnBackDashInput;
|
||||||
|
InputManager.Instance.OnGrabSmash_Event -= OnGrabSmashInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
_attackCts?.Cancel();
|
CancelAndDispose(ref _attackCts);
|
||||||
_attackCts?.Dispose();
|
CancelAndDispose(ref _motionCts);
|
||||||
_motionCts?.Cancel();
|
CancelAndDispose(ref _animationSpeedCts);
|
||||||
_motionCts?.Dispose();
|
CancelAndDispose(ref _actionVelocityCts);
|
||||||
_animationSpeedCts?.Cancel();
|
|
||||||
_animationSpeedCts?.Dispose();
|
if (_attackHitbox != null)
|
||||||
|
_attackHitbox.OnHit -= OnAttackHit;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FixedUpdate()
|
private void FixedUpdate()
|
||||||
@@ -198,9 +206,13 @@ private void OnJumpInput()
|
|||||||
private void OnDashInput() => ExecuteMotionNode(_dashRootNode);
|
private void OnDashInput() => ExecuteMotionNode(_dashRootNode);
|
||||||
private void OnRollInput() => ExecuteMotionNode(_rollRootNode);
|
private void OnRollInput() => ExecuteMotionNode(_rollRootNode);
|
||||||
private void OnBackDashInput() => ExecuteMotionNode(_backDashRootNode);
|
private void OnBackDashInput() => ExecuteMotionNode(_backDashRootNode);
|
||||||
|
private void OnGrabSmashInput() => ExecuteAttackNode(_grabSmashRootNode);
|
||||||
|
|
||||||
private void HandleComboInput(ComboInputType input)
|
private void HandleComboInput(ComboInputType input)
|
||||||
{
|
{
|
||||||
|
if (_isMotionActive && !CanTransitionFromCurrentNode(input))
|
||||||
|
return;
|
||||||
|
|
||||||
if (_attackCooldownTimer > 0f)
|
if (_attackCooldownTimer > 0f)
|
||||||
{
|
{
|
||||||
float elapsed = Time.time - _attackStartTime;
|
float elapsed = Time.time - _attackStartTime;
|
||||||
@@ -257,6 +269,16 @@ private void ExecuteMotionNode(ComboNode root)
|
|||||||
_comboWindowTimer = root.ComboWindow;
|
_comboWindowTimer = root.ComboWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ExecuteAttackNode(ComboNode root)
|
||||||
|
{
|
||||||
|
if (root == null || root.Action == null) return;
|
||||||
|
if (_attackCooldownTimer > 0f) return;
|
||||||
|
|
||||||
|
PerformAttack(root.Action);
|
||||||
|
_currentNode = root;
|
||||||
|
_comboWindowTimer = root.ComboWindow;
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateMotionCooldowns()
|
private void UpdateMotionCooldowns()
|
||||||
{
|
{
|
||||||
if (_motionCooldownTimers.Count == 0) return;
|
if (_motionCooldownTimers.Count == 0) return;
|
||||||
@@ -288,8 +310,8 @@ private void SetMotionCooldown(ActionData data)
|
|||||||
|
|
||||||
private async void PerformAttack(ActionData data, bool preserveHorizontalVelocity = false)
|
private async void PerformAttack(ActionData data, bool preserveHorizontalVelocity = false)
|
||||||
{
|
{
|
||||||
_attackCts?.Cancel();
|
CancelMotion();
|
||||||
_attackCts?.Dispose();
|
CancelAndDispose(ref _attackCts);
|
||||||
_attackCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
_attackCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
CancellationToken token = _attackCts.Token;
|
CancellationToken token = _attackCts.Token;
|
||||||
|
|
||||||
@@ -301,15 +323,17 @@ private async void PerformAttack(ActionData data, bool preserveHorizontalVelocit
|
|||||||
ClearActionLocks();
|
ClearActionLocks();
|
||||||
PlayActionAnimation(data);
|
PlayActionAnimation(data);
|
||||||
|
|
||||||
if (data.HasMotion)
|
PlayActionVelocity(data);
|
||||||
ApplyActionVelocity(data);
|
|
||||||
|
|
||||||
LockMovementIfNeeded(data, preserveHorizontalVelocity, true);
|
LockMovementIfNeeded(data, preserveHorizontalVelocity, true);
|
||||||
LockFacingIfNeeded(data);
|
LockFacingIfNeeded(data);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await HitRoutine(data, token);
|
if (data.IsGrab)
|
||||||
|
await GrabRoutine(data, token);
|
||||||
|
else
|
||||||
|
await HitRoutine(data, token);
|
||||||
}
|
}
|
||||||
catch (System.OperationCanceledException)
|
catch (System.OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -323,15 +347,16 @@ private async void PerformMotion(ActionData data)
|
|||||||
|
|
||||||
// 대시/구르기 같은 모션은 공격을 끊고 새로운 콤보 노드가 된다.
|
// 대시/구르기 같은 모션은 공격을 끊고 새로운 콤보 노드가 된다.
|
||||||
CancelAttack();
|
CancelAttack();
|
||||||
_motionCts?.Cancel();
|
CancelAndDispose(ref _motionCts);
|
||||||
_motionCts?.Dispose();
|
|
||||||
_motionCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
_motionCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
CancellationToken token = _motionCts.Token;
|
CancellationToken token = _motionCts.Token;
|
||||||
|
|
||||||
SetMotionCooldown(data);
|
SetMotionCooldown(data);
|
||||||
|
_isMotionActive = true;
|
||||||
ClearActionLocks();
|
ClearActionLocks();
|
||||||
FaceMotionDirection(data);
|
FaceMotionDirection(data);
|
||||||
PlayActionAnimation(data);
|
PlayActionAnimation(data);
|
||||||
|
PlayActionVelocity(data);
|
||||||
LockMovementIfNeeded(data);
|
LockMovementIfNeeded(data);
|
||||||
LockFacingIfNeeded(data);
|
LockFacingIfNeeded(data);
|
||||||
|
|
||||||
@@ -341,11 +366,15 @@ private async void PerformMotion(ActionData data)
|
|||||||
await MotionRoutine(data, token);
|
await MotionRoutine(data, token);
|
||||||
completed = true;
|
completed = true;
|
||||||
}
|
}
|
||||||
catch (System.OperationCanceledException) { }
|
catch (System.OperationCanceledException)
|
||||||
|
{
|
||||||
|
_isMotionActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (completed)
|
if (completed)
|
||||||
{
|
{
|
||||||
StopActionVelocity(data);
|
StopActionVelocity(data);
|
||||||
|
_isMotionActive = false;
|
||||||
ClearActionLocks();
|
ClearActionLocks();
|
||||||
PlayIdleAnimation();
|
PlayIdleAnimation();
|
||||||
}
|
}
|
||||||
@@ -359,9 +388,6 @@ private async Awaitable MotionRoutine(ActionData data, CancellationToken token)
|
|||||||
while (ShouldKeepActionPlaying(data, elapsed, duration))
|
while (ShouldKeepActionPlaying(data, elapsed, duration))
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
float normalizedTime = GetActionNormalizedTime(data, elapsed, duration);
|
|
||||||
ApplyActionVelocity(data, normalizedTime);
|
|
||||||
|
|
||||||
await Awaitable.NextFrameAsync(token);
|
await Awaitable.NextFrameAsync(token);
|
||||||
elapsed += Time.deltaTime;
|
elapsed += Time.deltaTime;
|
||||||
}
|
}
|
||||||
@@ -369,13 +395,34 @@ private async Awaitable MotionRoutine(ActionData data, CancellationToken token)
|
|||||||
|
|
||||||
private void CancelAttack()
|
private void CancelAttack()
|
||||||
{
|
{
|
||||||
_attackCts?.Cancel();
|
CancelAndDispose(ref _attackCts);
|
||||||
|
CancelAndDispose(ref _actionVelocityCts);
|
||||||
_attackCooldownTimer = 0f;
|
_attackCooldownTimer = 0f;
|
||||||
_pendingInput = null;
|
_pendingInput = null;
|
||||||
_attackHitbox?.Deactivate();
|
_attackHitbox?.Deactivate();
|
||||||
ClearActionLocks();
|
ClearActionLocks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CancelMotion()
|
||||||
|
{
|
||||||
|
_isMotionActive = false;
|
||||||
|
CancelAndDispose(ref _motionCts);
|
||||||
|
CancelAndDispose(ref _actionVelocityCts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanTransitionFromCurrentNode(ComboInputType input)
|
||||||
|
{
|
||||||
|
if (_comboWindowTimer <= 0f || _currentNode == null) return false;
|
||||||
|
|
||||||
|
foreach (var transition in _currentNode.Transitions)
|
||||||
|
{
|
||||||
|
if (transition.Trigger == input && transition.Next != null && transition.Next.Action != null)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private async Awaitable HitRoutine(ActionData data, CancellationToken token)
|
private async Awaitable HitRoutine(ActionData data, CancellationToken token)
|
||||||
{
|
{
|
||||||
float attackStartTime = Time.time;
|
float attackStartTime = Time.time;
|
||||||
@@ -402,6 +449,101 @@ private async Awaitable HitRoutine(ActionData data, CancellationToken token)
|
|||||||
PlayIdleAnimation();
|
PlayIdleAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Awaitable GrabRoutine(ActionData data, CancellationToken token)
|
||||||
|
{
|
||||||
|
Enemy target = FindGrabTarget(data);
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
await WaitForActionEnd(data, token);
|
||||||
|
ClearActionLocks();
|
||||||
|
PlayIdleAnimation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.BeginGrab(data.GrabbedAnimationState, _groundLayer.value);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
float elapsed = 0f;
|
||||||
|
float duration = GetActionDuration(data);
|
||||||
|
while (ShouldKeepActionPlaying(data, elapsed, duration))
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
float normalizedTime = GetActionNormalizedTime(data, elapsed, duration);
|
||||||
|
target.UpdateGrabPosition(GetGrabWorldPosition(data, normalizedTime));
|
||||||
|
|
||||||
|
await Awaitable.NextFrameAsync(token);
|
||||||
|
elapsed += Time.deltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.EndGrab();
|
||||||
|
target.TakeDamage(data.Damage, GetHitVelocity(data.HitVelocity), data.HitReactionAnimationState);
|
||||||
|
}
|
||||||
|
catch (System.OperationCanceledException)
|
||||||
|
{
|
||||||
|
target.EndGrab();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearActionLocks();
|
||||||
|
PlayIdleAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Enemy FindGrabTarget(ActionData data)
|
||||||
|
{
|
||||||
|
Vector2 playerPosition = _rb != null ? _rb.position : (Vector2)transform.position;
|
||||||
|
float grabRangeSqr = data.GrabRange * data.GrabRange;
|
||||||
|
|
||||||
|
if (_lastHitEnemy != null && _lastHitEnemy.isActiveAndEnabled &&
|
||||||
|
GetEnemySqrDistance(_lastHitEnemy, playerPosition) <= grabRangeSqr)
|
||||||
|
{
|
||||||
|
return _lastHitEnemy;
|
||||||
|
}
|
||||||
|
|
||||||
|
Collider2D[] hits = Physics2D.OverlapCircleAll(transform.position, data.GrabSearchRadius, _enemyLayer);
|
||||||
|
Enemy closest = null;
|
||||||
|
float closestSqrDistance = float.PositiveInfinity;
|
||||||
|
|
||||||
|
foreach (var hit in hits)
|
||||||
|
{
|
||||||
|
Enemy enemy = hit.GetComponentInParent<Enemy>();
|
||||||
|
if (enemy == null) continue;
|
||||||
|
|
||||||
|
float sqrDistance = GetEnemySqrDistance(enemy, playerPosition);
|
||||||
|
if (sqrDistance > grabRangeSqr) continue;
|
||||||
|
if (sqrDistance >= closestSqrDistance) continue;
|
||||||
|
|
||||||
|
closest = enemy;
|
||||||
|
closestSqrDistance = sqrDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float GetEnemySqrDistance(Enemy enemy, Vector2 playerPosition)
|
||||||
|
{
|
||||||
|
if (enemy == null) return float.PositiveInfinity;
|
||||||
|
return ((Vector2)enemy.transform.position - playerPosition).sqrMagnitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 GetGrabWorldPosition(ActionData data, float normalizedTime)
|
||||||
|
{
|
||||||
|
float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
|
||||||
|
Vector2 playerPosition = _rb != null ? _rb.position : (Vector2)transform.position;
|
||||||
|
Vector2 offset = data.GrabOffset;
|
||||||
|
float offsetXMultiplier = data.GrabOffsetXCurve != null ? data.GrabOffsetXCurve.Evaluate(normalizedTime) : 1f;
|
||||||
|
float offsetYMultiplier = data.GrabOffsetYCurve != null ? data.GrabOffsetYCurve.Evaluate(normalizedTime) : 1f;
|
||||||
|
offset.x *= facing * offsetXMultiplier;
|
||||||
|
offset.y *= offsetYMultiplier;
|
||||||
|
return playerPosition + offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAttackHit(IDamageable target)
|
||||||
|
{
|
||||||
|
if (target is Enemy enemy)
|
||||||
|
_lastHitEnemy = enemy;
|
||||||
|
}
|
||||||
|
|
||||||
private void ActivateAttackHitbox(ActionData data)
|
private void ActivateAttackHitbox(ActionData data)
|
||||||
{
|
{
|
||||||
Vector2 localPosition = GetAttackLocalPosition(data.Offset);
|
Vector2 localPosition = GetAttackLocalPosition(data.Offset);
|
||||||
@@ -475,7 +617,7 @@ private void LockMovementIfNeeded(ActionData data, bool preserveHorizontalVeloci
|
|||||||
if (!forceLock && data.CanMoveDuringAction) return;
|
if (!forceLock && data.CanMoveDuringAction) return;
|
||||||
|
|
||||||
// 이동 없는 공격은 직전 프레임의 걷기 속도를 이어받지 않게 한다.
|
// 이동 없는 공격은 직전 프레임의 걷기 속도를 이어받지 않게 한다.
|
||||||
if (!preserveHorizontalVelocity && !data.HasMotion)
|
if (!preserveHorizontalVelocity && data.Velocity == Vector2.zero)
|
||||||
_rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y);
|
_rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y);
|
||||||
|
|
||||||
_inputLockTimer = GetActionLockDuration(data);
|
_inputLockTimer = GetActionLockDuration(data);
|
||||||
@@ -493,7 +635,8 @@ private void PlayIdleAnimation()
|
|||||||
{
|
{
|
||||||
if (_anim == null) return;
|
if (_anim == null) return;
|
||||||
|
|
||||||
_animationSpeedCts?.Cancel();
|
CancelAndDispose(ref _animationSpeedCts);
|
||||||
|
CancelAndDispose(ref _actionVelocityCts);
|
||||||
_anim.speed = 1f;
|
_anim.speed = 1f;
|
||||||
if (!string.IsNullOrEmpty(_idleAnimationState))
|
if (!string.IsNullOrEmpty(_idleAnimationState))
|
||||||
_anim.Play(_idleAnimationState);
|
_anim.Play(_idleAnimationState);
|
||||||
@@ -503,8 +646,7 @@ private void PlayActionAnimation(ActionData data)
|
|||||||
{
|
{
|
||||||
if (_anim == null) return;
|
if (_anim == null) return;
|
||||||
|
|
||||||
_animationSpeedCts?.Cancel();
|
CancelAndDispose(ref _animationSpeedCts);
|
||||||
_animationSpeedCts?.Dispose();
|
|
||||||
_animationSpeedCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
_animationSpeedCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
|
|
||||||
_anim.speed = GetAnimationSpeed(data, 0f);
|
_anim.speed = GetAnimationSpeed(data, 0f);
|
||||||
@@ -517,6 +659,36 @@ private void PlayActionAnimation(ActionData data)
|
|||||||
ApplyAnimationSpeedCurve(data, _animationSpeedCts.Token);
|
ApplyAnimationSpeedCurve(data, _animationSpeedCts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PlayActionVelocity(ActionData data)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref _actionVelocityCts);
|
||||||
|
|
||||||
|
if (data.Velocity == Vector2.zero) return;
|
||||||
|
|
||||||
|
_actionVelocityCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
|
ApplyActionVelocityCurve(data, _actionVelocityCts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ApplyActionVelocityCurve(ActionData data, CancellationToken token)
|
||||||
|
{
|
||||||
|
float elapsed = 0f;
|
||||||
|
float duration = GetActionDuration(data);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (ShouldKeepActionPlaying(data, elapsed, duration))
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
float normalizedTime = GetActionNormalizedTime(data, elapsed, duration);
|
||||||
|
ApplyActionVelocity(data, normalizedTime);
|
||||||
|
|
||||||
|
await Awaitable.NextFrameAsync(token);
|
||||||
|
elapsed += Time.deltaTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.OperationCanceledException) { }
|
||||||
|
}
|
||||||
|
|
||||||
private async void ApplyAnimationSpeedCurve(ActionData data, CancellationToken token)
|
private async void ApplyAnimationSpeedCurve(ActionData data, CancellationToken token)
|
||||||
{
|
{
|
||||||
float duration = GetActionDuration(data);
|
float duration = GetActionDuration(data);
|
||||||
@@ -702,6 +874,20 @@ private void ClearActionLocks()
|
|||||||
_facingLockAction = null;
|
_facingLockAction = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CancelAndDispose(ref CancellationTokenSource cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (System.ObjectDisposedException) { }
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
|
|
||||||
private float GetActionDuration(ActionData data)
|
private float GetActionDuration(ActionData data)
|
||||||
{
|
{
|
||||||
return Mathf.Max(data.MotionDuration, data.HitTiming + data.HitDuration, 0.01f);
|
return Mathf.Max(data.MotionDuration, data.HitTiming + data.HitDuration, 0.01f);
|
||||||
@@ -722,7 +908,7 @@ private bool ShouldKeepActionPlaying(ActionData data, float elapsed, float durat
|
|||||||
|
|
||||||
private float GetActionNormalizedTime(ActionData data, float elapsed, float duration)
|
private float GetActionNormalizedTime(ActionData data, float elapsed, float duration)
|
||||||
{
|
{
|
||||||
if (!HasActionAnimation(data))
|
if (!ShouldUseAnimationLength(data))
|
||||||
return Mathf.Clamp01(elapsed / duration);
|
return Mathf.Clamp01(elapsed / duration);
|
||||||
|
|
||||||
AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0);
|
AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0);
|
||||||
|
|||||||
@@ -702,6 +702,32 @@ AnimatorState:
|
|||||||
m_MirrorParameter:
|
m_MirrorParameter:
|
||||||
m_CycleOffsetParameter:
|
m_CycleOffsetParameter:
|
||||||
m_TimeParameter:
|
m_TimeParameter:
|
||||||
|
--- !u!1102 &-4203153932972611291
|
||||||
|
AnimatorState:
|
||||||
|
serializedVersion: 6
|
||||||
|
m_ObjectHideFlags: 1
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: GrabSmash
|
||||||
|
m_Speed: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_Transitions: []
|
||||||
|
m_StateMachineBehaviours: []
|
||||||
|
m_Position: {x: 50, y: 50, z: 0}
|
||||||
|
m_IKOnFeet: 0
|
||||||
|
m_WriteDefaultValues: 1
|
||||||
|
m_Mirror: 0
|
||||||
|
m_SpeedParameterActive: 0
|
||||||
|
m_MirrorParameterActive: 0
|
||||||
|
m_CycleOffsetParameterActive: 0
|
||||||
|
m_TimeParameterActive: 0
|
||||||
|
m_Motion: {fileID: 7400000, guid: 7c16f79e8404ac7478767cd96faaeb72, type: 2}
|
||||||
|
m_Tag:
|
||||||
|
m_SpeedParameter:
|
||||||
|
m_MirrorParameter:
|
||||||
|
m_CycleOffsetParameter:
|
||||||
|
m_TimeParameter:
|
||||||
--- !u!1102 &-3985368095300963852
|
--- !u!1102 &-3985368095300963852
|
||||||
AnimatorState:
|
AnimatorState:
|
||||||
serializedVersion: 6
|
serializedVersion: 6
|
||||||
@@ -3121,6 +3147,9 @@ AnimatorStateMachine:
|
|||||||
- serializedVersion: 1
|
- serializedVersion: 1
|
||||||
m_State: {fileID: 6644522863449380584}
|
m_State: {fileID: 6644522863449380584}
|
||||||
m_Position: {x: 1140, y: -100, z: 0}
|
m_Position: {x: 1140, y: -100, z: 0}
|
||||||
|
- serializedVersion: 1
|
||||||
|
m_State: {fileID: -4203153932972611291}
|
||||||
|
m_Position: {x: 5280, y: 90, z: 0}
|
||||||
m_ChildStateMachines: []
|
m_ChildStateMachines: []
|
||||||
m_AnyStateTransitions: []
|
m_AnyStateTransitions: []
|
||||||
m_EntryTransitions: []
|
m_EntryTransitions: []
|
||||||
|
|||||||
BIN
Assets/03_Character/WhiteMan/Animations/GrabSmash.anim
LFS
Normal file
BIN
Assets/03_Character/WhiteMan/Animations/GrabSmash.anim
LFS
Normal file
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7c16f79e8404ac7478767cd96faaeb72
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 7400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/05_Data/Attack/GrabGroundSmash.asset
LFS
Normal file
BIN
Assets/05_Data/Attack/GrabGroundSmash.asset
LFS
Normal file
Binary file not shown.
8
Assets/05_Data/Attack/GrabGroundSmash.asset.meta
Normal file
8
Assets/05_Data/Attack/GrabGroundSmash.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c35c8bd4e0ad2cf41ab1af35fc81bbc0
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/05_Data/Attack/GrabSmash.asset
LFS
Normal file
BIN
Assets/05_Data/Attack/GrabSmash.asset
LFS
Normal file
Binary file not shown.
8
Assets/05_Data/Attack/GrabSmash.asset.meta
Normal file
8
Assets/05_Data/Attack/GrabSmash.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e3de0ce9283788a4c9a4a9486a1122e7
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/05_Data/Combo/Combo_GrabSmash.asset
LFS
Normal file
BIN
Assets/05_Data/Combo/Combo_GrabSmash.asset
LFS
Normal file
Binary file not shown.
8
Assets/05_Data/Combo/Combo_GrabSmash.asset.meta
Normal file
8
Assets/05_Data/Combo/Combo_GrabSmash.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c9a3bb08b50045a4395d19af97aab515
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -68,6 +68,15 @@
|
|||||||
"processors": "",
|
"processors": "",
|
||||||
"interactions": "",
|
"interactions": "",
|
||||||
"initialStateCheck": false
|
"initialStateCheck": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GrabSmash",
|
||||||
|
"type": "Button",
|
||||||
|
"id": "c42f3219-3aca-4678-8a86-b74565af3747",
|
||||||
|
"expectedControlType": "",
|
||||||
|
"processors": "",
|
||||||
|
"interactions": "",
|
||||||
|
"initialStateCheck": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"bindings": [
|
"bindings": [
|
||||||
@@ -191,6 +200,17 @@
|
|||||||
"action": "BackDash",
|
"action": "BackDash",
|
||||||
"isComposite": false,
|
"isComposite": false,
|
||||||
"isPartOfComposite": false
|
"isPartOfComposite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"id": "8635811d-29ac-4047-a3f7-d7a72f7e362e",
|
||||||
|
"path": "<Keyboard>/g",
|
||||||
|
"interactions": "",
|
||||||
|
"processors": "",
|
||||||
|
"groups": "",
|
||||||
|
"action": "GrabSmash",
|
||||||
|
"isComposite": false,
|
||||||
|
"isPartOfComposite": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user