* 플레이어를 게임오버 시키는 에너미를 생성한다.
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로.
완성
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의 [나머지 값]을 대입한다.
완성
이제 유령에게 이 스크립트를 붙인다.
그리고 Nav Mesh Agent 위치에 컴포넌트인 Nav Mesh Agent를 드래그 해 넣는다.
여기까지 하고, 다시 씬으로 넘어가서
씬 곳곳에 PointOfView가 달린 유령들을 배치한다.
Empty 오브젝트 생성, 이름 Waypoint.
빈 오브젝트는 씬에 있으면 보이지 않는다. 때문에 게임화면에선 안보이고 씬 화면에선 볼 수 있게
오브젝트의 모양인 상자를 누르면 라벨을 달 수 있다.
Waypoint 총 10개를 만들고 각각 위치를 넣어준다.
웨이포인트 총 10개 만들어서 위치 넣어주고
각 웨이포인트를 유령들에게 넣어준다.
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은 만든 직후 실행하는 것.
상당히 긴 시간이 걸리므로 여유를 갖고 하자.
'메타버스기반게임콘텐츠기획 > 그날의 강의' 카테고리의 다른 글
(10-2) 유니티unity 클리커 게임 만들기 2 - 클릭하면 골드가 올라가는 스크립트 (0) | 2021.12.28 |
---|---|
(10-1) 유니티unity 클리커 게임 만들기 - 버튼을 누르면 골드를 획득하는 스크립트 (0) | 2021.12.27 |
(9-4) 유니티 unity : 빛 효과를 넣어서 그럴듯해 보이게 환경(레벨) 설정, 캐릭터(에너미)의 이동 반경 설정, 카메라 설정, 포스트 프로세싱, UI 띄우기 (0) | 2021.12.23 |
(9-3) 유니티unity : 다운 받은 에셋으로 캐릭터, 움직임, 애니메이션 만들기 (0) | 2021.12.22 |
(9-2) 유니티unity 코딩: 매개변수, $, Beginner Code-for문 (0) | 2021.12.21 |