독수파출소 개발일지 #3 – 112 신고 시스템, Unity로 구현하기

설계는 됐다. 이제 실제로 만들어야 했다.

구현하면서 막혔던 것들, 처음 알게 된 것들을 그대로 기록해둔다. Unity를 처음 쓰는 사람이라면 비슷한 벽을 만날 테니까.


신고 데이터를 어떻게 구조화할까

첫 번째 고민은 신고 데이터를 어떻게 담느냐였다. 신고 유형, 무전 내용, 현장 위치, 도착 후 대화까지. 이걸 코드 안에 직접 넣으면 나중에 신고를 추가할 때마다 코드를 수정해야 한다.

Claude한테 물어봤더니 ScriptableObject를 쓰라고 했다.

“ScriptableObject는 코드 없이 데이터만 담는 에셋이에요. 신고 하나가 에셋 파일 하나가 되는 거예요. 나중에 신고 추가할 때 코드 안 건드리고 에셋만 만들면 돼요.”

이게 뭔지 처음엔 몰랐다. 근데 직접 써보니까 바로 이해됐다. Unity의 Project 창에서 우클릭 → Create로 신고 데이터를 파일처럼 만들 수 있었다. 마치 엑셀 시트 하나를 만드는 느낌이었다.

CallData 스크립트는 이렇게 만들었다.

[CreateAssetMenu(fileName = "NewCall", menuName = "DoksuPolice/Call Data")]
public class CallData : ScriptableObject
{
public string callType; // 신고 유형
public string callDescription; // 무전 내용
public Vector3 location; // 현장 위치
public DialogueData dialogueOnArrival; // 도착 후 대화
}

[CreateAssetMenu]가 붙으면 Project 창에서 우클릭 메뉴로 이 데이터를 바로 만들 수 있게 된다. 소음신고, 단순시비, 무전취식… 신고 하나마다 에셋 파일 하나.


무전 알림 UI

신고가 들어오면 화면 상단에 붉은 패널로 무전 내용이 뜨게 했다. Canvas에 Panel을 만들고, 안에 TextMeshPro 텍스트를 넣는 구조다.

처음에 헷갈렸던 건 Anchor 설정이었다. 어떻게 클릭하라는 것인지 잘 몰라서 계속 질문했다.

Q : 화면 상단에 고정시키고 싶은데 화면 크기가 바뀌면 위치가 달라져요.

A : “Shift + Alt를 누른 채 상단 중앙 Anchor를 클릭하면 위치와 Pivot이 동시에 설정돼요. 그러면 해상도가 바뀌어도 항상 상단 중앙에 붙어 있어요.”

이걸 알고 나서 UI 배치가 훨씬 편해졌다.

신고 패널은 3초만 보이고 자동으로 사라지게 했다. 처음엔 Invoke를 썼다가 문제가 생겼다.

Q : 대화창이 시간이 지나도 안 닫혀요.

A : “Time.timeScale = 0f로 시간을 멈춰놓으면 Invoke도 같이 멈춰요. WaitForSecondsRealtime을 쓰는 코루틴으로 바꿔야 해요. 이건 게임 시간이 아니라 실제 시간 기준이라 timeScale에 영향을 안 받아요.”

WaitForSeconds와 WaitForSecondsRealtime의 차이를 이때 처음 알았다.


바닥에 찍히는 빨간 마커

신고 위치를 표시하는 마커는 납작한 Cylinder 오브젝트로 만들었다. Scale을 (1, 0.1, 1)로 설정하면 바닥에 깔리는 원형 마커가 된다. 빨간 머티리얼을 입히고, 평소엔 비활성화해뒀다가 신고가 들어올 때만 켜지게.


랜덤 신고 발생 로직

처음엔 T키로 수동으로 신고를 발생시켜서 테스트했다. 시스템이 잘 돌아가는 걸 확인한 다음, 자동으로 랜덤 신고가 들어오도록 바꿨다.

CallManager에 신고 배열을 넣고, 일정 간격마다 랜덤으로 하나를 골라서 발생시키는 구조다.

IEnumerator AutoCallRoutine()
{
    yield return new WaitForSecondsRealtime(5f); // 시작 5초 후 첫 신고

    while (true)
    {
        if (!_callActive)
        {
            TriggerRandomCall();
        }
        float waitTime = Random.Range(minInterval, maxInterval);
        yield return new WaitForSecondsRealtime(waitTime);
    }
}

minInterval과 maxInterval은 Inspector에서 조정할 수 있게 했다. 테스트할 땐 10~25초, 실제 게임에서는 특정 스토리나 이벤트 후에 발생하도록 바꿀 예정이다.


신고 → 현장 → 대화로 연결하기

신고가 발생하고 마커를 따라가서 NPC에게 E키를 누르면 대화가 시작되는데, 동시에 마커가 자동으로 사라지게 연결했다.

PlayerInteraction에서 NPC와 대화를 시작할 때 CallManager에 “도착했다”는 신호를 보내는 방식이다.

if (callManager.IsCallActive())
{
    callManager.OnArrivedAtScene();
}

이 한 줄로 신고 처리 완료가 연결된다. 마커 제거, 신고 상태 초기화가 여기서 한번에 일어난다.


처음 알게 된 것들

이 시스템을 만들면서 처음 제대로 이해한 개념들이 있다.

ScriptableObject — 코드 없이 데이터를 에셋 파일로 관리하는 방법. 신고, 대화, 능력치 데이터 전부 이걸로 만들었다.

Coroutine — 시간 기반으로 일을 처리하는 방법. while(true) 루프가 게임을 멈추지 않고 계속 돌아가는 게 처음엔 신기했다.

Inspector 연결 — 코드에서 public으로 선언한 변수는 Inspector에서 드래그로 오브젝트를 연결할 수 있다. 이걸 알고 나서 스크립트 간 연결이 훨씬 쉬워졌다.


현재까지 돌아가는 것들

이 시스템을 완성했을 때 게임의 핵심 루프가 한 바퀴 돌아가게 됐다.

112 신고 수신 → 무전 알림 표시 → 마커 따라 이동 → NPC와 대화 → 선택지 처리 → 마커 제거

다음 글에서는 이 선택지가 실제 수치에 영향을 주는 능력치, 평판 시스템을 어떻게 만들었는지 이야기할 예정이다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