본문 바로가기
메타버스기반게임콘텐츠기획/그날의 강의

(9-5) 유니티unity - 게임오버 오브젝트 및 빌드, 게임오버 에너미, 게임오버UI, 씬 이동, 루트 제작, 음향 효과, 게임 실행파일 만들기

by Queenut 2021. 12. 24.

 * 플레이어를 게임오버 시키는 에너미를 생성한다.

 

1. 제자리에 가만히 선 에너미(가고일) 만들기

 

Assets → Models → Characters → Gargoyle.

 

가고일 옆에 있는 화살표 말고 프리팹으로 가고일을 열어보자.

이 화살표 말고!

 

1. 폴더에 있는 파일 클릭

2. 가고일 Inspector창에 있는 오픈 프리팹 클릭

 

 

씬 상에 있는 가고일을 편집하는 것이 아니라 프리팹을 수정한다.

게임 캐릭터 만들때처럼 애니메이션 붙이기

Assets → Animation → Animator 우클릭 → Animator Controller

 

 

가고일 캡슐 콜라이더

 

 

 

가고일 우클릭 → Empty Object 만들기 → 이름 PointOfView

그럼 자식객체 PointOfView 라는 것이 생성된다.

 

닿게 만들고 싶으니까 (충돌 아니라) 캡슐 콜라이더 is 트리거

z축으로

자식객체 만들어서 트리거되는 캡슐 콜라이더 붙여주기

 

 

C# Spript Observer 만들어서

아까 만든 포인트오브뷰에 붙이기(not 가고일!)

 

 

public class Observer : MonoBehaviour
{
    public GameObject player;
    bool isPlayerInRange = false;

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject == player)
        {
            isPlayerInRange = true;
        }
    }

    void OnTriggerEixt(Collider other)
    {
        if(other.gameObject == player)
        {
            isPlayerInRange = false;
        }
    }

}

GameObject player : 어떤 게임 오브젝트(플레이어)를 담을 수 있는 전역변수.

isPlayerInRange : 플레이어가 영역에 들어왔음을 의미하는 전역변수. 당연히 처음엔 아니니까 false로 초기화.

 

OnTriggerEnter() : 콜라이더 컴퍼넌트에서 다른 콜라이더가 충돌해서 겹쳐지는 첫 프레임 그 순간 한 번, 호출한다.

그리고 if문으로 그 부딪힌 게임 오브젝트가 플레이어가 맞다면 isPlayerInRange를 true로 바꾼다.

 

그리고 더해서, 반대로 트리거에서 떨어질 경우 false로 바꾸기 위해 OnTriggerEixt()를 만든다.

그리고 그 떨어진 오브젝트가 플레이어라면 isPlayerInRange를 false로.

 

 

그런데 만일 콜라이더가 캐릭터에 부딪히더라도, 캐릭터와 가고일 사이에 벽이 있을 수도 있다.

예를 들면 난 옆방인데 쟤가 반응해서 게임오버 될 수도 있다.

→ Update() 메서드를 만든다.

    private void Update()
    {
        if (isPlayerInRange)
        {
            Vector3 direction = player.transform.position - transform.position + Vector3.up;
            Ray ray = new Ray(transform.position, direction);
            RaycastHit raycastHit;

            if (Physics.Raycast(ray, out raycastHit))   //닿은 Physics가 플레이어라면
            {
                if (raycastHit.collider.gameObject == player) // 그럼 게임 오버.
                {
                    gameEnding.CaughtPlayer();
                }
            }

        }
    }

레이저의 방향(direction) = [플레이어의 위치] - [가고일 위치] + Vector3.up

ray 객체는 [지금 있는 위치]와 [방향]을 인수로 받는다.

 

raycastHit 레이저에 어떤 오브젝트가 닿았는지 검출해주는 것.

 

- out

: 레퍼런스에 대해 이해가 가능해야 사용가능한 문법으로,

raycastHit은 ~만 저장되기 때문에 이 변수에 메모리 주소를 넘겨주려고 out한 것.

레이캐스트라는 메서드에 레이와 결과값을 담을 수 있는 변수를 넘겨준 것.

