BT5-Dev #94: phase cooldown 0.5초·시작 시 안전 거리 측정 (좌우 반복·좁은 영역 차단)

PD 보고 (2026-05-08): 1) 벽 가장자리 좌우 반복 / 2) 좁은 영역 생성 Enemy 떨어짐

진단:
- 좌우 반복: phase 전환 직후 1~2 frame 미세 정지 → stuckTimer 트리거 → 또 phase+2 → 매 frame 반복
- 좁은 영역: patrol 거리 50~75m > 시작 위치 ↔ 절벽 거리 → 시작 즉시 절벽 영역 도달 → 떨어짐

정정 (BT94):
1. _phaseCooldown 영역 신규 (0.5초)
   - phase 전환 직후 절벽·벽 검출 비활성
   - cooldown 동안 Enemy 영역 반대 방향 이동 (충분 영역) → 안정
2. 시작 시 좌·우 walk 영역 안전 거리 측정 (MeasureSafeWalkDistance)
   - Awake 시점 0.5m 간격 Raycast 영역 절벽 검출까지 거리 측정
   - patrol 거리 = min(설정 거리, 측정 거리 - 1.5m 안전 margin)
   - _maxRightRange·_maxLeftRange 영역
3. SetNextPatrolTarget — _maxRightRange/_maxLeftRange cap 적용

효과:
- 좌우 반복 = phase cooldown 0.5초 = phase 전환 직후 검출 X → 안정
- 좁은 영역 = 시작 시 안전 거리 측정 → patrol 영역 절벽 영역 도달 X
- 일반 영역 = 영향 X (안전 거리 측정 ≥ patrolMaxRange)
This commit is contained in:
깃 관리자 2026-05-08 13:41:18 +09:00
parent 585eca1022
commit e453d1d07e
1 changed files with 74 additions and 37 deletions

View File

