프로젝트 개요
등산 중 발생한 산사태를 피해 하산하는 1인칭 3D 액션 레이싱 게임이다. 이름은 Run Away From Stones.
| 항목 | 내용 |
|---|---|
| 시기 | 2022년 4월 |
| 엔진 | Unity 2021.2.17f1 |
| 기여도 | 100% (1인 개발) |
| 장르 | 1인칭 3D 액션 레이싱 |
| 결과 | 실력 증명 용도로 제작. 이후 고도화해 전시 시연까지 진행 |
GTA5의 하산 레이스 모드에서 영감을 받았다. 떨어지는 장애물을 등 뒤에 두고 달려 내려가는 그 구도만 떼어내 독립적인 게임으로 구성해 보고 싶었다.

핵심 시스템 세 가지
- 1인칭 조작계 — 몰입감과 생동감을 위해 1인칭 시점을 골랐다. W/A/S/D 이동에 달리기와 미끄러지기를 붙였다.
- 랜덤 낙하 장애물 — 산 위에서 통나무, 바위, 돌덩이가 무작위로 굴러떨어진다.
- 맵과 환경 분기 — 일반 산과 눈 덮인 산 두 가지 환경을 만들었고, 눈 지형에서는 미끄러지는 물리 효과를 넣었다.
낙하물 스폰
장애물은 미리 배치하지 않고 일정 주기로 생성한다. GameManager 쪽 코드다.
void FixedUpdate()
{
if (isNakHa)
{
isNakHa = false;
// 무작위 낙하물을 무작위 스폰 위치에 생성
Instantiate(nakHa[Random.Range(0, nakHa.Count)],
pos[Random.Range(0, pos.Count)].position,
Quaternion.identity);
StartCoroutine(waitForit());
}
}
IEnumerator waitForit()
{
yield return new WaitForSeconds(2f);
isNakHa = true;
}
낙하물 프리팹 목록(nakHa)과 스폰 지점 목록(pos)에서 각각 하나씩 무작위로 뽑아 그 자리에 생성한다. 생성한 뒤 isNakHa 를 내리고, 코루틴이 2초 뒤에 다시 올린다.
이 코드를 지금 읽으면
- 플래그로 타이머를 구현했다.
FixedUpdate가 매 물리 프레임 돌면서isNakHa만 확인하고, 실제 대기는 코루틴이 한다. 결과는 2초 주기지만 경로가 두 군데로 나뉘어 있다. 코루틴 하나 안에서while (true) { spawn; yield return new WaitForSeconds(2f); }로 도는 편이 읽기 쉽다. FixedUpdate에 있을 이유가 없다. 스폰은 물리 연산과 무관한데 물리 주기에 묶여 있다.Time.fixedDeltaTime설정을 바꾸면 함께 영향을 받는다.- 주기가 2초 고정이다. 시작부터 끝까지 난이도가 같다. 경과 시간에 따라 간격을 줄이면 자연스러운 난이도 곡선이 생긴다.
- 정리 코드는 이 조각에 없다. 생성된 낙하물을 언제 없애는지는 이 코드만으로는 알 수 없다. 화면 밖으로 나간 것을 지우거나 오브젝트 풀로 돌려쓰는 처리가 없다면, 플레이가 길어질수록 씬의 오브젝트가 계속 늘어난다.
1인칭 캐릭터 제어
이동은 CharacterController 로 처리했다. 마우스 입력으로 시점을 돌리고, 키보드 입력을 조합해 이동·점프·중력을 계산한다.
void Update()
{
float mouseX = Input.GetAxisRaw("Mouse X") * mouseDps;
transform.Rotate(Vector3.up * mouseX);
// 걷기 애니메이션 제어
if (Mathf.Abs(Input.GetAxisRaw("Horizontal")) > 0 || Mathf.Abs(Input.GetAxisRaw("Vertical")) > 0)
anim.SetBool("Walk", true);
else
anim.SetBool("Walk", false);
// 지면에 닿아있을 때 이동 및 점프 계산
if (controller.isGrounded)
{
moveDirection = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
moveDirection = transform.TransformDirection(moveDirection);
moveDirection *= speed;
if (Input.GetButton("Jump"))
moveDirection.y = jumpSpeed;
}
// 중력 적용 후 이동
moveDirection.y -= gravity * Time.deltaTime;
controller.Move(moveDirection * Time.deltaTime);
}
y 에 계속 더해진다. 점프 중 방향 전환이 안 되는 대신 뛰어내린 순간의 관성이 그대로 살아 있어서, 경사면을 따라 내려가는 게임에는 오히려 맞는 감각이 나온다.이 코드를 지금 읽으면
GetAxisRaw와GetAxis를 섞어 쓴다. 애니메이션 판정에는GetAxisRaw, 이동에는GetAxis다.GetAxis는 값이 서서히 붙고GetAxisRaw는 즉시 −1/0/1 로 튄다. 입력을 뗀 직후GetAxis는 아직 0이 아닌데GetAxisRaw는 이미 0이라, 애니메이션이 먼저 멈추고 이동이 조금 더 미끄러진다.anim.SetBool("Walk", ...)를 매 프레임 문자열로 호출한다.Animator.StringToHash("Walk")로 한 번만 변환해 두고 int 버전을 쓰는 편이 낫다.- 시점 회전이 좌우뿐이다. 이 조각에는 상하 회전이 없다. 카메라 쪽에서 따로 처리했을 가능성이 크다.
- 중력을
Update에서Time.deltaTime으로 누적한다. 동작하지만 물리 계산은FixedUpdate쪽이 프레임 레이트에 덜 흔들린다. 앞의 스폰 코드가FixedUpdate에 있는 것과 정확히 반대다.
리소스를 다시 만든 이야기
제출 기한을 맞춰야 해서 처음에는 무료 에셋을 가져다 기초 형태만 잡았다. 이후 프로젝트를 고도화하면서 필요한 3D 오브젝트와 텍스처를 전부 직접 모델링해 갈아 끼웠다. 아트 톤이 제각각인 문제를 그렇게 정리했다.





정리
짧은 제출 기한이라는 압박 속에서도 필수 요구사항이 아니었던 기획서까지 직접 쓰고, 코어 게임플레이를 온전히 시연할 수 있는 프로토타입을 완성했다.
그리고 거기서 멈추지 않았다. 이후 에셋을 교체하고 게임 구조를 다듬으면서 프로토타입을 전시 가능한 수준으로 끌어올리는 경험을 얻었다. 결과적으로 여러 사람 앞에서 시연하는 데까지 갔다.
지금 코드를 다시 읽으면 FixedUpdate 와 Update 의 역할이 뒤바뀌어 있다거나, 스폰 주기가 고정이라거나 하는 것들이 눈에 들어온다. 당시에는 동작하는 게 우선이었고, 그 판단 자체는 틀리지 않았다고 생각한다. 다만 그때 넘어간 것들이 목록으로 남아 있다는 것도 사실이다.