티스토리 뷰

반응형

안녕하세요!

이번에는 NFLY 스튜디오에서 2015년도에 출시하여 많은 인기를 끌었던 

모바일게임 무한의계단을 만들어보려고 합니다. 

지금까지도 많은 사랑을 받고있고, 묘한 중독성이 있는 인디게임 입니다!

 

 

그런데 무한의계단 게임은 유니티로 구현한 자료가

제가 찾아봤을 때는 없는 것 같아서 만들고자 하시는 분들께 도움이 될 수 있을 것 같습니당😄

 

그래서 스프라이트 같은 리소스들이 찾아봐도 없더라구요,, 

그냥 돌아다니는 캡쳐 사진에서 힘들게 하나하나 따냈습니당 ㅠ.ㅠ 

 

제가 아직 배움 초기 단계에 있어서 실력이 부족합니다. 많은 의견과 피드백 해주세요ㅎㅎ

그리고 Github 소스코드와 실행영상은 다음 글에 있습니당!

 

 

 

 


 

 

1. Player, 애니메이션 구현

저는 캐릭터를 7가지 준비하였습니다. 

일단, Hierarchy 창을 보여드리겠습니다.

 Player이라는 빈 오브젝트의 자식들로 캐릭터 종류에 맞게 생성하였습니다.

 

각 캐릭터 오브젝트는 Rigidbody2D와 BoxCollider2D 컴포넌트를 추가합니다.

Rigidbody2D는 Kinematic 타입으로, 콜라이더는 isTrigger을 체크합니다. 

그리고 Edit Collider 하여 콜라이더의 범위를 다리 아래쪽으로 작게 수정합니다.

 

각 캐릭터 오브젝트의 자식에는

게임오버 시에 나오는 애니메이션에 사용될 AudioSource들을 넣어주었습니다.

 

 

 

Player의 애니메이션 2가지를 구현하겠습니다. 

- 가만히 있다가 계단을 올라갈 때 sprite가 바뀌는 Move 애니메이션

- 게임 오버 시의 Die 애니메이션

 

Animator를 각 캐릭터에 만들고, Animation 2개를 다음과 같이 구성하였습니다.

 

 

Move 애니메이션은 파라미터인 Move의 bool값이 true가 될 경우 실행되고,

움직이는 듯한 sprite로 바뀌는 애니메이션을 만들어주세요. 

 

 

Die 애니메이션은 파라미터인 Die의 bool값이 true가 될 경우에 실행됩니다.

놀라는 sprite 로 바뀌고, 오브젝트가 떨어지는 애니메이션을 만들어주세요.

이 애니메이션에는 캐릭터 오브젝트의 자식에 있던 AudioSoruce 3가지를 넣었습니다.

오디오소스의 PlayOnAwake를 체크하고 애니메이션에서 IsActive 속성으로 활성화할 때 

소리가 나도록 했습니다.

 

 

 

 

 

 

 

2. 계단 오브젝트 구현

계단은 무한의 계단이라는 게임 이름에 걸맞게 죽지 않는 이상 끝없이 나타나야합니다.

그리고, 이 게임은 오르기 버튼으로 계단을 올라가고,

계단의 방향이 바뀌는 지점에서 전환하기 버튼을 눌러 방향을 전환해야합니다.

 

플레이어가 계단을 올라가고, 방향을 바꾸는 것처럼 보이기 위하여

올라갈때마다 계단의 위치를 아래+왼/오른쪽으로 이동시켜주면 될 것 같습니다.

 

 

즉, 플레이어는 가만히 있고 계단들만 위치를 이동하는 것입니다.

빈 오브젝트 자식으로 계단 오브젝트 20개를 넣었습니다.

 

이 20개의 오브젝트들을 배열로 관리하여

y축이 일정 높이 이하가 된 계단 오브젝트는

다시 위에 생기는 순환 구조로 만들었습니다.

 

 

 

 

 

 

 

 

 

 

이 계단들을 관리하기 위한 GameManager 오브젝트와 스크립트를 만듭니다.

GameManager 스크립트의 일부분입니다.

