Unity 2018.3 에디터 화면. 왼쪽에 Scene 뷰와 러닝 게임 Game 뷰, 오른쪽에 Hierarchy 와 Project 패널이 있다
작업 당시 Unity 에디터. 아래 Game 뷰가 실제 플레이 화면이고, 오른쪽 Hierarchy 에 맵 세 개(Map · Map 1 · Map 2)와 DataManager · DeadZone 이 보인다.
프로젝트 요약'쿠키런' 스타일의 2D 횡스크롤 러닝 게임 메커니즘을 분석하고 직접 구현해 본 모작 프로토타입이다. 끊임없이 이동하는 맵 환경과 2단 점프, 자석·시간 연장 같은 아이템 상호작용 시스템을 중점적으로 개발했다.

무엇을 만들었나

인기 러닝 게임의 핵심 구동 방식을 파악하고, 무한 스크롤 환경에서의 오브젝트 관리와 물리 상호작용 로직을 학습하기 위해 만들었다. 장르가 단순해 보여도 '무한히 이어지는 것처럼 보이는 화면'을 실제로 어떻게 만드는지는 직접 짜 보기 전에는 감이 오지 않는 부분이었다.

항목내용
엔진Unity 2018.3.12f1
기여도100% (1인 개발)
구조GameManager 스테이지 순환 · DataManager 싱글톤 전역 상태
물리Rigidbody2D 기반 점프 / 2단 점프, 태그(Ground · Block) 분기
아이템코인 · 시간 연장 · 자석
상태완성
화면에 보이는 리소스에 대해이 프로토타입은 원작의 이미지 리소스를 참고용으로 가져다 쓴 학습용 모작이다. 일부 UI와 배경만 직접 제작했다. 화면에 보이는 캐릭터·배경 스프라이트의 권리는 원작자에게 있으며, 이 글은 상업적 목적이 아니라 구조와 코드를 설명하기 위한 기록이다.

달리는 건 캐릭터가 아니라 맵이다

플레이어는 사실상 제자리에 있다. 앞으로 나아가는 것처럼 보이기 위해 맵 전체가 왼쪽으로 일정한 속도로 이동한다. 스테이지 배경은 배열과 인덱싱 모듈러 연산으로 활성·비활성을 스왑하면서 무한히 이어지는 것처럼 연출했다.

// MapMove.cs
private void Update()
{
    if (!DataManager.Instance.PlayerDie)
    {
        // 맵 전체를 mapSpeed만큼 -x축으로 이동
        transform.Translate(-mapSpeed * Time.deltaTime, 0, 0);
    }
}

// GameManager.cs - 다음 스테이지 로드 로직
public void Next_Stage()
{
    // 배경 교체 플래그 토글 로직 생략...
    DataManager.Instance.stage += 1;

    // 준비된 스테이지 맵을 모듈러 연산으로 순환 배치
    if(DataManager.Instance.stage > StageMap.Length)
    {
        DataManager.Instance.stage = DataManager.Instance.stage % StageMap.Length;
        if(DataManager.Instance.stage == 0) DataManager.Instance.stage = StageMap.Length;
    }
    StageStart(); // 해당 인덱스의 맵을 우측 시작 위치로 재배치 후 활성화
}

스테이지 맵은 세 개만 만들어 두고 stage 인덱스가 배열 길이를 넘어가면 % 로 되돌린다. stage 가 0이 되는 경우만 따로 배열 길이로 보정하는데, 이 프로젝트의 인덱스가 1부터 시작하기 때문이다. 새 맵을 만들지 않고 이미 있는 맵을 오른쪽 시작 위치로 되돌려 다시 활성화한다.

2단 점프와 태그 분기

public void Jump_Btn()
{
    if (!DataManager.Instance.PlayerDie)
    {
        if (jumpCount == 0) // 첫 점프
        {
            gameObject.GetComponent<Rigidbody2D>().velocity = new Vector3(0, jump, 0);
            jumpCount += 1;
            PlayerAni_Jump();
        }
        else if (jumpCount == 1) // 2단 점프
        {
            gameObject.GetComponent<Rigidbody2D>().velocity = new Vector3(0, jump2, 0);
            jumpCount += 1;
        }
    }
}

