게임 엔진/Unity

Unity 13일차 - 아이템 받기 게임-2(배운거 총동원, 레벨 디자인)

FakeZero 2022. 2. 10. 00:59

  진짜 마지막 강좌입니다. 물론 [유니티 교과서]로 하는 강좌가 마지막이라는 겁니다. 또 새로운 정보가 있으면 올리도록 할 껍니다. 우선 만들던 게임을 완성시켜 봅시다.


7. 배운걸 총동원! - 2

  저번 시간에 공중에서 떨어질 오브젝트들을 만들었으니 이제 이걸 프리팹으로 만들어서 공중에서 랜덤으로 생성되도록 하겠습니다.

  Original Prefab으로 사과와 폭탄의 프리팹을 만들어줍시다.

  Hierarchy 탭에 있는 원본인 apple과 bomb 오브젝트는 삭제해주도록 합시다.

 

  이제 오브젝트를 떨어뜨리는 스크립트를 만듭시다. ItemGenerator라는 이름으로 새로운 C# 스크립트를 만들어주었습니다.

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

public class ItemGenerator : MonoBehaviour
{
    public GameObject applePrefab;
    public GameObject bombPrefab;
    float span = 1.0f;
    float delta = 0;

    void Update()
    {
        this.delta += Time.deltaTime;
        if(this.delta > this.span)
        {
            this.delta = 0;
            Instantiate(applePrefab);
        }
    }
}

  7일차에서 썼던 시간을 제어하는 코드를 쓰겠습니다. 1초에 한 번씩 사과가 떨어지게 만들었습니다. Instantiate 키워드는 인스턴스를 생성하는, 즉, 오브젝트를 생성하는 키워드입니다.

  ItemGenerator라는 빈 오브젝트를 만들고 거기에 ItemGenerator 스크립트를 적용해주겠습니다.

  이제 아웃렛 접속으로 사과와 폭탄 오브젝트를 각각 적용해줍시다. 이제 게임을 실행하면 사과가 떨어질 것입니다. 하지만 이래서는 같은 위치에서 1초 마다 사과가 떨어질 뿐입니다. 오브젝트가 떨어지는 위치를 무작위로 변경하고 떨어질 오브젝트도 달라지게 만들겠습니다.

  밑판의 x좌표와 z좌표는 위와 같이 되어있습니다. 따라서 난수를 생성해서 생성 위치를 랜덤으로 만들어 주겠습니다.

 

  ItemGenerator 스크립트를 다음과 같이 수정합시다.

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

public class ItemGenerator : MonoBehaviour
{
    public GameObject applePrefab;
    public GameObject bombPrefab;
    float span = 1.0f;
    float delta = 0;

    void Update()
    {
        this.delta += Time.deltaTime;
        if(this.delta > this.span)
        {
            this.delta = 0;
            GameObject item = Instantiate(applePrefab) as GameObject;
            float x = Random.Range(-1, 2);
            float z = Random.Range(-1, 2);
            item.transform.position = new Vector3(x, 4, z);
        }
    }
}

(18~21번 줄이 추가되고 변경되었습니다.)

  이제 타일 안의 무작위의 장소에서 사과가 떨어질 것입니다.

 

  이제 사과 뿐 아니라 일정 확률로 폭탄도 떨어지도록 해보겠습니다.

 

  다시 ItemGenerator 스크립트를 엽시다.

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

public class ItemGenerator : MonoBehaviour
{
    public GameObject applePrefab;
    public GameObject bombPrefab;
    float span = 1.0f;
    float delta = 0;
    int ratio = 2;

    void Update()
    {
        this.delta += Time.deltaTime;
        if(this.delta > this.span)
        {
            this.delta = 0;
            GameObject item;
            int dice = Random.Range(1, 11);
            if(dice <= this.ratio)
            {
                item = Instantiate(bombPrefab) as GameObject;
            }
            else
            {
                item = Instantiate(applePrefab) as GameObject;
            }
            float x = Random.Range(-1, 2);
            float z = Random.Range(-1, 2);
            item.transform.position = new Vector3(x, 4, z);
        }
    }
}

(11, 19~28번줄이 추가되었습니다.)

  dice의 값이 1~10중 2 이하면 폭탄을, 3 이상이면 사과를 내보내게 되어있습니다.

 

