2026-04-22 15:58:44 +00:00
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using Platformer.Gameplay;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using static Platformer.Core.Simulation;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Platformer.Mechanics
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// A simple controller for enemies. Provides movement control over a patrol path.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[RequireComponent(typeof(AnimationController), typeof(Collider2D))]
|
|
|
|
|
|
public class EnemyController : MonoBehaviour
|
|
|
|
|
|
{
|
2026-05-08 02:28:35 +00:00
|
|
|
|
public PatrolPath path; // legacy 호환·자동 patrol 영역 도입 후 미사용
|
2026-04-22 15:58:44 +00:00
|
|
|
|
public AudioClip ouch;
|
|
|
|
|
|
|
2026-05-07 06:29:34 +00:00
|
|
|
|
/// <summary>BT5-Dev #16 — Distance 기반 감지 영역 X 임계값 (전체 폭). 표준 platformer Enemy 옆 닿음 영역 0.6~0.8.</summary>
|
|
|
|
|
|
public float hitRangeX = 0.7f;
|
|
|
|
|
|
/// <summary>Y 임계값. 위/아래 둘 다 인정.</summary>
|
|
|
|
|
|
public float hitRangeY = 1.0f;
|
2026-05-07 06:50:17 +00:00
|
|
|
|
/// <summary>밟기 판정 — Player가 Enemy보다 위 거리. 발 닿는 느낌 영역(0.05~0.15).</summary>
|
|
|
|
|
|
public float stompMinDy = 0.1f;
|
2026-05-07 06:29:34 +00:00
|
|
|
|
|
2026-05-08 03:27:54 +00:00
|
|
|
|
// PD 명시 2026-05-08 — 자동 patrol (생성 위치 기준 좌/우 random 50~75 왕복·BT87 절반 정정)
|
|
|
|
|
|
public float patrolMinRange = 50f;
|
|
|
|
|
|
public float patrolMaxRange = 75f;
|
2026-05-08 02:28:35 +00:00
|
|
|
|
public float patrolArriveThreshold = 0.5f;
|
2026-05-08 03:52:08 +00:00
|
|
|
|
public float cliffCheckDistance = 1.0f; // BT92: 0.8→1.0 — 더 일찍 검출
|
|
|
|
|
|
public float cliffCheckDepth = 2.0f;
|
2026-05-08 03:16:38 +00:00
|
|
|
|
public LayerMask groundLayerMask = (1 << 0) | (1 << 16); // Layer 0 (지면) + Layer 16 (발판)
|
2026-05-08 03:52:08 +00:00
|
|
|
|
public float stuckThresholdTime = 0.3f; // BT92: 0.05→0.3 — phase 전환 직후 미세 정지 영역 무시 (좌우 반복 차단)
|
|
|
|
|
|
public float stuckMoveThreshold = 0.02f; // 정지 판정 거리 임계값
|
|
|
|
|
|
public float cliffSafePushDistance = 0.15f; // BT92: 절벽 검출 시 안전 영역 push 거리
|
2026-05-08 03:43:11 +00:00
|
|
|
|
public float waitMinTime = 1f; // patrol arrive·벽·절벽 후 대기 random 영역
|
2026-05-08 03:39:25 +00:00
|
|
|
|
public float waitMaxTime = 3f;
|
2026-05-08 02:28:35 +00:00
|
|
|
|
|
|
|
|
|
|
private float _startX;
|
|
|
|
|
|
private float _targetX;
|
|
|
|
|
|
private int _patrolPhase; // 0: right out / 1: right back / 2: left out / 3: left back
|
2026-05-08 03:39:25 +00:00
|
|
|
|
private float _lastX; // 벽 정지 검출용
|
|
|
|
|
|
private float _stuckTimer; // 벽 정지 누적 시간
|
|
|
|
|
|
private float _waitTimer; // BT89: arrive 후 대기 누적 시간 (1~3초 random)
|
2026-05-08 02:28:35 +00:00
|
|
|
|
|
|
|
|
|
|
internal PatrolPath.Mover mover; // legacy 호환·미사용
|
2026-04-22 15:58:44 +00:00
|
|
|
|
internal AnimationController control;
|
|
|
|
|
|
internal Collider2D _collider;
|
|
|
|
|
|
internal AudioSource _audio;
|
|
|
|
|
|
SpriteRenderer spriteRenderer;
|
|
|
|
|
|
|
|
|
|
|
|
public Bounds Bounds => _collider.bounds;
|
|
|
|
|
|
|
2026-05-07 06:29:34 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// PD 지시 2026-05-07 — Enemy 시각(SpriteRenderer) 영역 기반 충돌 감지용. CapsuleCollider2D는 작은 ground sensor 영역(0.45×0.09)이라 감지에 부적합.
|
|
|
|
|
|
/// SpriteRenderer 부재 시 Collider Bounds로 fallback.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public Bounds VisualBounds => spriteRenderer != null ? spriteRenderer.bounds : _collider.bounds;
|
|
|
|
|
|
|
|
|
|
|
|
PlayerController _cachedPlayer;
|
2026-05-07 06:55:18 +00:00
|
|
|
|
bool _ignoreCollisionApplied;
|
2026-05-07 06:29:34 +00:00
|
|
|
|
|
2026-04-22 15:58:44 +00:00
|
|
|
|
void Awake()
|
|
|
|
|
|
{
|
|
|
|
|
|
control = GetComponent<AnimationController>();
|
|
|
|
|
|
_collider = GetComponent<Collider2D>();
|
|
|
|
|
|
_audio = GetComponent<AudioSource>();
|
|
|
|
|
|
spriteRenderer = GetComponent<SpriteRenderer>();
|
|
|
|
|
|
|
2026-05-07 07:00:40 +00:00
|
|
|
|
// BT5-Dev #21 — Awake 시점 fallback 추가 (Player tag 영역 미설정 영역 대비)
|
2026-05-07 06:29:34 +00:00
|
|
|
|
var playerObj = GameObject.FindGameObjectWithTag("Player");
|
2026-05-07 07:00:40 +00:00
|
|
|
|
if (playerObj == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var pcfb = Object.FindFirstObjectByType<PlayerController>();
|
|
|
|
|
|
if (pcfb != null) playerObj = pcfb.gameObject;
|
|
|
|
|
|
}
|
2026-05-07 06:29:34 +00:00
|
|
|
|
if (playerObj != null && _collider != null)
|
2026-04-22 15:58:44 +00:00
|
|
|
|
{
|
2026-05-07 06:29:34 +00:00
|
|
|
|
var pc = playerObj.GetComponent<Collider2D>();
|
2026-05-07 07:00:40 +00:00
|
|
|
|
if (pc != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Physics2D.IgnoreCollision(_collider, pc, true);
|
|
|
|
|
|
_ignoreCollisionApplied = true;
|
|
|
|
|
|
}
|
2026-04-22 15:58:44 +00:00
|
|
|
|
}
|
2026-05-07 06:29:34 +00:00
|
|
|
|
|
2026-05-08 02:28:35 +00:00
|
|
|
|
// PD 명시 2026-05-08 — 자동 patrol 시작 위치 저장 + 첫 목표 설정
|
|
|
|
|
|
_startX = transform.position.x;
|
2026-05-08 02:33:57 +00:00
|
|
|
|
_lastX = _startX;
|
|
|
|
|
|
_stuckTimer = 0f;
|
2026-05-08 02:28:35 +00:00
|
|
|
|
_patrolPhase = 0;
|
|
|
|
|
|
SetNextPatrolTarget();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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; // 시작 위치 복귀
|
|
|
|
|
|
}
|
2026-04-22 15:58:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 03:39:25 +00:00
|
|
|
|
// BT89 — patrol·벽·절벽 검출·대기 영역 통합 처리
|
|
|
|
|
|
void UpdatePatrol()
|
2026-04-22 15:58:44 +00:00
|
|
|
|
{
|
2026-05-08 03:39:25 +00:00
|
|
|
|
// 대기 영역 — control.move.x = 0 + Timer 감소
|
|
|
|
|
|
if (_waitTimer > 0f)
|
|
|
|
|
|
{
|
|
|
|
|
|
_waitTimer -= Time.deltaTime;
|
|
|
|
|
|
if (control != null) control.move.x = 0f;
|
|
|
|
|
|
_lastX = transform.position.x;
|
|
|
|
|
|
_stuckTimer = 0f;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 02:28:35 +00:00
|
|
|
|
float dx = _targetX - transform.position.x;
|
|
|
|
|
|
|
2026-05-08 03:39:25 +00:00
|
|
|
|
// patrol arrive — 1~3초 대기 후 다음 phase
|
2026-05-08 02:28:35 +00:00
|
|
|
|
if (Mathf.Abs(dx) < patrolArriveThreshold)
|
2026-04-22 15:58:44 +00:00
|
|
|
|
{
|
2026-05-08 02:28:35 +00:00
|
|
|
|
_patrolPhase = (_patrolPhase + 1) % 4;
|
|
|
|
|
|
SetNextPatrolTarget();
|
2026-05-08 03:39:25 +00:00
|
|
|
|
_waitTimer = Random.Range(waitMinTime, waitMaxTime);
|
|
|
|
|
|
if (control != null) control.move.x = 0f;
|
|
|
|
|
|
_lastX = transform.position.x;
|
|
|
|
|
|
_stuckTimer = 0f;
|
|
|
|
|
|
return;
|
2026-04-22 15:58:44 +00:00
|
|
|
|
}
|
2026-05-07 06:29:34 +00:00
|
|
|
|
|
2026-05-08 02:28:35 +00:00
|
|
|
|
float moveDir = Mathf.Sign(dx);
|
|
|
|
|
|
|
2026-05-08 03:43:11 +00:00
|
|
|
|
// BT90 — 수평 Raycast 영역 폐기 (BT89 거짓 양성 — 같은 Tile cell 영역 검출)
|
|
|
|
|
|
// 벽 영역 = stuckTimer 영역 (50ms 정지 시 즉시 phase+2)으로 처리
|
2026-05-08 02:33:57 +00:00
|
|
|
|
|
2026-05-08 03:52:08 +00:00
|
|
|
|
// BT92 — 절벽 검출: 즉시 반대 방향 + transform 안전 push (1 frame 지연 영역에서 발 영역 절벽 진입 차단)
|
2026-05-08 03:39:25 +00:00
|
|
|
|
if (_collider != null)
|
2026-05-08 02:28:35 +00:00
|
|
|
|
{
|
|
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-05-08 03:52:08 +00:00
|
|
|
|
transform.position += new Vector3(-moveDir * cliffSafePushDistance, 0f, 0f);
|
2026-05-08 03:34:43 +00:00
|
|
|
|
_patrolPhase = (_patrolPhase + 2) % 4;
|
2026-05-08 03:16:38 +00:00
|
|
|
|
SetNextPatrolTarget();
|
|
|
|
|
|
_stuckTimer = 0f;
|
2026-05-08 03:39:25 +00:00
|
|
|
|
_lastX = transform.position.x;
|
2026-05-08 03:46:09 +00:00
|
|
|
|
dx = _targetX - transform.position.x;
|
|
|
|
|
|
if (control != null) control.move.x = Mathf.Clamp(dx, -1, 1);
|
2026-05-08 03:39:25 +00:00
|
|
|
|
return;
|
2026-05-08 02:28:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 03:46:09 +00:00
|
|
|
|
// BT91 — 벽 정지 (stuckTimer): 즉시 반대 방향 이동 (waitTimer X — 부들부들 차단)
|
2026-05-08 03:39:25 +00:00
|
|
|
|
if (Mathf.Abs(transform.position.x - _lastX) < stuckMoveThreshold)
|
|
|
|
|
|
{
|
|
|
|
|
|
_stuckTimer += Time.deltaTime;
|
|
|
|
|
|
if (_stuckTimer > stuckThresholdTime)
|
|
|
|
|
|
{
|
|
|
|
|
|
_patrolPhase = (_patrolPhase + 2) % 4;
|
|
|
|
|
|
SetNextPatrolTarget();
|
|
|
|
|
|
_stuckTimer = 0f;
|
|
|
|
|
|
_lastX = transform.position.x;
|
2026-05-08 03:46:09 +00:00
|
|
|
|
dx = _targetX - transform.position.x;
|
|
|
|
|
|
if (control != null) control.move.x = Mathf.Clamp(dx, -1, 1);
|
2026-05-08 03:39:25 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_stuckTimer = 0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
_lastX = transform.position.x;
|
|
|
|
|
|
|
|
|
|
|
|
if (control != null) control.move.x = Mathf.Clamp(dx, -1, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Update()
|
|
|
|
|
|
{
|
|
|
|
|
|
// BT89 — 자동 patrol + 즉시 벽 검출 + 1~3초 대기 영역
|
|
|
|
|
|
UpdatePatrol();
|
|
|
|
|
|
|
|
|
|
|
|
// 이하 — Player 충돌 검출 영역 (patrol 영역과 분리)
|
2026-05-08 02:28:35 +00:00
|
|
|
|
|
2026-05-07 06:29:34 +00:00
|
|
|
|
// PD 지시 2026-05-07 — Player ↔ Enemy 통과 가능이지만 Bounds.Intersects로 매 프레임 감지
|
|
|
|
|
|
if (_cachedPlayer == null)
|
|
|
|
|
|
{
|
2026-05-07 06:45:33 +00:00
|
|
|
|
// 1차: tag 영역 발견
|
2026-05-07 06:29:34 +00:00
|
|
|
|
var pgo = GameObject.FindGameObjectWithTag("Player");
|
|
|
|
|
|
if (pgo != null) _cachedPlayer = pgo.GetComponent<PlayerController>();
|
2026-05-07 06:45:33 +00:00
|
|
|
|
|
|
|
|
|
|
// 2차 fallback: tag 영역 미설정 영역에 대비해 PlayerController 영역 직접 검색
|
|
|
|
|
|
if (_cachedPlayer == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_cachedPlayer = Object.FindFirstObjectByType<PlayerController>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 06:40:39 +00:00
|
|
|
|
if (Time.frameCount % 60 == 0)
|
2026-05-07 06:45:33 +00:00
|
|
|
|
{
|
|
|
|
|
|
int allCount = Object.FindObjectsByType<PlayerController>(FindObjectsSortMode.None).Length;
|
|
|
|
|
|
Debug.Log($"[BT17-Update@{name}] f={Time.frameCount} cached={(_cachedPlayer != null ? _cachedPlayer.name : "NULL")} pgoTag={(pgo != null ? pgo.name : "NULL")} allPCcount={allCount}");
|
|
|
|
|
|
}
|
2026-05-07 06:29:34 +00:00
|
|
|
|
}
|
2026-05-07 06:55:18 +00:00
|
|
|
|
|
|
|
|
|
|
// BT20 — IgnoreCollision 영역 Awake 시점 Player 발견 X 영역 fallback. Update 영역에서 발견 직후 1회 영역 호출.
|
|
|
|
|
|
if (_cachedPlayer != null && !_ignoreCollisionApplied && _collider != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var pc = _cachedPlayer.GetComponent<Collider2D>();
|
|
|
|
|
|
if (pc != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Physics2D.IgnoreCollision(_collider, pc, true);
|
|
|
|
|
|
_ignoreCollisionApplied = true;
|
|
|
|
|
|
Debug.Log($"[BT20-Ignore@{name}] Player↔Enemy IgnoreCollision applied");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-07 06:29:34 +00:00
|
|
|
|
if (_cachedPlayer != null && _cachedPlayer.health != null && _cachedPlayer.health.IsAlive)
|
|
|
|
|
|
{
|
2026-05-07 09:02:19 +00:00
|
|
|
|
// BT5-Dev #36 — Distance 영역 폐기 → Bounds.Intersects 표준 영역 (Unity Physics AABB 정확)
|
|
|
|
|
|
// Player 빠른 통과 시 한 프레임 catch 정합. Layer 13↔14 OFF 영역 통과 가능 영역 그대로.
|
|
|
|
|
|
bool inRange = VisualBounds.Intersects(_cachedPlayer.Bounds);
|
2026-05-07 06:29:34 +00:00
|
|
|
|
|
2026-05-07 09:02:19 +00:00
|
|
|
|
const float PLAYER_COLLIDER_TO_VISUAL_FOOT = -0.17f;
|
|
|
|
|
|
float footY = _cachedPlayer.Bounds.min.y + PLAYER_COLLIDER_TO_VISUAL_FOOT;
|
|
|
|
|
|
float headY = VisualBounds.max.y;
|
|
|
|
|
|
float footHeadDelta = footY - headY;
|
2026-05-07 06:29:34 +00:00
|
|
|
|
|
|
|
|
|
|
if (inRange != _diagWasIntersecting)
|
|
|
|
|
|
{
|
2026-05-07 09:02:19 +00:00
|
|
|
|
Debug.Log($"[EnemyDiag@{name}] inRange={inRange} footHeadDelta={footHeadDelta:F2} VB={VisualBounds.size} PB={_cachedPlayer.Bounds.size}");
|
2026-05-07 06:29:34 +00:00
|
|
|
|
_diagWasIntersecting = inRange;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (inRange)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ev = Schedule<PlayerEnemyCollision>();
|
|
|
|
|
|
ev.player = _cachedPlayer;
|
|
|
|
|
|
ev.enemy = this;
|
2026-05-07 07:42:31 +00:00
|
|
|
|
ev.dyAtCollision = footHeadDelta;
|
2026-05-07 06:29:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool _diagWasIntersecting;
|
|
|
|
|
|
|
|
|
|
|
|
void OnDrawGizmos()
|
|
|
|
|
|
{
|
|
|
|
|
|
// BT5-Dev #15 진단 — Scene 영역 시각화 (Editor에서만 표시)
|
|
|
|
|
|
if (Application.isPlaying && spriteRenderer != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Gizmos.color = Color.red;
|
|
|
|
|
|
Gizmos.DrawWireCube(VisualBounds.center, VisualBounds.size);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Application.isPlaying && _collider != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Gizmos.color = Color.yellow;
|
|
|
|
|
|
Gizmos.DrawWireCube(_collider.bounds.center, _collider.bounds.size);
|
|
|
|
|
|
}
|
2026-04-22 15:58:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|