여러분들과 함께 게임 개발을 공부하는 베르입니다!

2021년 1월 24일 일요일에 진행한 프로그래밍 & 개발 입문자들이 느끼는 어려움에 대해 이야기하는 스트리밍을 편집한 두 번째 편입니다!

 

타임라인

0:00 QnA2 - 게임 서버는 어떻게 공부했나?

2:20 포트폴리오에 대한 이야기

4:39 개발자의 수준을 결정하는 것들

6:55 QnA3 - 인디 개발자 이야기

7:55 QnA4 - 게임 개발에 필요한 수학 지식?

9:06 QnA5 - 게임 기획?

10:55 QnA6 - 베르가 생각하는 유니티와 언리얼

12:54 QnA7 - 게임 엔진 개발 공부?

14:24 공부할게 너무 많다

15:54 아웃트로

반응형

Physics 

레이어로 Collider의 충돌 범위를 설정해서 특정한 충돌만 받아들이거나 무시하기

 

작성 기준 버전 :: 2019.2

 

[이 포스트의 내용은 유튜브 영상으로도 시청하실 수 있습니다]

 

이번 포스트에서는 유니티의 레이어로 Collider의 충돌 범위를 설정하는 방법을 알아보도록 하자.

 

두 개의 Collider가 충돌하면 OnCollision 혹은 OnTrigger 이벤트가 발생하며 개발자는 이 이벤트를 통해서 충돌을 감지했을 때 처리되어야 할 기능을 구현한다는 것을 기억할 것이다.

 

과연 게임에서는 어떤 상황과 이유에서 Collider의 충돌 범위를 설정해서 특정한 충돌을 받아들이거나 무시해야 할까?

 

만약 두 캐릭터가 서로를 향해서 총알을 발사한다고 생각해보자. 그런데 서로에게 발사한 총알끼리의 충돌을 무시하지 않고 그대로 두면 어떻게 될까? 총알끼리 충돌하면 서로에게 발사된 총알이 없어져 버리거나 튕겨져 나갈 것이다.

 

와! 총알을 쏴서 상대방의 총알을 막을 수 있는 게임이라니! 잘 만들면 꽤나 재밌고 멋있을 것 같은 컨셉이다.

 

하지만 대부분의 게임에서는 총알끼리의 충돌같은 건 구현하지 않고 무시하게 만들어버린다.

 

그 이유는 여러 가지가 있을 수 있는데 수많은 총알이 발생시키는 충돌로 게임의 성능이 저하될 수 있다는 것과 이런 총알로 총알을 맞출 수 있는 컨트롤 중심적인 시스템에서는 팬티만 입고 권총을 든 무시무시하고 고일 대로 고여버린 고인물이 달려와서 초보자가 쏜 총알을 모조리 막아버리고 초보자의 뚝배기를 터뜨려 버릴 수 있다는 것이다.

 

총알과 총알이 부딪히는 것 외에도 많은 문제가 있다.

 

어떠한 예외가 있을 수 있는지 살펴보자면 열심히 체력을 깎아놓은 몬스터가 힐팩에 스쳐서 건강해진다던가 플레이어는 던전 입구에서 헤매고 있는데 다른 층으로 넘어가는 콜라이더 앞에서 서성거리던 몬스터가 그 트리거를 건드려서 플레이어가 다음 층으로 넘아가던가 하는 많은 문제가 발생할 수 있다.

 

이걸 코드 레벨에서 막으려면 총알 클래스에는 충돌 검사를 할 때 충돌한 대상이 같은 총알이면 무시하는 코드를, 힐팩 클래스과 던전 층 이동용 트리거 클래스에서는 트리거에 닿은게 플레이어가 아니면 무시하는 코드를 작성해야 할 것이다. 이렇게 수동으로 일일이 예외를 막아야하는 경우가 많으면 많을수록 앞에서 언급한 것과 같은 어처구니가 없게 느껴지는 버그가 발생할 확률이 상승한다.

 

만약 이걸 별도의 코드 작업 없이 간단하고 일괄적으로 막을 수 있다면 당연히 그 방법을 써야될 것이다.

 

그게 바로 이번에 배울 유니티 레이어를 이용한 Collider 충돌 무시하기이다.

 