(프레임이 고정되어 있지 않기 때문에 Game 창에서 오브젝트가 너무 빠르게 내려오는 것입니다.)

  폭탄이 내려오는 것도 확인했습니다. 그러나 이대로 난이도 그대로라면 게임이 재미 없을 것입니다. 난이도의 쉬운 변경을 위해 게임 엔진 측에서 값을 변경할 수 있도록 public 메서드를 만들도록 하겠습니다.

 

  ItemGenerator 스크립트를 다시 엽시다.

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

public class ItemGenerator : MonoBehaviour
{
    public GameObject applePrefab;
    public GameObject bombPrefab;
    float span = 1.0f;
    float delta = 0;
    int ratio = 2;
    float speed = -0.03f;

    public void SetParameter(float span, float speed, int ratio)
    {
        this.span = span;
        this.speed = speed;
        this.ratio = ratio;
    }

    void Update()
    {
        this.delta += Time.deltaTime;
        if(this.delta > this.span)
        {
            this.delta = 0;
            GameObject item;
            int dice = Random.Range(1, 11);
            if(dice <= this.ratio)
            {
                item = Instantiate(bombPrefab) as GameObject;
            }
            else
            {
                item = Instantiate(applePrefab) as GameObject;
            }
            float x = Random.Range(-1, 2);
            float z = Random.Range(-1, 2);
            item.transform.position = new Vector3(x, 4, z);
            item.GetComponent <ItemController>().dropSpeed = this.speed;
        }
    }
}

(12, 14~19, 40번 줄이 추가되었습니다.)

  아이템이 내려오는 속도도 이곳에서 제어할 수 있게 하겠습니다.

    public void SetParameter(float span, float speed, int ratio)
    {
        this.span = span;
        this.speed = speed;
        this.ratio = ratio;
    }

  이 부분에서 조금 어리둥절 할 수도 있습니다. 왜 같은 변수끼리 대입하는거지? 라고 생각할 수도 있겠지만 현재 대입하고 있는 변수들은 서로 다른 변수입니다. this가 붙어 있는 변수는 ItemGenerator 클래스에서 처음 선언한, 즉, 9, 11, 12번 줄에서 선언했던 변수들입니다. 그리고 this가 붙어있지 않은 변수는 SetParameter 메서드에 넣은 인자입니다. 따라서 둘은 다른 변수인 것입니다.

 

  지금 만든 SetParameter 메서드는 다른 스크립트에서 이용하게 될 껍니다.


8. UI

  이제 점수를 기록할 UI를 만들어 보도록 합시다.

  Text UI를 만들어주고 이름을 Time이라고 해줍시다.

  앵커 포인트를 오른쪽 위로 설정해주고 나머지 설정을 위와 같이 설정해 줍시다.

 

  남은 시간을 표시하는 UI를 만들었으므로 점수를 표시하는 UI도 만듭시다.

  Point라는 Text UI를 만들고 위와 같이 설정합시다.

  위와 같이 생긴다면 정상입니다. 이제 UI를 제어할 감독 스크립트를 만듭시다.

 

  GameDirector이라는 새로운 C# 스크립트를 만들고 아래와 같이 프로그램 합시다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; //UI 라이브러리

public class GameDirector : MonoBehaviour
{
    GameObject timerText;
    float time = 60.0f;

    void Start()
    {
        this.timerText = GameObject.Find("Time");    
    }

    void Update()
    {
        this.time -= Time.deltaTime;
        this.timerText.GetComponent<Text>().text = this.time.ToString("F1");
    }
}

  일단 시간을 표시하는 UI부터 제어하겠습니다. 19번 줄이 잘 이해가 가지 않는다면 5일차 강좌를 참고하시면 됩니다.

  빈 오브젝트를 만들고 GameDirector라는 이름으로 만들어 줍시다. 이제 여기다 GameDirector 스크립트를 적용시킵시다.

 

  시간이 정상적으로 흘러간다는 것을 확인했다면 다음엔 포인트를 관리해봅시다.

 

  다시 GameDirector 스크립트를 열어봅시다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; //UI 라이브러리

public class GameDirector : MonoBehaviour
{
    GameObject timerText;
    GameObject pointText;
    float time = 60.0f;
    int point = 0;

    public void GetApple()
    {
        this.point += 100;
    }
    public void GetBomb()
    {
        this.point /= 2;
    }

