diff --git a/Assets/01_Scenes/GameScene.unity b/Assets/01_Scenes/GameScene.unity index 2618deb..5842a6b 100644 --- a/Assets/01_Scenes/GameScene.unity +++ b/Assets/01_Scenes/GameScene.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d99b9098b9796ccaead6c35e1691fe7ea32bb5ba0c3ec3a73849b6d04bea673 -size 52185 +oid sha256:df589de703853ce0d54bf577261b352b30ff6ab7c5e114ef76b34e1b4f3e036e +size 52274 diff --git a/Assets/02_Scripts/Combat/ActionData.cs b/Assets/02_Scripts/Combat/ActionData.cs index 2be1401..cdead20 100644 --- a/Assets/02_Scripts/Combat/ActionData.cs +++ b/Assets/02_Scripts/Combat/ActionData.cs @@ -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; } diff --git a/Assets/02_Scripts/Combat/AttackHitbox.cs b/Assets/02_Scripts/Combat/AttackHitbox.cs index 52fc283..fe58e50 100644 --- a/Assets/02_Scripts/Combat/AttackHitbox.cs +++ b/Assets/02_Scripts/Combat/AttackHitbox.cs @@ -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 _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; } } diff --git a/Assets/02_Scripts/Combat/IDamageable.cs b/Assets/02_Scripts/Combat/IDamageable.cs index ab7440e..bb39f7e 100644 --- a/Assets/02_Scripts/Combat/IDamageable.cs +++ b/Assets/02_Scripts/Combat/IDamageable.cs @@ -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); } diff --git a/Assets/02_Scripts/Enemy/Enemy.cs b/Assets/02_Scripts/Enemy/Enemy.cs index 0efa349..680da23 100644 --- a/Assets/02_Scripts/Enemy/Enemy.cs +++ b/Assets/02_Scripts/Enemy/Enemy.cs @@ -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 _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(); _anim = GetComponentInChildren(); _spriteRenderer = GetComponentInChildren(); + _bodyColliders = GetComponentsInChildren(); 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(); + + 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++) diff --git a/Assets/02_Scripts/Input/GameInput.cs b/Assets/02_Scripts/Input/GameInput.cs index 8221579..43f38c8 100644 --- a/Assets/02_Scripts/Input/GameInput.cs +++ b/Assets/02_Scripts/Input/GameInput.cs @@ -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"": ""/space"", + ""path"": ""/leftCtrl"", ""interactions"": """", ""processors"": """", ""groups"": """", ""action"": ""Roll"", ""isComposite"": false, ""isPartOfComposite"": false + }, + { + ""name"": """", + ""id"": ""4c51b02e-aa8f-4242-81e4-ab1c72a512df"", + ""path"": ""/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; /// /// Provides access to input actions defined in input action map "Player". /// @@ -393,6 +415,10 @@ public struct PlayerActions /// public InputAction @Roll => m_Wrapper.m_Player_Roll; /// + /// Provides access to the underlying input action "Player/BackDash". + /// + public InputAction @BackDash => m_Wrapper.m_Player_BackDash; + /// /// Provides access to the underlying input action map instance. /// 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; } /// @@ -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; } /// @@ -547,5 +579,12 @@ public interface IPlayerActions /// /// void OnRoll(InputAction.CallbackContext context); + /// + /// Method invoked when associated input action "BackDash" is either , or . + /// + /// + /// + /// + void OnBackDash(InputAction.CallbackContext context); } } diff --git a/Assets/02_Scripts/Managers/InputManager.cs b/Assets/02_Scripts/Managers/InputManager.cs index d1fe0dd..9722f66 100644 --- a/Assets/02_Scripts/Managers/InputManager.cs +++ b/Assets/02_Scripts/Managers/InputManager.cs @@ -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(); + } + } diff --git a/Assets/02_Scripts/Player/PlayerController.cs b/Assets/02_Scripts/Player/PlayerController.cs index 97214ae..34e9c08 100644 --- a/Assets/02_Scripts/Player/PlayerController.cs +++ b/Assets/02_Scripts/Player/PlayerController.cs @@ -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 _motionCooldownTimers = new(); private readonly List _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; diff --git a/Assets/03_Character/WhiteMan/Animations/Animators/PlayerAnimator.controller b/Assets/03_Character/WhiteMan/Animations/Animators/PlayerAnimator.controller index f69c704..106d456 100644 --- a/Assets/03_Character/WhiteMan/Animations/Animators/PlayerAnimator.controller +++ b/Assets/03_Character/WhiteMan/Animations/Animators/PlayerAnimator.controller @@ -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: [] diff --git a/Assets/03_Character/WhiteMan/Animations/BackDash.anim b/Assets/03_Character/WhiteMan/Animations/BackDash.anim new file mode 100644 index 0000000..1b68823 --- /dev/null +++ b/Assets/03_Character/WhiteMan/Animations/BackDash.anim @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b032bec477455535ca3efe3d725150c12e6bbb694004460828591443369b66fd +size 1792 diff --git a/Assets/03_Character/WhiteMan/Animations/BackDash.anim.meta b/Assets/03_Character/WhiteMan/Animations/BackDash.anim.meta new file mode 100644 index 0000000..6216c39 --- /dev/null +++ b/Assets/03_Character/WhiteMan/Animations/BackDash.anim.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: aa9ae735bd685934e953c3ca7b2bed96 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 7400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/03_Character/WhiteMan/Animations/Dash.anim b/Assets/03_Character/WhiteMan/Animations/Dash.anim index 2ffbb33..9f77f33 100644 --- a/Assets/03_Character/WhiteMan/Animations/Dash.anim +++ b/Assets/03_Character/WhiteMan/Animations/Dash.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bdeaf5f17c0f3397f9ac2cd242b174a5dfc2bad72682bba907d8e9161f4d8b6 +oid sha256:9062abf317665d25ba58f73fad76fc28775e22b7d89745cb1492e0b869c81b78 size 3191 diff --git a/Assets/03_Character/WhiteMan/Animations/KickA.anim b/Assets/03_Character/WhiteMan/Animations/KickA.anim index 11f0f8c..6fe2228 100644 --- a/Assets/03_Character/WhiteMan/Animations/KickA.anim +++ b/Assets/03_Character/WhiteMan/Animations/KickA.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de65d418e9005f1a1e222a9cbbee59f503caf97f91da7e794f60046d2bd68a72 +oid sha256:d81213c83f82037626845d2f566aaa00ffcab5d6321889adee2fae0e3f859b7d size 3169 diff --git a/Assets/03_Character/WhiteMan/Animations/KickB.anim b/Assets/03_Character/WhiteMan/Animations/KickB.anim index 81f8987..fa3482c 100644 --- a/Assets/03_Character/WhiteMan/Animations/KickB.anim +++ b/Assets/03_Character/WhiteMan/Animations/KickB.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae404550770a730e8543cbe7caba9add6c3325f74848d318768f187029a7df96 +oid sha256:0b4297abfdc5f9448122d85eae0b25776be106de6516c685cd5b8f609602bf09 size 2995 diff --git a/Assets/03_Character/WhiteMan/Animations/KickC.anim b/Assets/03_Character/WhiteMan/Animations/KickC.anim index 2c31e8a..077c4fa 100644 --- a/Assets/03_Character/WhiteMan/Animations/KickC.anim +++ b/Assets/03_Character/WhiteMan/Animations/KickC.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f84de4f85f06704a99786354ca42a7fa7947ef87f05b3621bfca072a875a1a7 +oid sha256:8048c78d0e07f2167a8ed03fd697c3a1b2a9815f222a4614cdfca2f201cf8168 size 3169 diff --git a/Assets/03_Character/WhiteMan/Animations/PunchA.anim b/Assets/03_Character/WhiteMan/Animations/PunchA.anim index dee9e2f..201c823 100644 --- a/Assets/03_Character/WhiteMan/Animations/PunchA.anim +++ b/Assets/03_Character/WhiteMan/Animations/PunchA.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6480972ef60b9b42d7e0ef7a2258f13a18fe4c2d239f6b6e980c63e46ce5b176 +oid sha256:3dc435e00b03d36be0b84140a31d7d7d5dba53faae26d9f3bec77bd2a806bd59 size 2651 diff --git a/Assets/03_Character/WhiteMan/Animations/PunchB.anim b/Assets/03_Character/WhiteMan/Animations/PunchB.anim index 2ade67a..8ef66e9 100644 --- a/Assets/03_Character/WhiteMan/Animations/PunchB.anim +++ b/Assets/03_Character/WhiteMan/Animations/PunchB.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8491890ac12000e24d46e1e61e9c58eaf9005e0e04f746172820e7dc7e60a58 +oid sha256:860d0d32a76397a8a9df87175d05aeac3a71fe14ae12eace0d1352bf468f4855 size 2304 diff --git a/Assets/03_Character/WhiteMan/Animations/PunchC.anim b/Assets/03_Character/WhiteMan/Animations/PunchC.anim index e1f8230..be01cc4 100644 --- a/Assets/03_Character/WhiteMan/Animations/PunchC.anim +++ b/Assets/03_Character/WhiteMan/Animations/PunchC.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:849c17fa52696f4421965869bda720c1e92d217d5a3da0238734366d5a52d03c +oid sha256:17ce0403da64205994dab8c45a0c637be5931307bc2081d628f1e84651db79c0 size 2825 diff --git a/Assets/03_Character/WhiteMan/Animations/Roll.anim b/Assets/03_Character/WhiteMan/Animations/Roll.anim index 2974535..7232f09 100644 --- a/Assets/03_Character/WhiteMan/Animations/Roll.anim +++ b/Assets/03_Character/WhiteMan/Animations/Roll.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73503adfb48a704642e9bd986637b1d10eed16f7581b6c7399cf299412ecd4c5 +oid sha256:bfaaa28cc6467ff05d275b46ba08cc429bdaee8f930fd090d39703901bd3b386 size 3368 diff --git a/Assets/05_Data/Attack/Kick_A.asset b/Assets/05_Data/Attack/Kick_A.asset index 55b6cf3..5ee313f 100644 --- a/Assets/05_Data/Attack/Kick_A.asset +++ b/Assets/05_Data/Attack/Kick_A.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e72e439b75af2ddbb67e3fb9297346bff5087d49fe2fabc63615de817fdce1ff -size 1296 +oid sha256:57190b0eb0b3bd08c67e1fd24a8e08d0d8d89ce08ed759528c61dbd0f369a2fa +size 1912 diff --git a/Assets/05_Data/Attack/Kick_B.asset b/Assets/05_Data/Attack/Kick_B.asset index 1a19813..5298b07 100644 --- a/Assets/05_Data/Attack/Kick_B.asset +++ b/Assets/05_Data/Attack/Kick_B.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3e53d72c36abb9f822b0b9f832bc719c750452181f0f78f31738d7a3dd2d6ca -size 1296 +oid sha256:e8c4077922a387ef9bba13029bf43d567b11d322290e966b122505ab9990ab08 +size 1912 diff --git a/Assets/05_Data/Attack/Kick_C.asset b/Assets/05_Data/Attack/Kick_C.asset index 0bec106..9b15ebc 100644 --- a/Assets/05_Data/Attack/Kick_C.asset +++ b/Assets/05_Data/Attack/Kick_C.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7285d53b1d2d6af02b39da4a2bff813b9bf2d14c5ed34915b3add5becb208c60 -size 1353 +oid sha256:a43d1454661777a56b272ad79c8fdfc74d5daa28ccd99195e12c131c2dc81911 +size 1911 diff --git a/Assets/05_Data/Attack/Punch_A.asset b/Assets/05_Data/Attack/Punch_A.asset index faedccb..e3e50c1 100644 --- a/Assets/05_Data/Attack/Punch_A.asset +++ b/Assets/05_Data/Attack/Punch_A.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:077405ba2ba24d22620cf099247a56fb65079cb02e8e4f7e77e814e313349c12 -size 1298 +oid sha256:afd2d37b4f1a94e256a53d7172f7f41309dce9f4d65d23a9ddae876983b60057 +size 1914 diff --git a/Assets/05_Data/Attack/Punch_B.asset b/Assets/05_Data/Attack/Punch_B.asset index 9a0bf75..441ef18 100644 --- a/Assets/05_Data/Attack/Punch_B.asset +++ b/Assets/05_Data/Attack/Punch_B.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50f23924228d74e237250fcd69b79d2459cec18b64d93a0746b07d274391f22f -size 1298 +oid sha256:50c618c20a7ec0a90de0de3c82dda08c6656099a9a1249274ce912747fe41fb4 +size 1914 diff --git a/Assets/05_Data/Attack/Punch_C.asset b/Assets/05_Data/Attack/Punch_C.asset index 5c89496..6b52f22 100644 --- a/Assets/05_Data/Attack/Punch_C.asset +++ b/Assets/05_Data/Attack/Punch_C.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83fc9e5a5da796b6f15018ee4639d761c74daf18d2e4780f14c875ea7dc585b5 -size 1358 +oid sha256:5185f046937873819fef034d6397ab0ec4d61c3e63d4ba7aeeead2675814badc +size 1916 diff --git a/Assets/05_Data/Combo/Combo_BackDash.asset b/Assets/05_Data/Combo/Combo_BackDash.asset new file mode 100644 index 0000000..9e2b72d --- /dev/null +++ b/Assets/05_Data/Combo/Combo_BackDash.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6825384fa9d376fd54d98398df25ac972e64da00910591617d379e566c855463 +size 572 diff --git a/Assets/05_Data/Combo/Combo_BackDash.asset.meta b/Assets/05_Data/Combo/Combo_BackDash.asset.meta new file mode 100644 index 0000000..1d6fe3b --- /dev/null +++ b/Assets/05_Data/Combo/Combo_BackDash.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: deeb30709bba78346aa208023b6e394e +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/05_Data/Motion/BackDash.asset b/Assets/05_Data/Motion/BackDash.asset new file mode 100644 index 0000000..ec4cf46 --- /dev/null +++ b/Assets/05_Data/Motion/BackDash.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66bcf2f16feac662b8eb5473d74634803ef09a8967eb659544f0c67f5623929a +size 3118 diff --git a/Assets/05_Data/Motion/BackDash.asset.meta b/Assets/05_Data/Motion/BackDash.asset.meta new file mode 100644 index 0000000..3f031fe --- /dev/null +++ b/Assets/05_Data/Motion/BackDash.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4565c3aff6145d340ae707850c3d844d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/11_Input/GameInput.inputactions b/Assets/11_Input/GameInput.inputactions index b3e12c4..52e3dd0 100644 --- a/Assets/11_Input/GameInput.inputactions +++ b/Assets/11_Input/GameInput.inputactions @@ -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": "/space", + "path": "/leftCtrl", "interactions": "", "processors": "", "groups": "", "action": "Roll", "isComposite": false, "isPartOfComposite": false + }, + { + "name": "", + "id": "4c51b02e-aa8f-4242-81e4-ab1c72a512df", + "path": "/space", + "interactions": "", + "processors": "", + "groups": "", + "action": "BackDash", + "isComposite": false, + "isPartOfComposite": false } ] }