지난번에 이어 게임을 만들어보자.
이번에 참고할 강의는 아래 강의이다.
https://www.youtube.com/watch?v=_Ue8P4tNXOg&list=PLFt_AvWsXl0ctd4dgE1F8g3uec4zKNRV0&index=26
이번 강의에서는 여러 기능을 추가하고, 게임 제작을 마무리한다.
시작해보자.
일단 먼저 게임에 점수 기능을 추가해보자.
Game 씬으로 이동하여 Score Keeper 스크립트와 오브젝트를 만들어 적용하자.
Score Keeper 스크립트를 작성하자.
| ScoreKeeper 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreKeeper : MonoBehaviour {
public static int score { get; private set; } // 점수
float lastEnemyKillTime; // 이전 적 처치 후 시간
int streakCount; // 연속 처치 카운트
float streakExpiryTime = 1; // 연속 처치 만료 시간
void Start() {
score = 0; // 점수 초기화
Enemy.OnDeathStatic += OnEnemyKilled; // 적 사망 이벤트 구독
FindObjectOfType<Player>().OnDeath += OnPlayerDeath; // 플레이어 사망 이벤트 구독
}
// ■ 적 사망 시 호출될 메소드
void OnEnemyKilled() {
if(Time.time < lastEnemyKillTime + streakExpiryTime) { // 적 처치 후 처치 만료 시간이 지나기 전인 경우
streakCount++; // 연속 처치 카운트 추가
}
else {
streakCount = 0; // 연속 처치 카운트 초기화
}
lastEnemyKillTime = Time.time; // 적 처치 시간 설정
score += 5 + (int)Mathf.Pow(2, streakCount); // 점수 추가
}
// ■ 플레이어 사망 시 호출될 메소드
void OnPlayerDeath() {
Enemy.OnDeathStatic -= OnEnemyKilled; // 적 사망 이벤트 구독 취소
}
}
적 사망 시 이벤트를 추가하기 위해 Enemy 스크립트도 수정한다.
| Enemy 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(UnityEngine.AI.NavMeshAgent))]
// 현재 오브젝트에 NavMeshAgent 를 포함
public class Enemy : LivingEntity
{
public enum State { Idle, Cashing, Attacking };
// 적의 상태, { 기본(아무것도 안함), 추적, 공격 }
State currentState; // 현재 상태
public ParticleSystem deathEffect; // 사망 이펙트 레퍼런스
public static event System.Action OnDeathStatic; // 적 사망 정적 이벤트
UnityEngine.AI.NavMeshAgent pathfinder; // 내비게이션 레퍼런스
Transform target; // 적의 타겟(플레이어) 트랜스폼
LivingEntity targetEntity; // 타겟(플레이어) 레퍼런스
Material skinMatreial; // 현재 오브젝트의 마테리얼
Color originalColor; // 현재 오브젝트의 기존 색상
float attackDistanceThreshold = 0.5f;
// 공격 거리 임계값(사거리). 유니티에서 단위 1은 1 meter 이다..!!
float timeBetweenAttacks = 1;
// 공격 시간 간격
float nextAttackTime;
// 실제 다음 공격 시간
float damage = 1;
// 적의 공격 데미지
float myCollisionRadius; // 자신의 충돌 범위 반지름
float targetConllisionRadius; // 타겟의 충돌 범위 반지름
bool hasTarget; // 타겟이 있는지 확인
void Awake()
{
pathfinder = GetComponent<UnityEngine.AI.NavMeshAgent>();
// NavMeshAgent 레퍼런스 생성
if (GameObject.FindGameObjectWithTag("Player") != null)
// 적 스폰 후 플레이어 오브젝트가 있는(플레이어가 살아있는) 경우
{
hasTarget = true; // 타겟(플레이어) 있음으로 설정
target = GameObject.FindGameObjectWithTag("Player").transform;
// 타겟으로 "Player" 라는 태그를 가진 오브젝트의 트랜스폼을 저장
targetEntity = target.gameObject.GetComponent<LivingEntity>();
// 위에서 저장 한 플레이어 오브젝트의 LivingEntity 컴포넌트를 저장
myCollisionRadius = GetComponent<CapsuleCollider>().radius;
// 자신의 충돌체(collider)의 반지름을 저장
targetConllisionRadius = target.GetComponent<CapsuleCollider>().radius;
// 타겟의 충돌체(collider)의 반지름을 저장
}
}
protected override void Start()
// override 로 부모 클래스의 메소드를 재정의.
{
base.Start(); // base 를 통해 부모 클래스의 기존 메소드를 호출.
if(hasTarget)
// 적 스폰 후 플레이어 오브젝트가 있는(플레이어가 살아있는) 경우
{
currentState = State.Cashing; // 기본 상태를 추적 상태로 설정
targetEntity.OnDeath += OnTargetDeath;
// 타겟 사망 메소드 추가.
StartCoroutine(UpdatePath());
// 지정된 시간마다 목적지 쪽으로 위치를 갱신하는 코루틴 실행
}
}
// ■ 정보 수정 메소드
public void SetCharactoristics(float moveSpeed, int hitsToKillPlayer, float enemyHealth, Color skinColour) {
pathfinder.speed = moveSpeed; // 적의 이동속도 설정
if (hasTarget) { // 목표 (플레이어)가 있다면
damage = Mathf.Ceil(targetEntity.startingHealth / hitsToKillPlayer); // 적의 공격력 설정
}
startingHealth = enemyHealth; // 적의 체력 설정
deathEffect.startColor = new Color(skinColour.r, skinColour.g, skinColour.b); // 적 사망 파티클 색상을 현재 적의 색상으로 변경
skinMatreial = GetComponent<Renderer>().material; // 현재 오브젝트의 마테리얼 저장.
skinMatreial.color = skinColour; // 적의 색상 설정
originalColor = skinMatreial.color; // 현재 색상 저장
}
// ■ 타격 메소드 오버라이딩
public override void TakeHit(float damage, Vector3 hitPoint, Vector3 hitDirection) {
AudioManager.instance.PlaySound("Impact", transform.position); // 현재 위치에 타격 사운드 재생
if(damage >= health) { // 데미지가 현재 체력 이상인 경우
if(OnDeathStatic != null) { // 구독자가 있는 경우
OnDeathStatic(); // 이벤트 호출
}
AudioManager.instance.PlaySound("Enemy Death", transform.position); // 현재 위치에 사망 사운드 재생
Destroy(Instantiate(deathEffect.gameObject, hitPoint, Quaternion.FromToRotation(Vector3.forward, hitDirection)) as GameObject, deathEffect.startLifetime);
// 이펙트(파티클)을 인스턴스화 하여 생성(FromToRotation 으로 방향 설정), 설정한 시간경과 후 파괴
}
base.TakeHit(damage, hitPoint, hitDirection);
// 부모 클래스의 기존 TakeHit 메소드 호출
}
void OnTargetDeath() // 타겟(플레이어)이 죽었을 때 호출되는 메소드
{
hasTarget = false; // 타겟 없음으로 설정
currentState = State.Idle; // 현재 상태를 기본(정지)상태로 변경
}
void Update()
{
if (hasTarget) // 타겟(플레이어)가 있는 경우
{
if (Time.time >= nextAttackTime) // 공격 시간이 지나면
{
float sqrDstToTarget = (target.position - transform.position).sqrMagnitude;
/* 자신(적)과 목표(플레이어) 사이의 거리를 저장.
두 오브젝트 사이의 거리를 측정하기 위해 Vector3 의 Distace 메소드를
쓰는 방법도 있지만 벡터에 제곱 연산이 들어가다 보니 연산 수가 많다.
두 오브젝트 사이의 실제 거리가 필요한 것이 아닌 비교값이 필요 한
것이므로 위와 같이 연산을 줄여 볼 수 있다. */
if (sqrDstToTarget <= Mathf.Pow(attackDistanceThreshold + targetConllisionRadius + myCollisionRadius, 2))
// 거리의 제곱과 임계 거리의 제곱을 비교하여 공격 범위 내인지 확인
{
nextAttackTime = Time.time + timeBetweenAttacks;
// 다음 공격 시간 지정
AudioManager.instance.PlaySound("Enemy Attack", transform.position); // 현재 위치에 공격 사운드 재생
StartCoroutine(Attack());
// 공격 코루틴 실행
}
}
}
}
IEnumerator Attack() // 적의 공격 코루틴
{
currentState = State.Attacking; // 공격 상태로 변경
pathfinder.enabled = false; // 공격 중 플레이어 추적 중지
Vector3 originalPosition = transform.position; // 자신(적)의 위치
Vector3 dirToTarget = (target.position - transform.position).normalized;
// 타겟으로의 방향 벡터 계산
Vector3 attackPosition = target.position - dirToTarget * (myCollisionRadius);
// 타겟(플레이어)의 위치
float attackSpeed = 3; // 공격 속도
float percent = 0;
skinMatreial = GetComponent<Renderer>().material; // 현재 오브젝트의 마테리얼 저장.
skinMatreial.color = Color.magenta; // 공격 시 색상 지정
bool hasAppliedDamage = false; // 데미지를 적용 중인지 확인
while(percent <= 1) // 찌르는 거리가 1 이하일 때 루프
{
if(percent >= .5f && hasAppliedDamage == false)
// 적이 공격 지점에 도달했고 데미지를 적용중이지 않은 경우
{
hasAppliedDamage = true; // 데미지 적용 중으로 설정
targetEntity.TakeDamage(damage); // 타겟(플레이어)에게 데미지 적용
}
percent += Time.deltaTime * attackSpeed;
float interpolation = (-Mathf.Pow(percent, 2) + percent) * 4;
/* 대칭 함수를 사용
원지점->공격지점 이동에서 보간 값을 참조한다. */
transform.position = Vector3.Lerp(originalPosition, attackPosition, interpolation);
/* Lerp 메소드는 내분점을 반환한다.
원점, 공격지점, 보간 값을 참조값으로 전달한다
보간 값이 0이면 원점에, 1이면 공격지점에 있게 된다. */
yield return null;
/* while 루프의 처리 사이에서 프레임을 스킵합니다.
이 지점에서 작업이 멈추고 Update 메소드 작업이 끝나고
다음 프레임으로 넘어가면 이 아래의 코드나 루프가 실행 */
}
skinMatreial.color = originalColor; // 공격이 끝나면 기존 색상으로 변경
currentState = State.Cashing; // 공격이 끝나면 추적 상태로 변경
pathfinder.enabled = true; // 공격이 끝나면 다시 플레이어 추적
}
IEnumerator UpdatePath()
// 해당 코루틴 실행 시 지정한 시간마다 루프문 내부 코드 반복
{
float refreshRate = .25f; // 루프 시간
while(hasTarget) // 타겟이 있을 때 반복
{
if(currentState == State.Cashing)
// 상태가 추적 상태인 경우
{
Vector3 dirToTarget = (target.position - transform.position).normalized;
// 타겟으로의 방향 벡터 계산
Vector3 targetPosition = target.position - dirToTarget * (myCollisionRadius + targetConllisionRadius + attackDistanceThreshold / 2);
/* 자신의 위치에서 자신과 타겟의 반지름 길이에 공격 사거리의 절반을 더하고, 방향 벡터를 곱한 값을 뺀다.
즉, 타겟(플레이어) 공격 가능한 위치를 타겟 위치로 정한다. */
if (!dead) // 죽지 않은 경우
{
pathfinder.SetDestination(targetPosition);
// 내비게이션의 목적지를 타겟(플레이어)의 위치로 설정
}
}
yield return new WaitForSeconds(refreshRate);
// refreshRate 시간만큼 기다림
}
}
}
이제 점수를 표시 할 텍스트 오브젝트 Score 를 Canvas 오브젝트 하위에 생성한다.
컴포넌트 - UI - 효과(Effects) - 윤곽선(Outline) 를 추가해 글자에 테두리를 추가해서 잘 보이게 하자.
GameUI 스크립트로 가서 점수를 표시하도록 해보자.
| GameUI 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.SceneManagement;
public class GameUI : MonoBehaviour {
public Image fadePlane; // 페이드 이미지(UI 배경) 오브젝트 레퍼런스
public GameObject gameOverUI; // 게임 오버 텍스트, 버튼 오브젝트 레퍼런스
public RectTransform newWaveBanner; // 배너 UI 레퍼런스
public TextMeshProUGUI newWaveTitle; // 새 웨이브 타이틀 텍스트 레퍼런스
public TextMeshProUGUI newWaveEnemyCount; // 새 웨이브 적 카운트 텍스트 레퍼런스
public TextMeshProUGUI scoreUI; // 점수 텍스트 레퍼런스
public TextMeshProUGUI gameOverScoreUI; // 최종 점수 텍스트 레퍼런스
public RectTransform healthBar; // 체력 바 레퍼런스
Spawner spawner; // 적 스폰기 레퍼런스
Player player; // 플레이어 레퍼런스
void Start() {
player = FindObjectOfType<Player>(); // 플레이어 오브젝트 할당
player.OnDeath += OnGameOver; // 플레이어 사망 이벤트 구독
}
void Update() {
scoreUI.text = ScoreKeeper.score.ToString("D6"); // 점수 텍스트 설정, "D6" 으로 6자리로 출력
float healthPercent = 0;
if(player != null) {
healthPercent = player.health / player.startingHealth; // 체력 퍼센트 계산
}
healthBar.localScale = new Vector3(healthPercent, 1, 1); // 체력 퍼센트에 따라 체력 바 크기 설정
}
void Awake() {
spawner = FindObjectOfType<Spawner>(); // 스포너 레퍼런스에 스포너 오브젝트 찾아서 할당
spawner.OnNewWave += OnNewWave; // 새 웨이브 시작 이벤트 구독
}
// ■ 새 웨이브 시작 메소드
void OnNewWave(int waveNumber) {
string[] numbers = { "One", "Two", "Three", "Four", "Five" }; // 숫자 텍스트 문자열
newWaveTitle.text = "- Wave " + numbers[waveNumber - 1] + " -"; // 새 웨이브 배너 타이틀 텍스트 설정
string enemyCountString = ((spawner.waves[waveNumber - 1].infinite) ? "Infinite" : spawner.waves[waveNumber - 1].enemyCount + "");
// 해당 웨이브가 무한 모드인지 판단, 맞다면 Infinite, 아니면 적 수를 출력한다
newWaveEnemyCount.text = "Enemy : " + enemyCountString; // 새 웨이브 적 카운트 텍스트 설정
StopCoroutine("AnimateNewWaveBanner"); // 새 웨이브 배너 애니메이션 코루틴 중지
StartCoroutine("AnimateNewWaveBanner"); // 새 웨이브 배너 애니메이션 코루틴
}
// ■ 플레이어 사망(게임 오버) 시 UI 처리 메소드
void OnGameOver() {
Cursor.visible = true; // 마우스 커서 보이도록 설정
StartCoroutine(Fade(Color.clear, new Color(0, 0, 0, .95f), 1)); // 배경 이미지 페이드 인 효과 코루틴 시작
gameOverScoreUI.text = scoreUI.text; // 최종 점수 텍스트 설정
scoreUI.gameObject.SetActive(false); // 게임 화면 점수 텍스트 오브젝트 비활성화
healthBar.transform.parent.gameObject.SetActive(false); // 체력 바 오브젝트 비활성화
gameOverUI.SetActive(true); // 게임 오버 텍스트, 버튼 오브젝트 활성화
}
// ■ 새 웨이브 배너 애니메이션 코루틴
IEnumerator AnimateNewWaveBanner() {
float delayTime = 1f; // 배너 대기 시간
float speed = 2.5f; // 배너 움직이는 시간
float animatePercent = 0; // 애니메이션 실행 퍼센트 설정
int dir = 1; // 배너 이동 방향, 양수면 위, 음수면 아래쪽
float endDelayTime = Time.time + 1 / speed + delayTime; // 배너 나타난 후 대기시간 설정
while(animatePercent >= 0) { // 애니메이션 실행
animatePercent += Time.deltaTime * speed * dir; // 애니메이션 실행 퍼센트 설정
if (animatePercent >= 1) { // 애니메이션 실행 퍼센트가 1 이상인 경우
animatePercent = 1; // 1로 설정
if(Time.time > endDelayTime) { // 현재 시간이 배너가 나타나고 대기시간까지 지난 경우
dir = -1; // 방향을 음수로 설정, 아래로 이동하도록 함
}
}
newWaveBanner.anchoredPosition = Vector2.up * Mathf.Lerp(-190, 45, animatePercent); // 배너의 위치를 설정
yield return null; // 다음 프레임으로 스킵
}
}
// ■ 이미지 페이드 효과 코루틴
IEnumerator Fade(Color from, Color to, float time) {
float speed = 1 / time; // 페이드 효과 속도
float percent = 0; // 이미지 색상(투명도) 변화 퍼센트
while(percent < 1) { // 색상(투명도) 변화가 1(100%)미만일 때
percent += Time.deltaTime * speed; // 색상(투명도) 변화율 계산
fadePlane.color = Color.Lerp(from, to, percent); // 색상(투명도) 지정
yield return null; // 다음 프레임으로 건너뜀
}
}
// UI Input 부분
// ■ 새 게임 시작 메소드
public void startNewGame() {
SceneManager.LoadScene("Game"); // 게임 씬, 게임을 다시 로드
}
// ■ 메인 메뉴 이동 메소드
public void ReturnToMainMenu() {
SceneManager.LoadScene("Menu"); // 메뉴 씬, 메뉴를 로드
}
}
실행해서 적을 잡아보자.
적을 잡을때마다 스코어가 증가하는 것을 볼 수 있다.
이번에는 플레이어 캐릭터의 체력을 보여주기 위해 화면 아래에 체력바를 추가하자.
Canvas 아래에 빈 오브젝트 Health Bar 를 생성하고,
그 아래에 체력을 보여줄 이미지 오브젝트 Bar 와 체력바의 뒷배경인 Backing 을 생성한 다음
각각 색상과 사이즈, 위치를 조정해주자.
이 때 체력을 표시할 이미지 오브젝트의 가운데에 위치한 파란 원을 왼쪽 끝으로 옮기자.
이렇게 하면 이미지의 x 사이즈가 조정될 때 사이즈가 오른쪽 끝부터 줄어든다.
즉 체력이 줄어드는것을 볼 수 있다.
GameUI 스크립트를 수정해 적에게 공격당한 경우 체력바가 줄어들도록 해보자.
| GameUI 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.SceneManagement;
public class GameUI : MonoBehaviour {
public Image fadePlane; // 페이드 이미지(UI 배경) 오브젝트 레퍼런스
public GameObject gameOverUI; // 게임 오버 텍스트, 버튼 오브젝트 레퍼런스
public RectTransform newWaveBanner; // 배너 UI 레퍼런스
public TextMeshProUGUI newWaveTitle; // 새 웨이브 타이틀 텍스트 레퍼런스
public TextMeshProUGUI newWaveEnemyCount; // 새 웨이브 적 카운트 텍스트 레퍼런스
public TextMeshProUGUI scoreUI; // 점수 텍스트 레퍼런스
public TextMeshProUGUI gameOverScoreUI; // 최종 점수 텍스트 레퍼런스
public RectTransform healthBar; // 체력 바 레퍼런스
Spawner spawner; // 적 스폰기 레퍼런스
Player player; // 플레이어 레퍼런스
void Start() {
player = FindObjectOfType<Player>(); // 플레이어 오브젝트 할당
player.OnDeath += OnGameOver; // 플레이어 사망 이벤트 구독
}
void Update() {
scoreUI.text = ScoreKeeper.score.ToString("D6"); // 점수 텍스트 설정, "D6" 으로 6자리로 출력
float healthPercent = 0;
if(player != null) {
healthPercent = player.health / player.startingHealth; // 체력 퍼센트 계산
}
healthBar.localScale = new Vector3(healthPercent, 1, 1); // 체력 퍼센트에 따라 체력 바 크기 설정
}
void Awake() {
spawner = FindObjectOfType<Spawner>(); // 스포너 레퍼런스에 스포너 오브젝트 찾아서 할당
spawner.OnNewWave += OnNewWave; // 새 웨이브 시작 이벤트 구독
}
// ■ 새 웨이브 시작 메소드
void OnNewWave(int waveNumber) {
string[] numbers = { "One", "Two", "Three", "Four", "Five" }; // 숫자 텍스트 문자열
newWaveTitle.text = "- Wave " + numbers[waveNumber - 1] + " -"; // 새 웨이브 배너 타이틀 텍스트 설정
string enemyCountString = ((spawner.waves[waveNumber - 1].infinite) ? "Infinite" : spawner.waves[waveNumber - 1].enemyCount + "");
// 해당 웨이브가 무한 모드인지 판단, 맞다면 Infinite, 아니면 적 수를 출력한다
newWaveEnemyCount.text = "Enemy : " + enemyCountString; // 새 웨이브 적 카운트 텍스트 설정
StopCoroutine("AnimateNewWaveBanner"); // 새 웨이브 배너 애니메이션 코루틴 중지
StartCoroutine("AnimateNewWaveBanner"); // 새 웨이브 배너 애니메이션 코루틴
}
// ■ 플레이어 사망(게임 오버) 시 UI 처리 메소드
void OnGameOver() {
Cursor.visible = true; // 마우스 커서 보이도록 설정
StartCoroutine(Fade(Color.clear, new Color(0, 0, 0, .95f), 1)); // 배경 이미지 페이드 인 효과 코루틴 시작
gameOverScoreUI.text = scoreUI.text; // 최종 점수 텍스트 설정
scoreUI.gameObject.SetActive(false); // 게임 화면 점수 텍스트 오브젝트 비활성화
healthBar.transform.parent.gameObject.SetActive(false); // 체력 바 오브젝트 비활성화
gameOverUI.SetActive(true); // 게임 오버 텍스트, 버튼 오브젝트 활성화
}
// ■ 새 웨이브 배너 애니메이션 코루틴
IEnumerator AnimateNewWaveBanner() {
float delayTime = 1f; // 배너 대기 시간
float speed = 2.5f; // 배너 움직이는 시간
float animatePercent = 0; // 애니메이션 실행 퍼센트 설정
int dir = 1; // 배너 이동 방향, 양수면 위, 음수면 아래쪽
float endDelayTime = Time.time + 1 / speed + delayTime; // 배너 나타난 후 대기시간 설정
while(animatePercent >= 0) { // 애니메이션 실행
animatePercent += Time.deltaTime * speed * dir; // 애니메이션 실행 퍼센트 설정
if (animatePercent >= 1) { // 애니메이션 실행 퍼센트가 1 이상인 경우
animatePercent = 1; // 1로 설정
if(Time.time > endDelayTime) { // 현재 시간이 배너가 나타나고 대기시간까지 지난 경우
dir = -1; // 방향을 음수로 설정, 아래로 이동하도록 함
}
}
newWaveBanner.anchoredPosition = Vector2.up * Mathf.Lerp(-190, 45, animatePercent); // 배너의 위치를 설정
yield return null; // 다음 프레임으로 스킵
}
}
// ■ 이미지 페이드 효과 코루틴
IEnumerator Fade(Color from, Color to, float time) {
float speed = 1 / time; // 페이드 효과 속도
float percent = 0; // 이미지 색상(투명도) 변화 퍼센트
while(percent < 1) { // 색상(투명도) 변화가 1(100%)미만일 때
percent += Time.deltaTime * speed; // 색상(투명도) 변화율 계산
fadePlane.color = Color.Lerp(from, to, percent); // 색상(투명도) 지정
yield return null; // 다음 프레임으로 건너뜀
}
}
// UI Input 부분
// ■ 새 게임 시작 메소드
public void startNewGame() {
SceneManager.LoadScene("Game"); // 게임 씬, 게임을 다시 로드
}
// ■ 메인 메뉴 이동 메소드
public void ReturnToMainMenu() {
SceneManager.LoadScene("Menu"); // 메뉴 씬, 메뉴를 로드
}
}
실행하여 공격당할시 체력바가 줄어드는지 확인해보자.
적에게 공격당하면 체력바가 줄어드는 것을 확인할 수 있다.
이번에는 플레이어 사망 시 보여줄 게임 오버 화면을 설정하자.
Game Over UI 오브젝트를 생성,
하위 오브젝트로 Game Over 텍스트를 출력할 TMP 오브젝트,
재시작을 위한 Play 버튼 오브젝트, Menu 버튼 오브젝트를 추가한다.
그리고 최종 점수를 출력하기 위해 Game Over Score 라는 TMP 오브젝트도 추가한다.
이제 GameUI 스크립트를 수정해 플레이어가 사망 시 게임오버 화면을 띄우도록 하자.
| GameUI 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.SceneManagement;
public class GameUI : MonoBehaviour {
public Image fadePlane; // 페이드 이미지(UI 배경) 오브젝트 레퍼런스
public GameObject gameOverUI; // 게임 오버 텍스트, 버튼 오브젝트 레퍼런스
public RectTransform newWaveBanner; // 배너 UI 레퍼런스
public TextMeshProUGUI newWaveTitle; // 새 웨이브 타이틀 텍스트 레퍼런스
public TextMeshProUGUI newWaveEnemyCount; // 새 웨이브 적 카운트 텍스트 레퍼런스
public TextMeshProUGUI scoreUI; // 점수 텍스트 레퍼런스
public TextMeshProUGUI gameOverScoreUI; // 최종 점수 텍스트 레퍼런스
public RectTransform healthBar; // 체력 바 레퍼런스
Spawner spawner; // 적 스폰기 레퍼런스
Player player; // 플레이어 레퍼런스
void Start() {
player = FindObjectOfType<Player>(); // 플레이어 오브젝트 할당
player.OnDeath += OnGameOver; // 플레이어 사망 이벤트 구독
}
void Update() {
scoreUI.text = ScoreKeeper.score.ToString("D6"); // 점수 텍스트 설정, "D6" 으로 6자리로 출력
float healthPercent = 0;
if(player != null) {
healthPercent = player.health / player.startingHealth; // 체력 퍼센트 계산
}
healthBar.localScale = new Vector3(healthPercent, 1, 1); // 체력 퍼센트에 따라 체력 바 크기 설정
}
void Awake() {
spawner = FindObjectOfType<Spawner>(); // 스포너 레퍼런스에 스포너 오브젝트 찾아서 할당
spawner.OnNewWave += OnNewWave; // 새 웨이브 시작 이벤트 구독
}
// ■ 새 웨이브 시작 메소드
void OnNewWave(int waveNumber) {
string[] numbers = { "One", "Two", "Three", "Four", "Five" }; // 숫자 텍스트 문자열
newWaveTitle.text = "- Wave " + numbers[waveNumber - 1] + " -"; // 새 웨이브 배너 타이틀 텍스트 설정
string enemyCountString = ((spawner.waves[waveNumber - 1].infinite) ? "Infinite" : spawner.waves[waveNumber - 1].enemyCount + "");
// 해당 웨이브가 무한 모드인지 판단, 맞다면 Infinite, 아니면 적 수를 출력한다
newWaveEnemyCount.text = "Enemy : " + enemyCountString; // 새 웨이브 적 카운트 텍스트 설정
StopCoroutine("AnimateNewWaveBanner"); // 새 웨이브 배너 애니메이션 코루틴 중지
StartCoroutine("AnimateNewWaveBanner"); // 새 웨이브 배너 애니메이션 코루틴
}
// ■ 플레이어 사망(게임 오버) 시 UI 처리 메소드
void OnGameOver() {
Cursor.visible = true; // 마우스 커서 보이도록 설정
StartCoroutine(Fade(Color.clear, new Color(0, 0, 0, .95f), 1)); // 배경 이미지 페이드 인 효과 코루틴 시작
gameOverScoreUI.text = scoreUI.text; // 최종 점수 텍스트 설정
scoreUI.gameObject.SetActive(false); // 게임 화면 점수 텍스트 오브젝트 비활성화
healthBar.transform.parent.gameObject.SetActive(false); // 체력 바 오브젝트 비활성화
gameOverUI.SetActive(true); // 게임 오버 텍스트, 버튼 오브젝트 활성화
}
// ■ 새 웨이브 배너 애니메이션 코루틴
IEnumerator AnimateNewWaveBanner() {
float delayTime = 1f; // 배너 대기 시간
float speed = 2.5f; // 배너 움직이는 시간
float animatePercent = 0; // 애니메이션 실행 퍼센트 설정
int dir = 1; // 배너 이동 방향, 양수면 위, 음수면 아래쪽
float endDelayTime = Time.time + 1 / speed + delayTime; // 배너 나타난 후 대기시간 설정
while(animatePercent >= 0) { // 애니메이션 실행
animatePercent += Time.deltaTime * speed * dir; // 애니메이션 실행 퍼센트 설정
if (animatePercent >= 1) { // 애니메이션 실행 퍼센트가 1 이상인 경우
animatePercent = 1; // 1로 설정
if(Time.time > endDelayTime) { // 현재 시간이 배너가 나타나고 대기시간까지 지난 경우
dir = -1; // 방향을 음수로 설정, 아래로 이동하도록 함
}
}
newWaveBanner.anchoredPosition = Vector2.up * Mathf.Lerp(-190, 45, animatePercent); // 배너의 위치를 설정
yield return null; // 다음 프레임으로 스킵
}
}
// ■ 이미지 페이드 효과 코루틴
IEnumerator Fade(Color from, Color to, float time) {
float speed = 1 / time; // 페이드 효과 속도
float percent = 0; // 이미지 색상(투명도) 변화 퍼센트
while(percent < 1) { // 색상(투명도) 변화가 1(100%)미만일 때
percent += Time.deltaTime * speed; // 색상(투명도) 변화율 계산
fadePlane.color = Color.Lerp(from, to, percent); // 색상(투명도) 지정
yield return null; // 다음 프레임으로 건너뜀
}
}
// UI Input 부분
// ■ 새 게임 시작 메소드
public void startNewGame() {
SceneManager.LoadScene("Game"); // 게임 씬, 게임을 다시 로드
}
// ■ 메인 메뉴 이동 메소드
public void ReturnToMainMenu() {
SceneManager.LoadScene("Menu"); // 메뉴 씬, 메뉴를 로드
}
}
이제 Game Over UI 오브젝트를 비활성화 해주고, 플레이어가 사망 시 게임오버 화면이 출력되는지 확인해보자.
이제 게임에서 씬이 Menu 씬과 Game 씬으로 나뉘어져 있으므로
MusicManager 스크립트를 수정해 각 씬에서 다른 노래가 재생되도록 하자.
| MusicManager 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class MusicManager : MonoBehaviour
{
public AudioClip mainTheme; // 메인 음악
public AudioClip menuTheme; // 메뉴 음악
string sceneName; // 씬 이름
void Start() {
OnLevelWasLoaded(0); // 씬 음악 재생 메소드 호출
}
// ■ 씬 음악 재생 메소드
void OnLevelWasLoaded(int sceneIndex) {
string newSceneName = SceneManager.GetActiveScene().name; // 현재 활성화충인 씬의 이름을 가져온다.
if(sceneName != newSceneName) { // 저장해둔 씬 이름이 현재 활성화된 씬의 이름과 다르면
sceneName = newSceneName; // 씬 이름을 다시 저장
Invoke("PlayMusic", .2f); // PlayMusic 메소드를 0.2초 후 호출
}
}
// ■ 음악 재생 메소드
void PlayMusic() {
AudioClip clipToPlay = null; // 재생할 클립을 생성
if(sceneName == "Menu") { // 현재 씬이 메뉴 씬인 경우
clipToPlay = menuTheme; // 재생할 클립을 메뉴 음악으로 설정
}
else if(sceneName == "Game") { // 현재 씬이 게임 씬인 경우
clipToPlay = mainTheme; // 재생할 클립을 메인 음악으로 설정
}
if(clipToPlay != null) { // 재생할 클립이 있는 경우
AudioManager.instance.PlayMusic(clipToPlay, 2); // 클립(음악)재생
Invoke("PlayMusic", clipToPlay.length); // 클립(음악)의 재생시간이 지나고 PlayMusic 메소드를 호출
}
}
}
이렇게 탑다운 슈팅 게임 제작을 마무리해본다.
사실 강의대로 완성은 해놓고 여러 기능들을 추가해보고 싶어서
이것저것 시도해보다가 마무리 글쓰는것을 깜빡했다..
그리고 강의에서는 비네팅, 색 수차 효과 등 화면에 여러 효과들을 추가해보았는데
내 버전에서는 해당 패키지를 찾을 수가 없어서 패스했다..
이번에 제작한 게임을 기반으로 여러 기능들을 추가하면
요즘 유행하는 뱀서류 게임도 만들어 볼 수 있을 것 같다.
블렌더도 공부해서 이번에 만든 게임에 적용해볼 수 있도록 해야겠다.
'Unity > 게임개발' 카테고리의 다른 글
[Unity] 게임 개발 일지 | 탑다운 슈팅 24 (0) | 2023.12.19 |
---|---|
[Unity] 게임 개발 일지 | 탑다운 슈팅 23 (0) | 2023.12.15 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 22 (0) | 2023.12.14 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 21 (0) | 2023.12.13 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 20 (1) | 2023.12.12 |