Unintend Project 회고

Unintend Project 회고

Unity로 만든 1인칭 호러 서바이벌 게임. 모바일 조작을 기준으로 고등학교 때 개발했던 프로젝트다.

왜 만들었나

고등학교 때 Unity로 이것저것 만들어 봤다.

간단한 게임도 많이 만들었는데 Unintend는 그중에서도 규모가 좀 컸다. 이전에는 캐릭터를 움직이거나 간단한 게임 시스템을 만들어 보는 정도였다면, 이번에는 게임 하나를 처음부터 끝까지 만들어 보고 싶었다.

그래서 1인칭 호러 게임을 만들었다.

플레이어가 맵을 돌아다니면서 아이템을 찾고, 퍼즐을 풀고, 몬스터를 피해 다음 구역으로 넘어가는 방식이다.

처음부터 전투 중심으로 만들 생각은 없었다.

오히려 플레이어가 뭔가를 할수록 위험해지는 쪽을 생각했다.

손전등을 켜면 주변을 볼 수 있다. 대신 몬스터에게 발견되기 쉬워진다.

총은 몬스터를 잠시 느리게 만들 수 있지만 총을 쏘면 그 위치로 몬스터가 온다.

퓨즈를 꽂거나 레버를 내리는 것도 진행하려면 해야 하지만 그 위치가 몬스터에게 전달된다.

이런 식이다.

게임 구조

지금 프로젝트에 남아 있는 건 스크립트와 빌드 파일 정도다.

씬이나 모델, 사운드 같은 에셋은 거의 남아 있지 않다. 그래서 지금 다시 보려면 코드부터 하나씩 읽어야 한다.

전체 진행은 크게 다음과 같았다.

StartScene -> loading -> inside Stage 1~3 -> outside2 Stage 4 -> endScene

StartScene에서 난이도와 옵션을 정하고 게임을 시작한다.

inside가 본편이고 스테이지 1~3이 여기 들어간다. 같은 맵을 여러 구역으로 나눠서 사용했다.

4스테이지에서는 outside2로 넘어가고 제한 시간 동안 도망친다.

마지막에 endScene에서 엔딩이 나온다.

스테이지

스테이지는 다음과 같이 구성했다.

스테이지클리어 조건몬스터
1퓨즈 4개 + 레버 2개A
2퓨즈 4개 + 레버 3개B
3퓨즈 4개 + 레버 4개C
4제한 시간 안에 탈출마지막 C

맵을 스테이지마다 새로 만든 건 아니다.

하나의 맵을 만들어 놓고 스테이지가 바뀌면 플레이어 위치를 다른 구역으로 옮겼다.

퓨즈는 puzrandum에서 16개 후보 중 4개를 뽑아서 배치했다.

그래서 같은 스테이지라도 퓨즈 위치가 매번 달라진다.

스테이지가 올라가면서 해야 할 일이 늘어나고 몬스터도 강해진다.

클리어하면 퍼즐 문을 열고 stage를 저장한다. 몬스터는 gohome()으로 맵 밖으로 보내고 비활성화했다.

지금 보면 상당히 단순한 방식인데, 당시에는 이걸로 충분했다.

플레이어와 모바일 조작

플레이어 컨트롤러는 CameraCtrl이다.

CharacterController를 사용했고 이동, 중력, 앉기, 달리기를 처리했다.

모바일 게임을 만들려고 했기 때문에 이동은 가상 조이스틱을 사용하고 화면 오른쪽을 터치해서 시야를 움직이게 했다.

속도는 걷기 6, 달리기 13, 앉기 3으로 설정했다.

여기서 단순히 속도만 바꾼 게 아니라 몬스터의 감지 범위도 같이 바꿨다.

걷기는 약 15, 앉기는 약 10, 달리기는 약 25~30 정도였다.

그래서 뛰면 빠르지만 그만큼 들키기 쉬웠다.

달릴 때 심장 소리도 커지게 했다.

손전등과 총은 각각 애니메이터를 따로 사용했다. 대화나 특정 이벤트가 발생하면 플레이어 이동을 막았다.

상호작용

상호작용은 Raiser에서 거의 다 처리했다.

카메라에서 레이캐스트를 쏘고 플레이어가 보고 있는 오브젝트를 확인하는 방식이다.

총알, 배터리, 열쇠, 퓨즈, 손전등, 총, 라디오를 줍는 것부터 문, 레버, 단서, 퓨즈 소켓까지 여기에 들어갔다.

HUD도 같이 처리했다.

지금 다시 보면 너무 많이 들어가 있다.

파일 하나가 약 900줄 정도였는데, 당시에는 기능이 하나 생길 때마다 그냥 여기에 추가했다.

모바일에서는 오브젝트를 정확하게 바라보기가 어려워서 바라보고 있는 물체에 따라 하단에 안내 문구도 띄웠다.

get fuse

down lever

Lock, find key

이런 식이다.

퍼즐과 몬스터

이 부분은 지금 다시 봐도 괜찮았다.

레버를 내리거나 퓨즈를 꽂으면 그 위치를 몬스터에게 넘겼다.

그래서 퍼즐을 푸는 순간 몬스터가 찾아온다.

