Projext/Assets/Scripts/Enemy/BossAI/BossPatterndebugger.cs
2026-02-20 03:14:55 +09:00

383 lines
16 KiB
C#

using UnityEngine;
using System.Collections.Generic;
/// <summary>
/// [디버그 전용] NorcielBoss 오브젝트에 추가하면
/// Game 뷰에 패턴 판정 정보를 실시간으로 표시합니다.
/// 테스트 후 제거하세요.
/// </summary>
[RequireComponent(typeof(NorcielBoss))]
public class BossPatternDebugger : MonoBehaviour
{
// ── 설정 ────────────────────────────────────────
[Header("디버그 표시 설정")]
[SerializeField] private bool showGizmos = true; // Scene 뷰 기즈모
[SerializeField] private bool showOnScreen = true; // Game 뷰 GUI
[SerializeField] private bool showConsoleLog = true; // 콘솔 로그
[SerializeField] private float logCooldown = 0.5f; // 콘솔 로그 최소 간격(초)
[Header("판정 거리 (BossMonster 값과 맞출 것)")]
[Tooltip("BossMonster의 attackRange 와 동일하게 설정")]
[SerializeField] private float attackRange = 3f;
[Tooltip("PlayerBehaviorTracker의 keepDistanceThreshold 와 동일하게 설정")]
[SerializeField] private float keepDistThreshold = 8f;
[Tooltip("PlayerBehaviorTracker의 closeRangeThreshold 와 동일하게 설정")]
[SerializeField] private float closeRangeThreshold = 3f;
// ── 내부 변수 ────────────────────────────────────
private NorcielBoss _boss;
private Transform _player;
private float _lastLogTime = -999f;
// 로그 기록 (화면 출력용)
private readonly Queue<string> _logQueue = new Queue<string>();
private const int MAX_LOG = 8;
// GUI 스타일 (OnGUI에서 한 번만 생성)
private GUIStyle _boxStyle;
private GUIStyle _labelStyle;
private GUIStyle _headerStyle;
// 상태 추적용
private string _lastDecision = "—";
private string _lastReason = "—";
private float _distToPlayer = 0f;
// 패치: BossMonster의 private 필드를 reflection 없이 추적하기 위한
// 현재 상태를 직접 Animator에서 읽어서 표시
private Animator _animator;
// ── 색상 ─────────────────────────────────────────
private static readonly Color COLOR_CLOSE = new Color(1f, 0.3f, 0.3f, 1f); // 빨강: 근접
private static readonly Color COLOR_CHASE = new Color(1f, 0.7f, 0f, 1f); // 주황: 추격
private static readonly Color COLOR_FAR = new Color(0.3f, 0.7f, 1f, 1f); // 파랑: 원거리
private static readonly Color COLOR_ATTACK = new Color(1f, 0.2f, 0.2f, 1f); // 진빨: 공격
private static readonly Color COLOR_IDLE = new Color(0.6f, 0.6f, 0.6f, 1f); // 회색: 대기
// ─────────────────────────────────────────────────
private void Awake()
{
_boss = GetComponent<NorcielBoss>();
_animator = GetComponent<Animator>();
}
private void Start()
{
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj != null) _player = playerObj.transform;
else Debug.LogWarning("[BossDebugger] Player 태그 오브젝트를 찾지 못했습니다.");
// BossMonster의 DecideAttack 로직을 후킹하기 위해 이벤트 구독
// → BossMonster 코드를 건드리지 않고 Animator State 이름을 폴링해서 판단
PushLog("✅ BossPatternDebugger 시작됨");
}
private void Update()
{
if (_player == null || _boss == null) return;
_distToPlayer = Vector3.Distance(transform.position, _player.position);
// ── 판정 분석 ──────────────────────────────────
string newDecision, newReason;
AnalyzeCurrentState(out newDecision, out newReason);
// 판정이 바뀔 때만 로그 기록
bool changed = (newDecision != _lastDecision);
_lastDecision = newDecision;
_lastReason = newReason;
if (changed && showConsoleLog && Time.time > _lastLogTime + logCooldown)
{
string msg = $"[BossDebug] {newDecision} | 거리: {_distToPlayer:F1}m | 이유: {newReason}";
Debug.Log(msg);
PushLog(msg);
_lastLogTime = Time.time;
}
}
// ── 상태 분석 ─────────────────────────────────────
private void AnalyzeCurrentState(out string decision, out string reason)
{
if (_animator == null) { decision = "❓ Animator 없음"; reason = "—"; return; }
AnimatorStateInfo state = _animator.GetCurrentAnimatorStateInfo(0);
string animState = GetCurrentAnimStateName(state);
// 거리 판정 구역
bool isInAttackRange = _distToPlayer <= attackRange;
bool isInCloseRange = _distToPlayer <= closeRangeThreshold;
bool isInKeepDistance = _distToPlayer >= keepDistThreshold;
// 애니메이션에서 현재 행동 판단
if (state.IsName("Roar") || state.IsName("Armature|Roar"))
{
decision = "🦁 포효 (전투 시작)";
reason = "배틀 시작 연출 중";
}
else if (IsAttackAnim(state))
{
string patternName = GetPatternFromAnim(state);
string distDesc = isInAttackRange
? $"근접 사거리 ({_distToPlayer:F1}m ≤ {attackRange}m)"
: $"원거리 ({_distToPlayer:F1}m)";
decision = $"⚔️ 공격: {patternName}";
reason = distDesc;
}
else if (state.IsName("Skill_Pickup") || state.IsName("Armature|Skill_Pickup"))
{
decision = "🎱 무기 줍기";
reason = "쇠공 회수 중";
}
else if (state.IsName("Monster_Walk") || state.IsName("Armature|Walk"))
{
if (_distToPlayer > attackRange)
{
decision = "🏃 추격 중";
reason = $"플레이어까지 {_distToPlayer:F1}m (사거리 {attackRange}m 초과)";
}
else
{
decision = "🚶 이동";
reason = $"거리 {_distToPlayer:F1}m";
}
}
else if (state.IsName("Monster_GetDamage") || state.IsName("Armature|GetDamage"))
{
decision = "💥 피격";
reason = "피격 애니메이션 재생 중";
}
else if (state.IsName("Monster_Die") || state.IsName("Armature|Die"))
{
decision = "💀 사망";
reason = "사망 처리 중";
}
else // Idle 또는 대기
{
if (isInAttackRange)
{
decision = "⏱️ 공격 대기 (쿨타임)";
reason = $"사거리 내 ({_distToPlayer:F1}m) — 패턴 간격 대기";
}
else if (_distToPlayer > attackRange)
{
decision = "👁️ 추격 준비";
reason = $"플레이어까지 {_distToPlayer:F1}m";
}
else
{
decision = "😴 대기";
reason = $"Idle 상태 ({animState})";
}
}
// ── 카운터 패턴 힌트 추가 ─────────────────────
if (isInKeepDistance)
reason += " [카운터: KeepDistance 감지 중]";
else if (isInCloseRange && !IsAttackAnim(state))
reason += " [카운터: CloseRange 감지 중]";
}
private bool IsAttackAnim(AnimatorStateInfo s)
{
return s.IsName("Attack_Throw") || s.IsName("Armature|Attack_Throw")
|| s.IsName("Attack_Swing") || s.IsName("Armature|Attack_Swing")
|| s.IsName("Attack_Slam") || s.IsName("Armature|Attack_Slam")
|| s.IsName("Skill_Dash_Ready") || s.IsName("Armature|Skill_Dash_Ready")
|| s.IsName("Skill_Dash_Go") || s.IsName("Armature|Skill_Dash_Go")
|| s.IsName("Skill_Smash_Charge") || s.IsName("Armature|Skill_Smash_Charge")
|| s.IsName("Skill_Smash_Impact") || s.IsName("Armature|Skill_Smash_Impact")
|| s.IsName("Skill_Sweep") || s.IsName("Armature|Skill_Sweep");
}
private string GetPatternFromAnim(AnimatorStateInfo s)
{
if (s.IsName("Attack_Throw") || s.IsName("Armature|Attack_Throw")) return "쇠공 던지기";
if (s.IsName("Skill_Dash_Ready") || s.IsName("Armature|Skill_Dash_Ready")) return "돌진 준비";
if (s.IsName("Skill_Dash_Go") || s.IsName("Armature|Skill_Dash_Go")) return "돌진 공격";
if (s.IsName("Skill_Smash_Charge") || s.IsName("Armature|Skill_Smash_Charge")) return "찍기 차징";
if (s.IsName("Skill_Smash_Impact") || s.IsName("Armature|Skill_Smash_Impact")) return "찍기 타격";
if (s.IsName("Skill_Sweep") || s.IsName("Armature|Skill_Sweep")) return "휩쓸기";
return "공격";
}
private string GetCurrentAnimStateName(AnimatorStateInfo s)
{
// shortNameHash로 이름을 역추적할 수 없으므로 IsName으로 확인
string[] names = {
"Monster_Idle","Monster_Walk","Monster_GetDamage","Monster_Die",
"Roar","Attack_Throw","Skill_Pickup",
"Skill_Dash_Ready","Skill_Dash_Go",
"Skill_Smash_Charge","Skill_Smash_Impact","Skill_Sweep"
};
foreach (var n in names) if (s.IsName(n)) return n;
return $"hash:{s.shortNameHash}";
}
// ── 로그 큐 관리 ──────────────────────────────────
private void PushLog(string msg)
{
_logQueue.Enqueue($"[{Time.time:F1}s] {msg}");
while (_logQueue.Count > MAX_LOG) _logQueue.Dequeue();
}
// ── OnGUI (Game 뷰 오버레이) ──────────────────────
private void OnGUI()
{
if (!showOnScreen) return;
// 스타일 초기화 (한 번만)
if (_boxStyle == null) InitStyles();
float panelX = 10f;
float panelY = 10f;
float panelW = 420f;
// ── 메인 패널 ──
GUI.color = new Color(0f, 0f, 0f, 0.75f);
GUI.Box(new Rect(panelX - 5, panelY - 5, panelW + 10, 210), GUIContent.none, _boxStyle);
GUI.color = Color.white;
float y = panelY;
// 제목
GUI.Label(new Rect(panelX, y, panelW, 22), "🔍 BOSS PATTERN DEBUGGER", _headerStyle); y += 24;
// 거리 바
DrawDistanceBar(panelX, y, panelW - 10, _distToPlayer); y += 36;
// 현재 판정
Color decisionColor = GetDecisionColor(_lastDecision);
GUI.color = decisionColor;
GUI.Label(new Rect(panelX, y, panelW, 22), $"판정: {_lastDecision}", _labelStyle); y += 22;
GUI.color = Color.white;
GUI.Label(new Rect(panelX, y, panelW, 22), $"이유: {_lastReason}", _labelStyle); y += 24;
// 구분선
GUI.color = new Color(1f, 1f, 1f, 0.3f);
GUI.Box(new Rect(panelX, y, panelW - 10, 1), GUIContent.none);
GUI.color = Color.white;
y += 6;
// 거리 수치
string distInfo = $"플레이어 거리: {_distToPlayer:F2}m | 공격사거리: {attackRange}m | 근접기준: {closeRangeThreshold}m";
GUI.Label(new Rect(panelX, y, panelW, 20), distInfo, _labelStyle); y += 22;
// ── 로그 패널 ──
float logY = panelY + 220;
GUI.color = new Color(0f, 0f, 0f, 0.65f);
GUI.Box(new Rect(panelX - 5, logY - 5, panelW + 10, MAX_LOG * 18 + 30), GUIContent.none, _boxStyle);
GUI.color = Color.white;
GUI.Label(new Rect(panelX, logY, panelW, 18), "📋 최근 판정 로그", _headerStyle); logY += 20;
foreach (string log in _logQueue)
{
GUI.Label(new Rect(panelX, logY, panelW, 18), log, _labelStyle);
logY += 18;
}
}
// ── 거리 게이지 바 ────────────────────────────────
private void DrawDistanceBar(float x, float y, float w, float dist)
{
float maxDist = keepDistThreshold + 2f;
float fillRatio = Mathf.Clamp01(dist / maxDist);
// 배경
GUI.color = new Color(0.2f, 0.2f, 0.2f, 1f);
GUI.Box(new Rect(x, y + 8, w, 14), GUIContent.none);
// 채우기
Color barColor = dist <= closeRangeThreshold ? COLOR_CLOSE
: dist <= attackRange ? COLOR_ATTACK
: dist >= keepDistThreshold ? COLOR_FAR
: COLOR_CHASE;
GUI.color = barColor;
GUI.Box(new Rect(x, y + 8, w * fillRatio, 14), GUIContent.none);
// 마커: attackRange
float markerX = x + (attackRange / maxDist) * w;
GUI.color = Color.yellow;
GUI.Box(new Rect(markerX - 1, y + 5, 2, 20), GUIContent.none);
// 마커: keepDistThreshold
float keepX = x + (keepDistThreshold / maxDist) * w;
GUI.color = COLOR_FAR;
GUI.Box(new Rect(keepX - 1, y + 5, 2, 20), GUIContent.none);
// 라벨
GUI.color = Color.white;
GUIStyle tiny = new GUIStyle(GUI.skin.label) { fontSize = 10 };
GUI.Label(new Rect(x, y, 100, 16), $"근접({closeRangeThreshold}m)", tiny);
GUI.Label(new Rect(markerX - 20, y, 60, 16), $"공격({attackRange}m)", tiny);
GUI.Label(new Rect(keepX - 20, y, 70, 16), $"거리유지({keepDistThreshold}m)", tiny);
GUI.color = Color.white;
}
private Color GetDecisionColor(string decision)
{
if (decision.Contains("공격")) return COLOR_ATTACK;
if (decision.Contains("추격")) return COLOR_CHASE;
if (decision.Contains("포효")) return Color.magenta;
if (decision.Contains("피격")) return Color.cyan;
if (decision.Contains("대기") || decision.Contains("쿨타임")) return COLOR_IDLE;
if (decision.Contains("원거리")) return COLOR_FAR;
return Color.white;
}
private void InitStyles()
{
_boxStyle = new GUIStyle(GUI.skin.box);
_labelStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 13,
fontStyle = FontStyle.Normal
};
_labelStyle.normal.textColor = Color.white;
_headerStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 14,
fontStyle = FontStyle.Bold
};
_headerStyle.normal.textColor = Color.yellow;
}
// ── Gizmos (Scene 뷰) ─────────────────────────────
private void OnDrawGizmos()
{
if (!showGizmos) return;
// 공격 사거리 (빨강)
Gizmos.color = new Color(1f, 0.2f, 0.2f, 0.35f);
Gizmos.DrawWireSphere(transform.position, attackRange);
// 근접 기준 (연빨강 채우기)
Gizmos.color = new Color(1f, 0.2f, 0.2f, 0.1f);
Gizmos.DrawSphere(transform.position, closeRangeThreshold);
// 원거리 유지 기준 (파랑)
Gizmos.color = new Color(0.3f, 0.7f, 1f, 0.2f);
Gizmos.DrawWireSphere(transform.position, keepDistThreshold);
if (_player == null) return;
// 현재 거리 선
bool isClose = _distToPlayer <= attackRange;
Gizmos.color = isClose ? new Color(1f, 0.2f, 0.2f, 0.9f)
: new Color(1f, 0.7f, 0f, 0.9f);
Gizmos.DrawLine(transform.position + Vector3.up * 1.5f,
_player.position + Vector3.up * 1.5f);
#if UNITY_EDITOR
// Scene 뷰 레이블
string label = $"▶ {_lastDecision}\n거리: {_distToPlayer:F1}m\n{_lastReason}";
UnityEditor.Handles.color = Color.white;
UnityEditor.Handles.Label(transform.position + Vector3.up * 3f, label);
#endif
}
}