본격적인 내용에 들어가기에 앞서 아래에 있는 unity-mouse-input-practice.zip 파일을 다운로드 받아서 패키지를 임포트하도록 한다.

 

unity-mouse-input-practice.zip
다운로드

 

그리고 패키지에 포함되어 있는 Simple Character Test 씬을 열도록 한다.

 

 

먼저 게임를 플레이시키고 게임 뷰에 클릭해보면 클릭을 한 번 할 때마다 총알이 한 발씩 나가는 것을 볼 수 있다.

 

void Fire()
{
    //if(Input.GetMouseButtonDown(0))
    if(Input.GetMouseButton(0))
    {
        Vector3 firePos = transform.position + animator.transform.forward + new Vector3(0f, 0.5f, 0f);
        var bullet = Instantiate(bulletPrefab, firePos, Quaternion.identity).GetComponent<Bullet>();
        bullet.Fire(animator.transform.forward);
    }
}

 

SimpleCharacterController 스크립트를 열어서 Fire 함수 안에 있는 GetMouseButtonDown 함수를 GetMouseButton으로 바꾼 뒤, 코드를 저장하고 에디터로 돌아간다.

 

 

다시 게임을 플레이시키고 게임 뷰에서 마우스 왼쪽 버튼을 꾹 누르고 있으면 총알이 쏟아져 나오는 것을 볼 수 있다.

 

private void OnTriggerEnter(Collider other)
{
    if(other.GetComponent<Bullet>() == null)
    {
        Destroy(gameObject);
    }
}

 

그 다음에는 Bullet을 찾아서 스크립트 에디터를 열어보면 아래 쪽에 있는 트리거 감지 이벤트인 OnTriggerEnter에 앞에서 말한 것처럼 충돌 감지 예외를 코드 레벨에서 수동으로 처리하고 있는 것이 보일 것이다. 같은 총알끼리 부딪혔을 때는 무시하도록 작성되어 있다.

 

private void OnTriggerEnter(Collider other)
{
    //if(other.GetComponent<Bullet>() == null)
    {
        Destroy(gameObject);
    }
}

 

이 부분을 주석 처리 해버리면 어떻게 될까? 한 번 테스트 해보자.

 

 

게임을 플레이시키고 마우스를 클릭해보면 발사된 총알끼리 부딪혀서 앞으로 나가지 못하고 바로 사라져버리는 걸 볼 수 있다.

 

그럼 이걸 어떻게 코드 레벨의 예외처리 없이 원래대로 동작하게 만들 수 있을까?

 

이제부터 그걸 알아보자.

 

  

프로젝트 뷰에서 Prefabs 폴더 안에 있는 Bullet 프리팹을 더블클릭해서 프리팹 수정 씬을 열어보자. 그럼 선택된 Bullet 프리팹 게임 오브젝트의 내용을 인스펙터 뷰에서 볼 수 있는데 게임 오브젝트의 이름 아래를 보면 태그와 함께 Default라고 표시된 레이어를 찾을 수 있다.

 

레이어를 클릭해보면 Default, TrasparentFX, Ignor Raycast, Water, UI가 있다.

 

이 레이어에는 여러가지 역할이 있지만 대표적인 것이 바로 지금 배우고 있는 충돌 무시 설정이다.

 

항목들 중에서 제일 아래에 있는 [Add Layer]를 선택하면 레이어를 직접 만들 수 있다.

 

 

Bullet이라는 레이어를 만들어보자.

 

 

그리고 다시 Bullet 프리팹으로 돌아가서 레이어를 Bullet으로 설정한다.

 

그 다음은 레이어의 충돌 설정을 할 차례이다.

 

 

상단 메뉴 바에서 [Edit > Project Settings]를 선택해서 프로젝트 세팅 뷰를 연다.

 

 

이중에 Physics 탭으로 들어가면 레이어끼리의 충돌 설정을 할 수 있다. 가장 아래 쪽을 보면 직각삼각형 형태로 배치된 체크박스들을 볼 수 있는데 행이나 열마다 레이어의 이름이 적혀있는 것을 알 수 있다.

 

