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
|
|
|
|
|
|
{
|
|
|
|
|
|
public AudioClip ouch;
|
|
|
|
|
|
|
2026-05-12 02:15:20 +00:00
|
|
|
|
// BT12-Dev 2026-05-11 — 밟기 공격 기능 제거 (PD 지시): hitRangeX·hitRangeY·stompMinDy 폐기.
|
|
|
|
|
|
// Enemy는 Player 공격에만 피해를 받으므로 Player ↔ Enemy 충돌 감지 자체 불필요.
|
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 04:28:32 +00:00
|
|
|
|
public float stuckThresholdTime = 0.15f; // BT93: 0.3→0.15 (150ms·밀림 누적 차단)
|
2026-05-08 03:52:08 +00:00
|
|
|
|
public float stuckMoveThreshold = 0.02f; // 정지 판정 거리 임계값
|
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;
|
2026-05-08 06:16:10 +00:00
|
|
|
|
private float _startY; // BT102: 시작 시 y 위치 — 떨어짐 검출 기준
|
|
|
|
|
|
public float fallThreshold = 1.0f; // BT102: _startY - fallThreshold 영역 미만 시 시작 위치 텔레포트
|
2026-05-08 02:28:35 +00:00
|
|
|
|
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; // 벽 정지 누적 시간
|
2026-05-08 04:41:18 +00:00
|
|
|
|
private float _waitTimer; // arrive 후 대기 누적 시간 (1~3초 random)
|
2026-05-08 04:50:32 +00:00
|
|
|
|
private float _phaseCooldown; // phase 전환 직후 절벽·벽 검출 비활성 시간
|
|
|
|
|
|
private const float PHASE_COOLDOWN = 1.0f; // BT95: 0.5→1.0 — 긴 영역 cooldown (좌우 반복 영구 차단)
|
2026-05-08 05:00:46 +00:00
|
|
|
|
private float _maxRightRange; // 시작 시 측정 — 우측 안전 patrol 거리
|
|
|
|
|
|
private float _maxLeftRange; // 시작 시 측정 — 좌측 안전 patrol 거리
|
|
|
|
|
|
private bool _isInitialized; // BT97: Start 측정 완료 영역 (Awake 이후 활성)
|
2026-05-08 02:28:35 +00:00
|
|
|
|
|
2026-04-22 15:58:44 +00:00
|
|
|
|
internal AnimationController control;
|
|
|
|
|
|
internal Collider2D _collider;
|
|
|
|
|
|
internal AudioSource _audio;
|
|
|
|
|
|
SpriteRenderer spriteRenderer;
|
2026-05-08 04:55:08 +00:00
|
|
|
|
Rigidbody2D _body; // BT96: KinematicObject body 영역 직접 set 위해 캐싱
|
2026-04-22 15:58:44 +00:00
|
|
|
|
|
|
|
|
|
|
public Bounds Bounds => _collider.bounds;
|
|
|
|
|
|
|
2026-05-07 06:29:34 +00:00
|
|
|
|
/// <summary>
|
2026-05-12 07:51:44 +00:00
|
|
|
|
/// BT12-Dev 2026-05-12 재정정 — SpriteRenderer (Visual) bounds 우선 (PD: Player 피해 X·CapsuleCollider 작음).
|
|
|
|
|
|
/// Visual.SpriteRenderer.bounds = sprite 실제 시각 영역·Player 접촉 판정 정합.
|
2026-05-07 06:29:34 +00:00
|
|
|
|
/// </summary>
|
2026-05-12 07:51:44 +00:00
|
|
|
|
public Bounds VisualBounds => spriteRenderer != null ? spriteRenderer.bounds : (_collider != null ? _collider.bounds : new Bounds());
|
2026-05-07 06:29:34 +00:00
|
|
|
|
|
|
|
|
|
|
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>();
|
2026-05-12 06:53:37 +00:00
|
|
|
|
// BT12-Dev 2026-05-12 — Visual 자식 분리: SpriteRenderer가 자식에 위치할 수 있음.
|
2026-04-22 15:58:44 +00:00
|
|
|
|
spriteRenderer = GetComponent<SpriteRenderer>();
|
2026-05-12 06:53:37 +00:00
|
|
|
|
if (spriteRenderer == null) spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
2026-05-08 04:55:08 +00:00
|
|
|
|
_body = GetComponent<Rigidbody2D>();
|
2026-04-22 15:58:44 +00:00
|
|
|
|
|
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-10 16:18:09 +00:00
|
|
|
|
// BT12-Dev 2026-05-11 — PlatformEffector2D 영역 IgnoreCollision (PD 지시: Enemy 발판 자유 통과)
|
|
|
|
|
|
// 발판 17개·Enemy 16개 — Awake 시점 일괄 IgnoreCollision 등록.
|
|
|
|
|
|
if (_collider != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var platforms = Object.FindObjectsByType<PlatformEffector2D>(FindObjectsSortMode.None);
|
|
|
|
|
|
foreach (var platform in platforms)
|
|
|
|
|
|
{
|
|
|
|
|
|
var platCol = platform.GetComponent<Collider2D>();
|
|
|
|
|
|
if (platCol != null) Physics2D.IgnoreCollision(_collider, platCol, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 16:39:54 +00:00
|
|
|
|
// BT12-Dev 2026-05-11 — Enemy ↔ Enemy IgnoreLayerCollision (PD 지시: 몬스터 영역 통과)
|
|
|
|
|
|
// Layer 14 (Enemy) ↔ Layer 14 collide X·전역 1회 적용 (Awake 영역 매번 호출 무관·idempotent).
|
|
|
|
|
|
Physics2D.IgnoreLayerCollision(14, 14, true);
|
|
|
|
|
|
|
2026-05-08 05:00:46 +00:00
|
|
|
|
// PD 명시 2026-05-08 — 자동 patrol 시작 위치 저장 (측정·target은 Start 시점)
|
2026-05-08 02:28:35 +00:00
|
|
|
|
_startX = transform.position.x;
|
2026-05-08 06:16:10 +00:00
|
|
|
|
_startY = transform.position.y; // BT102: 떨어짐 검출 기준
|
2026-05-08 02:33:57 +00:00
|
|
|
|
_lastX = _startX;
|
|
|
|
|
|
_stuckTimer = 0f;
|
2026-05-08 04:41:18 +00:00
|
|
|
|
_phaseCooldown = 0f;
|
2026-05-08 05:00:46 +00:00
|
|
|
|
_patrolPhase = 0;
|
|
|
|
|
|
_isInitialized = false;
|
|
|
|
|
|
}
|
2026-05-08 04:41:18 +00:00
|
|
|
|
|
2026-05-08 05:00:46 +00:00
|
|
|
|
void Start()
|
|
|
|
|
|
{
|
2026-05-08 06:42:28 +00:00
|
|
|
|
_startY = transform.position.y;
|
2026-05-08 06:24:25 +00:00
|
|
|
|
|
2026-05-12 08:03:49 +00:00
|
|
|
|
// BT12-Dev 2026-05-12 — MeasureSafeWalkDistance Tilemap 의존 폐기 (PD: Level 비활성·발판 가장자리 영역 멈춤).
|
|
|
|
|
|
// patrol 범위는 patrolMaxRange 사용·실제 절벽 회피는 매 frame cliffCheck Raycast가 담당.
|
|
|
|
|
|
_maxRightRange = patrolMaxRange;
|
|
|
|
|
|
_maxLeftRange = patrolMaxRange;
|
2026-05-08 02:28:35 +00:00
|
|
|
|
SetNextPatrolTarget();
|
2026-05-08 05:00:46 +00:00
|
|
|
|
_isInitialized = true;
|
2026-05-08 02:28:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 04:55:08 +00:00
|
|
|
|
// BT96 — 절벽·벽 검출 시 즉시 반대 방향 이동 (transform + body 동시 push·KinematicObject 정합)
|
|
|
|
|
|
void TriggerReverse(float moveDir, float pushDistance)
|
|
|
|
|
|
{
|
|
|
|
|
|
_patrolPhase = (_patrolPhase + 2) % 4;
|
|
|
|
|
|
SetNextPatrolTarget();
|
|
|
|
|
|
|
|
|
|
|
|
// transform + body 동시 push (KinematicObject body.position 영역 동기화)
|
|
|
|
|
|
Vector3 newPos = transform.position + new Vector3(-moveDir * pushDistance, 0f, 0f);
|
|
|
|
|
|
transform.position = newPos;
|
|
|
|
|
|
if (_body != null) _body.position = newPos;
|
|
|
|
|
|
|
|
|
|
|
|
// velocity.x 즉시 반대 방향 + move.x 반대 (다음 frame 안정 이동)
|
|
|
|
|
|
if (control != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
control.velocity = new Vector2(-moveDir * control.maxSpeed, control.velocity.y);
|
|
|
|
|
|
control.move.x = -moveDir;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_stuckTimer = 0f;
|
|
|
|
|
|
_phaseCooldown = PHASE_COOLDOWN;
|
|
|
|
|
|
_lastX = transform.position.x;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 06:34:53 +00:00
|
|
|
|
// BT107 — Tilemap 영역 cell 기반 측정 (PD 근본 진단: Raycast 영역 자체 부정확)
|
|
|
|
|
|
// Enemy 시작 위치 cell → 좌·우 연속 Tile 영역 끝 영역까지 거리 = 안전 patrol 거리
|
2026-05-08 07:26:36 +00:00
|
|
|
|
// BT110 (2026-05-08) — 다중 footPos offset fallback (PD 보고: 16건 중 8~9건 이동 X)
|
|
|
|
|
|
// 근본: Enemy 인스턴스마다 collider bounds·sprite bounds·transform.y 영역 변동
|
|
|
|
|
|
// → 다단 fallback (sprite/collider/transform × 다중 offset) 첫 HasTile cell 채택
|
2026-05-08 04:41:18 +00:00
|
|
|
|
float MeasureSafeWalkDistance(float dir)
|
|
|
|
|
|
{
|
2026-05-08 06:34:53 +00:00
|
|
|
|
if (_collider == null) return 0f;
|
|
|
|
|
|
|
|
|
|
|
|
var groundTilemaps = new System.Collections.Generic.List<UnityEngine.Tilemaps.Tilemap>();
|
|
|
|
|
|
var levelGo = GameObject.Find("Level");
|
|
|
|
|
|
if (levelGo != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var t = levelGo.GetComponent<UnityEngine.Tilemaps.Tilemap>();
|
|
|
|
|
|
if (t != null) groundTilemaps.Add(t);
|
|
|
|
|
|
}
|
|
|
|
|
|
var autoFgGo = GameObject.Find("AutoForeground");
|
|
|
|
|
|
if (autoFgGo != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var t = autoFgGo.GetComponent<UnityEngine.Tilemaps.Tilemap>();
|
|
|
|
|
|
if (t != null) groundTilemaps.Add(t);
|
|
|
|
|
|
}
|
2026-05-10 14:34:00 +00:00
|
|
|
|
// BT12-Dev 2026-05-10 — Tilemap 부재 fallback (Level 비활성·AutoForeground 부재 환경).
|
|
|
|
|
|
// Tilemap 없이 GameObject 기반 Composite Collider (InfiniteHorizontalGround) 영역 patrol.
|
|
|
|
|
|
// patrolMaxRange 영역 좌우 왕복·cliffCheck Raycast (Layer 0 GameObject hit) 영역 절벽 검출 정합.
|
|
|
|
|
|
if (groundTilemaps.Count == 0) return patrolMaxRange;
|
2026-05-08 06:34:53 +00:00
|
|
|
|
|
2026-05-08 07:26:36 +00:00
|
|
|
|
// BT110 — 다중 footPos offset fallback
|
|
|
|
|
|
// sprite·collider·transform 각 영역 발 후보 + 다중 offset (-0.1·-0.3·-0.5·-0.7·-1.0)
|
|
|
|
|
|
// 각 후보 → 모든 groundTilemap HasTile 검사 → 첫 HasTile cell 채택
|
|
|
|
|
|
float colliderFootY = _collider.bounds.min.y;
|
|
|
|
|
|
float spriteFootY = spriteRenderer != null ? spriteRenderer.bounds.min.y : colliderFootY;
|
|
|
|
|
|
float transformY = transform.position.y;
|
|
|
|
|
|
float[] candidateYs = new float[] {
|
|
|
|
|
|
spriteFootY - 0.1f, spriteFootY - 0.3f, spriteFootY - 0.5f, spriteFootY - 0.7f, spriteFootY - 1.0f,
|
|
|
|
|
|
colliderFootY - 0.1f, colliderFootY - 0.3f, colliderFootY - 0.5f, colliderFootY - 0.7f, colliderFootY - 1.0f,
|
|
|
|
|
|
transformY - 0.1f, transformY - 0.3f, transformY - 0.5f, transformY - 0.7f, transformY - 1.0f
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-08 06:34:53 +00:00
|
|
|
|
UnityEngine.Tilemaps.Tilemap startTm = null;
|
|
|
|
|
|
Vector3Int startCell = Vector3Int.zero;
|
2026-05-08 07:26:36 +00:00
|
|
|
|
float chosenFootY = 0f;
|
|
|
|
|
|
foreach (float candY in candidateYs)
|
2026-05-08 06:34:53 +00:00
|
|
|
|
{
|
2026-05-08 07:26:36 +00:00
|
|
|
|
Vector3 footPos = new Vector3(_startX, candY, 0f);
|
|
|
|
|
|
foreach (var tm in groundTilemaps)
|
2026-05-08 06:34:53 +00:00
|
|
|
|
{
|
2026-05-08 07:26:36 +00:00
|
|
|
|
Vector3Int cell = tm.WorldToCell(footPos);
|
|
|
|
|
|
if (tm.HasTile(cell))
|
|
|
|
|
|
{
|
|
|
|
|
|
startTm = tm;
|
|
|
|
|
|
startCell = cell;
|
|
|
|
|
|
chosenFootY = candY;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-05-08 06:34:53 +00:00
|
|
|
|
}
|
2026-05-08 07:26:36 +00:00
|
|
|
|
if (startTm != null) break;
|
2026-05-08 06:34:53 +00:00
|
|
|
|
}
|
2026-05-08 08:24:51 +00:00
|
|
|
|
#if UNITY_EDITOR && ENEMY_DIAG_VERBOSE
|
2026-05-08 07:26:36 +00:00
|
|
|
|
Debug.Log($"[Enemy@{name}] dir={dir} startY={transformY:F2} colliderFoot={colliderFootY:F2} spriteFoot={spriteFootY:F2} chosenFootY={chosenFootY:F2} startTm={(startTm!=null?startTm.name:"NULL")} startCell={startCell}");
|
2026-05-08 08:24:51 +00:00
|
|
|
|
#endif
|
2026-05-10 14:47:29 +00:00
|
|
|
|
// BT12-Dev 2026-05-10 — startTm 부재 fallback (Tilemap Count>0이나 Enemy 시작 cell HasTile X 환경).
|
|
|
|
|
|
// GameObject 기반 Composite Collider patrol 정합·patrolMaxRange 좌우 왕복.
|
|
|
|
|
|
if (startTm == null) return patrolMaxRange;
|
2026-05-08 06:34:53 +00:00
|
|
|
|
|
|
|
|
|
|
// 좌·우 연속 Tile 영역 끝 영역 검색
|
2026-05-08 08:13:25 +00:00
|
|
|
|
// BT111 (2026-05-08) — 위·아래 1 cell 인접 cell도 검색 (계단 영역 발판 연속 정합)
|
|
|
|
|
|
// PD 보고: 좁은 영역 1 cell 폭 발판 위 Enemy patrol 거리 0
|
|
|
|
|
|
// 근본: 좌·우 same-y cell 즉시 break → 계단·1 cell 폭 영역 발판 연속 미감지
|
|
|
|
|
|
// 정정: 다음 same-y / +1 / -1 중 어느 곳이라도 HasTile = 발판 연속
|
2026-05-08 06:34:53 +00:00
|
|
|
|
int dirCell = (int)Mathf.Sign(dir);
|
|
|
|
|
|
Vector3Int cellIter = startCell;
|
|
|
|
|
|
for (int steps = 0; steps < 500; steps++)
|
2026-05-08 04:41:18 +00:00
|
|
|
|
{
|
2026-05-08 08:13:25 +00:00
|
|
|
|
Vector3Int nextSame = cellIter + new Vector3Int(dirCell, 0, 0);
|
|
|
|
|
|
Vector3Int nextUp = nextSame + Vector3Int.up;
|
|
|
|
|
|
Vector3Int nextDown = nextSame + Vector3Int.down;
|
|
|
|
|
|
if (HasTileInAnyTilemap(groundTilemaps, nextSame)) cellIter = nextSame;
|
|
|
|
|
|
else if (HasTileInAnyTilemap(groundTilemaps, nextUp)) cellIter = nextUp;
|
|
|
|
|
|
else if (HasTileInAnyTilemap(groundTilemaps, nextDown)) cellIter = nextDown;
|
|
|
|
|
|
else break;
|
2026-05-08 04:41:18 +00:00
|
|
|
|
}
|
2026-05-08 06:34:53 +00:00
|
|
|
|
|
|
|
|
|
|
// 마지막 Tile cell 영역 center 영역까지 거리
|
|
|
|
|
|
Vector3 lastTileWorld = startTm.CellToWorld(cellIter) + new Vector3(0.5f, 0f, 0f);
|
|
|
|
|
|
float distance = Mathf.Abs(lastTileWorld.x - _startX);
|
|
|
|
|
|
return Mathf.Max(0f, distance - 0.5f); // 안전 margin 0.5m (Tile cell 영역 안)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static bool HasTileInAnyTilemap(System.Collections.Generic.List<UnityEngine.Tilemaps.Tilemap> tms, Vector3Int cell)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var tm in tms) if (tm != null && tm.HasTile(cell)) return true;
|
|
|
|
|
|
return false;
|
2026-05-08 04:41:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 02:28:35 +00:00
|
|
|
|
void SetNextPatrolTarget()
|
|
|
|
|
|
{
|
|
|
|
|
|
float range = Random.Range(patrolMinRange, patrolMaxRange);
|
|
|
|
|
|
switch (_patrolPhase)
|
|
|
|
|
|
{
|
2026-05-08 04:41:18 +00:00
|
|
|
|
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;
|
2026-05-08 02:28:35 +00:00
|
|
|
|
}
|
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 05:00:46 +00:00
|
|
|
|
// BT97 — Start 측정 완료 전까지 patrol 비활성 (AutoForeground Tile data 영역 활성 대기)
|
|
|
|
|
|
if (!_isInitialized)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (control != null) control.move.x = 0f;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 07:48:08 +00:00
|
|
|
|
// BT12-Dev 2026-05-12 — IsGrounded=False 영역 patrol·cliffCheck skip (PD: 공중 낙하 시 자연 낙하 우선)
|
|
|
|
|
|
// 피격 밀림·공중 spawn 영역 KinematicObject 자연 낙하 → 바닥/발판 정착 → IsGrounded=True → patrol 재개.
|
|
|
|
|
|
if (control == null || !control.IsGrounded)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (control != null) control.move.x = 0f;
|
|
|
|
|
|
_stuckTimer = 0f;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 06:34:53 +00:00
|
|
|
|
// BT107 — BT106 y 강제 고정 영역 폐기 (PD 보고: 공중 부유 영역 원인)
|
2026-05-08 06:29:22 +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 04:41:18 +00:00
|
|
|
|
// BT94 — phase cooldown 감소 (절벽·벽 검출 비활성 시간)
|
|
|
|
|
|
if (_phaseCooldown > 0f) _phaseCooldown -= Time.deltaTime;
|
|
|
|
|
|
|
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-12 08:01:00 +00:00
|
|
|
|
// BT12-Dev 2026-05-12 — cliffCheck Enemy 영역 가장자리 상대 (PD: 발판 가장자리 걸쳐 대기 정정)
|
|
|
|
|
|
// 절대 cliffCheckDistance 폐기·bounds.extents.x + edgeMargin 사용 → Enemy 영역 바로 앞 검사
|
2026-05-12 07:51:44 +00:00
|
|
|
|
if (_collider != null)
|
2026-05-08 02:28:35 +00:00
|
|
|
|
{
|
2026-05-12 08:01:00 +00:00
|
|
|
|
const float EDGE_MARGIN = 0.15f;
|
|
|
|
|
|
float fwdX = _collider.bounds.center.x + moveDir * (_collider.bounds.extents.x + EDGE_MARGIN);
|
|
|
|
|
|
float backX = _collider.bounds.center.x - moveDir * (_collider.bounds.extents.x + EDGE_MARGIN);
|
|
|
|
|
|
float footY = _collider.bounds.min.y + 0.05f;
|
2026-05-12 07:58:29 +00:00
|
|
|
|
|
2026-05-12 08:01:00 +00:00
|
|
|
|
RaycastHit2D groundHitFwd = Physics2D.Raycast(new Vector2(fwdX, footY), Vector2.down, cliffCheckDepth, groundLayerMask);
|
2026-05-12 07:58:29 +00:00
|
|
|
|
if (groundHitFwd.collider == null)
|
2026-05-08 02:28:35 +00:00
|
|
|
|
{
|
2026-05-12 08:01:00 +00:00
|
|
|
|
RaycastHit2D groundHitBack = Physics2D.Raycast(new Vector2(backX, footY), Vector2.down, cliffCheckDepth, groundLayerMask);
|
2026-05-12 07:58:29 +00:00
|
|
|
|
if (groundHitBack.collider == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 양측 cliff — 제자리 대기
|
|
|
|
|
|
if (control != null) control.move.x = 0f;
|
|
|
|
|
|
_waitTimer = Random.Range(waitMinTime, waitMaxTime);
|
|
|
|
|
|
_stuckTimer = 0f;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-12 07:51:44 +00:00
|
|
|
|
TriggerReverse(moveDir, 0.3f);
|
|
|
|
|
|
return;
|
2026-05-08 02:28:35 +00:00
|
|
|
|
}
|
2026-05-12 07:51:44 +00:00
|
|
|
|
}
|
2026-05-08 02:28:35 +00:00
|
|
|
|
|
2026-05-12 07:51:44 +00:00
|
|
|
|
if (_phaseCooldown <= 0f)
|
|
|
|
|
|
{
|
2026-05-08 04:41:18 +00:00
|
|
|
|
if (Mathf.Abs(transform.position.x - _lastX) < stuckMoveThreshold)
|
2026-05-08 03:39:25 +00:00
|
|
|
|
{
|
2026-05-08 04:41:18 +00:00
|
|
|
|
_stuckTimer += Time.deltaTime;
|
|
|
|
|
|
if (_stuckTimer > stuckThresholdTime)
|
2026-05-08 04:28:32 +00:00
|
|
|
|
{
|
2026-05-08 04:55:08 +00:00
|
|
|
|
TriggerReverse(moveDir, 0.2f);
|
2026-05-08 04:41:18 +00:00
|
|
|
|
return;
|
2026-05-08 04:28:32 +00:00
|
|
|
|
}
|
2026-05-08 04:41:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-05-08 03:39:25 +00:00
|
|
|
|
_stuckTimer = 0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
2026-05-12 07:40:10 +00:00
|
|
|
|
// BT12-Dev 2026-05-12 — BT102 떨어짐 검출 텔레포트 폐기 (PD 지시):
|
|
|
|
|
|
// 1. 절벽 닿을 때 텔레포트 회귀: TriggerReverse 방향 전환과 충돌해 텔레포트 우선 적용
|
|
|
|
|
|
// 2. 공중 spawn Enemy 회귀: 떨어진 후 fallThreshold 진입 시 spawn 위치 복귀
|
|
|
|
|
|
// 두 회귀 모두 BT102 텔레포트 원인 → 폐기. 자연 낙하 + TriggerReverse만 영역.
|
2026-05-08 05:13:23 +00:00
|
|
|
|
|
2026-05-12 02:24:42 +00:00
|
|
|
|
// BT12-Dev 2026-05-11 — 밟기 공격 폐기 (PD 지시): PlayerEnemyCollision 발화 제거.
|
2026-05-12 02:15:20 +00:00
|
|
|
|
// Player ↔ Enemy IgnoreCollision은 Awake 시점에 이미 적용되어 물리 통과 정합.
|
2026-05-12 02:24:42 +00:00
|
|
|
|
//
|
|
|
|
|
|
// PD 지시 2026-05-11 — Player ↔ Enemy 닿음 시 Player 피해 (밟기는 X·통과).
|
|
|
|
|
|
// Player IsGrounded 상태에서만 피격·공중(점프) 상태는 통과.
|
|
|
|
|
|
if (_cachedPlayer == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var pgo = GameObject.FindGameObjectWithTag("Player");
|
|
|
|
|
|
if (pgo != null) _cachedPlayer = pgo.GetComponent<PlayerController>();
|
|
|
|
|
|
if (_cachedPlayer == null) _cachedPlayer = Object.FindFirstObjectByType<PlayerController>();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_cachedPlayer != null && _cachedPlayer.health != null && _cachedPlayer.health.IsAlive)
|
|
|
|
|
|
{
|
2026-05-12 07:55:06 +00:00
|
|
|
|
// BT12-Dev 2026-05-12 — 2D AABB 검사 (Z 무시: Enemy z=0·Player z=1·Bounds.Intersects 3D 비교 항상 False).
|
|
|
|
|
|
var eb = VisualBounds;
|
|
|
|
|
|
var pb = _cachedPlayer.Bounds;
|
|
|
|
|
|
bool overlap2D = Mathf.Abs(eb.center.x - pb.center.x) < (eb.extents.x + pb.extents.x)
|
|
|
|
|
|
&& Mathf.Abs(eb.center.y - pb.center.y) < (eb.extents.y + pb.extents.y);
|
|
|
|
|
|
if (_cachedPlayer.IsGrounded && overlap2D)
|
2026-05-12 02:24:42 +00:00
|
|
|
|
{
|
|
|
|
|
|
if (!_cachedPlayer.health.IsInvulnerable)
|
|
|
|
|
|
{
|
|
|
|
|
|
_cachedPlayer.health.Decrement();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-07 06:29:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|