    void Start()
    {
        this.timerText = GameObject.Find("Time");
        this.pointText = GameObject.Find("Point");
    }

    void Update()
    {
        this.time -= Time.deltaTime;
        this.timerText.GetComponent<Text>().text = this.time.ToString("F1");
        this.pointText.GetComponent<Text>().text = this.point.ToString() + "point";
    }
}

(9, 11, 13~21, 26, 33번 줄이 추가 되었습니다.)

  사과를 먹으면 100점을 추가하고 폭탄을 먹으면 현재 점수를 반으로 줄이는 코드입니다. 하지만 이것만으로는 점수가 늘거나 줄거나 하지 않습니다. 만든 GetApple, GetBomb 메서드를 다른 곳에서 사용해야 합니다. 실제로 위에서 떨어지는 오브젝트를 받는 것은 바구니이므로 바구니에 적용된 스크립트를 통해 메서드를 사용하겠습니다.

 

  BasketController 스크립트를 열고 다음과 같이 수정합니다.

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

public class BasketController : MonoBehaviour
{
    public AudioClip appleSE;
    public AudioClip bombSE;
    AudioSource aud;
    GameObject director;

    void Start()
    {
    	Application.targetFrameRate = 60;
        this.director = GameObject.Find("GameDirector");
        this.aud = GetComponent<AudioSource>();
    }

    void OnTriggerEnter(Collider other)
    {
        if(other.gameObject.tag == "Apple")
        {
            this.director.GetComponent<GameDirector>().GetApple();
            this.aud.PlayOneShot(this.appleSE);
        }
        else
        {
            this.director.GetComponent<GameDirector>().GetBomb();
            this.aud.PlayOneShot(this.bombSE);
        }
        Destroy(other.gameObject);
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, Mathf.Infinity))
            {
                float x = Mathf.RoundToInt(hit.point.x);
                float z = Mathf.RoundToInt(hit.point.z);
                transform.position = new Vector3(x, 0, z);
            }
        }
    }
}

(10, 14, 15, 23, 28)번 줄이 추가되었습니다.)

  GameDirector 스크립트를 불러와서 GetApple과 GetBomb이 작동하도록 코드를 짰습니다. 그리고 프레임 고정 코드도 넣어 두었습니다.

 

  점수가 잘 올라가는 것도 확인되었습니다. 이제 게임 제작을 마무리 지어봅시다.


9. 레벨 디자인

  어렸을 적, 게임을 하고 있을 때에 부모님께서 자라고 하면 자주 하던 말이 있습니다. 한 판만 더 하고, 이것만 하고 같은 말들입니다. 이 말을 부모님께는 말을 안듣는 자녀의 말일 수 있지만 게임에 있어 이 말은 최고의 찬사가 아닌가 싶습니다. 한 번만 더, 조금만 더라는 말은 아직 이 게임을 그만두고 싶지 않고 더 하고 싶다는 뜻이기 때문입니다.

 

  위의 얘기의 요점은 게임은 지루해서는 안된다는 것입니다. 무슨 일이 있더라도 게이머가 흥미를 가지고 게임을 지속해나갈 수 있도록 해야합니다. 엔딩이 존재하는 게임의 경우 플레이어를 어떻게 해서든 중간에 게임을 끄지 않고 게임을 계속하게 만들어야 하고 엔딩이 존재하지 않는 게임의 경우에도 지속적인 업데이트나 밸런스 패치 등으로 플레이어가 게임에 흥미를 잃지 않도록 유도해야합니다.

 

  사람들이 게임에 흥미를 계속해서 가지게 하면서 게임을 계속하게 할 수 있는 장치 중 하나는 바로 레벨 디자인입니다. 이번 게임 같이 단순한 동작을 반복하는 게임의 경우 점점 난이도를 올리면 플레이어는 자신의 한계에 도전하려고 합니다. 이미 스마트폰에는 이런 게임들이 많이 있습니다. 아마 여러분들도 친구들과 반복되는 행동을 하는 게임을 하며 누가 더 많은 스코어를 올렸는지 자랑한 경험이 있을 것이라 생각합니다.

 

  이번 게임에서 레벨 디자인과 관련된 문제는 2가지 정도 있습니다.

  1. 제한시간이 너무 길어서 지루해진다.

  2. 제한시간동안 똑같은 작업을 반복해서 재미없다.

 

  하나씩 해결해 봅시다.

 

  1. 제한시간이 너무 길어서 지루해진다.

  이 문제의 해결 방법은 간단합니다. 단순히 제한 시간을 알맞게 줄이면 됩니다. 이 참에 제한시간이 다되면 게임이 끝나도록 해봅시다.

 

  GameDirector 스크립트를 열어서 수정합시다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; //UI 라이브러리