제일 앞 칸을 기준으로 보면 Default와 Bullet 사이는 체크가 되어있는데 이것은 Bullet 레이어와 Default 레이어의 오브젝트가 충돌하면 이 충돌을 감지하겠다는 뜻이다. 반대로 체크가 해제되면 두 레이어 사이의 충돌을 무시하겠다는 뜻이 된다.

 

우리는 Bullet끼리의 충돌을 무시할 계획이기 때문에 Bullet과 Bullet이 만나는 지점의 체크를 해제해준다.

 

프로젝트 세팅 뷰를 닫고 플레이를 시킨 뒤 총알을 발사해보자.

 

 

그럼 총알끼리 충돌해서 사라지지 않고 아주 잘 발사되는 것을 볼 수 있다.

 

단 레이어를 사용할 때 주의할 점은 사용자가 만들 수 있는 최대 레이어 갯수는 총 24개 뿐이기 때문에 불필요한 레이어를 함부로 남발하면 나중에 정작 필요한 레이어의 자리가 부족해지는 문제가 발생할 수 있다는 것이다.

 

반응형
  1. 료용 2020.04.22 01:51 신고

    베르님 유투브 파셨네요 좋은영상 기대하겠습니다.

    • wergia 2020.04.22 17:02 신고

      글만으로는 모자란 감이 있어서 구현 과정을 보여드리려고 만들었어요ㅎㅎ

Programming 

씬 불러오기

 

작성 기준 버전 :: 2019.2

 

[이 포스트의 내용은 유튜브 영상으로도 시청하실 수 있습니다]

 

유니티 엔진의 씬은 게임의 맵이나 레벨 개념에 해당한다. 그리고 대부분의 게임은 하나보다 많은 맵이나 레벨, 즉 씬으로 구성된다. 그래서 게임에서 다른 씬으로 넘어가기 위해서는 씬과 씬 사이를 이동하기 위해서 다른 씬을 불러오는 방법을 알아야 한다.

 

씬 세팅하기

 

 

먼저 프로젝트를 제일 처음 생성했을 때 있는 씬에 큐브 오브젝트를 생성하고 카메라 앞 왼쪽 화면에 보이게 적당히 배치한 다음 씬을 저장한다. [Ctrl + S] 단축키를 누르면 간단하게 씬의 변경 내용을 저장할 수 있다.

 

그리고 프로젝트 뷰의 Scenes 폴더 아래에 지금 로드되어 있는 씬인 Sample Scene의 이름을 Scene1로 변경한다.

 

그 다음에는 이 Scene1에서 이동하게 될 새로운 씬을 생성하자.

 

 

프로젝트 뷰에 우클릭하고 [Create > Scene] 항목을 선택하면 새로운 씬을 생성할 수 있다. 새로 생성한 씬의 이름은 Scene2로 한다.

 

 

이렇게 생성한 씬 애셋을 더블 클릭하면 하이어라키에 열려있던 씬이 Scene1에서 Scene2로 바뀐다.

 

 

그리고 Scene2에는 Shpere 게임 오브젝트를 생성해서 카메라 앞 오른쪽 화면에 보이게 배치한다. 이렇게 하면 Scene1에 있다가 Scene2를 불러오면 화면 앞에 있는 게임 오브젝트의 모양이 바뀌면서 씬이 바뀌었다는 것을 명확하게 알 수 있을 것이다.

 

Scene2를 저장하고 다시 Scene1로 돌아간다.

 

SceneMover 스크립트 작성하기

 

그 다음에는 SceneMover라는 이름으로 C# 스크립트를 생성하자.

 

using UnityEngine.SceneManagement;

 

제일 먼저 스크립트의 상단에 using UnityEngine.SceneManagement; 라는 코드를 작성한다. 이 코드는 개발자가 스크립트 에디터에게 "SceneMover.cs 파일에서 UnityEngine.SceneManagement 네임스페이스에 들어있는 씬과 관련된 기능을 사용하겠다"라고 알려주는 역할을 한다.

 

참고로 네임스페이스라는 것은 C#에서 특정한 기능을 묶어두는 데 주로 사용된다.

 

여기 UnityEngine.SceneManagement 네임스페이스에서는 유니티 엔진의 씬과 관련된 기능들이 담겨있다.

 

