2026-02-13 09:11:54 +00:00
|
|
|
|
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
|
2026-02-12 15:23:25 +00:00
|
|
|
|
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-02-13 09:11:54 +00:00
|
|
|
|
/// 플레이어의 전투 행동을 추적하고 보스 카운터 시스템에 보고합니다.
|
2026-02-10 15:29:22 +00:00
|
|
|
|
/// </summary>
|
2026-02-12 15:23:25 +00:00
|
|
|
|
public class PlayerBehaviorTracker : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerBehaviorTracker를
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
public static PlayerBehaviorTracker Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스 접근용 Instance를
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-13 09:11:54 +00:00
|
|
|
|
[Header("참조")] // 인스펙터 제목을 달거에요 -> 참조 설정을
|
|
|
|
|
|
[SerializeField] private Transform bossTransform; // 변수를 선언할거에요 -> 보스의 위치를 파악할 bossTransform을
|
|
|
|
|
|
private BossCounterSystem _bossSystem; // 변수를 선언할거에요 -> 보스 카운터 시스템 참조를
|
|
|
|
|
|
|
|
|
|
|
|
[Header("행동 추적 설정")] // 인스펙터 제목을 달거에요 -> 추적 설정을
|
|
|
|
|
|
[Tooltip("행동 추적 윈도우 크기(초)")] // 툴팁을 달거에요 -> 설명 문구를
|
|
|
|
|
|
[SerializeField] private float windowDuration = 10f; // 변수를 선언할거에요 -> 기록 유지 시간(10초)을
|
|
|
|
|
|
|
|
|
|
|
|
[Header("조건 설정")] // 인스펙터 제목을 달거에요 -> 조건 설정을
|
|
|
|
|
|
[SerializeField] private float keepDistanceThreshold = 8f; // 변수를 선언할거에요 -> 거리 유지 기준(8m)을
|
|
|
|
|
|
[SerializeField] private float keepDistanceTimeReq = 3f; // 변수를 선언할거에요 -> 거리 유지 필요 시간(3초)을
|
|
|
|
|
|
[SerializeField] private float closeRangeThreshold = 3f; // 변수를 선언할거에요 -> 근접 허용 기준(3m)을
|
|
|
|
|
|
[SerializeField] private int dashCountTrigger = 3; // 변수를 선언할거에요 -> 대쉬 남발 기준 횟수(3회)를
|
|
|
|
|
|
[SerializeField] private float dashCheckWindow = 5f; // 변수를 선언할거에요 -> 대쉬 남발 체크 시간(5초)을
|
|
|
|
|
|
|
|
|
|
|
|
// ── 내부 변수 ──
|
|
|
|
|
|
private float _distanceTimer; // 변수를 선언할거에요 -> 거리 유지 시간을 젤 타이머를
|
|
|
|
|
|
private float _lastReportTime; // 변수를 선언할거에요 -> 마지막으로 보스에게 보고한 시간을 (도배 방지)
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
|
|
|
|
|
// ── 내부 기록용 타임스탬프 리스트 ──
|
2026-02-12 15:23:25 +00:00
|
|
|
|
private List<float> dodgeTimestamps = new List<float>(); // 리스트를 만들거에요 -> 회피 시간들을 저장할 dodgeTimestamps를
|
2026-02-13 09:11:54 +00:00
|
|
|
|
private List<float> aimStartTimes = new List<float>(); // 리스트를 만들거에요 -> 조준 시작 시간들을
|
|
|
|
|
|
private List<float> aimEndTimes = new List<float>(); // 리스트를 만들거에요 -> 조준 종료 시간들을
|
|
|
|
|
|
private List<float> pierceShotTimestamps = new List<float>(); // 리스트를 만들거에요 -> 관통샷 시간들을
|
|
|
|
|
|
private List<float> totalShotTimestamps = new List<float>(); // 리스트를 만들거에요 -> 전체 사격 시간들을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-13 09:11:54 +00:00
|
|
|
|
private bool isAiming = false; // 변수를 초기화할거에요 -> 조준 중 여부를
|
|
|
|
|
|
private float currentAimStartTime; // 변수를 선언할거에요 -> 조준 시작 시간을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-13 09:11:54 +00:00
|
|
|
|
// ── 외부 프로퍼티 ──
|
|
|
|
|
|
public int DodgeCount => CountInWindow(dodgeTimestamps, windowDuration); // 값을 반환할거에요 -> 최근 회피 횟수를
|
|
|
|
|
|
public float AimHoldTime => CalculateAimHoldTime(); // 값을 반환할거에요 -> 조준 유지 시간을
|
|
|
|
|
|
public float PierceRatio => CalculatePierceRatio(); // 값을 반환할거에요 -> 관통샷 비율을
|
|
|
|
|
|
public int TotalShotsInWindow => CountInWindow(totalShotTimestamps, windowDuration); // 값을 반환할거에요 -> 총 발사 횟수를
|
|
|
|
|
|
|
|
|
|
|
|
private void Awake() // 함수를 실행할거에요 -> 초기화를
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
if (Instance != null && Instance != this) { Destroy(gameObject); return; } // 중복이면 파괴할거에요
|
|
|
|
|
|
Instance = this; // 값을 저장할거에요 -> 싱글톤 인스턴스를
|
2026-02-20 16:39:14 +00:00
|
|
|
|
}
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-13 09:11:54 +00:00
|
|
|
|
private void Start() // 함수를 실행할거에요 -> 시작 시
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
_bossSystem = FindObjectOfType<BossCounterSystem>(); // 찾을거에요 -> 씬에 있는 보스 시스템을
|
|
|
|
|
|
|
|
|
|
|
|
// 보스 트랜스폼이 연결 안 돼 있으면 자동으로 찾기 시도
|
|
|
|
|
|
if (bossTransform == null) // 조건이 맞으면 실행할거에요 -> 보스 연결이 안 되어 있다면
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
GameObject boss = GameObject.FindGameObjectWithTag("Boss"); // 찾을거에요 -> Boss 태그를 가진 오브젝트를
|
|
|
|
|
|
if (boss != null) bossTransform = boss.transform; // 연결할거에요 -> 찾은 보스의 트랜스폼을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 09:11:54 +00:00
|
|
|
|
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 행동 감지를
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
if (bossTransform == null || _bossSystem == null) return; // 조건이 맞으면 중단할거에요 -> 보스나 시스템이 없으면
|
|
|
|
|
|
|
|
|
|
|
|
float distance = Vector3.Distance(transform.position, bossTransform.position); // 거리를 계산할거에요 -> 플레이어와 보스 사이를
|
|
|
|
|
|
|
|
|
|
|
|
// 1. KeepDistance (거리 유지) 감지
|
|
|
|
|
|
if (distance >= keepDistanceThreshold) // 조건이 맞으면 실행할거에요 -> 설정된 거리보다 멀어졌다면
|
|
|
|
|
|
{
|
|
|
|
|
|
_distanceTimer += Time.deltaTime; // 더할거에요 -> 타이머에 시간을
|
|
|
|
|
|
if (_distanceTimer >= keepDistanceTimeReq) // 조건이 맞으면 실행할거에요 -> 유지 시간이 충족되었다면
|
|
|
|
|
|
{
|
|
|
|
|
|
ReportAction(CounterType.KeepDistance); // 보고할거에요 -> 거리 유지 행동을
|
|
|
|
|
|
_distanceTimer = 0f; // 초기화할거에요 -> 타이머를 (재감지 위해)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else // 조건이 틀리면 실행할거에요 -> 가까워졌다면
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
_distanceTimer = 0f; // 초기화할거에요 -> 타이머를 리셋
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. CloseRange (근접 허용) 감지
|
|
|
|
|
|
if (distance <= closeRangeThreshold) // 조건이 맞으면 실행할거에요 -> 초근접 상태라면
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Time.time > _lastReportTime + 2.0f) // 조건이 맞으면 실행할거에요 -> 쿨타임(2초)이 지났다면
|
|
|
|
|
|
{
|
|
|
|
|
|
ReportAction(CounterType.CloseRange); // 보고할거에요 -> 근접 허용 행동을
|
|
|
|
|
|
}
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 09:11:54 +00:00
|
|
|
|
// 보스 시스템에 행동 알림 (쿨타임 적용)
|
|
|
|
|
|
private void ReportAction(CounterType type) // 함수를 선언할거에요 -> 행동을 보고하는 함수를
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
if (_bossSystem != null) // 조건이 맞으면 실행할거에요 -> 시스템이 존재한다면
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
_bossSystem.RegisterPlayerAction(type); // 실행할거에요 -> 해당 행동 타입을 등록하는 함수를
|
|
|
|
|
|
_lastReportTime = Time.time; // 기록할거에요 -> 마지막 보고 시간을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════
|
2026-02-13 09:11:54 +00:00
|
|
|
|
// 외부 이벤트 기록
|
2026-02-10 15:29:22 +00:00
|
|
|
|
// ═══════════════════════════════════════════
|
|
|
|
|
|
|
2026-02-13 09:11:54 +00:00
|
|
|
|
public void RecordDodge() // 함수를 선언할거에요 -> 회피를 기록하는 함수를
|
|
|
|
|
|
{
|
|
|
|
|
|
float now = Time.time; // 값을 저장할거에요 -> 현재 시간을
|
|
|
|
|
|
dodgeTimestamps.Add(now); // 추가할거에요 -> 리스트에
|
|
|
|
|
|
|
|
|
|
|
|
// 3. FrequentDash (대쉬 남발) 감지
|
|
|
|
|
|
// 최근 N초(dashCheckWindow) 안에 대쉬가 몇 번 있었는지 확인
|
|
|
|
|
|
int recentDashes = CountInWindow(dodgeTimestamps, dashCheckWindow); // 셀거에요 -> 설정된 시간 내 대쉬 횟수를
|
|
|
|
|
|
|
|
|
|
|
|
if (recentDashes >= dashCountTrigger) // 조건이 맞으면 실행할거에요 -> 기준 횟수를 넘었다면
|
2026-02-20 16:39:14 +00:00
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
ReportAction(CounterType.FrequentDash); // 보고할거에요 -> 대쉬 남발 행동을
|
|
|
|
|
|
// 너무 자주 보고하지 않게 리스트 일부 정리하거나 쿨타임 활용 (여기선 ReportAction의 쿨타임에 의존)
|
2026-02-20 16:39:14 +00:00
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void RecordAimStart() // 함수를 선언할거에요 -> 조준 시작 기록을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
if (!isAiming) { isAiming = true; currentAimStartTime = Time.time; } // 상태를 바꿀거에요 -> 조준 중으로
|
2026-02-20 16:39:14 +00:00
|
|
|
|
}
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-13 09:11:54 +00:00
|
|
|
|
public void RecordAimEnd() // 함수를 선언할거에요 -> 조준 종료 기록을
|
|
|
|
|
|
{
|
|
|
|
|
|
if (isAiming) { isAiming = false; aimStartTimes.Add(currentAimStartTime); aimEndTimes.Add(Time.time); } // 추가할거에요 -> 시작/종료 시간을
|
2026-02-20 16:39:14 +00:00
|
|
|
|
}
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-13 09:11:54 +00:00
|
|
|
|
public void RecordShot(bool isPierce) // 함수를 선언할거에요 -> 발사 기록을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
totalShotTimestamps.Add(Time.time); // 추가할거에요 -> 전체 사격에
|
|
|
|
|
|
if (isPierce) pierceShotTimestamps.Add(Time.time); // 추가할거에요 -> 관통 사격에
|
2026-02-20 16:39:14 +00:00
|
|
|
|
}
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-13 09:11:54 +00:00
|
|
|
|
public void ResetForNewRun() // 함수를 선언할거에요 -> 초기화를
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
dodgeTimestamps.Clear(); aimStartTimes.Clear(); aimEndTimes.Clear();
|
|
|
|
|
|
pierceShotTimestamps.Clear(); totalShotTimestamps.Clear(); isAiming = false; // 비울거에요 -> 모든 리스트를
|
|
|
|
|
|
}
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-13 09:11:54 +00:00
|
|
|
|
// ═══════════════════════════════════════════
|
|
|
|
|
|
// 내부 계산 로직
|
|
|
|
|
|
// ═══════════════════════════════════════════
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-13 09:11:54 +00:00
|
|
|
|
private int CountInWindow(List<float> timestamps, float window) // 함수를 선언할거에요 -> 특정 시간 내 개수를 세는
|
|
|
|
|
|
{
|
|
|
|
|
|
float cutoff = Time.time - window; // 계산할거에요 -> 기준 시간을
|
|
|
|
|
|
// 리스트 정리 (오래된 데이터 삭제)
|
|
|
|
|
|
timestamps.RemoveAll(t => t < cutoff); // 삭제할거에요 -> 기준보다 오래된 것을
|
|
|
|
|
|
return timestamps.Count; // 반환할거에요 -> 남은 개수를
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private float CalculateAimHoldTime() // 함수를 선언할거에요 -> 조준 시간을 계산하는
|
|
|
|
|
|
{
|
|
|
|
|
|
float cutoff = Time.time - windowDuration; // 계산할거에요 -> 기준 시간을
|
|
|
|
|
|
float total = 0f; // 초기화할거에요 -> 총합을
|
|
|
|
|
|
for (int i = aimStartTimes.Count - 1; i >= 0; i--) // 반복할거에요 -> 역순으로
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
if (aimEndTimes[i] < cutoff) { aimStartTimes.RemoveAt(i); aimEndTimes.RemoveAt(i); continue; } // 삭제할거에요 -> 오래된 기록을
|
|
|
|
|
|
float start = Mathf.Max(aimStartTimes[i], cutoff); // 보정할거에요 -> 시작 시간을
|
|
|
|
|
|
total += aimEndTimes[i] - start; // 더할거에요 -> 조준 시간을
|
2026-02-20 16:39:14 +00:00
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
if (isAiming) { float start = Mathf.Max(currentAimStartTime, cutoff); total += Time.time - start; } // 더할거에요 -> 현재 조준 중인 시간을
|
|
|
|
|
|
return total; // 반환할거에요 -> 총 시간을
|
2026-02-20 16:39:14 +00:00
|
|
|
|
}
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-13 09:11:54 +00:00
|
|
|
|
private float CalculatePierceRatio() // 함수를 선언할거에요 -> 관통 비율 계산을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
int total = CountInWindow(totalShotTimestamps, windowDuration); // 셀거에요 -> 전체 발사를
|
|
|
|
|
|
if (total == 0) return 0f; // 반환할거에요 -> 0을
|
|
|
|
|
|
int pierce = CountInWindow(pierceShotTimestamps, windowDuration); // 셀거에요 -> 관통 발사를
|
|
|
|
|
|
return (float)pierce / total; // 반환할거에요 -> 비율을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
2026-02-12 15:23:25 +00:00
|
|
|
|
}
|