숨어 있다가 몬스터가 지나가기를 기다리고, 잠깐 나와서 퓨즈를 꽂고 다시 숨는 식으로 플레이하게 만들었다.

퓨즈에는 30초 쿨타임도 넣었다.

한 번에 여러 개를 꽂지 못하게 해서 다음 행동을 하기 전에 잠깐 기다려야 했다.

몬스터

몬스터는 A, B, C 세 종류다.

기본적으로 NavMeshAgent를 사용해서 순찰하고 플레이어를 발견하면 추격한다.

시야와 감지는 Follow에서 처리했다.

A, B, C가 완전히 다른 AI인 건 아니다.

기본 구조를 복사한 다음 속도나 시야 거리, 순찰 지점, 플레이어 행동에 대한 반응을 다르게 했다.

Monster A

가장 기본적인 몬스터다.

순찰 지점은 6개 정도고 시야 거리는 60, 추격 포기 시간은 10초였다.

첫 번째 스테이지에서 사용했다.

Monster B

손전등에 반응한다.

손전등이 켜져 있으면 더 공격적으로 따라온다.

플레이어 입장에서는 손전등을 켜야 앞을 볼 수 있는데, 켜면 몬스터에게 들키기 쉬워진다.

2스테이지에서 이 구조를 사용했다.

Monster C

3스테이지에서 등장한다.

순찰 지점이 26개 정도로 늘어나고 시야 거리도 100으로 설정했다.

추격 포기 시간도 22초로 늘렸다.

라디오를 사용하는 동안에는 플레이어가 움직이지 못하게 했다.

마지막 추격

마지막에는 MonsterC_Last가 나온다.

문이 열리고 100초가 시작된다.

여기서는 퍼즐을 풀 필요 없이 그냥 도망가면 된다.

앞쪽 스테이지에서 계속 퍼즐을 풀다가 마지막에는 아예 도망치는 것만 남도록 했다.

총과 캐비닛

총은 몬스터를 죽이는 무기가 아니다.

맞히면 약 5초 동안 몬스터의 속도가 크게 떨어진다.

총알은 최대 5발이다.

그리고 총을 쏘면 몬스터가 총을 쏜 위치로 찾아온다.

그래서 총을 쓰는 것 자체가 위험하다.

몬스터를 잠깐 늦추고 도망갈 것인지, 총알을 아껴둘 것인지 선택해야 한다.

캐비닛은 숨는 장소다.

들어가서 문을 닫고 약 3초가 지나면 cabinet_in이 켜지고 추격이 끊긴다.

NavMesh로 계속 쫓아오기만 하면 결국 플레이어가 계속 뛰기만 하게 된다.

숨을 곳을 만들어 놓으니 맵을 어디서 지나가야 할지 생각하게 됐다.

문과 손전등

문은 Slerp를 이용해서 천천히 열리게 했다.

몬스터도 Monster_Door를 통해 문을 직접 열 수 있게 만들었다.

플레이어가 닫아 놓은 문을 몬스터가 다시 여는 소리가 나게 했다.

손전등 배터리는 3000에서 줄어든다.

잔량에 따라 UI가 바뀌고 배터리가 다 떨어지면 불빛이 깜빡인다.

배터리를 교체할 때도 바로 100%가 되는 게 아니라 시간이 걸리게 했다.

교체하는 동안에는 주변을 신경 써야 한다.

그 외에도 흔들리는 의자나 움직이는 타이틀 배경 같은 연출을 조금 넣었다.

큰 기능은 아니지만 호러 게임에서는 이런 것들도 필요하다고 생각했다.

저장과 로딩

저장은 PlayerPrefs를 사용했다.

스테이지, 난이도, 언어와 손전등, 총, 라디오 보유 여부를 저장했다.

볼륨이나 감도, 화면 모드, 그래픽 품질도 저장했다.

대신 퓨즈 진행 상황이나 열린 문 같은 세부적인 상태까지 저장하지는 않았다.

그래서 죽으면 해당 스테이지의 시작 위치부터 다시 시작한다.

로딩은 LoadSceneAsync를 사용했다.

빈 로딩 씬을 먼저 거친 다음 다음 씬을 비동기로 불러오는 방식이다.

잘했다고 생각하는 부분

지금 코드를 보면 잘했다고 생각되는 부분도 있고, 왜 이렇게 만들었나 싶은 부분도 있다.

그중에서 가장 괜찮았던 건 게임 시스템을 서로 연결한 것이다.

레버와 퓨즈는 퍼즐이고, 총은 무기고, 달리기는 이동 기능이다.

그런데 이게 각각 따로 끝나지 않는다.

레버를 조작하면 몬스터가 온다.

퓨즈를 꽂아도 몬스터가 온다.

총을 쏴도 온다.

달리면 더 쉽게 들킨다.

손전등도 마찬가지다.

기능 하나를 추가할 때 다른 시스템에 영향을 주도록 만들었기 때문에 플레이할 때는 어느 정도 하나의 게임처럼 동작했다.

기술적으로도 한 프로젝트에서 여러 Unity 기능을 직접 연결해 봤다.