Vector3 beforePos,
startPos = new Vector3(-0.8f, -1.5f, 0),
leftPos = new Vector3(-0.8f, 0.4f, 0),
rightPos = new Vector3(0.8f, 0.4f, 0),
leftDir = new Vector3(0.8f, -0.4f, 0),
rightDir = new Vector3(-0.8f, -0.4f, 0);

public GameObject[] stairs;

//Stores whether to change direction for each stair index
public bool[] IsChangeDir = new bool[20];  

enum State { start, leftDir, rightDir }
State state = State.start;
    
void Awake() {
    StairsInit();
}

//Initially Spawn The Stairs
    void StairsInit() {
        for (int i = 0; i < 20; i++) {
            switch (state) {
                case State.start:
                    stairs[i].transform.position = startPos;
                    state = State.leftDir;
                    break;
                case State.leftDir:
                    stairs[i].transform.position = beforePos + leftPos;
                    break;
                case State.rightDir:
                    stairs[i].transform.position = beforePos + rightPos;
                    break;
            }
            beforePos = stairs[i].transform.position;

            if (i != 0) {
                //Coin object activation according to random probability
                if (Random.Range(1, 9) < 3) objectManager.MakeObj("coin", i);
                if (Random.Range(1, 9) < 3 && i < 19) {
                    if (state == State.leftDir) state = State.rightDir;
                    else if (state == State.rightDir) state = State.leftDir;
                    IsChangeDir[i + 1] = true;
                }
            }
        }
    }




    //Spawn The Stairs At The Random Location
    void SpawnStair(int num) {
        IsChangeDir[num + 1 == 20 ? 0 : num + 1] = false;
        beforePos = stairs[num == 0 ? 19 : num - 1].transform.position;
        switch (state) {
            case State.leftDir:
                stairs[num].transform.position = beforePos + leftPos;
                break;
            case State.rightDir:
                stairs[num].transform.position = beforePos + rightPos;
                break;
        }

        //Coin object activation according to random probability
        if (Random.Range(1, 9) < 3) objectManager.MakeObj("coin", num);
        if (Random.Range(1, 9) < 3) {
            if (state == State.leftDir) state = State.rightDir;
            else if (state == State.rightDir) state = State.leftDir;
            IsChangeDir[num+1 == 20? 0 : num+1] = true;
        }
    }



    //Stairs Moving Along The Direction       
    public void StairMove(int stairIndex, bool isChange, bool isleft) {
        if (player.isDie) return;

        //Move stairs to the right or left
        for (int i = 0; i < 20; i++) {
            if (isleft) stairs[i].transform.position += leftDir;
            else stairs[i].transform.position += rightDir;
        }

        //Move the stairs below a certain height
        for (int i = 0; i < 20; i++)
            if (stairs[i].transform.position.y < -5) SpawnStair(i);

        //Game over if climbing stairs is wrong
        if(IsChangeDir[stairIndex] != isChange) {
            GameOver();
            return;
        }
        
        //Optical illusion effect as if the background moves down
        backGround.transform.position += backGround.transform.position.y < -14f ?
            new Vector3(0, 4.7f, 0) : new Vector3(0, -0.05f, 0);
    }

필요한 변수들을 선언하고, stairs는 유니티에서 20개의 계단 오브젝트들을 지정합니다.

IsChangeDir배열은 계단 인덱스마다 방향전환을 해야하는 계단인지의 여부를 저장합니다. 

 

- StairInit 함수

Awake함수에서 StairsInit 함수를 선언하여 게임 시작 처음 계단들을 랜덤으로 스폰합니다.

현재 계단 인덱스의 위치를 beforePos에 저장하였다가 다음 계단 인덱스에서 방향에 따라

leftPos나 rightPos를 더하여 위치를 지정합니다. 방향 전환의 여부는 Random함수로 지정하였습니다. 

 

- SpawnStair 함수

SpawnStair함수도 StairsInit함수와 비슷합니다.

스폰할 하나의 계단 오브젝트의 인덱스를 매개변수로 받아 적절한 위치에 스폰합니다.

 

- StiarMove 함수

플레이어가 버튼을 누를 때마다 계단의 위치가 움직이도록 StairMove 함수를 호출합니다.

