[C# / Unity 를 이용한 게임 만들기] 고양이의 화살 피하기!

Patrick!·2023년 3월 5일
0
post-thumbnail

게임다운 게임을 만들기 위해서는 !

게임의 규칙시작과 종료의 지점에 대한 명확한 부분이 명시되어야한다.
장르에 맞는 규칙을 확실하게 해야 목표를 가지고 게임에 임할 수 있다.

1. 게임을 기획해보자.

사실 게임을 계획한다는 것은 그 게임의 운명을 결정하는 대부분에 해당할 만큼 매우 중요한 단계이다.
이 부분에서 게임 규칙의 큰 틀이 잡히며 스크립트를 작성하면서 세세하게 구현되게 될 것이다.
(기획단계에서 부터 게임을 어떻게 만들지 두근두근하면서 설레는 부분이라고 생각해 행복하다.)

책에서는 다음과 같이 게임의 규칙을 명명했다.

  1. 고양이(플레이어)가 화살을 지속적으로 피하는 게임을 만들려 한다.
  2. 고양이는 화살에 충돌판정이 발생하면 HP게이지가 줄어든다.
  3. 고양이의 HP게이지가 0이 되면 게임은 종료된다.
  4. 고양이는 화면하단의 이동버튼을 통해 조작할 수 있다.

이를 구현하기 위해서 프리팹(Prefab), 공장, 충돌판정, 아웃렛 접속 과 같은 부분이 필요하며 이번에는 내 것으로 만들어보자.

2. 기획한 것을 하나하나 만들어보자.

게임을 만들기 전에, 각각의 오브젝트가 어떤 부분을 맡는지 잊어서는 안된다.
움직여선 안될 오브젝트가 움직이거나 움직일 오브젝트가 움직이지 않는다면 개판이 될 것이다...

만들려는 게임에서는 고양이, 화살 만 움직이는 오브젝트에 해당한다.
움직이는 오브젝트는 이에 맞는 컨트롤러 스크립트가 필요하다.

그렇기에 고양이, 화살에 필요한 컨트롤러 스크립트를 먼저 만들어보자.

<고양이를 움직이게 하기 위한 Script>

void Update()
{
    /* 키가 눌렸는지 검사하는 메소드
    * Input.GetKeyDown()
    * 매개변수로 전달하는 키가 눌리는 순간을 ture를 한 번 반환
    * 이전에 배운 GetMouseButtonDown 메서드와 비슷한 개념
    */

    // 왼쪽 버튼을 클릭시 캐릭터의 좌표를 -3 시킨다
    if (Input.GetKeyDown(KeyCode.LeftArrow))
    {
        transform.Translate(-3, 0, 0);
    }

    // 오른쪽 버튼을 클릭시 캐릭터의 좌표를 3 시킨다.
    if (Input.GetKeyDown(KeyCode.RightArrow))
    {
        transform.Translate(3, 0, 0);
    }
}

고양이를 조작하기 위해서 화면에 왼쪽/오른쪽 버튼을 두어 조작하는데 이는 버튼이 눌린 것을 기점으로 하여 작동해야 한다.
Input.GetKeyDown(KeyCode.방향Arrow) 를 통해 True 값이 반환되면 이를 방향에 따라 3 / -3 을 이동하도록 스크립트를 구성했다.

<화살을 움직이게 하기 위한 Script>

void Update()
    {
        // 프레임마다 등속으로 하락시킨다.
        transform.Translate(0, -0.1f, 0);

        // 화면 밖으로 나오면 오브젝트를 소멸시킨다.
        if (transform.position.y < -5.0f) Destroy(gameObject);
    }

화살의 경우, 하락하는 구조를 가지고 있기에 Y축의 이동이 요구된다.
그리고 Y축으로 이동하면서 화면 밖으로 이동한 gameObject의 경우, 존재하면 의미가 없고(메모리를 차지할 수 있기에)
삭제하는 과정을 진행시켜주어야 한다.

초기 계획대로 만들어진 게임의 화면


게임 오브젝트를 배치하다보면 고양이가 배경화면에 의해 가려질 경우가 발생한다.
그럼 고양이가 배치가 되었는지 안되었는지을 알 수가 없다.
이는 레이어의 순서가 존재하는데 (PPT로 생각하면 편할 것이다.) 이에 대한 순서를 정해준다면
배경이 게임 오브젝트를 가리는 일은 없을 것이다.

Order in Layer의 초기값은 0 이지만 이를 1로 바꾸는 순간 숨겨져있던 게임오브젝트(고양이와 화살)이 드러나게 된다.

3. 계획한 것을 이루기 위해서는 새로운 기술을 배워야한다.

현재까지 만든 게임에 심각한 문제
1. 고양이가 서있는 Y좌표로 화살이 떨어지지만 고양이와의 충돌 판정이 없기에 오브젝트가 그냥 지나가는 상황이다.
2. 화살이 하나만 떨어지고 끝난다.
3. 고양이가 충돌 판정으로 인해 게임의 규칙중 하나인 hp게이지가 줄어드는 부분이 없다.

첫 번째 문제 를 해결하자!

충돌을 감지하는 부분충돌을 감지한 다음의 움직임을 정하는 부분이라는 [충돌 판정] / [충돌 반응]을 만들어 보도록 하자.

A. 충돌 판정

오브젝트끼리 닿았는지 엄밀히 감지하려면 오브젝트의 윤곽선이 닿았는지 검사해야 한다.
현업에서는 계산량이 매우 많고 스크립트도 복잡한 부분이기에 중요한 부분으로 여겨진다.

여기서는 오브젝트의 형상을 단순하게 원형이라 가정하고 원의 중심좌표와 반경을 사용해 충돌 판정을 만들어보겠습니다.


이는 피타고라스의 정리에 해당하는 개념을 적용하면 간단하게 만들 수 있다.

여기서 두 원의 중심 간 거리 d가 r1 + r2보다 크면 두 원은 충돌하지 않는다.
반대로 d가 r1 + r2보다 작다면 충돌합니다.

이를 <화살을 움직이게 하기 위한 Script>에 적용시켜야 합니다.

public class ArrowController : MonoBehaviour
{
    GameObject player; // GameObject는 적용하고자 하는 프로젝트의 오브젝트 이름을 사용하는 것이 정석!

    void Start()
    {
        // 게임 내의 오브젝트와 연결을 짓기 위한 부분! 꼭 필요하다
        this.player = GameObject.Find("player");
    }

    void Update()
    {
        // 프레임마다 등속으로 하락시킨다.
        transform.Translate(0, -0.01f, 0);

        // 화면 밖으로 나오면 오브젝트를 소멸시킨다.
        if (transform.position.y < -5.0f) Destroy(gameObject);

        // 충돌판정
        Vector2 p1 = transform.position; // 화살의 중심 좌표값을 담는 Vector
        Vector2 p2 = this.player.transform.position; // GameObject를 통해 찾은 player의 중심 좌표값을 담은 Vector

        // p1 과 p2의 충돌여부를 위한 계산
        Vector2 dir = p1 - p2; // p2에서 p1으로 향하는 dir 벡터를 추출
        float d = dir.magnitude; // dir 벡터를 통해 d를 계산

        float r1 = 0.5f; // 화살의 반경
        float r2 = 1.0f; // player의 반경

        if (d < r1 + r2) Destroy(gameObject);

    }
}

주황선은 오브젝트의 충돌판정을 인지하는데 사용되는 중심 좌표의 반경 입니다.
빨간선은 오브젝트 간의 중심 좌표간의 거리를 나타내며, 여기서 충돌여부를 결정합니다.
여기서 두 오브젝트 사이의 거리 d가 반경합(r1 + r2)미만이면 충돌했다고 하고 Destroy(gameObject);를 실행합니다.


두 번째 문제 를 해결하자!

지금 상태는 하나의 화살이 처음에 한 번 떨어지고 끝나기에 게임이 시작과 동시에 끝나는 어이없는 상황이 펼쳐지고 있습니다. 여기에 필요한 기술이 바로 '프리팹과 공장' 입니다.

B. 프리팹과 공장

이전까지 배운 컨트롤러 스크립트와는 개념이 다르다.
'양산기계'가 '설계도'대로 '제품'을 생산하는 구조
설계도 = 이러한 제품을 반들고 싶다! 라는 '견본 파일'과 같은 개념 -> 유니티에서 이를 프리팹(prefab)
양산 기계 = '제너레이터 스크립트' / 제품 = '인스턴스' 정도로 받아들이면 된다.

즉, 같은 오브젝트를 많이 만들고 싶을 때 주로 '프리팹'을 사용한다.
-> 그리고 프리팹을 사용하면 좋은점이 '유지보수적인 측면'에서 아주 좋다는 것이다!

여기서 프리팹으로 만들 '제품'에 해당하는 것은 '화살'이기에 ArrowGenerator를 만들어야 한다.

<화살을 만드는 공장 ArrowGenerator Script>

public class ArrowGenerator : MonoBehaviour
{
    public GameObject arrowPrefab;
    float span = 1.0f;
    float delta = 0;

    void Update()
    {
        /* 이 부분에서 화살이 1개씩 생성되는 로직을 구현
         * 1. 1초가 흐르는 것을 어떻게 구현했는가.
         * => Time.deltaTime;은 프레임과 프레임 사이의 시간 차이를 delta변수에 모아서
         *    'delta > span 값보다 큰 -> 1 보다 크다면' 이라는 작업을 통해 시간의 개념을 만들었다.
         *    
         * 2. 화살표는 어떻게 구현했는가.
         * => GameObject를 생성하는데 이 GameObject는 Instantiate(Prefab);이라는 함수로 만들어진다.
         *    'Instantiate느 인자값으로 Prefab을 넣으면 Instance로 반환해주는 함수'
         *    이렇게 생성된 GameObject를 Random(범위)를 통해 X축 좌표를 생성하여
         *    Vector3(x, y, z)로 만들어 위치하게 한다.
         */
        this.delta += Time.deltaTime;
        if (this.delta > this.span)
        {
            this.delta = 0;
            GameObject go = Instantiate(arrowPrefab);
            int px = Random.Range(-6, 7);
            go.transform.position = new Vector3(px, 7, 0);
        }
    }
}

이를 통해 '화살'을 만드는 공장을 만들었다! 하지만 한가지 문제가 있다.

기존에 만든 'ArrowController' 와 'ArrowGenerator'는 별개의 스크립트라는 것이다.
-> 이는 곧 '화살 충돌판정'과 '화살 공장'이 따로 놀고 있으며 서로를 모른다는 것이다.

이 둘을 연결해주는 방법으로는 '아웃렛 접속'이 있다!

C. 아웃렛 접속 (콘센트를 연결하는 개념)

프리팹을 사용하는 공장(xxxGenerator.script)에서 프리팹을 받는 부분을 public으로 접근 지정자를 설정한다. -> 콘센트
xxxGenerator.script의 inspector창을 보면 public으로 선언한 변수가 스크립트 밑에 생성된다. -> 플러그
이 둘을 연결할 수 있도록 public으로 선언된 변수창prefab을 드래그&드롭을 통해 연결한다.

ArrowGenerator의 inspector창에 연결된 Prefab을 확인할 수 있다.


세 번째 문제 를 해결하자!

지금은 화살에 맞아도 고양이에게 아무런 제약이 없어 무작정 맞으면서 가만히 있어도 된다. 게임의 종료를 정의하지 않은 것과 같기에 이와 같은 상황을 해결하기 위해서 'img를 이용한 hpGauge'를 만들어보자.

D. UI의 고정화 (앵커포인트)

설계한 화면의 크기가 기기에 전부다 맞는 것은 아니며 PC 환경이냐 모바일 이냐에 따라 위치가 달라진다. '닻을 내리는 장소'를 뜻하는 앵커포인트는 해당 위치에 오브젝트를 고정하는 기능이다.

위치를 오른쪽 상단위의 -120, -120, 0 좌표에 상시 표출되도록 설정했으며 [Source Image]의 경우 hp_gauge를 사용했다.

이로써 게임의 종료조건으로 사용할 hpGauge를 게임화면에 배치하는 것은 마쳤다. 하지만 이 UI를 갱신해야하는 문제가 남아있다.
이는 UI Image 에서 제공하는 Fill기능을 사용하면 된다고 한다. [Fill Amount]의 값을 바꾸면 이미지를 줄이거나 늘릴 수 있다고 한다.]