private void OnCollisionEnter2D(Collision2D collision)
{
    if(collision.gameObject.tag.CompareTo("Ground") == 0)
    {
        jumpCount = 0; // 바닥 착지 시 점프 횟수 초기화
        PlayerAni_Rnu();
    }
    else if (collision.gameObject.tag.CompareTo("Block") == 0)
    {
        DataManager.Instance.playTimeCurrent -= 2f; // 장애물 충돌 페널티
        PlayerAni_Rnu();
    }
}

jumpCount 하나로 1단과 2단을 가른다. 점프 세기를 jumpjump2 로 따로 둬서 2단 점프의 느낌을 다르게 줄 수 있다. velocity 를 더하지 않고 대입하기 때문에 떨어지는 중에 2단 점프를 눌러도 항상 같은 높이로 올라간다.

착지 판정과 장애물 판정은 둘 다 OnCollisionEnter2D 안에서 태그로 갈린다. Ground 면 점프 횟수를 0으로 되돌리고, Block 이면 남은 시간을 2초 깎는다. 러닝 게임에서 장애물의 대가가 '죽음'이 아니라 '시간'인 구조다.

자석 아이템: 코인이 스스로 다가온다

자석을 먹으면 DataManager.Instance.margnetTimeCurrent 라는 전역 값이 켜진다. 끌어당기는 주체는 플레이어가 아니라 각 코인 자신이다. 코인이 매 프레임 플레이어와의 거리를 재고, 인식 범위 안이면 자기가 플레이어 쪽으로 움직인다.

private void Update()
{
    player = GameObject.FindGameObjectWithTag("PlayerPosition");
    float distance = Vector2.Distance(gameObject.transform.position, player.transform.position);

    // 자석 버프가 활성화 되어 있고, 플레이어가 살아있다면
    if (DataManager.Instance.PlayerDie == false && DataManager.Instance.margnetTimeCurrent > 0)
    {
        if(distance < 6) // 인식 범위 내
        {
            // 방향 벡터 연산 후 정규화(normalized)하여 플레이어 쪽으로 이동
            Vector2 dir = player.transform.position - transform.position;
            transform.Translate(dir.normalized * DataManager.Instance.itemMoveSpeed * Time.deltaTime, Space.World);
        }
    }
}

전역 상태는 싱글톤으로

점수, 제한 시간, 자석 유지 시간, 생존 여부를 DataManager 하나로 모았다. 씬이 바뀌어도 파괴되지 않게 DontDestroyOnLoad 를 걸어 두고, 여러 컴포넌트가 Instance 로 접근한다.

public static DataManager Instance { get; private set; }

private void Awake()
{
    if(Instance == null)
    {
        DontDestroyOnLoad(gameObject);
        Instance = this;
    }
    else
    {
        Destroy(gameObject);
    }
}

남은 과제

정리하면서 그대로 남겨 둔 부분이 있다. 스테이지와 아이템을 켜고 끄는 과정에서 SetActive 호출이 잦고, 자석 로직이 매 프레임 FindGameObjectWithTag 를 부른다. 코인 하나하나가 자기 Update 에서 이걸 하기 때문에 화면에 코인이 많을수록 그대로 비례해서 늘어난다. 이 프로젝트에서는 여기까지 손대지 않았고, 오브젝트 풀링 구조로 개선해야 할 과제로 남겼다.

아래는 하지 않은 일이다같은 구조를 다시 만든다면 검토해 볼 수 있는 방향을 적어 둔다. 이 프로토타입에 실제로 적용한 내용이 아니다.
현재 구조부담검토해 볼 방향
코인마다 Update 에서 FindGameObjectWithTag코인 수에 비례해 탐색 비용 증가플레이어 참조를 한 번만 캐싱하거나, 자석 쪽에서 범위 질의
아이템·스테이지 SetActive 반복활성/비활성 전환 비용과 GC 부담오브젝트 풀링으로 인스턴스 재사용
스테이지 세 개 순환같은 지형이 반복돼 보임아이템·장애물 배치를 매번 다시 뽑아 조합을 바꿈

정리