public class GameDirector : MonoBehaviour
{
    GameObject timerText;
    GameObject pointText;
    GameObject generator;
    float time = 30.0f;
    int point = 0;

    public void GetApple()
    {
        this.point += 100;
    }
    public void GetBomb()
    {
        this.point /= 2;
    }

    void Start()
    {
        Application.targetFrameRate = 60;
        this.generator = GameObject.Find("ItemGenerator");
        this.timerText = GameObject.Find("Time");
        this.pointText = GameObject.Find("Point");
    }

    void Update()
    {
        this.time -= Time.deltaTime;

        if(time< 0)
        {
            this.time = 0;
            this.generator.GetComponent<ItemGenerator>().SetParameter(10000.0f, 0, 0);
        }
        this.timerText.GetComponent<Text>().text = this.time.ToString("F1");
        this.pointText.GetComponent<Text>().text = this.point.ToString() + "point";
    }
}

(10, 11, 26, 35~39번 줄이 추가되고 변경되었습니다.)

  제한 시간을 30초로 줄이고 시간이 음수가 되면 게임이 끝나도록 설정했습니다.

 

  게임이 끝나자 더 이상 오브젝트가 내려오지 않습니다.

 

  2. 제한시간동안 똑같은 작업을 반복해서 재미없다.

  이제 진짜로 레벨 디자인이 필요한 구간입니다. 보통 레벨 디자인은 처음엔 쉽게 하다가 점점 어렵게 하다가도 제한시간이 끝나가는 마지막에 조금 난이도를 풀어주어서 플레이어가 자신의 힘으로 해냈다고 생각하게 만드는 것이 정석입니다. 이 게임에도 그렇게 적용하도록 하겠습니다.

남은 시간 생성 속도 낙하 속도 폭탄 비율
30~23초 1초 간격 -0.03 20%
23~12초 0.8초 간격 -0.04 40%
12~5초 0.5초 간격 -0.05 60%
5~0초 0.7초 간격 -0.04 30%

  위와 같이 난이도를 적용해봅시다. GameDirector 스크립트를 열고 다음과 같이 수정합니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; //UI 라이브러리

public class GameDirector : MonoBehaviour
{
    GameObject timerText;
    GameObject pointText;
    GameObject generator;
    float time = 30.0f;
    int point = 0;

    public void GetApple()
    {
        this.point += 100;
    }
    public void GetBomb()
    {
        this.point /= 2;
    }

    void Start()
    {
        Application.targetFrameRate = 60;
        this.generator = GameObject.Find("ItemGenerator");
        this.timerText = GameObject.Find("Time");
        this.pointText = GameObject.Find("Point");
    }

    void Update()
    {
        this.time -= Time.deltaTime;

        if(time< 0)
        {
            this.time = 0;
            this.generator.GetComponent<ItemGenerator>().SetParameter(10000.0f, 0, 0);
        }
        else if(0 <= this.time && this.time < 5)
        {
            this.generator.GetComponent<ItemGenerator>().SetParameter(0.7f, -0.04f, 3);
        }
        else if (5 <= this.time && this.time < 12)
        {
            this.generator.GetComponent<ItemGenerator>().SetParameter(0.5f, -0.05f, 6);
        }
        else if (12 <= this.time && this.time < 23)
        {
            this.generator.GetComponent<ItemGenerator>().SetParameter(0.8f, -0.04f, 4);
        }
        else if (23 <= this.time && this.time < 30)
        {
            this.generator.GetComponent<ItemGenerator>().SetParameter(1.0f, -0.03f, 2);
        }

        this.timerText.GetComponent<Text>().text = this.time.ToString("F1");
        this.pointText.GetComponent<Text>().text = this.point.ToString() + "point";
    }
}

(40~55번줄이 추가되었습니다.)

