티스토리 뷰

반응형

Unity로 예전에 인기가 많았던 모바일 게임 Flappy Bird를 처음부터 끝까지 혼자 개발해보려고 해요!

다들 해보셨던 기억이 있나요? 화면을 터치하면 새가 점프하고 기둥이나 바닥에 부딪히지 않고

기둥 사이를 잘 지나가면 점수가 올라가는 게임입니당!😎

 

Flappy Bird 플레이 장면

 

저는 유니티 엔진을 기반으로 개발해보려고 합니다.

아직 유니티를 배우게 된 지 얼마 되지 않은 개발 초보입니당 ㅠ.ㅠ

혹시 조언이나 개선해야 할 부분에 대해서 많은 의견주세요!!😊

 

 

 

 

 

리소스 다운 받기

먼저 구현을 위한 플래피 버드 리소스를 다운받아주세요!

구글에서 검색하면 되구 저는 참고로 이곳에서 다운받았습니당

Sprite  https://www.spriters-resource.com/mobile/flappybird/sheet/59894/ d

 

Mobile - Flappy Bird - Version 1.2 Sprites - The Spriters Resource

 

www.spriters-resource.com

Sound  https://www.sounds-resource.com/mobile/flappybird/sound/5309/

 

Mobile - Flappy Bird - Everything - The Sounds Resource

 

www.sounds-resource.com

 

 

 

 

 

유니티로 구현하기

 

프로젝트를 먼저 생성하고 Asset에 다운받은 리소스를 드래그하여 넣어주세요!

그리고 Game의 화면 비율에서 모바일 버젼을 임의로 추가합니다.

9:19나 9:16 비율이면 될 것 같아요! 추가하고 그 비율로 이용하면 됩니다.

 

 

1. 오브젝트 만들기

다운받은 Sprite의 새, 윗 기둥, 아랫 기둥, 바닥을 Scene에 드래그하여 플레이어 오브젝트를 생성합니다.

모두 Transform을 적절하게 수정하고, Order in Layer 또한 적절하게 바꾸어서 화면에 보이는

우선순위를 정해주세요!

 

 

 

 

 

2. Player 움직임 구현하기

게임 시작 장면에서 보면, 플레이어가 위아래로 조금씩 왔다갔다하며 날갯짓 하는 모습을 보입니다.

이를 애니메이션으로 만들어줍니다. 플레이어에 Animator를 만들고 Animation을 생성합니다.

저는 PlayerIdle(가만히 있는 애니메이션)과 PlayerMove(움직이는 애니메이션)을 만들었습니다.

 

PlayerMove : Sprite를 활용하여 Player가 날갯짓을 하도록 애니메이션을 만들어준다. y축 또한 위아래로 움직이게 만든다.  

 

 

 

 

애니메이션을 만든 후에,

플레이어에게 물리적 요소를 추가하기 위해 Rigidbody2D와 Polygon Collider2D 컴포넌트를 넣어줍니다.

 

 

Rigidbody2D의 중력과 다른 요소들을 조금 수정하였습니다.

그리고 다른 오브젝트와의 충돌을 위해 콜라이더의 isTrigger을 체크하여주세요!

 

 

 

 

 

다음은 C# 스크립트를 만들고 플레이어 오브젝트에 넣어줍니다. 

실제 게임에서 보면 플레이어가 점프를 할 때 15도 정도 회전을 하고

떨어지는 동안에도 -90도 방향으로 계속 회전을 합니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    public float jumpPower;
    public bool isStart = false;
    public bool isDie = false;

    Rigidbody2D rigid;
    Animator anim;

    void Awake()
    {
        rigid = GetComponent<Rigidbody2D>();
        anim = GetComponent<Animator>();
    }

    void Update()
    {
        //점프    
        //Mobile : Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began
        if (Input.GetButtonDown("Jump") && !isDie)
        {
        	//일정한 높이로 점프
            rigid.velocity = new Vector2(rigid.velocity.x, jumpPower);  
            transform.rotation = Quaternion.Euler(0, 0, 15);  //점프할 시에 15도로 회전
            anim.enabled = false;
            isStart = true;
        }

        if (!isStart) return;

        //회전
        if (rigid.velocity.y < -4 && rigid.velocity.y > -7)
                transform.Rotate(0, 0, -6f);

        //플레이어가 화면 밖으로 나가지 않도록 이동 제한
        if (transform.position.y > 4.8f)
            rigid.position = new Vector2(-1.2f, 4.8f);
    }
}

일단, 컴퓨터로 실행하기 위해 스페이스를 눌렀을 때 점프를 하도록 합니다.

