스킬 레벨 증감 로직과 포인트 트랜잭션 설계
1. 시스템 요구 사항
이 게시글의 스킬 UI는 데이터 기반 동적 생성을 중심으로 설계했다.
스킬 개수와 구성은 PlayerManager의 데이터 배열이 결정하며, UI는 그 데이터를 읽어 프리팹을 생성하고 아이콘과 이름을 바인딩한다.
버튼 이벤트는 각 항목이 정확한 인덱스를 참조하도록 캡처 변수를 사용해 클로저 문제를 예방했고, 입력은 Update 기반 폴링이 아니라 Button.onClick 이벤트 기반으로 처리하여 불필요한 프레임 연산을 줄였다.
또한 생성(CreateSkillList)과 상태 동기화(UpdateSkillList)를 분리해, UI가 재생성 없이도 상태만 갱신할 수 있는 구조를 만들었다.
이 분리는 이후 게시글에서 다룰 레벨 변경 규칙, 스킬 포인트 트랜잭션, 오류 처리를 UI 생성 구조와 섞지 않게 해주며, 기능 확장 시에도 각 책임이 명확하게 유지되도록 돕는다.
2. 설계 목표
- 스킬 포인트와 레벨 변경을 하나의 트랜잭션 흐름으로 설계
- 최대/최소 레벨 제한을 명확하게 구조화
- 포인트 차감과 복구의 대칭성 유지
- 실패 시 데이터 변경 완전 차단
- 상태 변경 이후 UI와 스탯을 즉시 동기화
3. 흐름도
(+ / - 버튼 클릭)
↓
UpdateSkillLevel(skillWD, index, isIncreasing)
↓
조건 검증
↓
(성공) → 포인트 변경 + 레벨 변경
(실패) → 오류 메시지 + return
↓
UI 텍스트 갱신
↓
statusManager.SetSkillLevelText()
이 구조는 입력을 받은 뒤 반드시 '검증 → 변경 → 반영' 순서를 유지한다.
검증 실패 시에는 반드시 return으로 흐름을 차단해 데이터 일관성을 보장한다.
4. 구현
4.1. 레벨 증감의 단일 진입점
void UpdateSkillLevel(GameObject skillWD, int index, bool isIncreasing)
{
PlayerSkillData skill = playerManager.PlayerSkills[index];
if (isIncreasing)
{
if (playerManager.CurrentSkillPoint > 0
&& skill.CurrentLevel < 5
&& skill.IsAcquisition)
{
playerManager.CurrentSkillPoint--;
skill.CurrentLevel++;
}
else if (skill.CurrentLevel == 5)
{
errorTxt.text = "더 이상 증가 불가능!!";
StartCoroutine(ErrorActive(0.5f));
return;
}
}
else
{
if (skill.CurrentLevel > 1)
{
playerManager.CurrentSkillPoint++;
skill.CurrentLevel--;
}
else if (skill.CurrentLevel == 1 && skill.IsAcquisition)
{
errorTxt.text = "더 이상 감소 불가능!!";
StartCoroutine(ErrorActive(0.5f));
return;
}
}
haveSkillPoint.text = "스킬 포인트 : " + playerManager.CurrentSkillPoint.ToString();
skillWD.transform.Find("SkillLevel")
.GetComponent<TextMeshProUGUI>().text
= skill.CurrentLevel.ToString();
statusManager.SetSkillLevelText();
}UpdateSkillLevel은 스킬 레벨 변경의 유일한 진입점이다.
증가와 감소를 별도 함수로 나누지 않고 하나의 함수에서 bool isIncreasing 플래그로 구분한 이유는, 두 로직이 구조적으로 동일한 트랜잭션 흐름을 공유하기 때문이다.
만약 분리하면 검증, UI 갱신, 스탯 반영 코드가 중복되기 쉽고 유지보수 지점이 늘어난다.
함수 초반에서 PlayerSkillData skill을 지역 변수로 가져오는 것은 가독성과 반복 접근 비용을 줄이기 위한 선택이다.
배열 접근을 매번 playerManager.PlayerSkills[index]로 수행하는 대신, 한 번 참조를 확보하면 이후 코드가 명확해진다.
증가 로직에서는 세 가지 조건을 동시에 검사한다.
먼저 CurrentSkillPoint가 0보다 커야 한다는 것은 포인트가 존재해야 레벨을 올릴 수 있다는 의미다.
두 번째로 CurrentLevel이 5 미만이어야 한다.
여기서 5는 최대 레벨이다.
이 값은 하드코딩되어 있지만, 현재 프로젝트 설계에서 모든 스킬이 동일한 최대 레벨을 가진다는 전제이므로 단순성과 가독성을 우선한 선택이다.
이후 확장 시에는 SkillData 내부에 MaxLevel 필드를 두는 구조로 확장 가능하다.
세 번째 조건인 skill.IsAcquisition은 해당 스킬이 획득된 상태인지 확인하는 역할을 한다.
스킬을 획득하지 않은 상태에서 레벨을 올리는 것은 구조적으로 허용되지 않는다.
이 조건을 모두 통과했을 때만 포인트를 감소시키고 레벨을 증가시킨다.
여기서 중요한 점은 포인트 감소와 레벨 증가가 한 블록 안에 존재한다는 것이다.
이 두 연산은 반드시 동시에 이루어져야 하는 하나의 트랜잭션이다.
만약 포인트만 감소하고 레벨이 증가하지 않거나, 반대로 레벨만 증가하고 포인트가 감소하지 않으면 데이터 일관성이 깨진다.
최대 레벨에 도달한 상태에서 증가를 시도하면 errorTxt.text에 메시지를 넣고 StartCoroutine을 호출한 뒤 return으로 함수 실행을 종료한다.
return을 명시적으로 사용한 이유는, 이후 UI 갱신 코드가 실행되지 않도록 하기 위함이다.
실패 시에는 어떠한 상태 변경도 일어나지 않아야 한다는 설계 원칙을 지키는 구조다.
감소 로직은 증가와 대칭 구조를 가진다.
CurrentLevel이 1보다 클 때만 감소가 가능하다.
최소 레벨을 1로 둔 이유는 스킬을 획득한 상태라면 최소 1레벨은 유지한다는 설계 의도 때문이다.
레벨을 감소시키면 사용했던 포인트를 복구하는 방식으로 CurrentSkillPoint를 증가시킨다.
이 역시 증가 로직과 대칭 구조를 유지해, 사용한 포인트가 정확히 복구되도록 한다.
레벨 변경이 성공적으로 이루어진 뒤에는 UI 텍스트를 즉시 갱신한다.
haveSkillPoint.text는 현재 포인트를 문자열로 변환해 표시한다.
ToString은 C#의 기본 형변환 메서드이며, UI 텍스트는 문자열만 출력할 수 있기 때문에 필수적으로 사용된다.
이후 해당 스킬 항목의 SkillLevel 텍스트를 갱신한다.
transform.Find로 자식 오브젝트를 찾아 텍스트를 설정하는 방식은 단순하고 즉각적이다.
단점은 계층 탐색 비용이 있지만, 버튼 클릭 시에만 실행되므로 성능 부담은 크지 않다.
마지막으로 statusManager.SetSkillLevelText()를 호출한다.
이 함수는 스킬 레벨 변화가 실제 플레이어 스탯에 반영되도록 만드는 역할을 한다.
즉, UI 변경으로 끝나는 것이 아니라 게임 로직에 영향을 주는 계산이 이어진다.
이 호출을 레벨 변경 이후에 배치함으로써 '데이터 변경 → 스탯 재계산' 순서를 명확히 유지한다.
4.2. 일시적 오류 피드백 처리
IEnumerator ErrorActive(float time)
{
errorPanel.SetActive(true);
yield return new WaitForSeconds(time);
errorPanel.SetActive(false);
}ErrorActive는 일정 시간 동안 오류 패널을 활성화했다가 자동으로 비활성화하는 코루틴이다.
Unity의 Coroutine은 별도의 스레드를 생성하는 것이 아니라, 메인 스레드에서 yield 지점까지 실행한 뒤 다음 프레임들에서 이어서 실행하는 구조다.
WaitForSeconds는 지정된 시간만큼 대기하도록 만드는 Unity의 yield instruction이다.
이 구조를 Update 기반 타이머로 구현할 수도 있다.
예를 들어 float timer를 증가시키다가 일정 시간이 지나면 패널을 끄는 방식이다.
하지만 그 방식은 매 프레임 조건을 검사해야 하고, 코드 가독성이 떨어진다.
코루틴은 잠깐 보여주고 자동으로 끈다는 의도를 코드 흐름 자체로 표현한다는 점에서 가독성이 높다.
단점은 동일 오류가 연속 발생하면 코루틴이 중첩 실행될 수 있다는 점이다.
다만 현재 구조에서는 마지막 코루틴 종료 시점에 패널이 꺼지므로 큰 문제는 되지 않는다.
더 엄격하게 관리하려면 StopCoroutine 또는 플래그 기반 제어를 추가할 수 있다.
5. 개발 의도
이 스킬 레벨 시스템은 단순한 숫자 증감이 아니라, 포인트라는 자원을 사용하는 트랜잭션 구조로 설계했다.
모든 레벨 변경은 조건 검증을 통과한 뒤에만 수행되며, 실패 시에는 즉시 return으로 흐름을 차단한다.
증가와 감소는 대칭 구조를 유지해 포인트 손실이나 중복 지급이 발생하지 않도록 설계했다.
또한 레벨 변경 이후에는 UI 갱신과 스탯 재계산을 즉시 수행해, 화면 표시와 실제 게임 로직이 항상 동기화되도록 했다.
오류 피드백은 코루틴 기반으로 구현해 사용자가 명확한 피드백을 받도록 하면서도, 메인 로직 흐름을 복잡하게 만들지 않았다.
이 구조는 데이터 일관성과 사용자 경험을 동시에 고려한 설계이며, UI와 게임 로직 사이의 연결 고리를 명확히 드러내는 구조다.