[image Type] = Filled / [Fill Method] = Radial 360 / [Fill Origin] = Top / [Fill Amount] = 1 로 설정했는데

이는 Filled 로 설정하면 hpGauge를 기존 게임의 HP가 줄어드는 개념처럼 만들 수 있으며 이에 대한 방향 또한 설정할 수 있다. (매우 신기하면서 유용한 개념이다!)

자세한 내용은 Unity 공식 문서에서! https://docs.unity3d.com/2019.1/Documentation/ScriptReference/UI.Image.FillMethod.html


위의 세 문제를 해결했으니 게임을 관리하는 스크릡트를 작성해보자! (매우 설레는 과정)

감독 스크립트를 추가하기 위해서 'GameDirector'라는 GameObject를 만들고 Script파일 까지 만들어보자.

<게임을 감독하는 GameDirector Script>

public class GameDirector : MonoBehaviour
{
    GameObject hpGauge;

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

    
    public void DecreaseHp()
    {
        this.hpGauge.GetComponent<Image>().fillAmount -= 0.1f;
    }
}

이제 이를 화살에 맞았을 때 적용시켜보자.
GameDirector 파일에서 DecreaseHp() 함수를 화살이 맞았을 때 화살이 없어지는 부분에 추가해주면 된다.

<충돌 판정을 결정하는 ArrowController Script>

if (d < r1 + r2)
{
      GameObject director = GameObject.Find("GameDirector"); // 게임 오브젝트를 인식하고 
      director.GetComponent<GameDirector>().DecreaseHp(); // 게임 오브젝트의 함수를 실행시키는 개념
      
      Destroy(gameObject);
}

이게 감독 스크립트까지 정상적으로 연동해주었으니 게임을 실행 시켜보면 화살에 충돌 판정이 일어날 때, Hp게이지가 줄어드는 상황을 확인할 수 있다!!


이번에 배운 개념

  1. 충돌판정
  2. 프리팹
  3. 아웃렛 접속
  4. 앵커포인트

출처 : 그림으로 이해하고 만들면서 익히는 유니티 교과서[개정5판] - 기타무라 마나미

profile
C++와 Unreal Engine / C#과 Unity / Katalon Studio를 통한 자동화 테스트 등을 하루하루 공부한 기록

0개의 댓글