Projext/Assets/02_Scripts/Player/Stats/PlayerLevelSystem.cs

197 lines
14 KiB
C#
Raw Normal View History

2026-02-12 15:23:25 +00:00
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
using System.Collections.Generic; // 리스트 기능을 사용할거에요 -> System.Collections.Generic을
using TMPro; // 텍스트메쉬프로 기능을 사용할거에요 -> TMPro를
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.UI; // UI 기능을 사용할거에요 -> UnityEngine.UI를
2026-01-29 06:58:38 +00:00
2026-02-12 15:23:25 +00:00
public class PlayerLevelSystem : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerLevelSystem을
2026-01-29 06:58:38 +00:00
{
2026-02-12 15:23:25 +00:00
[Header("--- 참조 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 참조 --- 를
[SerializeField] private Stats stats; // 변수를 선언할거에요 -> 스탯 스크립트인 stats를
[SerializeField] private PlayerHealth pHealth; // 변수를 선언할거에요 -> 체력 스크립트인 pHealth를
2026-02-12 15:23:25 +00:00
[Header("레벨 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 레벨 설정 을
public int level = 1; // 변수를 초기화할거에요 -> 현재 레벨을 1로
public int currentExp = 0; // 변수를 초기화할거에요 -> 현재 경험치를 0으로
[SerializeField] private int[] expTable; // 배열을 선언할거에요 -> 레벨별 필요 경험치 테이블인 expTable을
2026-01-29 06:58:38 +00:00
2026-02-12 15:23:25 +00:00
[Header("UI")] // 인스펙터 창에 제목을 표시할거에요 -> UI 를
[SerializeField] private Image expFillImage; // 변수를 선언할거에요 -> 경험치바 이미지인 expFillImage를
[SerializeField] private TextMeshProUGUI expText; // 변수를 선언할거에요 -> 경험치 텍스트인 expText를
[SerializeField] private TextMeshProUGUI levelText; // 변수를 선언할거에요 -> 레벨 텍스트인 levelText를
2026-01-29 06:58:38 +00:00
[Header("UI 자동 탐색 이름 (폴백 모드용)")] // 인스펙터 창에 제목을 표시할거에요 -> UI 자동 탐색 설정을
[Tooltip("경험치 Fill 이미지의 오브젝트 이름 (자동 탐색용)")] // 툴팁을 달거에요 -> 설명을
[SerializeField] private string expFillName = "EXP_Filled"; // 변수를 선언할거에요 -> 경험치바 Fill 오브젝트 이름을
[Tooltip("경험치 텍스트의 오브젝트 이름 (자동 탐색용)")] // 툴팁을 달거에요 -> 설명을
[SerializeField] private string expTextParentName = "EXP_Bar"; // 변수를 선언할거에요 -> 경험치 텍스트 부모 이름을
[Tooltip("레벨 텍스트 부모 오브젝트 이름 (자동 탐색용)\n" +
"이 오브젝트의 자식 TMP를 레벨 텍스트로 사용합니다")] // 툴팁을 달거에요 -> 설명을
[SerializeField] private string levelTextParentName = "Power Value"; // 변수를 선언할거에요 -> 레벨 텍스트 부모 오브젝트 이름을
2026-02-12 15:23:25 +00:00
public static System.Action OnLevelUp; // 이벤트를 선언할거에요 -> 레벨업 시 호출될 정적 이벤트 OnLevelUp을
2026-01-29 06:58:38 +00:00
2026-02-12 15:23:25 +00:00
private int RequiredExp // 프로퍼티를 선언할거에요 -> 필요 경험치를 반환하는 RequiredExp를
2026-01-29 06:58:38 +00:00
{
get
{
2026-02-12 15:23:25 +00:00
int index = level - 1; // 인덱스를 계산할거에요 -> 레벨에서 1을 뺀 값으로
if (index >= expTable.Length) return expTable[expTable.Length - 1]; // 조건이 맞으면 반환할거에요 -> 만렙이면 마지막 필요 경험치를
return expTable[index]; // 반환할거에요 -> 현재 레벨의 필요 경험치를
2026-01-29 06:58:38 +00:00
}
}
private void Awake() // 함수를 실행할거에요 -> 초기화 시 UI 자동 탐색을
{
AutoFindUI(); // 실행할거에요 -> UI 요소 자동 탐색을 (인스펙터 참조가 비어있을 경우)
}
2026-02-12 15:23:25 +00:00
private void OnEnable() { MonsterClass.OnMonsterKilled += GainExp; UpdateExpUI(); } // 함수를 실행할거에요 -> 몬스터 처치 이벤트 구독과 UI 갱신을
private void OnDisable() { MonsterClass.OnMonsterKilled -= GainExp; } // 함수를 실행할거에요 -> 몬스터 처치 이벤트 구독 해제를
2026-01-29 06:58:38 +00:00
2026-02-12 15:23:25 +00:00
void GainExp(int amount) // 함수를 선언할거에요 -> 경험치를 획득하는 GainExp를
2026-01-29 06:58:38 +00:00
{
2026-02-12 15:23:25 +00:00
currentExp += amount; // 값을 더할거에요 -> 획득한 경험치를 현재 경험치에
while (currentExp >= RequiredExp) { currentExp -= RequiredExp; LevelUp(); } // 반복할거에요 -> 경험치가 꽉 찼다면 레벨업을
UpdateExpUI(); // 실행할거에요 -> UI 갱신 함수를
2026-01-29 06:58:38 +00:00
}
2026-02-12 15:23:25 +00:00
void LevelUp() // 함수를 선언할거에요 -> 레벨업 처리를 하는 LevelUp을
2026-01-29 06:58:38 +00:00
{
2026-02-12 15:23:25 +00:00
if (level >= expTable.Length + 1) { currentExp = 0; return; } // 조건이 맞으면 중단할거에요 -> 최대 레벨이라면 경험치만 비우고 리턴
level++; // 값을 증가시킬거에요 -> 레벨을 1만큼
// ✨ 힘 대신 공격력(+10) 증가
2026-02-12 15:23:25 +00:00
if (stats != null) stats.AddBaseLevelUpStats(1000f, 10f); // 실행할거에요 -> 스탯 증가 함수를 (체력 1000, 공격력 10)
2026-01-29 06:58:38 +00:00
2026-02-12 15:23:25 +00:00
if (pHealth != null) pHealth.RefreshHealthUI(); // 실행할거에요 -> 체력 UI 갱신을
StartCoroutine(DelayedCardPopup()); // 코루틴을 시작할거에요 -> 카드 선택 팝업 지연 실행을
}
2026-01-29 06:58:38 +00:00
2026-02-12 15:23:25 +00:00
private IEnumerator DelayedCardPopup() // 코루틴 함수를 선언할거에요 -> 카드 팝업을 지연시키는 DelayedCardPopup을
{
2026-02-12 15:23:25 +00:00
yield return new WaitForSeconds(1.5f); // 기다릴거에요 -> 1.5초 동안
OnLevelUp?.Invoke(); // 이벤트를 실행할거에요 -> 레벨업 알림 이벤트를
2026-01-29 06:58:38 +00:00
}
2026-02-12 15:23:25 +00:00
void UpdateExpUI() // 함수를 선언할거에요 -> 경험치 UI를 갱신하는 UpdateExpUI를
2026-01-29 06:58:38 +00:00
{
2026-02-12 15:23:25 +00:00
float fill = (float)currentExp / RequiredExp; // 비율을 계산할거에요 -> 현재 경험치 나누기 필요 경험치로
if (expFillImage != null) expFillImage.fillAmount = fill; // 값을 설정할거에요 -> 게이지 채움 정도를
if (expText != null) expText.text = $"{currentExp} / {RequiredExp}"; // 텍스트를 바꿀거에요 -> 현재/필요 경험치 수치로
if (levelText != null) levelText.text = $"Lv. {level}"; // 텍스트를 바꿀거에요 -> 현재 레벨로
2026-01-29 06:58:38 +00:00
}
// ─────────────────────────────────────────────────────────────
// UI 자동 탐색 (폴백 모드에서 인스펙터 참조가 끊겼을 때)
// ─────────────────────────────────────────────────────────────
/// <summary>
/// 인스펙터에서 연결이 안 된 UI 요소를 이름으로 자동 탐색합니다.
/// 프리팹으로 생성 시 크로스 프리팹 참조가 끊기는 문제를 해결합니다.
/// </summary>
private void AutoFindUI() // 함수를 정의할거에요 -> UI 자동 탐색을
{
// ── 1. 경험치 Fill 이미지 탐색 ──
if (expFillImage == null) // 조건이 맞으면 실행할거에요 -> Fill 이미지가 비어있으면
{
GameObject fillObj = GameObject.Find(expFillName); // 찾을거에요 -> 이름으로 Fill 오브젝트를
if (fillObj != null) // 조건이 맞으면 실행할거에요 -> 찾았다면
{
expFillImage = fillObj.GetComponent<Image>(); // 가져올거에요 -> Image 컴포넌트를
if (expFillImage != null) Debug.Log($"[PlayerLevelSystem] expFillImage 자동 탐색 성공: {fillObj.name}"); // 로그를 찍을거에요
}
}
// ── 2. 경험치 텍스트 탐색 (EXP_Bar 하위의 TMP) ──
if (expText == null) // 조건이 맞으면 실행할거에요 -> 경험치 텍스트가 비어있으면
{
GameObject parentObj = GameObject.Find(expTextParentName); // 찾을거에요 -> EXP_Bar 부모 오브젝트를
if (parentObj != null) // 조건이 맞으면 실행할거에요 -> 부모를 찾았다면
{
expText = parentObj.GetComponentInChildren<TextMeshProUGUI>(true); // 가져올거에요 -> 자식의 TMP 컴포넌트를 (비활성 포함)
if (expText != null) Debug.Log($"[PlayerLevelSystem] expText 자동 탐색 성공: {expText.gameObject.name}"); // 로그를 찍을거에요
}
}
// ── 3. 레벨 텍스트 탐색 ("Power Value" 하위의 TMP) ──
if (levelText == null) // 조건이 맞으면 실행할거에요 -> 레벨 텍스트가 비어있으면
{
// 부모 오브젝트 이름으로 찾고, 그 자식의 TMP를 가져오기
GameObject lvlParent = GameObject.Find(levelTextParentName); // 찾을거에요 -> "Power Value" 부모 오브젝트를
if (lvlParent != null) // 조건이 맞으면 실행할거에요 -> 부모를 찾았다면
{
levelText = lvlParent.GetComponentInChildren<TextMeshProUGUI>(true); // 가져올거에요 -> 자식의 TMP 컴포넌트를 (비활성 포함)
if (levelText != null) Debug.Log($"[PlayerLevelSystem] levelText 자동 탐색 성공: {lvlParent.name} → {levelText.gameObject.name}"); // 로그를 찍을거에요
}
// 부모로도 못 찾았으면 "power" 또는 "level" 키워드로 전체 탐색
if (levelText == null) // 조건이 맞으면 실행할거에요 -> 여전히 없다면
{
TextMeshProUGUI[] allTexts = FindObjectsOfType<TextMeshProUGUI>(true); // 찾을거에요 -> 씬의 모든 TMP를
for (int i = 0; i < allTexts.Length; i++) // 반복할거에요 -> 각 TMP마다
{
// 부모 오브젝트 이름도 함께 확인 (자신 또는 부모에 키워드가 있는지)
string objName = allTexts[i].gameObject.name.ToLower(); // 변환할거에요 -> 오브젝트 이름을 소문자로
string parentName = (allTexts[i].transform.parent != null) // 가져올거에요 -> 부모 이름을
? allTexts[i].transform.parent.name.ToLower() : ""; // 부모가 없으면 빈 문자열로
bool isMatch = objName.Contains("power") || objName.Contains("level") // 조건을 검사할거에요 -> 이름에 키워드가 있는지
|| parentName.Contains("power") || parentName.Contains("level"); // 또는 부모 이름에 키워드가 있는지
bool isNotExp = !objName.Contains("exp") && !parentName.Contains("exp"); // 조건을 검사할거에요 -> EXP 관련이 아닌지
if (isMatch && isNotExp) // 조건이 맞으면 실행할거에요 -> 매칭되고 EXP가 아니면
{
levelText = allTexts[i]; // 설정할거에요 -> 레벨 텍스트로
Debug.Log($"[PlayerLevelSystem] levelText 키워드 탐색 성공: {levelText.gameObject.name} (부모: {parentName})"); // 로그를 찍을거에요
break; // 중단할거에요 -> 첫 번째 매칭된 것으로 충분
}
}
}
}
// ── 4. 탐색 결과 로그 ──
if (expFillImage == null) Debug.LogWarning("[PlayerLevelSystem] ⚠️ expFillImage를 찾지 못했습니다!"); // 경고를 찍을거에요
if (expText == null) Debug.LogWarning("[PlayerLevelSystem] ⚠️ expText를 찾지 못했습니다!"); // 경고를 찍을거에요
if (levelText == null) Debug.LogWarning($"[PlayerLevelSystem] ⚠️ levelText를 찾지 못했습니다! ('{levelTextParentName}' 하위에 TMP가 있는지 확인하세요)"); // 경고를 찍을거에요
}
// ─────────────────────────────────────────────────────────────
// 외부 연결 API (DungeonSceneSetup 폴백 모드용)
// ─────────────────────────────────────────────────────────────
/// <summary>
/// 런타임에서 플레이어 Stats와 PlayerHealth 참조를 설정합니다.
/// DungeonSceneSetup의 폴백 모드에서 호출됩니다.
/// </summary>
public void SetPlayerReferences(Stats newStats, PlayerHealth newHealth) // 함수를 정의할거에요 -> 플레이어 참조 런타임 설정을
{
if (newStats != null) stats = newStats; // 설정할거에요 -> 새 스탯 참조를
if (newHealth != null) pHealth = newHealth; // 설정할거에요 -> 새 체력 참조를
AutoFindUI(); // 실행할거에요 -> UI가 아직 안 찾아졌으면 다시 탐색
UpdateExpUI(); // 실행할거에요 -> UI 즉시 갱신을
Debug.Log("[PlayerLevelSystem] 플레이어 참조 연결 완료"); // 로그를 찍을거에요
}
/// <summary>
/// 런타임에서 UI 참조를 직접 설정합니다.
/// DungeonSceneSetup이 UI 요소를 찾아서 전달할 때 사용합니다.
/// </summary>
public void SetUIReferences(Image newExpFill, TextMeshProUGUI newExpText, TextMeshProUGUI newLevelText) // 함수를 정의할거에요 -> UI 참조 직접 설정을
{
if (newExpFill != null) expFillImage = newExpFill; // 설정할거에요 -> 경험치 Fill 이미지를
if (newExpText != null) expText = newExpText; // 설정할거에요 -> 경험치 텍스트를
if (newLevelText != null) levelText = newLevelText; // 설정할거에요 -> 레벨 텍스트를
UpdateExpUI(); // 실행할거에요 -> UI 즉시 갱신을
Debug.Log("[PlayerLevelSystem] UI 참조 직접 설정 완료"); // 로그를 찍을거에요
}
/// <summary>
/// 외부에서 강제로 UI를 갱신할 때 사용합니다.
/// DungeonSceneSetup의 지연 갱신에서 호출됩니다.
/// </summary>
public void ForceRefreshUI() // 함수를 정의할거에요 -> 강제 UI 갱신을
{
AutoFindUI(); // 실행할거에요 -> UI 재탐색을 (혹시 이전에 못 찾았으면)
UpdateExpUI(); // 실행할거에요 -> UI 갱신을
}
}