플레이어 환경 설정 UI 구조

목차

1. 요구 사항

2. 설계 목표

3. 흐름도

4. 구현

        4.1. 초기화 구조

       4.2. 입력 값 제공 계층

       4.3. 조작키

5. 개발 의도

1. 시스템 요구 사항

게임 플레이 중 플레이어가 체감하는 조작감은 마우스 회전 감도 같은 환경 설정 값에 의해 크게 달라진다.

따라서 프로젝트에는 사용자가 설정 UI에서 슬라이더를 조절하면, 그 입력값이 즉시 게임 플레이(카메라/시야 회전)에 반영되는 구조가 필요하다.

본 프로젝트는 UGUI가 아니라 NGUI 기반이므로, UISlider.value(0~1)를 실제 게임에서 사용할 의미 있는 수치(예: 회전 속도 10~600)로 변환해 제공해야 한다.

이때 SettingUi_는 오디오를 재생하거나 카메라를 직접 회전시키는 실행 계층이 아니라, 현재 설정값을 계산하여 제공하는 UI 계층으로 동작해야 한다.

또한 조작키 안내처럼 화면 표시 여부만 토글하는 UI도 필요하며, 복잡한 상태 저장 없이도 현재 활성 상태를 기준으로 즉시 on/off가 가능해야 한다.

2. 설계  목표

- 카메라/시야 회전 계층은 SettingUi_에서 값을 받아 소비하는 구조로 구성

- 조작키 안내는 GameObject 활성 상태를 통한 단순 토글 구현

3. 흐름도

[NGUI SettingUi_ (View/Input Provider)]

│ 사용자가 UISlider 조작 (0~1 값 변경)

[SettingUi_.MouseSensitivity()]

│ Mathf.Lerp로 0~1을 10~600으로 매핑 후 반환

[MainCamera.Update()]

│ 매 프레임 최신 감도 값을 조회하여 rotationSpeed에 반영

[CameraRotation()]

입력(Move X/Y)과 결합되어 시야 회전 결과가 결정됨

이 흐름의 핵심은 SettingUi_가 설정값을 저장/전달하는 UI 계층이고, MainCamera가 그 값을 소비해 실제 회전을 수행하는 실행 계층이라는 분리다.

슬라이더 값이 바뀌면 SettingUi_는 다음 호출에서 즉시 다른 값을 반환하고, MainCamera는 매 프레임 이를 다시 읽기 때문에 별도의 이벤트 연결 없이도 항상 최신 설정이 적용된다.

4. 구현

4.1. 초기화 구조
void Awake()
{
    if (instance == null)
    {
        instance = this;
        DontDestroyOnLoad(this.gameObject);
    }
    else Destroy(this.gameObject);
        LoadResource();
}

void LoadResource()
{
    mouseSlider = GameObject.Find("MouseSlider").GetComponent<UISlider>();
    bgmSlider = GameObject.Find("BgmSlider").GetComponent<UISlider>();
    sfxSlider = GameObject.Find("SfxSlider").GetComponent<UISlider>();
}

Awake에서 싱글톤을 구성하고 DontDestroyOnLoad를 적용한 이유는 설정 UI는 씬이 바뀌어도 지속되는 전역 서비스 성격이기 때문이다.

Unity에서 씬 로드가 발생하면 기본적으로 오브젝트가 파괴되는데, 환경 설정이 씬마다 초기화되면 플레이 경험이 불안정해진다.

따라서 한 번 생성된 SettingUi_ 인스턴스를 유지하고, 중복 생성 시 Destroy로 제거해 단일 진실 원천을 강제한다.

LoadResource는 NGUI 위젯을 런타임에 찾아 캐싱한다.

NGUI 프로젝트에서는 UGUI처럼 직관적인 인스펙터 바인딩이나 자동 참조가 없는 경우가 많아서, GameObject.Find 기반으로 빠르게 구성하는 패턴이 자주 사용된다.

장점은 인스펙터 세팅 의존을 줄이고 프리팹 구성이 단순해진다는 점이다.

반대로 단점은 오브젝트 이름 변경에 취약하고, Find 자체가 탐색 비용을 가진다는 점이다.

다만 이 코드는 Awake에서 한 번만 탐색하고 이후에는 캐싱된 레퍼런스를 쓰므로, 매 프레임 탐색 같은 치명적인 비용 구조는 피하고 있다.

프로젝트가 커지면 SerializeField로 직접 주입하거나, UI 루트에서 레퍼런스를 묶어 전달하는 방식으로 개선할 수 있다.

4.2. 입력 값 제공 계층
// SettingUi_.cs
public float MouseSensitivity()
{
    mouseSensitivity = Mathf.Lerp(10f, 600f, mouseSlider.value);
    return mouseSensitivity;
}