CharacterController로 플레이어를 만들고 NavMeshAgent로 몬스터를 움직였다.

Raycast로 상호작용을 만들고 Animator로 행동을 처리했다.

AudioSourceAudioMixer도 사용했고, PlayerPrefs로 저장을 만들었다.

LoadSceneAsync를 이용한 로딩도 직접 구현했다.

지금 보면 전부 Unity에서 흔하게 사용하는 기능이다.

그래도 직접 하나의 게임에 넣어 본 것과 각각 따로 공부한 것은 차이가 있었다.

아쉬웠던 부분

가장 큰 문제는 코드 구조다.

Raiser가 너무 크다.

상호작용부터 HUD, 아이템, 퍼즐, 몬스터 호출까지 한 곳에 들어가 있다.

CameraCtrl도 이동만 담당하지 않는다.

앉기, 손전등, 총, 감지 콜라이더, 모바일 시야까지 같이 들어가 있다.

당시에는 기능이 필요하면 해당 스크립트에 계속 추가했다.

지금 다시 만든다면 역할별로 나눌 것이다.

몬스터 A, B, C도 복사한 부분이 많다.

처음에는 빨리 만들 수 있어서 편했는데 나중에 수정할 때 문제가 된다.

공통 AI를 하나 만들고 속도나 시야 거리 같은 값만 데이터로 빼는 방식이 더 나았을 것 같다.

이름도 엉망이다.

Scritps, moster_c, leber, test 같은 이름이 그대로 남아 있다.

당시에는 일단 동작하면 됐는데, 프로젝트를 다시 열어 보니 이런 것부터 눈에 들어온다.

GameObject.Find도 많이 사용했고 태그 문자열로 긴 if-else를 만드는 방식도 많았다.

상호작용을 인터페이스나 컴포넌트로 나눴다면 지금보다 훨씬 보기 좋았을 것이다.

코루틴도 필요 이상으로 많이 사용했다.

UpdateFixedUpdate에서 계속 StartCoroutine을 호출하는 코드가 있었다.

당시에는 부드럽게 움직이게 하려고 그렇게 만든 것 같은데, 지금 보면 단순한 보간은 그냥 업데이트에서 처리하는 게 낫다.

이 프로젝트 이후

Unintend 다음에 만든 프로젝트들을 보면 조금씩 구조가 달라진다.

예를 들어 MemoryCard에서는 Manager와 Processor처럼 역할을 나누려고 했다.

Unintend에서 하나의 파일에 기능을 계속 넣다 보니 나중에 수정하기 힘들었던 경험이 영향을 줬다.

처음부터 좋은 구조를 알고 만든 것은 아니었다.

직접 만들어 보고 불편해진 다음에야 왜 나눠야 하는지 알게 된 부분이 많았다.

다시 만든다면

게임의 핵심은 크게 바꾸지 않을 것 같다.

뛰면 들킨다.

손전등을 켜면 보이지만 몬스터에게도 보인다.

총을 쏘면 몬스터가 온다.

퍼즐을 풀면 그 위치가 노출된다.

이 부분이 Unintend에서 만들고 싶었던 게임이었다.

대신 코드는 다시 만들 것이다.

몬스터는 공통 AI와 스테이지별 데이터로 나누고, 상호작용은 IInteractable 같은 인터페이스를 사용할 것 같다.

CameraCtrl도 이동, 시야, 장비 등을 나눌 것이다.

저장 역시 필요한 게임 상태를 따로 정의할 것이다.

그리고 폴더와 클래스 이름부터 제대로 지을 것이다.

마치며

Unintend는 지금 기준으로 보면 잘 만든 코드라고 하기는 어렵다.

Raiser 하나에 너무 많은 기능이 들어가 있고, 몬스터 코드는 복사한 부분이 많고, 이름도 엉망이다.

그런데 고등학교 때 이 프로젝트를 만들면서 처음으로 게임의 여러 시스템을 하나로 묶어 봤다.

플레이어 이동, 모바일 입력, 몬스터 AI, 레이캐스트 상호작용, 퍼즐, 사운드, 저장, 로딩, UI까지 직접 연결했다.

무엇보다 플레이어가 하는 행동이 게임에 어떤 결과를 만드는지를 생각해 보게 됐다.

손전등을 켜면 잘 보이지만 위험하다.

총을 쏘면 살 수 있지만 위치가 노출된다.

달리면 빨라지지만 들키기 쉽다.

퍼즐을 풀어야 진행할 수 있지만 그 과정에서 몬스터가 찾아온다.

이런 관계를 직접 구현해 본 경험은 이후 프로젝트에서도 계속 남았다.

지금 프로젝트 폴더에 남은 것은 스크립트와 빌드 파일뿐이다.

맵과 에셋은 거의 사라졌지만 코드에는 당시 만들었던 게임의 흔적이 남아 있다.

좋은 구조를 가진 프로젝트는 아니었다.

그래도 게임 하나를 실제로 끝까지 만들어 봤고, 그 과정에서 다음 프로젝트에서 무엇을 바꿔야 하는지도 알게 됐다.

그것만으로도 Unintend는 꽤 의미 있는 프로젝트였다.