public class SceneMover : MonoBehaviour

{

    void Update()

    {

        if(Input.GetKeyDown(KeyCode.Space))

        {

            SceneManager.LoadScene("Scene2");

        }

    }

}

 

네임스페이스 선언이 끝나면 Update 함수에서 스페이스 키를 입력을 받았을 때, SceneManager.LoadScene 함수를 호출하게 만든다. 이 함수로 다른 씬을 불러올 수 있다. 

 

매개변수에 불러오고자하는 씬의 이름이나 번호를 넣으면 된다. 우리는 Scene1에서 Scene2로 이동할 생각이기 때문에 Scene2라는 이름을 넣어주면 된다.

 

코드를 저장하고 에디터로 돌아가서 게임 오브젝트를 하나 만들고 거기에 SceneMover 컴포넌트를 추가한다.

 

 

그리고 플레이 버튼을 누르고 스페이스 키를 눌러보면 Scene2라는 씬이 빌드 세팅에 추가되지 않았거나 애셋 번들로부터 불러올 수 없어서 로드할 수 없다는 로그가 나온다.

 

유니티에서 씬을 불러오기 위해서는 씬이 저장된 애셋 번들이 있든지 아니면 씬이 빌드 세팅에 추가되어 있어야 한다.

 

빌드 세팅에 씬 추가하기

 

씬을 빌드 세팅에 추가 시켜보자.

 

 

우선 상단 메뉴바에서 [File > Build Settings...] 항목을 선택하면 빌드 세팅 창이 열린다.

 

 

창 위쪽에 비어있는 Scenes In Build 칸을 볼 수 있는데 여기에 추가된 씬들은 게임에 포함되어 빌드된다.

 

만들어둔 Scene1과 Scene2를 Scenes In Build 칸에 끌어다 놓으면 Scenes In Build 칸에 추가한 씬이 표시될 것이다.

 

빌드 세팅 창을 끄고 다시 플레이를 시킨 뒤, 스페이스 버튼을 눌러보면 Scene1에서 Scene2로 이동하면서 Cube 오브젝트가 사라지고 Sphere 오브젝트가 나타난다.

 

이게 바로 제일 기본적인 씬 이동 방법이다.

 

Additive로 씬 불러오기

 

첫 번째 방법은 씬을 완전히 이동하는 방법이었다면 이번에는 기존의 씬을 남겨둔 상태로 새로운 씬을 겹쳐서 불러오는 방법을 알아보자.

 

public class SceneMover : MonoBehaviour

{

    void Update()

    {

        if(Input.GetKeyDown(KeyCode.Space))

        {

            SceneManager.LoadScene("Scene2", LoadSceneMode.Additive);

        }

    }

}

 

매개변수 "Scene2"뒤에 콤마(,)를 찍으면 두 번째 매개변수로 넣을 수 있는 옵션이 나타나는데, 형식이 LoadSceneMode 열거형인 것을 알 수 있다.

 

LoadSceneMode 모드에는 Single과 Addtive, 두 가지가 있는데 우선 Single은 기본 옵션으로 앞에서 구현한 것처럼 씬을 불러오면 이전 씬은 완전히 사라지고 새로운 씬으로 바뀌는 옵션이다. 그리고 Additive는 앞의 씬을 남겨두고 거기에 얹어서 새로운 씬을 불러오는 것이다.

 

두 번째 매개변수에 LoadSceneMode.Additive를 넣어준 뒤 코드를 저장하고 에디터로 돌아가서 게임을 플레이 시킨 다음에 스페이스 키를 누르면 Scene1이 남아있는 상태로 Scene2가 불러와지는 것을 볼 수 있다.

 

다만 콘솔 창을 보면 로그가 굉장히 많이 발생하고 씬을 불러오기 전보다 매우 밝은 것을 알 수 있는데, 이것은 씬 안에 소리를 감지하는 Audio Listener라는 컴포넌트와 Directional Light가 두 개가 있어서 발생하는 문제이다. 그리고 메인 카메라도 중복으로 두 개가 있는 상태이다.

 

이런 문제들 때문에 Addtive로 불러올 씬에는 불필요한 카메라나 Directional Light를 만들지 않는 것이 좋다.

 