방향에 맞게 모든 계단 오브젝트들을 움직이며, y축이 -5이하가 된 계단 오브젝트는 

다시 위로 올라가 위치할 수 있도록 SpawnStair 함수를 호출합니다.

그리고 방향전환 버튼과 오르기 버튼을 알맞게 눌렀는지 IsChangeDir 배열과 일치 여부를 검사한 후 

틀렸다면 GameOver 함수를 호출합니다.

 

 

 

 

 

 

각 플레이어들에게 넣어줄 Player 스크립트도 보겠습니다.

public class Player : MonoBehaviour
{
    public Animator anim;
    public AudioSource[] sound;
    public GameManager gameManager;
    public bool isleft = true, isDie = false;
    public int characterIndex, stairIndex, money;

    void Awake() {
        anim = gameObject.GetComponent<Animator>();
    }

    public void Climb(bool isChange)
    {
        if (isChange) isleft = !isleft;
        gameManager.StairMove(stairIndex, isChange, isleft);
        if ((++stairIndex).Equals(20)) stairIndex = 0;
        MoveAnimation();
    }


    public void MoveAnimation()
    {
        //Change left and right when changing direction
        if (!isleft)
            transform.rotation = Quaternion.Euler(0, -180, 0);
        else
            transform.rotation = Quaternion.Euler(0, 0, 0);

        if (isDie) return;
        anim.SetBool("Move",true);
        gameManager.PlaySound(1);
        Invoke("IdleAnimation", 0.05f);        
    }

    public void IdleAnimation()
    {
        anim.SetBool("Move", false);
    }
}

 

Climb 함수는 오르기/방향전환하기 버튼을 누를때마다 호출되고, isChange를 매개변수로 받는데 방향전환하기 버튼을 눌렀다면 true로 호출되며 이는 isleft 를 바꾼다.

 

MoveAnimation 함수는 isleft의 bool값에 따라 플레이어가 y축을 기준으로 회전해야하므로 rotation을 바꿔준다. 

그리고 플레이어의 애니메이션들의 파라미터를 적절히 바꾸어준다.

 

 

 

 

 

 

3. Scene 구성 및 UI

일단 저는  Scene을 2개 만들었습니다.

- 메인메뉴가 보이고 게임을 하는 Scene

- 캐릭터를 선택하는 캐릭터 선택창 Scene

 

 

 

첫번째 씬의 UI 대해서 설명드리겠습니다.

Canvas의 인스펙터는 다음과 같이 수정하였습니다.

 

 

 

 

그리고 먼저 Hierarchy를 보여드리겠습니다.

- MainMenuUI : 무한의계단 게임 어플을 시작하면 가장 먼저 보이는 메인메뉴 UI

- GameProgressUI : 게임 시작 버튼을 누르면 보이는 게임 진행 중의 UI

- GameOverUI : 게임을 진행하다 게임오버가 된 경우 보이는 UI

- PausedUI : 게임 진행 중 일시정지 버튼을 눌렀을 때 보이는 UI

- SettingUI : 메인메뉴(MainMenuUI)에서 설정버튼을 눌렀을 때 보이는, 설정 UI

- RankingUI : 메인메뉴(MainMenuUI)에서 랭킹버튼을 눌렀을 때 보이는, 랭킹 UI

 

 

 

 

① MainMenuUI

게임을 실행하면 가장 먼저 보이는 메인메뉴 화면입니다.

- Title 이미지

- 랭킹 버튼

- 돈 배경 이미지 + 돈을 나타낼 Text

- 설정 버튼

- 캐릭터 선택 Scene으로 가는 버튼

- 게임 시작 버튼

 

 

 

 

 

 

 

 

 

 

 

② GameProgressUI

게임 진행 중의 UI입니다.

- 게이지 이미지

- 일시정지 버튼

- 점수 Text

- 돈 배경 이미지 + 돈을 나타낼 Text

- 오르기 버튼

- 방향전환 버튼

- 설명 이미지 + 설명 Text 

 

 

 

 

 

 

 

 

 

 

③ GameOverUI 

게임 오버 시에 보이는 UI입니다. 이 UI에 애니메이션 또한 적용했습니다..