모바일에서 실행하기 위해서는 Input.GetButtonDown("Jump") 이 부분 대신 위의 주석 내용을 넣으시면 됩니다!

 

Rigidbody2D의 velocity를 통해 일정한 높이(jumpPower)로 점프하도록 합니다.

그리고 첫 화면에서 실행되는 애니메이션을 취소하고 게임을 시작해야 하므로,

애니메이션의 enabled를 false로 만들어주세요. 

 

점프를 하고 떨어질 동안에는 -90도를 향해 회전해야 하기 때문에

저 같은 경우는 속도 범위를 적절하게 제한하여 z축이 -6씩 회전하도록 하였습니다.

이렇게 되면 Update문은 계속 호출되기 때문에 -90도까지 회전이 되는 것을 볼 수 있어요!

 

그리고 플레이어가 하늘 위로 끝없이 점프가 가능하면 안되겠죠? 그래서 y축 이동 제한을 두었습니다.

 

 

 

 

 

3. Floor를 동적으로 표현하여 배경의 생동감 더하기

게임을 보면 바닥이 왼쪽으로 계속 움직이고, 이는 마치 플레이어가 오른쪽으로 움직이는 것처럼 보입니다.

이런 Floor의 동적인 부분을 구현해보겠습니다.

 

일단 Floor 오브젝트에 Rigidbody2D와 Box Collider2D 컴포넌트를 추가합니다.

Rigidbody2D의 Body Type은 Static으로 설정하고, 콜라이더에서는 isTrigger을 체크합니다. (Player와의 충돌을 위해)

 

 

Floor의 Scale을 카메라 넓이보다 조금 더 크게 설정하고, 스크립트를 하나 만들어서 Floor에 넣습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Floor : MonoBehaviour
{
    public float speed;
    public Player player;

    //바닥을 동적으로 표현하여 생동감있는 배경을 표현
    //플레이어가 마치 오른쪽으로 움직이는 것처럼 보임
    void Update()
    {  
    	if (player.isDie) return;

        Vector3 curPos = transform.position;
        Vector3 nextPos = Vector3.left * speed * Time.deltaTime;
        transform.position = curPos + nextPos;

        if (transform.position.x < -0.4)
            transform.position = new Vector3(0, transform.position.y,0);        
    }
}

Player가 죽지 않았을 때, Floor가 왼쪽으로 일정한 속도를 가지고 움직이도록 합니다.

현재의 위치에 Vector3.left * speed * Time.deltaTime 을 더해주는 것이죠. 여기서 Time.deltaTime을 곱하는 것은

PC의 성능과는 무관하게 동등한 조건의 속도를 내기 위함입니다!

 

그렇게 왼쪽으로 Floor가 움직이다가 x좌표가 어느정도 이하가 되면, 다시 원위치를 시키도록 하여

Floor가 화면에서 벗어날만큼 왼쪽으로 움직이지 않고 반복이 가능하도록 합니다. 

 

 

 

 

 

4. 기둥과 점수존을 프리펩으로 만들고 오브젝트 풀링 기법 사용하기

윗기둥과 아랫기둥은 위에서 게임오브젝트로 만드셨죠? 그러면 플레이어가 닿을 때 점수가 올라갈 ScoreZone을 만들겠습니당

ScoreZone 은 빈 게임 오브젝트로 만들어주세요. 그리고 윗기둥, 아랫기둥, ScoreZone 세 오브젝트 모두에  

Rigidbody2D와 Box Collider2D를 넣어주시고, Rigidbody의 type은 kinematic으로 해주세요.

이 오브젝트들은 다른 물리 요소가 필요 없이 움직임만 구현할 것이기 때문입니다. 그리고 콜라이더에서 isTrigger 체크 필수!

 

 

이 오브젝트들을 프리펩으로 만들기 위해 Asset 폴더에 Prefabs 라는 폴더를 만들어주세요! (꼭 철자가 같아야 해용)

그 폴더에 오브젝트 세개를 다 드래그해서 넣고 프리펩이 생성되었다면, Hierarchy에 있었던 세 오브젝트는

지우시면 됩니다. 

 

 

여기서 Prefab이란? 여러 컴포넌트로 이미 구성이 완성된 재사용 가능한 게임 오브젝트 입니다.

나중에 속성을 변경할 때 일일히 게임 오브젝트의 속성을 바꾸지 않아도 프리펩의 속성만 변경하면

생성되는 게임오브젝트에 모두 반영되기 때문에 유용히 쓰입니다.

기둥은 게임을 진행하는 동안에는 계속해서 생성되기 때문에 프리펩을 사용했습니다!

 

 

 

오브젝트를 생성하기 위해서는 Instantiate, 삭제를 위해서는 Destory를 코드에서 사용하지만 