UISlider.value는 NGUI에서 0~1 정규화 값이다.

그런데 실제 게임에서 필요한 감도는 회전 속도라는 물리적 의미를 가진 값이며, 보통 더 넓은 범위를 사용한다.

Mathf.Lerp는 선형 보간 함수로, 시작값(10)과 끝값(600) 사이를 t(0~1) 비율로 변환한다.

즉 슬라이더가 0이면 10, 1이면 600, 중간이면 그 사이 값이 된다.

이 방식의 장점은 UI의 표현 범위(슬라이더 0~1)와 실제 게임 파라미터 범위를 분리할 수 있다는 점이다.

UI는 항상 0~1만 다루고, 게임 로직에서 필요한 의미 있는 숫자는 반환값으로 제공된다.

단점은 감도 체감이 사람에 따라 선형이 아닐 수 있다는 점이다.

예를 들어 낮은 감도 구간은 더 촘촘하고 높은 감도 구간은 더 빠르게 변해야 하는 경우가 많다.

그런 요구가 생기면 Lerp 대신 커브(제곱/로그)나 AnimationCurve를 적용해 비선형 매핑을 만들면 된다.

현재는 단순성과 예측 가능한 조절감을 우선해서 선형 매핑을 선택한 구현이다.

// MainCamera.cs
void Update()
{
    if(SettingUi_.instance != null) rotationSpeed = SettingUi_.instance.MouseSensitivity();
    ...
}

MouseSensitivity를 MainCamera.Update에서 매 프레임 호출하는 이유는 설정 변경이 즉시 반영되어야 하는 값이기 때문이다.

Unity 입력과 카메라 회전은 프레임 단위로 갱신되며, 사용자가 슬라이더를 움직이는 동안에도 즉각적인 체감을 주는 것이 자연스럽다.

FixedUpdate는 물리 틱 기반이라 UI/시야 갱신과 박자가 어긋날 수 있고, 프레임 단위의 입력 반응성을 요구하는 카메라 제어에는 Update가 더 적합하다.

Update 기반의 Polling 구조는 이벤트 연결이 필요 없어서 의존성 관리가 단순해진다.

반면 값이 바뀌지 않아도 매 프레임 호출된다는 단점이 있지만, 여기서는 단순한 수치 계산(Mathf.Lerp)과 대입만 수행하므로 비용이 매우 낮다.

즉, 항상 최신 상태 보장을 위해 Polling을 선택했고, 현재 규모에서는 충분히 합리적인 트레이드오프다.

4.3. 조작키
public void Show()
{
    if (showKey.activeSelf) showKey.SetActive(false);
    else showKey.SetActive(true);
}

Show는 조작키 안내 패널을 토글하는 UI 함수다.

NGUI에서는 버튼 클릭 이벤트에 함수를 직접 연결하는 방식이 일반적이며, 이 함수는 클릭될 때마다 showKey 오브젝트의 활성 상태를 반전시킨다.

activeSelf는 현재 오브젝트가 스스로 활성화 상태인지를 의미한다.

이 구조는 별도의 bool 플래그를 두지 않고, GameObject의 활성 상태 자체를 진실값으로 사용한다.

장점은 상태가 중복 저장되지 않는다는 점이다.

bool을 따로 두면 bool은 true인데 오브젝트는 꺼져 있는 불일치가 생길 수 있는데, 여기서는 그런 문제가 원천 차단된다.

단점은 UI 표시 상태가 곧 로직 상태가 되므로, UI 구조 변경이 곧 동작 변경으로 이어질 수 있다는 결합이 생긴다.

하지만 이 기능은 표시만 하는 보조 UI이며, 프로젝트 규모 기준으로 단순성이 더 중요하다고 판단한 선택이다.

5. 개발 의도

이 게시글에서 보여주려는 포인트는 NGUI 기반 프로젝트에서도 환경 설정 값을 UI에서 계산해 제공하고, 실행 계층이 이를 소비하는 구조를 만들 수 있다는 점이다.

SettingUi_는 카메라를 직접 돌리거나 오디오를 직접 제어하지 않는다.

대신 슬라이더 입력을 게임에서 의미 있는 수치로 변환해 반환하고, MainCamera 같은 실행 계층이 그 값을 받아 실제 회전 동작을 수행한다.

이렇게 역할을 분리하면 설정 UI의 구조가 바뀌더라도 카메라 로직은 최소 수정으로 유지될 수 있고, 반대로 카메라 회전 방식이 개선되더라도 UI는 값 제공 역할만 유지하면 된다.

결과적으로 이 구조는 확장(비선형 감도 커브, 저장/로드, 프리셋 적용)에도 자연스럽게 이어지는 기반이 된다.