- 게임오버 Text

- Panel 배경 이미지

   - Best Panel 배경 이미지

      - BEST 글자 Text

      - 최고점수 Text

   - SCORE 글자 Text

   - 점수 Text

- 메인메뉴 버튼

- 게임 다시하기 버튼

 

 

 

 

 

 

 

 

④ PausedUI

게임 진행 중에 일시정지 버튼을 누르면 보이는 UI입니다.

- 배경 이미지

- PAUSED 글자 Text

- 배경음악 On/Off 버튼

- 효과음 On/Off 버튼

- 진동 On/Off 버튼

- 메인메뉴 버튼

- 계속하기 버튼

 

 

 

 

 

 

 

 

 

 

⑤ SettingUI

메인메뉴에서 설정 버튼을 누르면 보이는 UI입니다.

- 배경 이미지

- SETTING 글자 Text

- 나가기 버튼

- 배경음악 On/Off 버튼

효과음 On/Off 버튼

진동 On/Off 버튼

 

 

 

 

 

 

 

 

 

 

 

RankingUI 

메인메뉴에서 랭킹 버튼을 누르면 보이는 UI입니다.

- 배경 이미지

- RANKING 글자 Text

- 나가기 버튼

- 단상 이미지

- 1 글자 Text

- 2 글자 Text

- 3 글자 Text

- 1등 캐릭터 이미지

- 2등 캐릭터 이미지

- 3등 캐릭터 이미지

- 1등 점수 Text

- 2등 점수 Text

- 3등 점수 Text

 

 

 

 

 

 

 

 

 

 

 

4. 게이지 구현하기

무한의 계단 게임은 상단에 게이지가 있고, 움직이지 않으면 게이지가 줄어들며 0이되면 게임오버가 됩니다.

움직이면 게이지가 올라가고, 점수가 높아질수록 게이지가 줄어드는 속도는 빨라집니다.

 

 

 

GameProgressUI에서 게이지바 이미지를 만들고, 게이지 이미지를 만듭니다.

게이지 이미지는 인스펙터에서 빨간색으로 지정하고, Image Type을 Filled로 합니다. 

가로로 크기가 줄었다 늘었다 하므로 Horizontal, 왼쪽을 기준으로 하므로 Left로 지정합니다.

Fill Amout를 줄일수록, 왼쪽을 기준으로 크기가 줄어듭니다. 이를 이용하여 게이지를 구현합니다.

 

 

 

GameManager 스크립트에 다음을 추가합니다.

	public Image gauge;
	public bool gaugeStart = false;
	float gaugeRedcutionRate = 0.0025f;

	void Awake() {     
        GaugeReduce();
        StartCoroutine("CheckGauge");
    }
    
    
 	//#.Gauge
    void GaugeReduce() {
        if (gaugeStart) {
            //Gauge Reduction Rate Increases As Score Increases
            if (score > 30) gaugeRedcutionRate = 0.0033f;
            if (score > 60) gaugeRedcutionRate = 0.0037f;
            if (score > 100) gaugeRedcutionRate = 0.0043f;
            if (score > 150) gaugeRedcutionRate = 0.005f;
            if (score > 200) gaugeRedcutionRate = 0.005f;
            if (score > 300) gaugeRedcutionRate = 0.0065f;
            if (score > 400) gaugeRedcutionRate = 0.0075f;
            gauge.fillAmount -= gaugeRedcutionRate;
        }
        Invoke("GaugeReduce", 0.01f);
    }

    
    IEnumerator CheckGauge() {
        while (gauge.fillAmount != 0) {
            yield return new WaitForSeconds(0.4f);
        }
        GameOver();
    }


    void GameOver() {
        //Animation
        anim[0].SetBool("GameOver", true);
        player.anim.SetBool("Die", true);

        //UI
        ShowScore();
        pauseBtn.SetActive(false);

        player.isDie = true;
        player.MoveAnimation();

        CancelInvoke();  //GaugeBar Stopped      
    }


    //Show score after game over
    void ShowScore() {
        finalScoreText.text = score.ToString();
        dslManager.SaveRankScore(score);
        bestScoreText.text = dslManager.GetBestScore().ToString();

        //When the highest score is recorded
        if (score == dslManager.GetBestScore() && score != 0)
            UI[2].SetActive(true);
    }    
    

