2026-05-18 공격에 의한 이동량 초기화

This commit is contained in:
2026-05-18 11:54:10 +09:00
parent ea3a4fbbcc
commit adcd69c537
3 changed files with 24 additions and 21 deletions

View File

@@ -14,7 +14,7 @@ public class AttackHitbox : MonoBehaviour
private void Awake() private void Awake()
{ {
_collider = GetComponent<CircleCollider2D>(); _collider = GetComponent<CircleCollider2D>();
// The player body does not collide with enemies; this trigger is the only attack contact. // 플레이어 몸체는 적과 물리 충돌하지 않으므로, 공격 판정은 이 트리거만 사용한다.
_collider.isTrigger = true; _collider.isTrigger = true;
_collider.enabled = false; _collider.enabled = false;
} }
@@ -30,7 +30,7 @@ public void Activate(ActionData data, Vector2 localPosition, Vector2 hitVelocity
_alreadyHit.Clear(); _alreadyHit.Clear();
_collider.enabled = true; _collider.enabled = true;
// Catch enemies already inside the hitbox on the same frame it opens. // 판정이 켜진 순간 이미 범위 안에 있던 적도 같은 프레임에 잡아낸다.
ScanImmediateOverlap(); ScanImmediateOverlap();
} }
@@ -54,7 +54,7 @@ private void TryDamage(Collider2D other)
{ {
if ((_targetLayer.value & (1 << other.gameObject.layer)) == 0) return; if ((_targetLayer.value & (1 << other.gameObject.layer)) == 0) return;
// Hurtboxes may live on child objects, while damage handling usually lives on the root. // 피격 콜라이더는 자식에 있고, 데미지 처리는 루트에 있을 수 있다.
if (!other.TryGetComponent<IDamageable>(out var target)) if (!other.TryGetComponent<IDamageable>(out var target))
target = other.GetComponentInParent<IDamageable>(); target = other.GetComponentInParent<IDamageable>();
if (target == null) return; if (target == null) return;

View File

@@ -73,13 +73,18 @@ public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReac
if (_anim != null && !string.IsNullOrEmpty(hitReactionAnimationState)) if (_anim != null && !string.IsNullOrEmpty(hitReactionAnimationState))
_anim.Play(hitReactionAnimationState); _anim.Play(hitReactionAnimationState);
// HitVelocity is an immediate launch/knockback velocity, not an additive force. // 새 피격이 반응 속도를 전부 결정하므로, 이전 튕김/넉백 속도는 먼저 제거한다.
if (_rb != null) if (_rb != null)
{ {
_hitReactionTimer = 0f;
_rb.linearVelocity = Vector2.zero;
_lastVelocity = Vector2.zero;
Vector2 nextVelocity = GetHitReactionVelocity(hitVelocity); Vector2 nextVelocity = GetHitReactionVelocity(hitVelocity);
if (nextVelocity != Vector2.zero) if (nextVelocity != Vector2.zero)
{ {
_rb.linearVelocity = nextVelocity; _rb.linearVelocity = nextVelocity;
_lastVelocity = nextVelocity;
_hitReactionTimer = _hitReactionDuration; _hitReactionTimer = _hitReactionDuration;
} }
} }
@@ -117,13 +122,11 @@ private void OnCollisionExit2D(Collision2D collision)
private Vector2 GetHitReactionVelocity(Vector2 hitVelocity) private Vector2 GetHitReactionVelocity(Vector2 hitVelocity)
{ {
// Airborne follow-up hits pop the enemy with a fixed Y velocity for stable combos. // 공중 추가타는 고정된 세로 속도를 쓰되, 이전 물리 속도는 절대 이어받지 않는다.
if (_hitReactionTimer <= 0f || _isGrounded) Vector2 nextVelocity = hitVelocity;
return hitVelocity; if (!_isGrounded)
nextVelocity.y = _airborneHitYVelocity;
Vector2 currentVelocity = _rb.linearVelocity;
Vector2 nextVelocity = hitVelocity == Vector2.zero ? currentVelocity : hitVelocity;
nextVelocity.y = _airborneHitYVelocity;
return nextVelocity; return nextVelocity;
} }
@@ -141,7 +144,7 @@ private void UpdateGroundedState(Collision2D collision)
private void BounceOffWall(Vector2 wallNormal) private void BounceOffWall(Vector2 wallNormal)
{ {
// While in hit reaction, side-wall impacts reflect the current knockback. // 피격 반응 중 옆벽에 부딪히면 현재 넉백 속도를 반사한다.
Vector2 incomingVelocity = _lastVelocity.sqrMagnitude > _rb.linearVelocity.sqrMagnitude Vector2 incomingVelocity = _lastVelocity.sqrMagnitude > _rb.linearVelocity.sqrMagnitude
? _lastVelocity ? _lastVelocity
: _rb.linearVelocity; : _rb.linearVelocity;

View File

@@ -114,7 +114,7 @@ private void OnDestroy()
private void FixedUpdate() private void FixedUpdate()
{ {
// Sample collision probes first; movement, wall slide, and jump decisions all use these flags. // 이동, 벽타기, 점프 판단이 모두 이 값을 쓰므로 충돌 체크를 먼저 갱신한다.
_isGrounded = Physics2D.OverlapCircle(_groundCheck.position, _groundCheckRadius, _groundLayer); _isGrounded = Physics2D.OverlapCircle(_groundCheck.position, _groundCheckRadius, _groundLayer);
_isTouchingLeftWall = Physics2D.OverlapCircle(_wallCheckLeft.position, _wallCheckRadius, _groundLayer); _isTouchingLeftWall = Physics2D.OverlapCircle(_wallCheckLeft.position, _wallCheckRadius, _groundLayer);
_isTouchingRightWall = Physics2D.OverlapCircle(_wallCheckRight.position, _wallCheckRadius, _groundLayer); _isTouchingRightWall = Physics2D.OverlapCircle(_wallCheckRight.position, _wallCheckRadius, _groundLayer);
@@ -142,7 +142,7 @@ private void FixedUpdate()
if (IsTouchingWall && !_isGrounded && _rb.linearVelocity.y < -_wallSlideSpeed) if (IsTouchingWall && !_isGrounded && _rb.linearVelocity.y < -_wallSlideSpeed)
_rb.linearVelocity = new Vector2(_rb.linearVelocity.x, -_wallSlideSpeed); _rb.linearVelocity = new Vector2(_rb.linearVelocity.x, -_wallSlideSpeed);
// Player and Enemy bodies do not physically collide; only ground/walls clamp player velocity. // 플레이어와 적 몸체는 물리 충돌하지 않고, 땅/벽만 캐스트로 이동을 막는다.
ClampVelocityToGround(); ClampVelocityToGround();
} }
@@ -215,7 +215,7 @@ private void HandleComboInput(ComboInputType input)
private void ExecuteComboInput(ComboInputType input) private void ExecuteComboInput(ComboInputType input)
{ {
// Continue from the current combo node while its window is open. // 콤보 입력 가능 시간이 열려 있으면 현재 노드에서 다음 연계로 이어간다.
if (_comboWindowTimer > 0f && _currentNode != null) if (_comboWindowTimer > 0f && _currentNode != null)
{ {
foreach (var transition in _currentNode.Transitions) foreach (var transition in _currentNode.Transitions)
@@ -231,7 +231,7 @@ private void ExecuteComboInput(ComboInputType input)
} }
} }
// No active combo route matched, so start from the input's root node. // 이어갈 연계가 없으면 입력에 맞는 루트 노드부터 새로 시작한다.
ComboNode root = input switch ComboNode root = input switch
{ {
ComboInputType.Punch => _punchRootNode, ComboInputType.Punch => _punchRootNode,
@@ -318,7 +318,7 @@ private async void PerformMotion(ActionData data)
{ {
if (data == null || IsMotionOnCooldown(data)) return; if (data == null || IsMotionOnCooldown(data)) return;
// Motions such as dash/roll interrupt attacks and become the new combo node. // 대시/구르기 같은 모션은 공격을 끊고 새로운 콤보 노드가 된다.
CancelAttack(); CancelAttack();
_motionCts?.Cancel(); _motionCts?.Cancel();
_motionCts?.Dispose(); _motionCts?.Dispose();
@@ -387,7 +387,7 @@ private async Awaitable HitRoutine(ActionData data, CancellationToken token)
if (data.HitTiming > 0f) if (data.HitTiming > 0f)
await Awaitable.WaitForSecondsAsync(data.HitTiming, token); await Awaitable.WaitForSecondsAsync(data.HitTiming, token);
// Hit windows are represented by a real trigger collider instead of overlap checks. // 타격 가능 시간은 범위 검사 대신 실제 트리거 콜라이더를 켜서 표현한다.
ActivateAttackHitbox(data); ActivateAttackHitbox(data);
float activeTime = Mathf.Max(data.HitDuration, 0.02f); float activeTime = Mathf.Max(data.HitDuration, 0.02f);
@@ -416,7 +416,7 @@ private void ActivateAttackHitbox(ActionData data)
private void EnsureAttackHitbox() private void EnsureAttackHitbox()
{ {
// Allow designers to assign a hitbox, but create one automatically for simple setup. // 인스펙터에서 직접 넣을 수도 있고, 없으면 기본 공격 판정을 자동으로 만든다.
if (_attackHitbox != null) if (_attackHitbox != null)
{ {
SetAttackHitboxLayer(); SetAttackHitboxLayer();
@@ -439,7 +439,7 @@ private void EnsureAttackHitbox()
private void SetAttackHitboxLayer() private void SetAttackHitboxLayer()
{ {
// The hitbox must not inherit the Player layer, because Player vs Enemy physics is disabled. // 플레이어와 적의 물리 충돌이 꺼져 있으므로 공격 판정은 플레이어 레이어를 쓰면 안 된다.
int defaultLayer = LayerMask.NameToLayer("Default"); int defaultLayer = LayerMask.NameToLayer("Default");
if (defaultLayer >= 0) if (defaultLayer >= 0)
_attackHitbox.gameObject.layer = defaultLayer; _attackHitbox.gameObject.layer = defaultLayer;
@@ -471,7 +471,7 @@ private void LockMovementIfNeeded(ActionData data, bool preserveHorizontalVeloci
{ {
if (data.CanMoveDuringAction) return; if (data.CanMoveDuringAction) return;
// Non-moving attacks should not inherit walking velocity from the previous frame. // 이동 없는 공격은 직전 프레임의 걷기 속도를 이어받지 않게 한다.
if (!preserveHorizontalVelocity && !data.HasMotion) if (!preserveHorizontalVelocity && !data.HasMotion)
_rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y); _rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y);
@@ -660,7 +660,7 @@ private float GetClosestHitDistance(Vector2 direction, float distance, int layer
useTriggers = false useTriggers = false
}; };
// Cast every non-trigger body collider so high-speed motion cannot tunnel into level geometry. // 빠른 이동이 지형을 뚫지 않도록 트리거가 아닌 몸체 콜라이더를 전부 캐스트한다.
float closest = float.PositiveInfinity; float closest = float.PositiveInfinity;
for (int i = 0; i < _bodyColliders.Length; i++) for (int i = 0; i < _bodyColliders.Length; i++)
{ {