프로젝트 개요

등산 중 발생한 산사태를 피해 하산하는 1인칭 3D 액션 레이싱 게임이다. 이름은 Run Away From Stones.

항목내용
시기2022년 4월
엔진Unity 2021.2.17f1
기여도100% (1인 개발)
장르1인칭 3D 액션 레이싱
결과실력 증명 용도로 제작. 이후 고도화해 전시 시연까지 진행

GTA5의 하산 레이스 모드에서 영감을 받았다. 떨어지는 장애물을 등 뒤에 두고 달려 내려가는 그 구도만 떼어내 독립적인 게임으로 구성해 보고 싶었다.

Unity 에디터에서 본 경사면 맵. 녹색 슬로프 위에 나무와 바위가 배치되어 있다
에디터에서 본 맵 전경. 경사면을 따라 나무와 바위를 배치했다.

핵심 시스템 세 가지

낙하물 스폰

장애물은 미리 배치하지 않고 일정 주기로 생성한다. 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초 뒤에 다시 올린다.

이 코드를 지금 읽으면

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);
}
isGrounded 안에서만 방향을 갱신하는 구조이동 벡터를 땅에 닿아 있을 때만 새로 만든다. 공중에 뜬 동안에는 직전에 정해진 방향과 속도가 그대로 유지되고, 중력만 y 에 계속 더해진다. 점프 중 방향 전환이 안 되는 대신 뛰어내린 순간의 관성이 그대로 살아 있어서, 경사면을 따라 내려가는 게임에는 오히려 맞는 감각이 나온다.

이 코드를 지금 읽으면

리소스를 다시 만든 이야기

제출 기한을 맞춰야 해서 처음에는 무료 에셋을 가져다 기초 형태만 잡았다. 이후 프로젝트를 고도화하면서 필요한 3D 오브젝트와 텍스처를 전부 직접 모델링해 갈아 끼웠다. 아트 톤이 제각각인 문제를 그렇게 정리했다.

박스 형태로 단순하게 모델링된 로우폴리 캐릭터
플레이어 캐릭터. 면을 최소한으로 줄인 로우폴리 형태로 만들었다.
직육면체를 쌓아 만든 통나무 더미 3D 모델
낙하물 중 하나인 통나무 더미.
정육면체를 계단식으로 쌓아 만든 바위 3D 모델
바위. 큐브를 계단처럼 쌓아 덩어리를 만들었다.
삼단 박스와 기둥으로 이루어진 로우폴리 나무 모델
배경에 세울 나무.
계단 형태의 단차가 들어간 경사면 지형 3D 모델
코스가 되는 경사면 지형. 계단 형태의 단차를 넣어 내려가는 길을 만들었다.
직접 만들면서 얻은 것로우폴리로 통일하면 모델링 시간이 줄 뿐 아니라 에셋마다 톤이 어긋나는 문제가 애초에 생기지 않는다. 무료 에셋을 여러 곳에서 모으면 폴리곤 밀도와 텍스처 해상도가 제각각이라, 한 화면에 놓았을 때 그것부터 눈에 걸린다.

정리

짧은 제출 기한이라는 압박 속에서도 필수 요구사항이 아니었던 기획서까지 직접 쓰고, 코어 게임플레이를 온전히 시연할 수 있는 프로토타입을 완성했다.

그리고 거기서 멈추지 않았다. 이후 에셋을 교체하고 게임 구조를 다듬으면서 프로토타입을 전시 가능한 수준으로 끌어올리는 경험을 얻었다. 결과적으로 여러 사람 앞에서 시연하는 데까지 갔다.

지금 코드를 다시 읽으면 FixedUpdateUpdate 의 역할이 뒤바뀌어 있다거나, 스폰 주기가 고정이라거나 하는 것들이 눈에 들어온다. 당시에는 동작하는 게 우선이었고, 그 판단 자체는 틀리지 않았다고 생각한다. 다만 그때 넘어간 것들이 목록으로 남아 있다는 것도 사실이다.

당시 기록만들면서 남긴 글이 개인 블로그에 있다. 기록 보기