난이도를 추가했으니 이제 테스트 해 보도록 합시다.

 

  드디어 게임이 완성되었습니다. 이전보다 폭탄이 많이 나와서 꽤나 긴장감이 있게 되었습니다.

 

  마지막에 가서 왜 다시 쉽게 만드는지 의문이 들테지만 이 기법은 이미 많은 게임에서 차용하고 있는 방법입니다. 게임이 막바지에 다다르면 게이머의 긴장감은 최고조가 되긴 하지만 그간의 플레이 때문에 피로해지고 마지막이라는 생각에 긴장이 풀려서 오히려 게임오버 되는 경우도 많습니다. 이 기법은 그런식으로 게임오버가 되는 경우를 일부러 줄이려는 장치입니다. 즉, 눈속임이죠. 난이도 조정을 통해 적절한 순간에 플레이어를 클리어 시켜줌으로써 플레이어가 달성감을 가지게 하고 게임에 질리지 않도록 유도합니다.

 

  전에도 게임은 보이는게 다라고 말씀드린 적이 있습니다. 이제 이 말을 여기서 조금 더 확장시키도록 하겠습니다. 게임은 느껴지는게 다 입니다. 설사 개발자가 의도하지 않았더라도 플레이어가 그렇게 느낀다고 하면 그런겁니다. 그렇기 때문에 게임을 만든 후에는 반드시 플레이해서 어딜 수정하는 것이 좋을지 확인하고 검수하는 과정이 반드시 반드시 필요합니다. 이것을 알파 테스트라고 합니다.

 

  어떤 게임 회사들은 게임이 어느정도 완성되면 플레이하는 역할을 게이머들에게 맡기기도 합니다. 그것이 베타 테스트입니다. 게임 회사들은 버그를 최대한 잡아내고 레벨 디자인을 더욱 수월하게 하기 위해서 가능한 많은 플레이 데이터를 모으려 합니다. 그것이 이 베타 테스트의 의의입니다.

 

  이번에는 바로 난이도를 수정했지만 실제 게임을 만들때는 직접 플레이 함으로써 알파 테스트를 진행하고 다른 사람에게 플레이 시켜서 베타 테스트를 진행해서 게임을 조정하게 될 것입니다.


  이것을 마지막으로 기타무라 마나미 저자의 [유니티 교과서](개정3판)을 이용한 강좌는 마무리 하겠습니다.

 

  여러분 어떠셨습니까? 게임 엔진에 대하여 좀 이해가 깊어졌습니까? 친숙해졌습니까? 코딩을 어떤 방향으로 해야하는지 감각이 잡혔습니까? 새로운 것을 해보고 그것을 이용한다는 것에 두근거렸습니까? 어떤 감정을 느끼고 어떤 경험을 얻었던 간에 그 경험들은 여러분들의 게임 개발 인생에 도움을 줄 것입니다.

 

  책을 이용한 강좌는 끝났지만 이전에도 얘기했듯 아직 Unity에는 수많은 기능과 가능성이 잠재되어 있습니다. 여러분들이 제 강좌를 통해 보신 것들은 극히 일부에 지나지 않죠. 장래에 어떤 게임 엔진을 쓰게되건 간에 그 프로그램을 최대한 활용할 수 있도록 항상 공부하십시오. 게임 개발을 위해 태어난 프로그램과 친해지십시오. 그것은 분명 여러분들의 게임 개발에 더욱 퀄리티를 부여할 것입니다.

 

  긴 강좌, 그리고 여기까지 오는 여정, 정말 수고하셨습니다. 저는 또 다른 새로운 강좌를 들고 여러분들을 찾아 뵙도록 하겠습니다.

 

  다시 한번 긴 글들 읽고 따라하시느라 수고하셨고 감사했습니다.

 


기본적으로 사람들이 재미를 느끼는 '놀이'는 나라에 관계없이 통할 수 있다. 
사람들이 진심으로 즐겁게 느끼는 것이 무엇인지를 생각하고 제품을 만든다면 세계 어디에서도 팔 수 있다.

  성공하기 위해서는 첫째, 어느 누구도 대신할 수 없는 제품을 만들어야 한다.
유사한 제품이 나올 수 있게 만들면 실패한다.
  둘째, 잘 팔릴 수 있다는 확신이 서기 전에는 제품을 판매하지 말아야 한다.

-닌텐도의 전설의 게임 프로듀서, 슈퍼 마리오, 젤다의 전설, 포켓몬스터의 아버지, 미야모토 시게루-