저는 그렇게 하지 않고, 오브젝트 풀링 (Object Pooling) 기법을 사용하였습니다.

 

오브젝트 풀링이란?  오브젝트를 생성과 삭제를 하지 않고 활성화/비활성화로 구현하는 것을 말합니다.

( Instantiate과 Destroy을 쓰지 않는 기법). Instantiate과 Destroy을 사용하게 되면 가비지 콜렉터를 호출하게 되고,

가비지 콜렉터 호출이 많을수록 성능이 낮아지게 됩니다.

 

즉, 게임 최적화를 위해서 오브젝트 풀링 기법을 사용하였습니다. 😄

여기서 사용한 것은 간단한 배열 방식이고 보다 구체화된 기법도 있으니 구글링을 참고해주세요.

 

일단 ObjectManager라는 빈 게임 오브젝트를 만들고, 스크립트를 만들어 넣습니다. 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ObjectManager : MonoBehaviour
{
    //프리펩
    public GameObject pillarUpPrefab;
    public GameObject pillarDownPrefab;
    public GameObject scoreZonePrefab;

    GameObject[] pillarUp;
    GameObject[] pillarDown;
    GameObject[] scoreZone;
    GameObject[] targetPool;


    void Awake()
    {
        pillarUp = new GameObject[3];
        pillarDown = new GameObject[3];
        scoreZone = new GameObject[3];
        Generate();
    }

    //Instantiate로 생성 후 오브젝트 비활성화 해둠
    void Generate()
    {
        for(int i=0; i< pillarUp.Length; i++)
        {
            pillarUp[i] = Instantiate(pillarUpPrefab);
            pillarUp[i].SetActive(false);
        }

        for (int i = 0; i < pillarDown.Length; i++)
        {
            pillarDown[i] = Instantiate(pillarDownPrefab);
            pillarDown[i].SetActive(false);
        }

        for (int i = 0; i < scoreZone.Length; i++)
        {
            scoreZone[i] = Instantiate(scoreZonePrefab);
            scoreZone[i].SetActive(false);
        }
    }


    //원하는 오브젝트를 활성화시킴
    public GameObject MakeObj(string name)
    {
        switch (name)
        {
            case "pillarUp":
                targetPool = pillarUp;
                break;
            case "pillarDown":
                targetPool = pillarDown;
                break;
            case "scoreZone":
                targetPool = scoreZone;
                break;
        }

        for(int i=0; i<targetPool.Length; i++)
        {
            if (!targetPool[i].activeSelf)
            {
                targetPool[i].SetActive(true);
                return targetPool[i];
            }
        }
        return null;
    }

  

	//오브젝트 움직임 멈추기
    public void StopObj(string name)
    {
        switch (name)
        {
            case "pillarUp":
                targetPool = pillarUp;
                break;
            case "pillarDown":
                targetPool = pillarDown;
                break;
            case "scoreZone":
                targetPool = scoreZone;
                break;
        }

        for (int i = 0; i < targetPool.Length; i++)
        {
            if (targetPool[i].activeSelf)
            {
                Rigidbody2D rigid;
                rigid = targetPool[i].GetComponent<Rigidbody2D>();
                rigid.velocity = new Vector2(0,0);
            }
        }
    }
}

 

처음에 유니티에서 프리펩을 넣을 변수를 public으로 만들어줍니다. 윗기둥, 아랫기둥, ScoreZone세 개를 선언합니다.

 

그 다음 GameObject[] 배열로 선언합니다. targetPool은 코드의 간결화를 위해 선언한 배열입니다.

Awake 함수에서 한 번에 등장할 개수를 고려하여 배열 길이를 할당하고, 생성해주기 위한 Generate 함수를 호출합니다.

 

 

- Generate 함수에서는 Instantiate로 생성한 인스턴스를 배열에 저장한후, 비활성화 시킵니다.

- MakeObj는 오브젝트에 접근할 수 있는 함수로, 비활성화된 오브젝트에 접근하여 활성화 후 반환합니다.

- StopObj는 게임 오버 시에 사용되기 위한 함수로, 오브젝트에 접근하여 활성화 되어있는 오브젝트의 움직임이 멈추도록 하였습니다. 

 

 

 

그리고 프리펩을 넣을 변수는 유니티에서 꼭 할당해주세요!

오른쪽의 프리펩들을 ObjectManager의 각 변수에 할당시켜준다

 

 

 

 

 

 

 

 

이 오브젝트들을 생성하고 활성하하여 적절한 위치에 스폰하는 것은 다음 포스트에서 하겠습니다.

 

감사합니당😍

 

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