기록 보관소

[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 게임 완성하기[BE5] 본문

유니티 프로젝트/3D 쿼터뷰 액션게임

[Unity/유니티] 기초-3D 쿼터뷰 액션 게임: 게임 완성하기[BE5]

JongHoon 2022. 4. 13. 23:52

개요

유니티 입문과 독학을 위해서 아래 링크의 골드메탈님의 영상들을 보며 진행 상황 사진 또는 캡처를 올리고 배웠던 점을 요약해서 적는다.

현재는 영상들을 보고 따라하고 배우는 것에 집중할 것이며, 영상을 모두 보고 따라한 후에는 개인 프로젝트를 설계하고 직접 만드는 것이 목표다.

https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2 

 

유니티 강좌 기초 채널 Basic

유니티 개발을 처음 시작하시는 입문자 분들을 위한 기초 채널. [ 프로젝트 ] B00 ~ B12 (BE1) : 유니티 필수 기초 B13 ~ B19 (BE2) : 2D 플랫포머 B20 ~ B26 (BE3) : 2D 탑다운 대화형 RPG B27 ~ B37 (BE4) : 2D 종스크롤

www.youtube.com


3D 쿼터뷰 액션 게임: 게임 완성하기[BE5]

1. 스테이지 관리

상점 입구인 Zone을 복사해서 Start Zone으로 만들어준다
위치와 크기, 색깔 등을 변경해주고 Shop 스크립트와 태그를 제거해준다
표시를 위해서 3D Text를 추가
Text 입력 및 설정
StartZone 스크립트 파일 생성

//GameManager 스크립트 파일

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

public class GameManager : MonoBehaviour {
	public GameObject menuCam;
	public GameObject gameCam;
	public Player player;
	public Boss boss;
	public GameObject itemShop;
	public GameObject weaponShop;
	public GameObject startZone;
	public int stage;
	public float playTime;
	public bool isBattle;
	public int enemyCntA;
	public int enemyCntB;
	public int enemyCntC;

	public GameObject menuPanel;
	public GameObject gamePanel;
	public Text maxScoreTxt;
	public Text scoreTxt;
	public Text stageTxt;
	public Text playTimeTxt;
	public Text playerHealthTxt;
	public Text playerAmmoTxt;
	public Text playerCoinTxt;
	public Image weapon1Img;
	public Image weapon2Img;
	public Image weapon3Img;
	public Image weaponRImg;
	public Text enemyATxt;
	public Text enemyBTxt;
	public Text enemyCTxt;
	public RectTransform bossHealthGroup;
	public RectTransform bossHealthBar;

	void Awake() {
		maxScoreTxt.text = string.Format("{0:n0}", PlayerPrefs.GetInt("MaxScore"));	//세자리 수마다 , 추가
	}

	public void GameStart() {
		menuCam.SetActive(false);
		gameCam.SetActive(true);

		menuPanel.SetActive(false);
		gamePanel.SetActive(true);

		player.gameObject.SetActive(true);
	}

	public void StageStart() {
		itemShop.SetActive(false);
		weaponShop.SetActive(false);
		startZone.SetActive(false);

		isBattle = true;
		StartCoroutine(InBattle());
	}

	public void StageEnd() {
		player.transform.position = Vector3.up * 0.8f;
		
		itemShop.SetActive(true);
		weaponShop.SetActive(true);
		startZone.SetActive(true);

		isBattle = false;
		stage++;
	}

	IEnumerator InBattle() {
		yield return new WaitForSeconds(5);
		StageEnd();
	}

	void Update() {
		if (isBattle)
			playTime += Time.deltaTime;
	}

	void LateUpdate() {
		//좌측 상단 UI
		scoreTxt.text = string.Format("{0:n0}", player.score);

		//우측 상단 UI
		stageTxt.text = "STAGE " + stage;
		int hour = (int)(playTime / 3600);	//시
		int min = (int)((playTime - hour * 3600) / 60);   //분
		int second = (int)(playTime % 60);	//초
		playTimeTxt.text = string.Format("{0:00}", hour) + ":" + string.Format("{0:00}", min) + ":" + string.Format("{0:00}", second);

		//플레이어(좌측 하단) UI
		playerHealthTxt.text = player.health + " / " + player.maxHealth;
		playerCoinTxt.text = string.Format("{0:n0}", player.coin);
		if (player.equipWeapon == null)
			playerAmmoTxt.text = "- / " + player.ammo;
		else if (player.equipWeapon.type == Weapon.Type.Melee)
			playerAmmoTxt.text = "- / " + player.ammo;
		else
			playerAmmoTxt.text = player.equipWeapon.curAmmo + " / " + player.ammo;

		//무기(중앙 하단) UI
		weapon1Img.color = new Color(1, 1, 1, (player.hasWeapons[0] ? 1 : 0));
		weapon2Img.color = new Color(1, 1, 1, (player.hasWeapons[1] ? 1 : 0));
		weapon3Img.color = new Color(1, 1, 1, (player.hasWeapons[2] ? 1 : 0));
		weaponRImg.color = new Color(1, 1, 1, (player.hasGrenades > 0 ? 1 : 0));

		//몬스터 숫자(우측 하단) UI
		enemyATxt.text = enemyCntA.ToString();
		enemyBTxt.text = enemyCntB.ToString();
		enemyCTxt.text = enemyCntC.ToString();

		//보스 체력(중앙 상단) UI
		bossHealthBar.localScale = new Vector3((float)boss.curHealth / boss.maxHealth, 1, 1);
	}
}
  • StartZone에 닿으면 스테이지를 시작하도록 StageStart와 StageEnd 함수, 그리고 테스트를 위한 5초를 주는 코루틴을 추가하고, 또한 스테이지 시작시 앞에서 만들었던 StartZone과 아이템 상점, 무기 상점은 비활성화 되어야하므로 변수로 추가해주었다.
//StartZone 스크립트 파일

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

public class StartZone : MonoBehaviour {
	public GameManager manager;

	void OnTriggerEnter(Collider other) {
		if (other.gameObject.tag == "Player")
			manager.StageStart();
	}
}

StartZone 변수 할당
GameManager 변수 할당
Player Tag 설정
실행 후 StartZone에 다가가면
StartZone에 닿으니 상점과 StartZone이 없어졌다
5초 뒤 Stage가 끝나면서 생성된 상점과 StartZone


2. 몬스터 프리펩

//Enemy 스크립트 파일

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

public class Enemy : MonoBehaviour {
	public enum Type { A, B, C, D };
	public Type enemyType;
	public int maxHealth;
	public int curHealth;
	public int score;
	public Transform target;
	public BoxCollider meleeArea;
	public GameObject bullet;
	public GameObject[] coins;
	public bool isChase;
	public bool isAttack;
	public bool isDead;

	public Rigidbody rigid;
	public BoxCollider boxCollider;
	public MeshRenderer[] meshs;
	public NavMeshAgent nav;
	public Animator anim;

	void Awake() {
		rigid = GetComponent<Rigidbody>();
		boxCollider = GetComponent<BoxCollider>();
		meshs = GetComponentsInChildren<MeshRenderer>();
		nav = GetComponent<NavMeshAgent>();
		anim = GetComponentInChildren<Animator>();

		if (enemyType != Type.D)
			Invoke("ChaseStart", 2);
	}

	void ChaseStart() {
		isChase = true;
		anim.SetBool("isWalk", true);
	}

	void Update() {
		if (nav.enabled && enemyType != Type.D) {
			nav.SetDestination(target.position);
			nav.isStopped = !isChase;
		}
	}

	void FreezeVelocity() {
		if (isChase) {
			rigid.velocity = Vector3.zero;
			rigid.angularVelocity = Vector3.zero;
		}
	}

	void Targeting() {
		if (!isDead && enemyType != Type.D) {
			float targetRadius = 0;
			float targetRange = 0;

			switch (enemyType) {
				case Type.A:
					targetRadius = 1.5f;
					targetRange = 3f;
					break;
				case Type.B:
					targetRadius = 1f;
					targetRange = 12f;
					break;
				case Type.C:
					targetRadius = 0.5f;
					targetRange = 25f;
					break;
			}

			RaycastHit[] rayHits = Physics.SphereCastAll(transform.position, targetRadius, transform.forward, targetRange, LayerMask.GetMask("Player"));

			if (rayHits.Length > 0 && !isAttack) {
				StartCoroutine(Attack());
			}
		}
	}

	IEnumerator Attack() {
		isChase = false;
		isAttack = true;
		anim.SetBool("isAttack", true);

		switch(enemyType) {
			case Type.A:
				yield return new WaitForSeconds(0.2f);
				meleeArea.enabled = true;

				yield return new WaitForSeconds(1f);
				meleeArea.enabled = false;

				yield return new WaitForSeconds(1f);
				break;
			case Type.B:
				yield return new WaitForSeconds(0.1f);
				rigid.AddForce(transform.forward * 20, ForceMode.Impulse);
				meleeArea.enabled = true;

				yield return new WaitForSeconds(0.5f);
				rigid.velocity = Vector3.zero;
				meleeArea.enabled = false;

				yield return new WaitForSeconds(2f);
				break;
			case Type.C:
				yield return new WaitForSeconds(0.5f);
				GameObject instantBullet = Instantiate(bullet, transform.position, transform.rotation);
				Rigidbody rigidBullet = instantBullet.GetComponent<Rigidbody>();
				rigidBullet.velocity = transform.forward * 20;

				yield return new WaitForSeconds(2f);
				break;
		}

		isChase = true;
		isAttack = false;
		anim.SetBool("isAttack", false);
	}

	void FixedUpdate() {
		Targeting();
		FreezeVelocity();
	}

	void OnTriggerEnter(Collider other) {
		if (other.tag == "Melee") {
			Weapon weapon = other.GetComponent<Weapon>();
			curHealth -= weapon.damage;
			Vector3 reactVec = transform.position - other.transform.position;

			StartCoroutine(OnDamage(reactVec, false));
		}
		else if (other.tag == "Bullet") {
			Bullet bullet = other.GetComponent<Bullet>();
			curHealth -= bullet.damage;
			Vector3 reactVec = transform.position - other.transform.position;
			Destroy(other.gameObject);

			StartCoroutine(OnDamage(reactVec, false));
		}
	}

	public void HitByGrenade(Vector3 explosionPos) {
		curHealth -= 100;
		Vector3 reactVec = transform.position - explosionPos;

		StartCoroutine(OnDamage(reactVec, true));
	}

	IEnumerator OnDamage(Vector3 reactVec, bool isGrenade) {
		foreach(MeshRenderer mesh in meshs)
			mesh.material.color = Color.red;

		if (curHealth > 0) {
			yield return new WaitForSeconds(0.1f);

			foreach (MeshRenderer mesh in meshs)
				mesh.material.color = Color.white;
		}
		else {
			foreach (MeshRenderer mesh in meshs)
				mesh.material.color = Color.gray;
			gameObject.layer = 12;  //Enemy Dead 레이어로 변경
			isDead = true;
			isChase = false;
			nav.enabled = false;
			anim.SetTrigger("doDie");

			Player player = target.GetComponent<Player>();
			player.score += score;
			int ranCoin = Random.Range(0, 3);
			Instantiate(coins[ranCoin], transform.position, Quaternion.identity);

			if (isGrenade) {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up * 5;
				rigid.freezeRotation = false;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
				rigid.AddTorque(reactVec * 15, ForceMode.Impulse);
			}
			else {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
			}

			Destroy(gameObject, 4);
		}
	}
}
  • 기존 Enemy 스크립트에 없던 score와 떨어트릴 동전 변수를 추가해서 처치시 플레이어가 점수를 얻고, 동전 3종류 중 하나를 떨어트리도록 변경하였다. 그리고 스테이지 문제가 있으니 보스 시체 또한 4초 뒤 사라지도록 변경했다.

Enemy A 변수 할당
Enemy B 변수 할당
Enemy C 변수 할당
Enemy D 변수 할당
Enemy A B C D의 위치를 0 0 0 으로 설정하고, 프리펩으로 만들어준다


3. 몬스터 관리

Start Zone을 복사해서 스크립트 파일과 콜라이더를 제거한다
색정도만 변경해서 3개 더 복사한다
Zone들을 동서남북에 위치하게 만들고 이름을 변경해준다
Empty로 그룹으로 만들었다

//Enemy 스크립트 파일

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

public class Enemy : MonoBehaviour {
	public enum Type { A, B, C, D };
	public Type enemyType;
	public int maxHealth;
	public int curHealth;
	public int score;
	public GameManager manager;
	public Transform target;
	public BoxCollider meleeArea;
	public GameObject bullet;
	public GameObject[] coins;
	public bool isChase;
	public bool isAttack;
	public bool isDead;

	public Rigidbody rigid;
	public BoxCollider boxCollider;
	public MeshRenderer[] meshs;
	public NavMeshAgent nav;
	public Animator anim;

	void Awake() {
		rigid = GetComponent<Rigidbody>();
		boxCollider = GetComponent<BoxCollider>();
		meshs = GetComponentsInChildren<MeshRenderer>();
		nav = GetComponent<NavMeshAgent>();
		anim = GetComponentInChildren<Animator>();

		if (enemyType != Type.D)
			Invoke("ChaseStart", 2);
	}

	void ChaseStart() {
		isChase = true;
		anim.SetBool("isWalk", true);
	}

	void Update() {
		if (nav.enabled && enemyType != Type.D) {
			nav.SetDestination(target.position);
			nav.isStopped = !isChase;
		}
	}

	void FreezeVelocity() {
		if (isChase) {
			rigid.velocity = Vector3.zero;
			rigid.angularVelocity = Vector3.zero;
		}
	}

	void Targeting() {
		if (!isDead && enemyType != Type.D) {
			float targetRadius = 0;
			float targetRange = 0;

			switch (enemyType) {
				case Type.A:
					targetRadius = 1.5f;
					targetRange = 3f;
					break;
				case Type.B:
					targetRadius = 1f;
					targetRange = 12f;
					break;
				case Type.C:
					targetRadius = 0.5f;
					targetRange = 25f;
					break;
			}

			RaycastHit[] rayHits = Physics.SphereCastAll(transform.position, targetRadius, transform.forward, targetRange, LayerMask.GetMask("Player"));

			if (rayHits.Length > 0 && !isAttack) {
				StartCoroutine(Attack());
			}
		}
	}

	IEnumerator Attack() {
		isChase = false;
		isAttack = true;
		anim.SetBool("isAttack", true);

		switch(enemyType) {
			case Type.A:
				yield return new WaitForSeconds(0.2f);
				meleeArea.enabled = true;

				yield return new WaitForSeconds(1f);
				meleeArea.enabled = false;

				yield return new WaitForSeconds(1f);
				break;
			case Type.B:
				yield return new WaitForSeconds(0.1f);
				rigid.AddForce(transform.forward * 20, ForceMode.Impulse);
				meleeArea.enabled = true;

				yield return new WaitForSeconds(0.5f);
				rigid.velocity = Vector3.zero;
				meleeArea.enabled = false;

				yield return new WaitForSeconds(2f);
				break;
			case Type.C:
				yield return new WaitForSeconds(0.5f);
				GameObject instantBullet = Instantiate(bullet, transform.position, transform.rotation);
				Rigidbody rigidBullet = instantBullet.GetComponent<Rigidbody>();
				rigidBullet.velocity = transform.forward * 20;

				yield return new WaitForSeconds(2f);
				break;
		}

		isChase = true;
		isAttack = false;
		anim.SetBool("isAttack", false);
	}

	void FixedUpdate() {
		Targeting();
		FreezeVelocity();
	}

	void OnTriggerEnter(Collider other) {
		if (other.tag == "Melee") {
			Weapon weapon = other.GetComponent<Weapon>();
			curHealth -= weapon.damage;
			Vector3 reactVec = transform.position - other.transform.position;

			StartCoroutine(OnDamage(reactVec, false));
		}
		else if (other.tag == "Bullet") {
			Bullet bullet = other.GetComponent<Bullet>();
			curHealth -= bullet.damage;
			Vector3 reactVec = transform.position - other.transform.position;
			Destroy(other.gameObject);

			StartCoroutine(OnDamage(reactVec, false));
		}
	}

	public void HitByGrenade(Vector3 explosionPos) {
		curHealth -= 100;
		Vector3 reactVec = transform.position - explosionPos;

		StartCoroutine(OnDamage(reactVec, true));
	}

	IEnumerator OnDamage(Vector3 reactVec, bool isGrenade) {
		foreach(MeshRenderer mesh in meshs)
			mesh.material.color = Color.red;

		if (curHealth > 0) {
			yield return new WaitForSeconds(0.1f);

			foreach (MeshRenderer mesh in meshs)
				mesh.material.color = Color.white;
		}
		else {
			foreach (MeshRenderer mesh in meshs)
				mesh.material.color = Color.gray;
			gameObject.layer = 12;  //Enemy Dead 레이어로 변경
			isDead = true;
			isChase = false;
			nav.enabled = false;
			anim.SetTrigger("doDie");

			Player player = target.GetComponent<Player>();
			player.score += score;
			int ranCoin = Random.Range(0, 3);
			Instantiate(coins[ranCoin], transform.position, Quaternion.identity);

			switch(enemyType) {
				case Type.A:
					manager.enemyCntA--;
					break;
				case Type.B:
					manager.enemyCntB--;
					break;
				case Type.C:
					manager.enemyCntC--;
					break;
				case Type.D:
					manager.enemyCntD--;
					break;
			}

			if (isGrenade) {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up * 5;
				rigid.freezeRotation = false;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
				rigid.AddTorque(reactVec * 15, ForceMode.Impulse);
			}
			else {
				reactVec = reactVec.normalized;
				reactVec += Vector3.up;
				rigid.AddForce(reactVec * 5, ForceMode.Impulse);
			}

			Destroy(gameObject, 4);
		}
	}
}
//GameManager 스크립트 파일

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

public class GameManager : MonoBehaviour {
	public GameObject menuCam;
	public GameObject gameCam;
	public Player player;
	public Boss boss;
	public GameObject itemShop;
	public GameObject weaponShop;
	public GameObject startZone;
	public int stage;
	public float playTime;
	public bool isBattle;
	public int enemyCntA;
	public int enemyCntB;
	public int enemyCntC;
	public int enemyCntD;

	public Transform[] enemyZones;
	public GameObject[] enemies;
	public List<int> enemyList;

	public GameObject menuPanel;
	public GameObject gamePanel;
	public Text maxScoreTxt;
	public Text scoreTxt;
	public Text stageTxt;
	public Text playTimeTxt;
	public Text playerHealthTxt;
	public Text playerAmmoTxt;
	public Text playerCoinTxt;
	public Image weapon1Img;
	public Image weapon2Img;
	public Image weapon3Img;
	public Image weaponRImg;
	public Text enemyATxt;
	public Text enemyBTxt;
	public Text enemyCTxt;
	public RectTransform bossHealthGroup;
	public RectTransform bossHealthBar;

	void Awake() {
		enemyList = new List<int>();
		maxScoreTxt.text = string.Format("{0:n0}", PlayerPrefs.GetInt("MaxScore"));	//세자리 수마다 , 추가
	}

	public void GameStart() {
		menuCam.SetActive(false);
		gameCam.SetActive(true);

		menuPanel.SetActive(false);
		gamePanel.SetActive(true);

		player.gameObject.SetActive(true);
	}

	public void StageStart() {
		itemShop.SetActive(false);
		weaponShop.SetActive(false);
		startZone.SetActive(false);

		foreach (Transform zone in enemyZones)
			zone.gameObject.SetActive(true);

		isBattle = true;
		StartCoroutine(InBattle());
	}

	public void StageEnd() {
		player.transform.position = Vector3.up * 0.8f;
		
		itemShop.SetActive(true);
		weaponShop.SetActive(true);
		startZone.SetActive(true);

		foreach (Transform zone in enemyZones)
			zone.gameObject.SetActive(false);

		isBattle = false;
		stage++;
	}

	IEnumerator InBattle() {
		if (stage % 5 == 0) {
			enemyCntD++;
			GameObject instantEnemy = Instantiate(enemies[3], enemyZones[0].position, enemyZones[0].rotation);
			Enemy enemy = instantEnemy.GetComponent<Enemy>();
			enemy.target = player.transform;
			enemy.manager = this;   //GameManager 보내주기
			boss = instantEnemy.GetComponent<Boss>();
		}
		else {
			for (int index = 0; index < stage; index++) {
				int ran = Random.Range(0, 3);
				enemyList.Add(ran);

				switch (ran) {
					case 0:
						enemyCntA++;
						break;
					case 1:
						enemyCntB++;
						break;
					case 2:
						enemyCntC++;
						break;
				}
			}

			while (enemyList.Count > 0) {
				int ranZone = Random.Range(0, 4);
				GameObject instantEnemy = Instantiate(enemies[enemyList[0]], enemyZones[ranZone].position, enemyZones[ranZone].rotation);
				Enemy enemy = instantEnemy.GetComponent<Enemy>();
				enemy.target = player.transform;
				enemy.manager = this;	//GameManager 보내주기
				enemyList.RemoveAt(0);
				yield return new WaitForSeconds(4f);
			}
		}

		while(enemyCntA + enemyCntB + enemyCntC + enemyCntD > 0) {
			yield return null;
		}

		yield return new WaitForSeconds(4f);
		boss = null;
		StageEnd();
	}

	void Update() {
		if (isBattle)
			playTime += Time.deltaTime;
	}

	void LateUpdate() {
		//좌측 상단 UI
		scoreTxt.text = string.Format("{0:n0}", player.score);

		//우측 상단 UI
		stageTxt.text = "STAGE " + stage;
		int hour = (int)(playTime / 3600);	//시
		int min = (int)((playTime - hour * 3600) / 60);   //분
		int second = (int)(playTime % 60);	//초
		playTimeTxt.text = string.Format("{0:00}", hour) + ":" + string.Format("{0:00}", min) + ":" + string.Format("{0:00}", second);

		//플레이어(좌측 하단) UI
		playerHealthTxt.text = player.health + " / " + player.maxHealth;
		playerCoinTxt.text = string.Format("{0:n0}", player.coin);
		if (player.equipWeapon == null)
			playerAmmoTxt.text = "- / " + player.ammo;
		else if (player.equipWeapon.type == Weapon.Type.Melee)
			playerAmmoTxt.text = "- / " + player.ammo;
		else
			playerAmmoTxt.text = player.equipWeapon.curAmmo + " / " + player.ammo;

		//무기(중앙 하단) UI
		weapon1Img.color = new Color(1, 1, 1, (player.hasWeapons[0] ? 1 : 0));
		weapon2Img.color = new Color(1, 1, 1, (player.hasWeapons[1] ? 1 : 0));
		weapon3Img.color = new Color(1, 1, 1, (player.hasWeapons[2] ? 1 : 0));
		weaponRImg.color = new Color(1, 1, 1, (player.hasGrenades > 0 ? 1 : 0));

		//몬스터 숫자(우측 하단) UI
		enemyATxt.text = enemyCntA.ToString();
		enemyBTxt.text = enemyCntB.ToString();
		enemyCTxt.text = enemyCntC.ToString();

		//보스 체력(중앙 상단) UI
		if (boss != null) {
			bossHealthGroup.anchoredPosition = Vector3.down * 30;
			bossHealthBar.localScale = new Vector3((float)boss.curHealth / boss.maxHealth, 1, 1);
		}
		else {
			bossHealthGroup.anchoredPosition = Vector3.up * 200;
		}
	}
}
  • 앞에서 만들었던 지역에서 적들이 생성되게 만들고, 또한 적들을 프리펩으로 만들어서 Enemy 스크립트에서 지정했던 일부 변수들을  GameManager를 통해서 지정해주었고, 보스전이 아닐 때 보스 체력 UI가 보이지 않도록 바꾸었다.

GameManager 변수 할당
스테이지가 시작되자 생성된 적 수를 표시해준다
5스테이지가 되자 보스가 소환되었고 체력 UI가 생겼다.
시간이 지나자 스테이지가 바뀌고 보스 체력바도 다시 사라졌다


4. 게임 오버

Player Animator에 Die 애니메이션 및 Trigger형 매개변수 doDie 추가
Tansition 설정

  • Die 애니메이션은 죽으면 플레이어 또한 더이상 활동을 못하게 할 것이므로 Exit에 Transition을 연결하지 않는다.

플레이어 사망시 사용할 화면을 표시하기 위해 Menu Panel을 복사해서 만들어준다
이미지를 삭제하고 버튼 이름과 텍스트를 제거했다. 그리고 점수 앞에 최고 점수라는 표시로 Best Score Text를 추가했다.
GameOver Text 추가 및 Best Score Text 비활성화

//Player 스크립트 파일

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

public class Player : MonoBehaviour {
    public float speed;
	public GameObject[] weapons;
	public bool[] hasWeapons;
	public GameObject[] grenades;
	public int hasGrenades;
	public GameObject grenadeObj;
	public Camera followCamera;
	public GameManager manager;

	public int ammo;
	public int coin;
	public int health;
	public int score;

	public int maxAmmo;
	public int maxCoin;
	public int maxHealth;
	public int maxHasGrenades;

	float hAxis;
    float vAxis;

    bool wDown;
    bool jDown;
	bool fDown;
	bool gDown;
	bool rDown;
	bool iDown;
	bool sDown1;	//망치
	bool sDown2;	//권총
	bool sDown3;	//기관총

    bool isJump;
	bool isDodge;
	bool isSwap;
	bool isReload;
	bool isFireReady = true;
	bool isBorder;
	bool isDamage;
	bool isShop;
	bool isDead = false;

    Vector3 moveVec;
	Vector3 dodgeVec;

    Animator anim;
    Rigidbody rigid;
	MeshRenderer[] meshs;

	GameObject nearObject;
	public Weapon equipWeapon;
	int equipWeaponIndex = -1;
	float fireDelay;

    void Awake() {
        rigid = GetComponent<Rigidbody>();
        anim = GetComponentInChildren<Animator>();  //Player 자식 오브젝트에 있으므로
		meshs = GetComponentsInChildren<MeshRenderer>();
    }

    void Update() {
        GetInput();
        Move();
        Turn();
        Jump();
		Grenade();
		Attack();
		Reload();
		Dodge();
		Swap();
		Interation();
    }

    void GetInput() {
        hAxis = Input.GetAxisRaw("Horizontal");	//좌우 방향키
        vAxis = Input.GetAxisRaw("Vertical");	//상하 방향키
        wDown = Input.GetButton("Walk");	//shift 키
		jDown = Input.GetButtonDown("Jump");	//스페이스바
		fDown = Input.GetButton("Fire1");   //마우스 왼쪽 클릭
		gDown = Input.GetButtonDown("Fire2");   //마우스 오른쪽 클릭
		rDown = Input.GetButtonDown("Reload");	//R키
		iDown = Input.GetButtonDown("Interation");	//E키
		sDown1 = Input.GetButton("Swap1");	//번호 1번 키
		sDown2 = Input.GetButton("Swap2");	//번호 2번 키
		sDown3 = Input.GetButton("Swap3");	//번호 3번 키
	}

    void Move() {
        moveVec = new Vector3(hAxis, 0, vAxis).normalized;  //normalized : 방향 값이 1로 보정된 벡터

		if (isDodge)	//회피 중일때는
			moveVec = dodgeVec; //회피하는 중인 방향으로 유지

		if (isSwap || isReload || !isFireReady || isDead)	//무기 교체, 재장전, 공격 중, 사망일때는
			moveVec = Vector3.zero;	//멈추기

		if (!isBorder)
			transform.position += moveVec * speed * (wDown ? 0.3f : 1f) * Time.deltaTime;

		anim.SetBool("isRun", (moveVec != Vector3.zero));   //이동을 멈추면
        anim.SetBool("isWalk", wDown);
    }

    void Turn() {
		//키보드로 회전
		transform.LookAt(transform.position + moveVec); //나아갈 방향 보기

		//마우스로 회전
		if (fDown && !isDead) {
			Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
			RaycastHit rayHit;
			if (Physics.Raycast(ray, out rayHit, 100)) {
				Vector3 nextVec = rayHit.point - transform.position;
				nextVec.y = 0;
				transform.LookAt(transform.position + nextVec);
			}
		}
    }

    void Jump() {
        if (jDown && (moveVec == Vector3.zero) && !isJump && !isDodge && !isSwap && !isDead) {	//움직이지 않고 점프
            rigid.AddForce(Vector3.up * 15, ForceMode.Impulse);
			anim.SetBool("isJump", true);
			anim.SetTrigger("doJump");
			isJump = true;
        }
    }

	void Grenade() {
		if (hasGrenades == 0)
			return;

		if (gDown && !isReload && !isSwap && !isDead) {
			Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
			RaycastHit rayHit;
			if (Physics.Raycast(ray, out rayHit, 100)) {
				Vector3 nextVec = rayHit.point - transform.position;
				nextVec.y = 10;

				GameObject instantGrenade = Instantiate(grenadeObj, transform.position, transform.rotation);
				Rigidbody rigidGrenade = instantGrenade.GetComponent<Rigidbody>();
				rigidGrenade.AddForce(nextVec, ForceMode.Impulse);
				rigidGrenade.AddTorque(Vector3.back * 10, ForceMode.Impulse);

				hasGrenades--;
				grenades[hasGrenades].SetActive(false);
			}
		}
	}

	void Attack() {
		if (equipWeapon == null)
			return;

		fireDelay += Time.deltaTime;
		isFireReady = (equipWeapon.rate < fireDelay);

		if (fDown && isFireReady && !isDodge && !isSwap && !isReload && !isShop && !isDead) {
			equipWeapon.Use();
			anim.SetTrigger(equipWeapon.type == Weapon.Type.Melee ? "doSwing" : "doShot");
			fireDelay = 0;
		}
	}

	void Reload() {
		if (equipWeapon == null)
			return;

		if (equipWeapon.type == Weapon.Type.Melee)
			return;

		if (ammo == 0)
			return;

		if (rDown && !isJump && !isDodge && !isSwap && isFireReady && !isShop && !isDead) {
			anim.SetTrigger("doReload");
			isReload = true;

			Invoke("ReloadOut", 3);
		}
	}

	void ReloadOut() { 
		int reAmmo = (ammo < equipWeapon.maxAmmo) ? ammo : equipWeapon.maxAmmo;
		equipWeapon.curAmmo = reAmmo;
		ammo -= reAmmo;
		isReload = false;
	}

	void Dodge() {
		if (jDown && (moveVec != Vector3.zero) && !isJump && !isDodge && !isSwap && !isDead) {    //이동하면서 점프
			dodgeVec = moveVec;
			speed *= 2;
			anim.SetTrigger("doDodge");
			isDodge = true;

			Invoke("DodgeOut", 0.4f);
		}
	}

	void DodgeOut() {
		speed *= 0.5f;
		isDodge = false;
	}

	void Swap() {
		if (sDown1 && (!hasWeapons[0] || equipWeaponIndex == 0))	return;
		if (sDown2 && (!hasWeapons[1] || equipWeaponIndex == 1))	return;
		if (sDown3 && (!hasWeapons[2] || equipWeaponIndex == 2))	return;

		int weaponIndex = -1;
		if (sDown1) weaponIndex = 0;
		if (sDown2) weaponIndex = 1;
		if (sDown3) weaponIndex = 2;

		if ((sDown1 || sDown2 || sDown3) && !isJump && !isDodge && !isDead) {
			if (equipWeapon != null)
				equipWeapon.gameObject.SetActive(false);

			equipWeaponIndex = weaponIndex;
			equipWeapon = weapons[weaponIndex].GetComponent<Weapon>();
			equipWeapon.gameObject.SetActive(true);

			anim.SetTrigger("doSwap");
			isSwap = true;
			Invoke("SwapOut", 0.4f);
		}
	}

	void SwapOut() {
		isSwap = false;
	}

	void Interation() {
		if (iDown && nearObject != null && !isJump && !isDodge && !isDead) {
			if (nearObject.tag == "Weapon") {
				Item item = nearObject.GetComponent<Item>();
				int weaponIndex = item.value;
				hasWeapons[weaponIndex] = true;

				Destroy(nearObject);
			}
			else if(nearObject.tag == "Shop") {
				Shop shop = nearObject.GetComponent<Shop>();
				shop.Enter(this);
				isShop = true;
			}
		}
	}

	void FreezeRotation() {
		rigid.angularVelocity = Vector3.zero;
	}

	void StopToWall() {
		Debug.DrawRay(transform.position, transform.forward * 5, Color.green);
		isBorder = Physics.Raycast(transform.position, moveVec, 5, LayerMask.GetMask("Wall"));
	}

	void FixedUpdate() {
		FreezeRotation();
		StopToWall();
	}

	void OnCollisionEnter(Collision collision) {
		if (collision.gameObject.tag == "Floor") {
			anim.SetBool("isJump", false);
			isJump = false;
		}
	}

	void OnTriggerEnter(Collider other) {
		if (other.tag == "Item") {
			Item item = other.GetComponent<Item>();
			switch(item.type) {
				case Item.Type.Ammo:
					ammo += item.value;
					if (ammo > maxAmmo)
						ammo = maxAmmo;
					break;
				case Item.Type.Coin:
					coin += item.value;
					if (coin > maxCoin)
						coin = maxCoin;
					break;
				case Item.Type.Heart:
					health += item.value;
					if (health > maxHealth)
						health = maxHealth;
					break;
				case Item.Type.Grenade:
					if (hasGrenades == maxHasGrenades)
						return;

					grenades[hasGrenades].SetActive(true);
					hasGrenades += item.value;
					if (hasGrenades > maxHasGrenades)
						hasGrenades = maxHasGrenades;
					break;
			}
			Destroy(other.gameObject);
		}
		else if (other.tag == "EnemyBullet") {
			if (!isDamage) {
				Bullet enemyBullet = other.GetComponent<Bullet>();
				health -= enemyBullet.damage;


				bool isBossAtk = other.name == "Boss Melee Area";
				StartCoroutine(OnDamage(isBossAtk));
			}

			if (other.GetComponent<Rigidbody>() != null)    //Rigidbody 유무를 판단(미사일)
				Destroy(other.gameObject);
		}
	}

	IEnumerator OnDamage(bool isBossAtk) {
		isDamage = true;
		foreach(MeshRenderer mesh in meshs) {
			mesh.material.color = Color.yellow;
		}

		if (isBossAtk)
			rigid.AddForce(transform.forward * -25, ForceMode.Impulse);

		if (health <= 0 && !isDead)
			OnDie();

		yield return new WaitForSeconds(1f);

		isDamage = false;

		foreach (MeshRenderer mesh in meshs) {
			mesh.material.color = Color.white;
		}

		if (isBossAtk)
			rigid.velocity = Vector3.zero;
	}

	void OnDie() {
		anim.SetTrigger("doDie");
		isDead = true;
		manager.GameOver();
	}

	void OnTriggerStay(Collider other) {
		if (other.tag == "Weapon" || other.tag == "Shop")
			nearObject = other.gameObject;
	}

	void OnTriggerExit(Collider other) {
		if (other.tag == "Weapon")
			nearObject = null;
		else if (other.tag == "Shop") {
			Shop shop = nearObject.GetComponent<Shop>();
			shop.Exit();
			isShop = false;
			nearObject = null;
		}
	}
}
//GameManager 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour {
	public GameObject menuCam;
	public GameObject gameCam;
	public Player player;
	public Boss boss;
	public GameObject itemShop;
	public GameObject weaponShop;
	public GameObject startZone;
	public int stage;
	public float playTime;
	public bool isBattle;
	public int enemyCntA;
	public int enemyCntB;
	public int enemyCntC;
	public int enemyCntD;

	public Transform[] enemyZones;
	public GameObject[] enemies;
	public List<int> enemyList;

	public GameObject menuPanel;
	public GameObject gamePanel;
	public GameObject overPanel;
	public Text maxScoreTxt;
	public Text scoreTxt;
	public Text stageTxt;
	public Text playTimeTxt;
	public Text playerHealthTxt;
	public Text playerAmmoTxt;
	public Text playerCoinTxt;
	public Image weapon1Img;
	public Image weapon2Img;
	public Image weapon3Img;
	public Image weaponRImg;
	public Text enemyATxt;
	public Text enemyBTxt;
	public Text enemyCTxt;
	public RectTransform bossHealthGroup;
	public RectTransform bossHealthBar;
	public Text curScoreText;
	public Text bestText;

	void Awake() {
		enemyList = new List<int>();
		maxScoreTxt.text = string.Format("{0:n0}", PlayerPrefs.GetInt("MaxScore"));	//세자리 수마다 , 추가
	}

	public void GameStart() {
		menuCam.SetActive(false);
		gameCam.SetActive(true);

		menuPanel.SetActive(false);
		gamePanel.SetActive(true);

		player.gameObject.SetActive(true);
	}

	public void GameOver() {
		gamePanel.SetActive(false);
		overPanel.SetActive(true);
		curScoreText.text = scoreTxt.text;

		int maxScore = PlayerPrefs.GetInt("MaxsScore");
		if (player.score > maxScore) {
			bestText.gameObject.SetActive(true);
			PlayerPrefs.SetInt("MaxsScore", player.score);
		}

	}

	public void Restart() {
		SceneManager.LoadScene(0);
	}

	public void StageStart() {
		itemShop.SetActive(false);
		weaponShop.SetActive(false);
		startZone.SetActive(false);

		foreach (Transform zone in enemyZones)
			zone.gameObject.SetActive(true);

		isBattle = true;
		StartCoroutine(InBattle());
	}

	public void StageEnd() {
		player.transform.position = Vector3.up * 0.8f;
		
		itemShop.SetActive(true);
		weaponShop.SetActive(true);
		startZone.SetActive(true);

		foreach (Transform zone in enemyZones)
			zone.gameObject.SetActive(false);

		isBattle = false;
		stage++;
	}

	IEnumerator InBattle() {
		if (stage % 5 == 0) {
			enemyCntD++;
			GameObject instantEnemy = Instantiate(enemies[3], enemyZones[0].position, enemyZones[0].rotation);
			Enemy enemy = instantEnemy.GetComponent<Enemy>();
			enemy.target = player.transform;
			enemy.manager = this;   //GameManager 보내주기
			boss = instantEnemy.GetComponent<Boss>();
		}
		else {
			for (int index = 0; index < stage; index++) {
				int ran = Random.Range(0, 3);
				enemyList.Add(ran);

				switch (ran) {
					case 0:
						enemyCntA++;
						break;
					case 1:
						enemyCntB++;
						break;
					case 2:
						enemyCntC++;
						break;
				}
			}

			while (enemyList.Count > 0) {
				int ranZone = Random.Range(0, 4);
				GameObject instantEnemy = Instantiate(enemies[enemyList[0]], enemyZones[ranZone].position, enemyZones[ranZone].rotation);
				Enemy enemy = instantEnemy.GetComponent<Enemy>();
				enemy.target = player.transform;
				enemy.manager = this;	//GameManager 보내주기
				enemyList.RemoveAt(0);
				yield return new WaitForSeconds(4f);
			}
		}

		while(enemyCntA + enemyCntB + enemyCntC + enemyCntD > 0) {
			yield return null;
		}

		yield return new WaitForSeconds(4f);
		boss = null;
		StageEnd();
	}

	void Update() {
		if (isBattle)
			playTime += Time.deltaTime;
	}

	void LateUpdate() {
		//좌측 상단 UI
		scoreTxt.text = string.Format("{0:n0}", player.score);

		//우측 상단 UI
		stageTxt.text = "STAGE " + stage;
		int hour = (int)(playTime / 3600);	//시
		int min = (int)((playTime - hour * 3600) / 60);   //분
		int second = (int)(playTime % 60);	//초
		playTimeTxt.text = string.Format("{0:00}", hour) + ":" + string.Format("{0:00}", min) + ":" + string.Format("{0:00}", second);

		//플레이어(좌측 하단) UI
		playerHealthTxt.text = player.health + " / " + player.maxHealth;
		playerCoinTxt.text = string.Format("{0:n0}", player.coin);
		if (player.equipWeapon == null)
			playerAmmoTxt.text = "- / " + player.ammo;
		else if (player.equipWeapon.type == Weapon.Type.Melee)
			playerAmmoTxt.text = "- / " + player.ammo;
		else
			playerAmmoTxt.text = player.equipWeapon.curAmmo + " / " + player.ammo;

		//무기(중앙 하단) UI
		weapon1Img.color = new Color(1, 1, 1, (player.hasWeapons[0] ? 1 : 0));
		weapon2Img.color = new Color(1, 1, 1, (player.hasWeapons[1] ? 1 : 0));
		weapon3Img.color = new Color(1, 1, 1, (player.hasWeapons[2] ? 1 : 0));
		weaponRImg.color = new Color(1, 1, 1, (player.hasGrenades > 0 ? 1 : 0));

		//몬스터 숫자(우측 하단) UI
		enemyATxt.text = enemyCntA.ToString();
		enemyBTxt.text = enemyCntB.ToString();
		enemyCTxt.text = enemyCntC.ToString();

		//보스 체력(중앙 상단) UI
		if (boss != null) {
			bossHealthGroup.anchoredPosition = Vector3.down * 30;
			bossHealthBar.localScale = new Vector3((float)boss.curHealth / boss.maxHealth, 1, 1);
		}
		else {
			bossHealthGroup.anchoredPosition = Vector3.up * 200;
		}
	}
}

