using System.Collections; using System.Collections.Generic; using Platformer.Gameplay; using UnityEngine; using static Platformer.Core.Simulation; namespace Platformer.Mechanics { /// /// A simple controller for enemies. Provides movement control over a patrol path. /// [RequireComponent(typeof(AnimationController), typeof(Collider2D))] public class EnemyController : MonoBehaviour { public PatrolPath path; public AudioClip ouch; /// BT5-Dev #16 — Distance 기반 감지 영역 X 임계값 (전체 폭). 표준 platformer Enemy 옆 닿음 영역 0.6~0.8. public float hitRangeX = 0.7f; /// Y 임계값. 위/아래 둘 다 인정. public float hitRangeY = 1.0f; /// 밟기 판정 — Player가 Enemy보다 위 거리. 발 닿는 느낌 영역(0.05~0.15). public float stompMinDy = 0.1f; internal PatrolPath.Mover mover; internal AnimationController control; internal Collider2D _collider; internal AudioSource _audio; SpriteRenderer spriteRenderer; public Bounds Bounds => _collider.bounds; /// /// PD 지시 2026-05-07 — Enemy 시각(SpriteRenderer) 영역 기반 충돌 감지용. CapsuleCollider2D는 작은 ground sensor 영역(0.45×0.09)이라 감지에 부적합. /// SpriteRenderer 부재 시 Collider Bounds로 fallback. /// public Bounds VisualBounds => spriteRenderer != null ? spriteRenderer.bounds : _collider.bounds; PlayerController _cachedPlayer; bool _ignoreCollisionApplied; void Awake() { control = GetComponent(); _collider = GetComponent(); _audio = GetComponent(); spriteRenderer = GetComponent(); // BT5-Dev #21 — Awake 시점 fallback 추가 (Player tag 영역 미설정 영역 대비) var playerObj = GameObject.FindGameObjectWithTag("Player"); if (playerObj == null) { var pcfb = Object.FindFirstObjectByType(); if (pcfb != null) playerObj = pcfb.gameObject; } if (playerObj != null && _collider != null) { var pc = playerObj.GetComponent(); if (pc != null) { Physics2D.IgnoreCollision(_collider, pc, true); _ignoreCollisionApplied = true; } } // BT5-Dev #17 marker — 본 영역 출력 시 새 코드 영역 적용 정합. 출력 X = Editor Asset Refresh 영역 미수행 Debug.Log($"[BT17-MARKER@{name}] sr={(spriteRenderer != null ? "OK" : "NULL")} hitX={hitRangeX} hitY={hitRangeY} stomp={stompMinDy} | colB={_collider?.bounds.size} vB={VisualBounds.size}"); } void Update() { if (path != null) { if (mover == null) mover = path.CreateMover(control.maxSpeed * 0.5f); control.move.x = Mathf.Clamp(mover.Position.x - transform.position.x, -1, 1); } // PD 지시 2026-05-07 — Player ↔ Enemy 통과 가능이지만 Bounds.Intersects로 매 프레임 감지 if (_cachedPlayer == null) { // 1차: tag 영역 발견 var pgo = GameObject.FindGameObjectWithTag("Player"); if (pgo != null) _cachedPlayer = pgo.GetComponent(); // 2차 fallback: tag 영역 미설정 영역에 대비해 PlayerController 영역 직접 검색 if (_cachedPlayer == null) { _cachedPlayer = Object.FindFirstObjectByType(); } if (Time.frameCount % 60 == 0) { int allCount = Object.FindObjectsByType(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}"); } } // BT20 — IgnoreCollision 영역 Awake 시점 Player 발견 X 영역 fallback. Update 영역에서 발견 직후 1회 영역 호출. if (_cachedPlayer != null && !_ignoreCollisionApplied && _collider != null) { var pc = _cachedPlayer.GetComponent(); if (pc != null) { Physics2D.IgnoreCollision(_collider, pc, true); _ignoreCollisionApplied = true; Debug.Log($"[BT20-Ignore@{name}] Player↔Enemy IgnoreCollision applied"); } } if (_cachedPlayer != null && _cachedPlayer.health != null && _cachedPlayer.health.IsAlive) { // BT5-Dev #30 — 시각 영역 정합 보정: Player Collider 발(Bounds.min.y)이 시각 발보다 ~0.17 단위 위 (Offset y +0.10) Vector3 ePos = transform.position; Vector3 pPos = _cachedPlayer.transform.position; float dx = Mathf.Abs(pPos.x - ePos.x); const float PLAYER_COLLIDER_TO_VISUAL_FOOT = -0.17f; // Hero1 sprite 발 위치 보정 float footY = _cachedPlayer.Bounds.min.y + PLAYER_COLLIDER_TO_VISUAL_FOOT; // Player 시각 발 float headY = VisualBounds.max.y; // Enemy 시각 머리 (sprite 위 여백 ≈ 0) float footHeadDelta = footY - headY; // 0 = 시각 발이 시각 머리 닿음 bool inRange = dx < hitRangeX && Mathf.Abs(pPos.y - ePos.y) < hitRangeY; if (inRange != _diagWasIntersecting) { Debug.Log($"[EnemyDiag@{name}] inRange={inRange} footHeadDelta={footHeadDelta:F2} dx={dx:F2}"); _diagWasIntersecting = inRange; } if (inRange) { var ev = Schedule(); ev.player = _cachedPlayer; ev.enemy = this; ev.dyAtCollision = footHeadDelta; } } } 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); } } } }