레이캐스트힛이라는 변수에 부딪혔을 때의 정보를 담아준다.

 

 


    private void OnTriggerEnter(Collider other)   // 어제 한, 다른 콜라이더에 부딪혔을 때.
    {
        if (other.gameObject == player)      //그 부딪힌 게임오브젝트가 플레이어가 맞다면,
        {
            isPlayerInRange = true;
        }
    }

    void OnTriggerEixt(Collider other)
    {
        if(other.gameObject == player)
        {
            isPlayerInRange = false;
        }
    }
}

9-4에서 한, 콜라이더에 부딪힌 게임 오브젝트가 플레이어가 맞다면

'영역안에 들어온 것이 플레이어가 맞다면'이란 변수를 true 로, 또한 그 오브젝트가 빠져나가면 false로.

 

 

 

완성

Observer.cs
0.00MB

 


 

2. 게임오버UI 이미지 만들기

 

 

어제 만든 Canvas에 새로운 UI → Image.

 

 

이 GameoverBackgroud에 다시 UI → Image → 이번에는 caught 이미지.

 

 

게임오버백그라운드에 캔버스그룹 컴포넌트 추가.

 

 

 

어제 만든 게임엔딩에 전역변수로

    public CanvasGroup gameoverImageCanvasGroup;
    public float displayGameOverImageDuration;
    bool isPlayerCaught = false;

추가

 

 

전역변수의 영역에서

[Header("??")] : 에디터상에서 변수가 너무 많아지면 길어서 하나하나 보기 어려우니까 하나로 묶어주자.

어트리뷰트 검색해보라고..

    [Header("Game Clear")]
    public CanvasGroup exitImageCanvasGroup;
    bool isPlayerExit = false;
    float timer = 0f;
    public float fadeDuration;
    public float displayExitImageDuration;

    [Header("Game Over")]
    public CanvasGroup gameoverImageCanvasGroup;
    public float displayGameOverImageDuration;
    bool isPlayerCaught = false;

이러면 에디터 내에서 이렇게 보인다.

 

가고일은 Observer라는 스크립트가 붙은 애들을 TriggerEnter해서 검출한다.

그러나 GameEnding 스크립트에는 없으니까 퍼블릭 메서드를 만들어 호출할 수 있게 한다.

    public void CaughtPlayer()
    {
        isPlayerCaught = true;
    }

 

그리고 잡혔을 때도 어떤 행동을 해줘야 하니까 Update()메서드에 if문을 쓰고 싶은데 어차피 어제 만든 isPlayerEixt와 비슷한 역할을 하니까, 그냥 공용으로 쓸 수 있는 함수를 만들자.

    void EndLevel(CanvasGroup canvasGroup, float displayDuration, bool doRestart)
    {
        timer += Time.deltaTime;
        canvasGroup.alpha = timer / fadeDuration;

        if (timer > fadeDuration + displayDuration)
        {
            if (doRestart)
            {

            }
            else
            {
                Application.Quit();
            }
        }
    }

그냥 복사붙여넣기 해서 변수 이름만 바꾸고 Application.Quit()이 아니라 게임 재시작하게 만들어주면 되지만

비슷한 역할을 하는 것은 묶어주는 것이 연산이 더 빠르므로...

EndLevel이란 새로운 메서드를 만들고,

매개변수로 [캔버스 그룹], [어떤 이미지를 호출할지 선택하는 Duration], [재시작 할건지] 를 받는다.

 

그리고 해당하는 메서드가 실행하면

1. timer가 모든 컴퓨터에서 동일한 속도로 업데이트 되도록 보정하기 위해 Time.deltaTime 을 더하고,

2. CanvasGroup의 알파값에 timer를 fadeDuration으로 나눈 값을 대입하고

 

3. 만약 이 timer값이 fadeDuration + displayDuration보다 크다면

4. 게임오버로 재시작을 하는 조건과

5. 엔딩에 다다라 어플리케이션이 꺼지는 조건을 달아준다.

 

 

그리고 Update()메서드를 간단하게 만들어준다.

    private void Update()
    {
        if (isPlayerExit)
        {
            EndLevel(exitImageCanvasGroup, displayExitImageDuration, false);
        }

        if (isPlayerCaught)
        {
            EndLevel(gameoverImageCanvasGroup, displayGameOverImageDuration, true);
        }
    }

 

 

 


 