@ -39,7 +39,11 @@ namespace Platformer.Mechanics
private int _patrolPhase; // 0: right out / 1: right back / 2: left out / 3: left back
private float _lastX; // 벽 정지 검출용
private float _stuckTimer; // 벽 정지 누적 시간
private float _waitTimer; // BT89: arrive 후 대기 누적 시간 (1~3초 random)
private float _waitTimer; // arrive 후 대기 누적 시간 (1~3초 random)
private float _phaseCooldown; // BT94: phase 전환 직후 절벽·벽 검출 비활성 시간
private const float PHASE_COOLDOWN = 0.5f;
private float _maxRightRange; // BT94: 시작 시 측정 — 우측 안전 patrol 거리
private float _maxLeftRange; // BT94: 시작 시 측정 — 좌측 안전 patrol 거리
internal PatrolPath.Mover mover; // legacy 호환·미사용
internal AnimationController control;
@ -86,19 +90,46 @@ namespace Platformer.Mechanics
_startX = transform.position.x;
_lastX = _startX;
_stuckTimer = 0f;
_phaseCooldown = 0f;
// BT94 — 시작 시 좌·우 안전 patrol 거리 측정 (좁은 영역 Enemy 떨어짐 차단)
_maxRightRange = MeasureSafeWalkDistance(1f);
_maxLeftRange = MeasureSafeWalkDistance(-1f);
_patrolPhase = 0;
SetNextPatrolTarget();
}
// BT94 — 시작 위치에서 좌·우 walk 영역 절벽 검출까지 거리 측정 (안전 margin 1.5m 차감)
float MeasureSafeWalkDistance(float dir)
{
float groundY = transform.position.y - 0.34f; // 발 영역 위 0.05m (Capsule offset 영역 기반 추정)
for (float d = 0.5f; d <= patrolMaxRange; d += 0.5f)
{
Vector2 origin = new Vector2(_startX + dir * d, groundY);
RaycastHit2D hit = Physics2D.Raycast(origin, Vector2.down, cliffCheckDepth, groundLayerMask);
if (hit.collider == null) return Mathf.Max(0f, d - 1.5f);
}
return patrolMaxRange;
}
void SetNextPatrolTarget()
{
float range = Random.Range(patrolMinRange, patrolMaxRange);
switch (_patrolPhase)
{
case 0: _targetX = _startX + range; break; // 우측 random 이동
case 1: _targetX = _startX; break; // 시작 위치 복귀
case 2: _targetX = _startX - range; break; // 좌측 random 이동
case 3: _targetX = _startX; break; // 시작 위치 복귀
case 0: // 우측 random — 안전 거리 cap
_targetX = _startX + Mathf.Min(range, _maxRightRange);
break;
case 1:
_targetX = _startX;
break;
case 2: // 좌측 random — 안전 거리 cap
_targetX = _startX - Mathf.Min(range, _maxLeftRange);
break;
case 3:
_targetX = _startX;
break;
}
}
@ -115,6 +146,9 @@ namespace Platformer.Mechanics
return;
}
// BT94 — phase cooldown 감소 (절벽·벽 검출 비활성 시간)
if (_phaseCooldown > 0f) _phaseCooldown -= Time.deltaTime;
float dx = _targetX - transform.position.x;
// patrol arrive — 1~3초 대기 후 다음 phase
@ -134,48 +168,51 @@ namespace Platformer.Mechanics
// BT90 — 수평 Raycast 영역 폐기 (BT89 거짓 양성 — 같은 Tile cell 영역 검출)
// 벽 영역 = stuckTimer 영역 (50ms 정지 시 즉시 phase+2)으로 처리
// BT93 — 절벽 검출: velocity.x = 0 강제 (관성 차단·발 절벽 진입 X) + phase+2 + 반대 방향 이동
// transform.position 직접 set 폐기 (BT92 영역·KinematicObject body.position 충돌)
if (_collider != null)
// BT94 — 절벽·벽 검출은 phase cooldown 영역 끝난 후 활성 (좌우 반복 차단)
if (_phaseCooldown <= 0f)
{
Vector2 footAhead = new Vector2(
_collider.bounds.center.x + moveDir * cliffCheckDistance,
_collider.bounds.min.y + 0.05f
);
RaycastHit2D groundHit = Physics2D.Raycast(footAhead, Vector2.down, cliffCheckDepth, groundLayerMask);
if (groundHit.collider == null)
// 절벽 검출 — velocity.x = 0 + phase+2 + cooldown 활성
if (_collider != null)
{
if (control != null)
Vector2 footAhead = new Vector2(
_collider.bounds.center.x + moveDir * cliffCheckDistance,
_collider.bounds.min.y + 0.05f
);
RaycastHit2D groundHit = Physics2D.Raycast(footAhead, Vector2.down, cliffCheckDepth, groundLayerMask);
if (groundHit.collider == null)
{
control.velocity = new Vector2(0f, control.velocity.y);
if (control != null) control.velocity = new Vector2(0f, control.velocity.y);
_patrolPhase = (_patrolPhase + 2) % 4;
SetNextPatrolTarget();
_stuckTimer = 0f;
_phaseCooldown = PHASE_COOLDOWN;
_lastX = transform.position.x;
dx = _targetX - transform.position.x;
if (control != null) control.move.x = Mathf.Clamp(dx, -1, 1);
return;
}
_patrolPhase = (_patrolPhase + 2) % 4;
SetNextPatrolTarget();
_stuckTimer = 0f;
_lastX = transform.position.x;
dx = _targetX - transform.position.x;
if (control != null) control.move.x = Mathf.Clamp(dx, -1, 1);
return;
}
}
// BT93 — 벽 정지 (stuckTimer 150ms): velocity.x = 0 + phase+2 + 반대 방향
if (Mathf.Abs(transform.position.x - _lastX) < stuckMoveThreshold)
{
_stuckTimer += Time.deltaTime;
if (_stuckTimer > stuckThresholdTime)
// 벽 정지 (stuckTimer 150ms): velocity.x = 0 + phase+2 + cooldown 활성
if (Mathf.Abs(transform.position.x - _lastX) < stuckMoveThreshold)
{
if (control != null)
_stuckTimer += Time.deltaTime;
if (_stuckTimer > stuckThresholdTime)
{
control.velocity = new Vector2(0f, control.velocity.y);
if (control != null) control.velocity = new Vector2(0f, control.velocity.y);
_patrolPhase = (_patrolPhase + 2) % 4;
SetNextPatrolTarget();
_stuckTimer = 0f;
_phaseCooldown = PHASE_COOLDOWN;
_lastX = transform.position.x;
dx = _targetX - transform.position.x;
if (control != null) control.move.x = Mathf.Clamp(dx, -1, 1);
return;
}
_patrolPhase = (_patrolPhase + 2) % 4;
SetNextPatrolTarget();
}
else
{
_stuckTimer = 0f;
_lastX = transform.position.x;
dx = _targetX - transform.position.x;
if (control != null) control.move.x = Mathf.Clamp(dx, -1, 1);
return;
}
}
else