비동기 방식 씬 불러오기

 

지금까지 사용한 LoadScene 함수는 동기 방식 함수이다. 이게 무슨 의미인가하면 LoadScene 함수로 씬을 호출하면 씬을 불러오는 과정이 끝날 때까지 다른 일을 아무 것도 하지 못한다는 뜻이다.

 

이 예시처럼 씬에 고작 오브젝트가 몇 개만 있는 게임이라면 LoadScene 함수만으로 씬을 불러와도 되겠지만 최신 게임들처럼 씬 하나에 엄청나게 많은 오브젝트들이 들어있는 무거운 게임이라면 씬을 불러오는 긴 시간동안 아무 것도 하지 못하게 될 것이다.

 

다른 씬을 불러오는 도중에 팁을 보여준다든지 플레이어에게 미니 게임을 할 수 있게 한다든지 해서 씬이 불러와지는 시간에 플레이어가 지루함을 느끼지 않도록 하려면 다른 작업을 처리할 수 있어야 한다. 거기에 필요한게 비동기 방식 씬 불러오기이다.

 

public class SceneMover : MonoBehaviour

{

    void Update()

    {

        if(Input.GetKeyDown(KeyCode.Space))

        {

            StartCoroutine(LoadSceneCoroutine());

        }

    }

 

    IEnumerator LoadSceneCoroutine()

    {

        yield return SceneManager.LoadSceneAsync("Scene2");

    }

}

 

LoadSceneAsync 함수를 사용하면 함수가 씬을 불러오는 도중에 다른 작업을 처리할 수 있게 된다.

 

단, 이 함수를 제대로 사용하려면 코루틴과 함께 사용해야 한다.

 

코드를 저장하고 에디터로 돌아가서 게임을 플레이 시킨 다음 스페이스 키를 누르면 사실 눈에 띄는 변화는 없다.

 

비동기 씬 로딩이 효과가 있으려면 그만큼 불러올 씬이 무겁고 불러올게 많아야 의미가 있다.

 

비동기 씬 불러오기 방식을 응용하는 방법은 아래의 포스트에서 확인할 수 있다.

 

[로딩 화면 구현하기(UI 방식)]

 

[로딩 화면 구현하기(로딩 씬 방식)]

반응형

Programming 

코루틴(Coroutine) 다루기

 

작성 기준 버전 :: 2019.2

 

[이 포스트는 유튜브 영상을 통해서도 시청하실 수 있습니다]

 

Update 함수는 게임 오브젝트가 활성화된 상태에서 매 프레임 호출되어 수행된다. 그래서 유니티 엔진으로 게임을 만들 때는 대부분의 게임 동작을 Update 함수에서 작동하도록 구현한다.

 

그런데 Update 함수는 멈추지 않고 계속해서 동작하는 함수이기 때문에 여기서 일시적으로 돌아가는 서브 동작을 구현하는 것과 어떤 다른 동작이 처리되는 것을 기다리는 기능을 구현하기는 매우 까다롭다.

 

그리고 Update 함수에서 해당 기능을 구현하기 어렵지 않다고 하더라도, 잠시 돌아가는 기능을 Update 함수에 모두 구현하는 것은 비대한 몸집의 Update 함수를 만들어 내서 나중에 게임을 유지보수하는 것이 매우 어려워지는 결과를 낳게 된다.

 

이렇게 한 컴포넌트 내에서 Update 함수와 따로 일시적으로 돌아가는 서브 동작을 구현하거나, 어떤 다른 작업이 처리되는 것을 기다리는 기능을 구현하는데 쓰이는 것이 바로 코루틴이다.

 

Update로 구현한 공격 딜레이

 

코루틴이 필요할 법한 간단한 예시를 들기 위해서 스페이스 키를 누르면 캐릭터가 공격했다고 가정하고 딜레이를 줘서 그 시간 동안에는 다시 공격을 하지 못하게 만드는 기능을 만들어 보자.

 

public class Attacker : MonoBehaviour

{

    public bool isDelay;

    public float delayTime = 2f;

 

    float timer = 0f;

    void Update()