3. 잡혔다면 씬을 이동하자(씬을 다시 불러온다=처음으로 돌아간다.)

 

위의 if(doRestart)에 실행할 것을 넣어주기 위해 네임스페이스 추가하고.

using UnityEngine.SceneManagement;

 

if문 안에 로드할 씬이 어떤 씬인지 알려주자.

        if (timer > fadeDuration + displayDuration)
        {
            if (doRestart)
            {
                SceneManager.LoadScene(0);
            }
            ...

SceneManagement 클래스 안에 구현되어있는 메서드.

 

씬을 지정하는 방법은 두 가지가 있다.

1. 큰따옴표를 넣어 씬의 이름을 넣는 방법.

SceneManager.LoadScene("");

하고, 따옴표 안에 씬의 이름을 넣는다. 혹시 헷갈리면 에러가 날 수 있으므로 주의!

 

=> SceneManager.LoadScene("MainScene");

 

2. 씬의 번호를 넣는 방법

에디터의 리본메뉴 File → Build Settings 하면 플랫폼 별 씬을 만들 수 있는데,

Add Open Scenes 으로 지금 열려있는 씬을 추가하거나 드래그해 옮길 수 있다.

이때 씬 오른쪽 끝에 번호가 붙는데 이것이 각 씬마다 해당하는 넘버이다. 이동하고자 하는 씬을 괄호에 넣으면 된다.

 

=> SceneManager.LoadScene(0);

 

빌드세팅을 추하가지 않으면 숫자로 쓸 수 없으니 주의.

빌드 = 실행파일을 만들다.

 

 

 

4. GameEnding 스크립트에서 만든 것들을 Observer 스크립트에서 접근하게 만든다.

 

public class Observer : MonoBehaviour
{
    public GameEnding gameEnding;
    ...

게임 엔딩이라는 전역 변수 추가.

 

 

Update()메서드에 조건 중에, 

    private void Update()
    {
        if (isPlayerInRange)
        {
            Vector3 direction = player.transform.position - transform.position + Vector3.up;
            Ray ray = new Ray(transform.position, direction);
            RaycastHit raycastHit;

            if (Physics.Raycast(ray, out raycastHit))
            {
                if (raycastHit.collider.gameObject == player)
                {
                    gameEnding.CaughtPlayer();
                }
            }

        }
    }

게임오버되는 조건 추가.

GameEnding의 caughtplayer 메서드 호출.

 

 

에디터로 돌아가서,

가고일이 가진 PointOfView에 옵저버 스크립트 추가.

전역변수 Player와 GameEnding에 각각 오브젝트 추가

 

 

게임엔딩 오브젝트에 GameoverBackground 추가

 

 

이렇게 완성된 가고일을 여러개 복사+붙여넣기해서 맵 이곳 저곳에 놓는다.

 1. Ctrl+C → Ctrl+V 해도 괜찮고

 2. 오브젝트 우클릭 →Duplicate 해도 된다.

 

 

 

Q. 씬 이동은 했는데 그대로 멈춤! 그리고 아래 오류 메시지

 

A. 질량(Mass)이 다르다고 오류가 난 것;; 어떻게 계산한 것인지는 모르지만 아래로 수정하니 괜찮아졌다.

 

 

Q. 복사 붙여넣기 한 가고일이 자꾸 칸에 맞게 움직여요. 자유롭게 둘 수 없어요.

 

A. 아래 버튼이 활성화되어 있었다.

 

 


 

 

5. 맵을 이동하며 움직이는 에너미(유령) 만들기

 

가고일과 같이 Assets → Models → Characters → Ghost.

프리팹으로 만들고, 애니메이터 붙이고, 애니메이션 폴더에 있는 Walk 붙여준다.

 

Capsule Collider 컴포넌트 추가.

Rigidbody 컴포넌트 추가.

- Use Gravity 해제(유령이니까)

- Is Kinematic 체크

 : 이전에 캐릭터만들 때 벽에 부딪히면 빙글빙글 돌았다. 이건 Rigidbody에서 다른 충돌체와 부딪혔을 때 어떻게 회전하고 움직일지를 계산해서 좌표값이 바뀌었기 때문.

 그러나 이 에너미는 유령이기 때문에 다른 콜라이더와 부딪혀도 영향을 받지 않는다.

 때문에 '어디 부딪혀도 회전이나 가속도와 같은 연산이 달라지지 않겠다'는 의미로 체크하는 것.

Is Trigger랑 좀 비슷한 느낌이긴 함. 의도가 다름.

 

 

아까 가고일에게 붙인 PointOfView 오브젝트를 프리팹으로 만들어서 유령에게 붙이기.

 

 

 

6. 유령에게 움직임을 설정하기

어제 만든 Navigation Mesh를 기억하나?

유령 Add Component → Navigation → Nav Mesh Agent.

 

그럼 원기둥이 생기는데 이것이 Navigation Agent의 범위.

Radius를 0.25로, Speed를 1.5로 바꾼다.

- Stopping Distance : 네비게이션 메쉬가 끝나는 지점에서 얼마만큼의 거리를 두고 멈출지.

 0.2로 주기.

 

 

 

이대로 두어도 알아서 네비게이션 내를 움직이겠지만 루트를 만들고 싶다면?

 

7. 움직이는 루트를 만들어주기

출발점과 도착점을 정해주고 알아서 그 안에 움직이게 하자.

 

C# Script → 이름 WaypointPatrol.

 

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

using UnityEngine.AI; : 유니티 자체 엔진 AI를 쓰겠다는 네임스페이스 추가.

 

 

public class WaypointPatrol : MonoBehaviour
{
    public NavMeshAgent navMeshAgent;
    public Transform[] waypoints;
    int curWaypointIndex = 0;

NavMeshAgent navMeshAgent : Nav Mesh Agent에 접근할 수 있는 변수 제작.

 

Transform[] waypoints : (다른 움직임이 늘어날지도 모르니)여러개의 정보를 한 번에 처리하고 싶다. 그래서 Transform의 정보를 어레이[]waypoints 변수를 만든다.

 

curWaypointIndex : waypoints가 가진 요소 하나하나에 접근하려면 인덱스 번호를 붙여서 0번 인덱스, 1번 인덱스... 불러야 함. 그걸 위한 정수형 변수 생성. 그리고 0으로 초기화.

 

 

 

    private void Start()
    {
        navMeshAgent.SetDestination(waypoints[0].position);
    }

이제 처음 게임이 시작되면 Nav Mesh Agent의 위치를 waypoints에 있는 정보들 중 첫 번째, 0번의 위치를 시작값으로 가진다.

 

 

 

이제 매 프레임마다 업데이트할 것 생성.

    ...
    private void Update()
    {
        if (navMeshAgent.remainingDistance < navMeshAgent.stoppingDistance)
        {
            curWaypointIndex++;
            curWaypointIndex %= waypoints.Length;
            navMeshAgent.SetDestination(waypoints[curWaypointIndex].position);
        }
    }
}

만약if, [Nav Mesh Agent가 가야할 거리]가 [멈춰야 하는 거리(Stopping Distance)]보다 작을 때,

다음 장소(포인트)로 넘어가게 만든다.

 

Nav Mesh Agent의 도착지점을 curWaypointIndex의 위치로 바꾼다.

 

그럼 처음에 0으로 시작했지만, 이제 다음 포인트(1)로 넘어가야 한다.

curWaypointIndex++; 하면 인덱스의 번호가 1씩 늘어난다.

 

그런데 이 인덱스의 값이 계속 늘어나면 어느 순간,

만약 waypoints에 3개가 들어있었다면 3번 더해지고 난 이후에 3이 됐을 때, curWaypointIndex에 없는 숫자를 호출하게 되어 에러가 난다.

즉, 숫자가 계속 추가되긴 하는데 인덱스 내의 숫자를 순회하고 싶다.

 

curWaypointIndex %= waypoints.Length; : [전체 waypoints의 길이] 나누기 curWaypointIndex의 [나머지 값]을 대입한다.

 

 

완성

WaypointPatrol.cs
0.00MB

 

 

이제 유령에게 이 스크립트를 붙인다.

그리고 Nav Mesh Agent 위치에 컴포넌트인 Nav Mesh Agent를 드래그 해 넣는다.

 

여기까지 하고, 다시 씬으로 넘어가서

씬 곳곳에 PointOfView가 달린 유령들을 배치한다.

유령들의 위치

 

 

Empty 오브젝트 생성, 이름 Waypoint.

빈 오브젝트는 씬에 있으면 보이지 않는다. 때문에 게임화면에선 안보이고 씬 화면에선 볼 수 있게

오브젝트의 모양인 상자를 누르면 라벨을 달 수 있다.

 

 

Waypoint 총 10개를 만들고 각각 위치를 넣어준다.

 

 

웨이포인트 총 10개 만들어서 위치 넣어주고

각 웨이포인트를 유령들에게 넣어준다.

차례대로 각 유령들에게 2개, 2개, 4개, 2개 씩 붙여준다.

 

 

 

8. 음향효과 넣기

8-1. 배경음악

Empty 오브젝트 → 이름은 Ambient

 

컴포넌트 Audio Source 추가.

음산한 바람소리가 나는 음향효과인 SFXHouseAmbience를 넣고,

- Play On Awake : 씬이 켜졌을 때 바로 재생.

- Loop : 반복 재생

볼륨은 0.5.

 

이제 좀 이해가 가는데... 오브젝트는 알만툴의 이벤트라고 생각하면 될 것 같다.

 

8-2. 게임오버와 엔딩에 효과음

Empty 오브젝트 2개 → 이름 각각 Escape, Caught.

두 개 모두 Audio Source 컴포넌트 추가.

 

Escape에는 SFXWin

Caught에는 SFXGameOver

붙여주고

 

GameEnding 스크립트로.

 

 

    public AudioSource exitAudio;
    public AudioSource caughtAudio;

두 전역변수를 각각의 헤더에 맞게 추가한다.

이제 오브젝트를 연결할 수 있게 되었다.

 

    bool hasAudioPalyed = false;

그리고 이미 재생중인데 또 재생되면 안되므로 bool 변수를 만든다.

각각 만들 필요는 없고 하나만 추가하자.

 

 

    void EndLevel(CanvasGroup canvasGroup, float displayDuration, bool doRestart, AudioSource audioSource)
    {
        timer += Time.deltaTime;
        canvasGroup.alpha = timer / fadeDuration;

        if (hasAudioPalyed == false)
        {
            audioSource.Play();
            hasAudioPalyed = true;
        }


        if (timer > fadeDuration + displayDuration)
        {
            if (doRestart)
            {
                SceneManager.LoadScene(0);
            }
            else
            {
                Application.Quit();
            }
        }
    }

AudioSource audioSource : 메서드에 Audio Source 매개변수 추가.

Update()의 if문인 isPlayerExit와 isPlayerCaught에서 사용한 EndLevel메서드에도 각각에 맞는 인수 추가.

 

if (hasAudioPalyed == false) {~~} : 오디오 재생이 안 된 상태(false)일때만 재생되도록 if문 추가.

 

 

저장 후, GameEnding 오브젝트에 붙은 스크립트의 새로운 칸들에게 Audio 오브젝트 추가.

 

 

 

9. 게임 실행파일 만들기

먼저 파일에 각종 설정을 한다.

 

Edit → Project Setting → Player

- Company Name : 제작한 회사 혹은 제작자의 이름.

- Product Name : 이 게임의 이름.

- Version : 업데이트 할 시 추가되는 그 버전.

- Default Icon : 설치된 파일의 아이콘 모양.

- Default Cursor : 게임의 커서.

 

- Resolution and Presentation : 처음 실행할 때 어떤 모양으로 실행할 지. 창모드, 풀스크린... 그리고 각각의 설정...

 

 

이제 파일을 만든다.

File → Build Settings

어떤 플랫폼으로 제작할 것인지, 해당 플랫폼에서의 설정은 무엇인지 등등.

Build를 누르면 게임 실행파일로 만들어준다.

Build And Run은 만든 직후 실행하는 것.

 

상당히 긴 시간이 걸리므로 여유를 갖고 하자.