EerieVillage/Assets/Scripts/Mechanics/EnemyController.cs

206 lines
9.1 KiB
C#
Raw Normal View History

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
{
public PatrolPath path; // legacy 호환·자동 patrol 영역 도입 후 미사용
public AudioClip ouch;
/// <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;
/// <summary>밟기 판정 — Player가 Enemy보다 위 거리. 발 닿는 느낌 영역(0.05~0.15).</summary>
public float stompMinDy = 0.1f;
// PD 명시 2026-05-08 — 자동 patrol (생성 위치 기준 좌/우 random 100~150 왕복)
public float patrolMinRange = 100f;
public float patrolMaxRange = 150f;
public float patrolArriveThreshold = 0.5f;
public float cliffCheckDistance = 0.6f; // 발 앞 절벽 검출 거리
public float cliffCheckDepth = 0.8f; // 발 아래 Raycast 거리
public LayerMask groundLayerMask = ~0; // 절벽 검출용 ground layer (default = 모든 layer)
private float _startX;
private float _targetX;
private int _patrolPhase; // 0: right out / 1: right back / 2: left out / 3: left back
internal PatrolPath.Mover mover; // legacy 호환·미사용
internal AnimationController control;
internal Collider2D _collider;
internal AudioSource _audio;
SpriteRenderer spriteRenderer;
public Bounds Bounds => _collider.bounds;
/// <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;
bool _ignoreCollisionApplied;
void Awake()
{
control = GetComponent<AnimationController>();
_collider = GetComponent<Collider2D>();
_audio = GetComponent<AudioSource>();
spriteRenderer = GetComponent<SpriteRenderer>();
// BT5-Dev #21 — Awake 시점 fallback 추가 (Player tag 영역 미설정 영역 대비)
var playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj == null)
{
var pcfb = Object.FindFirstObjectByType<PlayerController>();
if (pcfb != null) playerObj = pcfb.gameObject;
}
if (playerObj != null && _collider != null)
{
var pc = playerObj.GetComponent<Collider2D>();
if (pc != null)
{
Physics2D.IgnoreCollision(_collider, pc, true);
_ignoreCollisionApplied = true;
}
}
// PD 명시 2026-05-08 — 자동 patrol 시작 위치 저장 + 첫 목표 설정
_startX = transform.position.x;
_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; // 시작 위치 복귀
}
}
void Update()
{
// PD 명시 2026-05-08 — PatrolPath 영역 폐기·자동 patrol (생성 위치 기준 좌/우 random 100~150 왕복) + 절벽 검출
float dx = _targetX - transform.position.x;
if (Mathf.Abs(dx) < patrolArriveThreshold)
{
_patrolPhase = (_patrolPhase + 1) % 4;
SetNextPatrolTarget();
dx = _targetX - transform.position.x;
}
float moveDir = Mathf.Sign(dx);
// 절벽 검출 — 발 앞 영역 Raycast 지면 X 시 시작 위치로 즉시 복귀
if (Mathf.Abs(dx) > patrolArriveThreshold && _collider != 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)
{
// 절벽 영역 → 시작 위치 즉시 복귀 (반대 방향 phase 강제)
_patrolPhase = (_patrolPhase % 2 == 0) ? _patrolPhase + 1 : _patrolPhase;
_targetX = _startX;
dx = _targetX - transform.position.x;
moveDir = Mathf.Sign(dx);
}
}
control.move.x = Mathf.Clamp(dx, -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<PlayerController>();
// 2차 fallback: tag 영역 미설정 영역에 대비해 PlayerController 영역 직접 검색
if (_cachedPlayer == null)
{
_cachedPlayer = Object.FindFirstObjectByType<PlayerController>();
}
if (Time.frameCount % 60 == 0)
{
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}");
}
}
// 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");
}
}
if (_cachedPlayer != null && _cachedPlayer.health != null && _cachedPlayer.health.IsAlive)
{
// BT5-Dev #36 — Distance 영역 폐기 → Bounds.Intersects 표준 영역 (Unity Physics AABB 정확)
// Player 빠른 통과 시 한 프레임 catch 정합. Layer 13↔14 OFF 영역 통과 가능 영역 그대로.
bool inRange = VisualBounds.Intersects(_cachedPlayer.Bounds);
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;
if (inRange != _diagWasIntersecting)
{
Debug.Log($"[EnemyDiag@{name}] inRange={inRange} footHeadDelta={footHeadDelta:F2} VB={VisualBounds.size} PB={_cachedPlayer.Bounds.size}");
_diagWasIntersecting = inRange;
}
if (inRange)
{
var ev = Schedule<PlayerEnemyCollision>();
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);
}
}
}
}