    {

        if (Input.GetKeyDown(KeyCode.Space))

        {

            if (!isDelay)

            {

                isDelay = true;

                Debug.Log("Attack");

            }

            else

            {

                Debug.Log("Delay");

            }

        }

 

        // 업데이트로 구현한 공격 딜레이

        if (isDelay)

        {

            timer += Time.deltaTime;

            if (timer >= delayTime)

            {

                timer = 0f;

                isDelay = false;

            }

        }

    }

}

 

위와 같이 코드를 작성하고 플레이 해보면, 처음 스페이스 키를 누르면 "Attack" 로그가 나오지만, 딜레이가 지나기 전에 다시 누르면 "Delay" 로그가 표시된다.

 

그리고 로그가 지난 이후에 스페이스 키를 눌러야 "Attack" 로그가 출력된다.

 

간단한 기능이라 구현이 그리 어렵지는 않았지만, 앞에서 이야기한 것과 같이 게임 기능이 계속해서 추가될 때마다 "공격 딜레이가 발생했을 때만" 동작하는 코드 같은 일시적 동작 코드가 Update 함수에 계속해서 늘어나면, Update 함수가 비대화되고 유지보수가 어려워진다.

 

코루틴으로 구현한 공격 딜레이

 

똑같은 기능을 이번에는 코루틴으로 구현해보자.

 

// 코루틴으로 구현한 공격 딜레이

IEnumerator CountAttackDelay()

{

    yield return new WaitForSeconds(delayTime);

    isDelay = false;

}

 

먼저 코루틴을 사용하기 위해서는 코루틴 함수를 만들어야 한다.

 

코루틴 함수를 만드는 방법은 간단하게 반환형만 IEnumerator로 만들어주면 된다.

 

그리고 yield return이란 코드를 작성해주면 된다. 이것은 코루틴에서 동작하는 제어권을 유니티에 다시 돌려준다는 뜻이다. 이 yield return 지점에 도착하면 코루틴은 반환 타입으로 정의한 만큼 코드 동작을 중지하고 제어권을 유니티에 돌려준다. 그리고 반환 타입의 조건이 충족되면 이 다음 줄부터 다시 코루틴이 동작한다.

 

코루틴이 제어권을 얼마나 양보할 지 정하는 반환 타입에는 여러 가지가 있다.

 

// 한 프레임 기다림

yield return null;

// 게임 시간으로 1초 기다림(time scale에 영향받음)

yield return new WaitForSeconds(1f);

// 실제 시간으로 1초 기다림(time scale에 영향받지 않음)

yield return new WaitForSecondsRealtime(1f);

// 다음 FixedUpdate 끝날 때까지 기다림

yield return new WaitForFixedUpdate();

// 다음 프레임의 Update와 모든 렌더링이 끝날 때까지 기다림

yield return new WaitForEndOfFrame();

 

코루틴을 모두 작성하고 나면 Update 함수를 아래와 같이 수정하면 된다.

 

void Update()

{

    if (Input.GetKeyDown(KeyCode.Space))

    {

        if (!isDelay)

        {

            isDelay = true;

            Debug.Log("Attack");

            StartCoroutine(CountAttackDelay());

        }

        else

        {

            Debug.Log("Delay");

        }

    }

 

    // 업데이트로 구현한 공격 딜레이

    //if (isDelay)

    //{

    //    timer += Time.deltaTime;

    //    if (timer >= delayTime)

    //    {

    //        timer = 0f;

    //        isDelay = false;

    //    }

    //}

}

 

코루틴 함수는 실행할 때 일반 함수처럼 호출하는 것이 아니라, StartCoroutine 함수를 이용해서 호출해야 한다.

 

코루틴으로 공격 딜레이를 구현하면 Update 함수에서 전부 구현하는 것보다 훨씬 쉽고 간단하게 똑같은 기능을 구현할 수 있다.

 

코루틴과 관련된 포스트

 

코루틴과 관련하여 추가로 확인할 만한 포스트는 아래와 같다.

 

[코루틴 내부에서 무한 루프를 사용할 때 주의점]

 

[코루틴의 호출 시점에 대한 주의점]

 

[커스텀으로 yield return 조건 만들기]

 

반응형
  1. psh 2021.11.15 02:40

    베르님 감사합니당!

+ Recent posts