Player 변수 할당
GameManager 변수 할당
버튼에 Restart 함수 적용
체력을 낮추고 적에게 공격받으니 사망 모션이 뜨면서 화면이 바뀌었다
버튼을 클릭하니 다시 시작되었다.

+) 버튼이 키보드 입력으로 클릭되는 경우 방지

Button의 Navigation을 Automatic에서 None으로 변경해준다


5. 빌드

//GameManager 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour {
	public GameObject menuCam;
	public GameObject gameCam;
	public Player player;
	public Boss boss;
	public GameObject itemShop;
	public GameObject weaponShop;
	public GameObject startZone;
	public int stage;
	public float playTime;
	public bool isBattle;
	public int enemyCntA;
	public int enemyCntB;
	public int enemyCntC;
	public int enemyCntD;

	public Transform[] enemyZones;
	public GameObject[] enemies;
	public List<int> enemyList;

	public GameObject menuPanel;
	public GameObject gamePanel;
	public GameObject overPanel;
	public Text maxScoreTxt;
	public Text scoreTxt;
	public Text stageTxt;
	public Text playTimeTxt;
	public Text playerHealthTxt;
	public Text playerAmmoTxt;
	public Text playerCoinTxt;
	public Image weapon1Img;
	public Image weapon2Img;
	public Image weapon3Img;
	public Image weaponRImg;
	public Text enemyATxt;
	public Text enemyBTxt;
	public Text enemyCTxt;
	public RectTransform bossHealthGroup;
	public RectTransform bossHealthBar;
	public Text curScoreText;
	public Text bestText;

	void Awake() {
		enemyList = new List<int>();
		maxScoreTxt.text = string.Format("{0:n0}", PlayerPrefs.GetInt("MaxScore")); //세자리 수마다 , 추가

		if (PlayerPrefs.HasKey("MaxScore"))
			PlayerPrefs.SetInt("MaxScore", 0);
	}

	public void GameStart() {
		menuCam.SetActive(false);
		gameCam.SetActive(true);

		menuPanel.SetActive(false);
		gamePanel.SetActive(true);

		player.gameObject.SetActive(true);
	}

	public void GameOver() {
		gamePanel.SetActive(false);
		overPanel.SetActive(true);
		curScoreText.text = scoreTxt.text;

		int maxScore = PlayerPrefs.GetInt("MaxsScore");
		if (player.score > maxScore) {
			bestText.gameObject.SetActive(true);
			PlayerPrefs.SetInt("MaxsScore", player.score);
		}

	}

	public void Restart() {
		SceneManager.LoadScene(0);
	}

	public void StageStart() {
		itemShop.SetActive(false);
		weaponShop.SetActive(false);
		startZone.SetActive(false);

		foreach (Transform zone in enemyZones)
			zone.gameObject.SetActive(true);

		isBattle = true;
		StartCoroutine(InBattle());
	}

	public void StageEnd() {
		player.transform.position = Vector3.up * 0.8f;
		
		itemShop.SetActive(true);
		weaponShop.SetActive(true);
		startZone.SetActive(true);

		foreach (Transform zone in enemyZones)
			zone.gameObject.SetActive(false);

		isBattle = false;
		stage++;
	}

	IEnumerator InBattle() {
		if (stage % 5 == 0) {
			enemyCntD++;
			GameObject instantEnemy = Instantiate(enemies[3], enemyZones[0].position, enemyZones[0].rotation);
			Enemy enemy = instantEnemy.GetComponent<Enemy>();
			enemy.target = player.transform;
			enemy.manager = this;   //GameManager 보내주기
			boss = instantEnemy.GetComponent<Boss>();
		}
		else {
			for (int index = 0; index < stage; index++) {
				int ran = Random.Range(0, 3);
				enemyList.Add(ran);

				switch (ran) {
					case 0:
						enemyCntA++;
						break;
					case 1:
						enemyCntB++;
						break;
					case 2:
						enemyCntC++;
						break;
				}
			}

			while (enemyList.Count > 0) {
				int ranZone = Random.Range(0, 4);
				GameObject instantEnemy = Instantiate(enemies[enemyList[0]], enemyZones[ranZone].position, enemyZones[ranZone].rotation);
				Enemy enemy = instantEnemy.GetComponent<Enemy>();
				enemy.target = player.transform;
				enemy.manager = this;	//GameManager 보내주기
				enemyList.RemoveAt(0);
				yield return new WaitForSeconds(4f);
			}
		}

		while(enemyCntA + enemyCntB + enemyCntC + enemyCntD > 0) {
			yield return null;
		}

		yield return new WaitForSeconds(4f);
		boss = null;
		StageEnd();
	}

	void Update() {
		if (isBattle)
			playTime += Time.deltaTime;
	}

	void LateUpdate() {
		//좌측 상단 UI
		scoreTxt.text = string.Format("{0:n0}", player.score);

		//우측 상단 UI
		stageTxt.text = "STAGE " + stage;
		int hour = (int)(playTime / 3600);	//시
		int min = (int)((playTime - hour * 3600) / 60);   //분
		int second = (int)(playTime % 60);	//초
		playTimeTxt.text = string.Format("{0:00}", hour) + ":" + string.Format("{0:00}", min) + ":" + string.Format("{0:00}", second);

		//플레이어(좌측 하단) UI
		playerHealthTxt.text = player.health + " / " + player.maxHealth;
		playerCoinTxt.text = string.Format("{0:n0}", player.coin);
		if (player.equipWeapon == null)
			playerAmmoTxt.text = "- / " + player.ammo;
		else if (player.equipWeapon.type == Weapon.Type.Melee)
			playerAmmoTxt.text = "- / " + player.ammo;
		else
			playerAmmoTxt.text = player.equipWeapon.curAmmo + " / " + player.ammo;

		//무기(중앙 하단) UI
		weapon1Img.color = new Color(1, 1, 1, (player.hasWeapons[0] ? 1 : 0));
		weapon2Img.color = new Color(1, 1, 1, (player.hasWeapons[1] ? 1 : 0));
		weapon3Img.color = new Color(1, 1, 1, (player.hasWeapons[2] ? 1 : 0));
		weaponRImg.color = new Color(1, 1, 1, (player.hasGrenades > 0 ? 1 : 0));

		//몬스터 숫자(우측 하단) UI
		enemyATxt.text = enemyCntA.ToString();
		enemyBTxt.text = enemyCntB.ToString();
		enemyCTxt.text = enemyCntC.ToString();

		//보스 체력(중앙 상단) UI
		if (boss != null) {
			bossHealthGroup.anchoredPosition = Vector3.down * 30;
			bossHealthBar.localScale = new Vector3((float)boss.curHealth / boss.maxHealth, 1, 1);
		}
		else {
			bossHealthGroup.anchoredPosition = Vector3.up * 200;
		}
	}
}
  • 첫 실행시 최고 점수가 기록되어 있지 않을 것이므로, 0으로 초기화해주는 작업을 Awake에 추가해주었다.

이제 플레이시 게임 화면이 이렇게 나오도록 세팅해준다
Build Settings. 위에 Scenes In Build가 비어있으면 Add Open Scenes를 눌러 추가해준다.
Player Settings 설정. 16:9를 기준으로 제작했으므로 설정을 16:9만 남겨둔다.
빌드 완료
실행한 모습
무기 구매도 문제없다
사망시 화면