Awake함수에서 GaugeReduce함수와 StartCoroutin으로 CheckGauge 함수를 호출합니다.

GaugeReduce는 게이지의 fillAmout를 줄이는 함수이고, Invoke를 통해 재귀호출한다. 

움직이지 않으면 계속해서 게이지가 줄어드는 것을 나타냅니다.

그리고 점수가 높아질수록 게이지의 fillAmout 감소율을 높입니다.

 

CheckGauge는 코루틴 함수로, 계속해서 게이지의 fillAmout가 0이되었는지 검사하여 GameOver함수를 호출합니다.

Update문에 쓰는 것 보다 코루틴을 이용하는 것이 최적화에 도움이 됩니다!

 

GameOver함수에서는 여러가지를 처리하고, CancelInvoke를 통해 게이지가 줄어드는것을 멈춥니다.

그리고, 점수를 보여줄 ShowScore함수를 호출합니다.

 

 

 

 

 

 

 

 

 

 

5. 코인 구현 (Prefab)

무한의 계단 게임에서는 계단을 올라가다보면 코인을 먹을 수 있습니다.

코인은 랜덤으로 스폰하는데, 프리펩으로 구현했습니다. 에셋 폴더에 Prefabs 폴더를 만들고, 코인 오브젝트를 넣습니다.

이 코인 프리펩에 Rigidbody2D와 CapsuleCollider2D 컴포넌트를 추가합니다.

Rigidbody2D는 Kinematic 타입으로, 콜라이더는 isTrigger을 체크합니다. 

 

 

그리고 ObjectManager 오브젝트를 만들고 스크립트를 만듭니다.

public class ObjectManager : MonoBehaviour
{
    public GameManager gameManager;
    public GameObject coinPrefab;

    GameObject[] coin;
    GameObject[] targetPool;

    void Awake()
    {
        coin = new GameObject[20];
        Generate();
    }

    void Generate()
    {
        for(int i=0; i<coin.Length; i++)
        {
            coin[i] = Instantiate(coinPrefab, gameManager.stairs[i].transform);
            coin[i].transform.position += new Vector3(0, 0.6f, 0);
            coin[i].SetActive(false);
        }     
    }


    public void MakeObj(string type, int index)
    {
        switch (type)
        {
            case "coin":
                targetPool = coin;
                break;
        }

        if (!targetPool[index].activeSelf)
        {
            targetPool[index].SetActive(true);
        }
    }
}

coinPrefab에 프리펩 폴더에있는 코인 프리펩을 드로그앤드랍합니다.

ObjectManager 스크립트는 오브젝트 풀링을 하는 역할을 합니다.

항상 오브젝트를 Instantiate(생성)를 하고, Destroy(삭제)를 하면 최적화에 도움이 되지 않습니다.

그래서 일정 갯수를 생성해놓고, 오브젝트를 활성화/비활성화 하는 식으로 구현합니다.

 

 

coin[i] = Instantiate(coinPrefab, gameManager.stairs[i].transform);

위 코드를 보면 코인 오브젝트를 생성할 때, gameManager.stairs[i].transform의 자식으로 생성했습니다.

즉, 계단 오브젝트의 자식으로 생성하여 랜덤한 확률로 활성화/비활성화합니다. 

게임을 시작하면 사진과 같이 계단 오브젝트의 자식으로

코인 오브젝트가 생성되고, 활성화 또는 비활성화 됩니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

플레이어가 코인을 먹기위해서 충돌을 검사하고 돈을 증가시킵니다.

Player의 스크립트에 다음을 추가합니다.

 private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.tag == "Coin")
        {
            collision.gameObject.SetActive(false);
            gameManager.PlaySound(0);
            money += 2;
            dslManager.LoadMoney(money);
        }
    }

 

 

 

 

 

 

오늘은 여기까지 포스팅을 마치고 , 

다음에 계속해서 설명하겠습니다. 감사합니다😁~

 

 

 

 

 

 

 

반응형
댓글
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday