타워 디펜스 게임 (3) - UI, 타워 설치
8. UI
게임 플레이 화면보다 위에 있는 오버레이 캔버스 UI를 생성하고
플레이에 필요한 표시, 버튼을 배치
상단: 플레이어 라이프, 일시정지 버튼
하단: 상점(타워 구매),
왼쪽: 코스트(골드),
오른쪽: 다음 웨이브까지의 시간
using UnityEngine;
using TMPro;
using System.Collections;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class UIManager : MonoBehaviour
{
TextMeshProUGUI playerLifeUI;
TextMeshProUGUI GameOverRoundUI;
TextMeshProUGUI levelUI;
GameObject GameOver;
GameObject Menu;
GameObject LevelClear;
FadeManager fm;
bool isGameOverActive;
bool isLevelClearedActive;
// 버튼 동작 중 추가 입력 방지
// -> 주로 씬 전환이 페이드 인 효과 시작 후 2초 뒤에 이루어지므로 그 사이 추가 입력이 들어가면 먹통이 됨
// -> 씬 전환하려고 기다리는 중 추가 입력을 받지 못하게 하기 위함
bool isSceneChangeActived;
int nextLevel = PlayerStatus.curretStage + 1;
// Start is called before the first frame update
void Start()
{
// 자식 오브젝트 중 비활성화 상태를 포함한 필요한 오브젝트를 찾음
GameOver = transform.Find("GameOver").gameObject;
Menu = transform.Find("Menu").gameObject;
LevelClear = transform.Find("LevelClear").gameObject;
// 비활성화 된 오브젝트까지 포함해 필요한 TMP를 가져옴
TextMeshProUGUI[] allUI = FindObjectsOfType<TextMeshProUGUI>(true);
for (int i = 0; i < allUI.Length; i++)
{
if (allUI[i].name.Contains("Life"))
playerLifeUI = allUI[i];
else if (allUI[i].name.Contains("Round"))
GameOverRoundUI = allUI[i];
else if (allUI[i].name.Contains("Level"))
levelUI = allUI[i];
}
fm = GameObject.Find("FadeCanvas").GetComponent<FadeManager>();
// 시작과 동시에 현재 라이프를 UI에 갱신
playerLifeUI.text = $"<sprite=0> {PlayerStatus.GetLife()}";
isGameOverActive = false;
isLevelClearedActive = false;
isSceneChangeActived = false;
}
// Update is called once per frame
void Update()
{
// 게임 오버가 될 경우 한번만
if (!GameManager.isPlayerAlive && !isGameOverActive)
{
// 게임 오버 패널 활성화
GameOver.SetActive(true);
// 해당 if문 내의 구문 여러 번 호출 방지
isGameOverActive = true;
// 현재 라운드를 받아와서 표시
GameOverRoundUI.text = $"Round : {PlayerStatus.GetRound()}";
}
// 레벨 클리어할 경우 1번만
if(GameManager.isLevelCleared && !isLevelClearedActive)
{
// 레벨 클리어 패널 활성화
LevelClear.SetActive(true);
// 클리어한 스테이지 표시
levelUI.text = $"LEVEL : {PlayerStatus.curretStage}";
// 클라이언트에 클리어한 스테이지 저장
PlayerPrefs.SetInt("maxClearedStage", PlayerStatus.curretStage);
// 마지막 스테이지 클리어 시 Continue 버튼 비활성화
if (PlayerStatus.curretStage.Equals(PlayerStatus.maxStage))
LevelClear.GetComponentsInChildren<Button>()[0].interactable = false;
// 마지막 스테이지가 아니고 Continue 버튼이 비활성화 상태라면 (마지막 스테이지 클리어 후 이전 난이도 클리어할 경우 컨티뉴 버튼을 활성화 해줘야 한다)
else if (!LevelClear.GetComponentsInChildren<Button>()[0].interactable)
LevelClear.GetComponentsInChildren<Button>()[0].interactable = true;
// 해당 if문 내의 구문 여러 번 호출 방지
isLevelClearedActive = true;
}
// 라이프 변화가 있을 경우 한번만 UI에 표시
if (PlayerStatus.GetLifeChanged())
{
playerLifeUI.text = $"<sprite=0> {PlayerStatus.GetLife()}";
PlayerStatus.SetLifeChanged();
}
// ESC 키 입력 시 일시 정지 / 재개
if(Input.GetKeyDown(KeyCode.Escape))
PauseORcontinuePressed();
}
// 게임 플레이 도중 일시 정지 버튼 or PAUSE 상태에서 Continue 버튼을 눌렀을 때
public void PauseORcontinuePressed()
{
if (isSceneChangeActived)
return;
// 게임 정지, 메뉴창 팝업 or 정상 속도, 메뉴창 닫기 토글
if (Menu.activeSelf)
{
Menu.SetActive(false);
Time.timeScale = 1.0f;
}
else
{
Menu.SetActive(true);
Time.timeScale = 0.0f;
}
}
// 재도전 버튼(일시 정지 화면 or 게임 오버 화면)
public void RetryButtenPressed()
{
// 버튼 중복 입력 방지 처리
if (isSceneChangeActived)
return;
isSceneChangeActived = true;
// 시간이 정지해 있다면 원래 속도로 돌려주고
if (Time.timeScale == 0f)
Time.timeScale = 1.0f;
// 필요한 파라미터 초기화
PlayerStatus.Initialize();
string sceneNameToLoad = SceneManager.GetActiveScene().name;
// 페이드 아웃 효과 후 해당 레벨 씬 다시 로드
StartCoroutine(LoadScene(sceneNameToLoad));
}
// Menu 버튼 (메인 메뉴 화면으로)
public void MenuButtonPressed()
{
// 버튼 중복 입력 방지 처리
if (isSceneChangeActived)
return;
isSceneChangeActived = true;
// 메뉴창이 열려있을 경우 정상 속도, 메뉴창 닫기
if (Menu.activeSelf)
{
Menu.SetActive(false);
Time.timeScale = 1.0f;
}
// 필요한 파라미터 초기화
PlayerStatus.Initialize();
string sceneNameToLoad = "MainMenuScene";
// 페이드 아웃 효과 후 메인 메뉴로
StartCoroutine(LoadScene(sceneNameToLoad));
}
// Continue : 다음 스테이지 버튼 클릭 시
public void ContinueButtonPressed()
{
// 버튼 중복 입력 방지 처리
if (isSceneChangeActived)
return;
isSceneChangeActived = true;
PlayerStatus.curretStage = nextLevel;
PlayerStatus.Initialize();
string sceneNameToLoad;
// 100 이상의 스테이지에 대하여 Level 뒤에 스테이지를 그대로 붙임
if (nextLevel / 100 !=0)
sceneNameToLoad = $"Level{nextLevel}";
// 10~99 (두 자리 정수) 스테이지에 대하여 Level0 뒤에 스테이지를 붙임
else if(nextLevel / 10 != 0)
sceneNameToLoad = $"Level0{nextLevel}";
// 1~9 (1 자리 정수) 스테이지에 대하여 Level00 뒤에 스테이지를 붙임
else
sceneNameToLoad = $"Level00{nextLevel}";
StartCoroutine(LoadScene(sceneNameToLoad));
}
IEnumerator LoadScene(string sceneName)
{
// 페이드 아웃 효과
StartCoroutine(fm.FadeInOut());
// 페이드 아웃 효과를 보여주기 위해 다음 씬 로드 전에 딜레이를 줌
const float delayForSceneChange = 2f;
yield return new WaitForSeconds(delayForSceneChange);
SceneManager.LoadScene(sceneName);
}
}
해당 스크립트는 OverlayCanvas 오브젝트에 붙어 있으며
UI 각 요소에 접근할 수 있게끔 UI 오브젝트 요소에 대한 지칭, 초기화
각 버튼을 눌렀을 때 동작하는 내용들에 대한 메서드를 가지고 있습니다.
버튼 종류 : 일시정지, 재개, 처음부터 재시작, 타이틀로 나가기
이를 구현하기 위해 타임스케일, 씬 불러오기, 씬 전환 전에 페이드아웃(점점 화면이 검게 됨) 을 사용하였습니다.
페이드 아웃은 아래와 같이 구현했습니다.
using System.Collections;
using UnityEngine;
public class FadeManager : MonoBehaviour
{
Animator Fade;
bool isFadeOut;
// Start is called before the first frame update
void Start()
{
isFadeOut = false;
Fade = transform.Find("Fade").GetComponent<Animator>();
// 씬 로드 직후 자동으로 페이드 인 효과
StartCoroutine(FadeInOut());
}
// 화면 효과 : 페이드 인, 아웃
public IEnumerator FadeInOut()
{
// 페이드 이미지 활성화
if(!Fade.gameObject.activeSelf)
Fade.gameObject.SetActive(true);
// 홀수 번째 호출 시 : Fade In (해당 씬 불러올 때)
if (!isFadeOut)
{
// 2초간 페이드 인 이펙트를 보여주고
yield return new WaitForSeconds(2f);
// 두 번째 실행부터 메인 메뉴로 가기 때문에 첫 실행 마지막에 true로 바꿔줌
isFadeOut = true;
// 해당 이미지는 비활성화
Fade.gameObject.SetActive(false);
}
// 짝수 번째 호출 시 : Fade Out
else
{
// 다음 호출 시 Fade In
isFadeOut = false;
// SetFloat을 1f로 바꿔두어도 다음 함수 호출 때는 0이 되어있음 그러니 2번째 호출 직후 1로 변경해야 제대로 동작
Fade.SetFloat("isFadeOut", 1f);
yield return null;
}
}
}
9. 타워 설치
설치에 필요한 절차는 아래와 같습니다.
타워 선택 버튼 터치 >> 설치에 필요한 코스트가 충분한가? >> 충분하면 선택 >> 지정한 장소가 설치할 장소가 맞는가? >> 설치 가능한 타일 위라면 설치
1) 상점 UI
using UnityEngine;
using TMPro;
public class Shop : MonoBehaviour
{
// 모든 타워 최대 업그레이드 단계 지정
public const int MaxUpgrade = 3;
// [SerializeField] : [Serializable]을 써서 선언한 클래스 혹은 구조체를 인스펙터 창에서 볼 수 있게 됨
//[SerializeField]
public TurretBlueprint BasicTurret;
public TurretBlueprint MissileLauncher;
public TurretBlueprint LaserBeamer;
public TextMeshProUGUI Gold;
private void Start()
{
// 모든 터렛, 업그레이드 단계별 가격 지정
BasicTurret.price = new int[Shop.MaxUpgrade] { 100, 150, 200 };
MissileLauncher.price = new int[Shop.MaxUpgrade] { 250, 300, 350 };
LaserBeamer.price = new int[Shop.MaxUpgrade] { 350, 400, 450 };
}
void Update()
{
Gold.text = $"G : {PlayerStatus.GetGold()}";
}
// 각 터렛 모양의 버튼을 누를 경우 해당하는 메서드 호출
// 설치할 터렛 변수에 선택한 것과 같은 터렛의 프리팹, 가격 정보가 들어있는 블루프린트를 할당
public void BuyBasicTurret()
{
BuildManager.BMInstance.SetTurretToInstall(BasicTurret);
}
public void BuyMissileLauncher()
{
BuildManager.BMInstance.SetTurretToInstall(MissileLauncher);
}
public void BuyLaserBeamer()
{
BuildManager.BMInstance.SetTurretToInstall(LaserBeamer);
}
}
제일 아래 3가지 메서드는 각각의 타워 구매 버튼에 할당하였습니다.
유저가 만든 클래스는 일반적으로 인스펙터에서 매개변수로 출현하지 않기에 메서드를 각각 나눠 썼습니다.
2) 타일
using UnityEngine;
using UnityEngine.EventSystems;
public class Tiles : MonoBehaviour
{
// 랜더러 컴포넌트를 통해 메터리얼에 접근
Renderer render;
// 마우스가 올라갔을 때 변할 색상 -> 인스펙터 창에서 컬러 픽커 창으로 설정 가능함
//public Color hoverColor;
// 기존 색상을 저장할 변수
//Color originColor;
// 기존 재료를 저장할 변수
Material originMaterial;
// 패턴이 있는 재료를 받아올 변수
public Material hoverMaterial;
public Material notEnoughMoney;
GameObject turret;
BuildManager bm;
public TurretBlueprint tb;
// 배치하면 타워가 타일에 파뭍혀서 위로 올려줄 오프셋 지정
float offset = 0.5f;
// 해당 타일에 설치된 터렛 레벨
int turretLV;
private void Start()
{
// 레벨은 0부터
turretLV = 0;
// 트랜스폼을 통해 랜더러 인스턴스 생성
render = transform.GetComponent<Renderer>();
// 기존 메터리얼을 할당
originMaterial = render.material;
bm = BuildManager.BMInstance;
turret = null;
}
public GameObject GetInstalledTurret() => turret;
// 마우스가 해당 오브젝트 위에 있을 때
private void OnMouseOver()
{
// 선택한 터렛이 있고 UI위에서 마우스 입력이 일어나지 않은 경우, 마우스를 타일 위로 가져가면 해당 타일 패턴(메터리얼) 변경
if (bm.GetTurretToInstall() != null && EventSystem.current.IsPointerOverGameObject() == false)
{
// 소지금이 충분하다면, 설치 가능 표시. 아닐 경우 설치 불가 표시
if(bm.IsGoldEnough(0))
render.material = hoverMaterial;
else
render.material = notEnoughMoney;
}
if(bm.GetTurretToInstall() == null)
render.material = originMaterial;
}
// 마우스가 해당 오브젝트 위치에서 벗어났을 때
private void OnMouseExit()
{
// 원래 재질로 돌아옴
render.material = originMaterial;
}
// 마우스 좌클릭하면
private void OnMouseDown()
{
// UI위에서 마우스 입력이 일어나지 않은 경우
if (EventSystem.current.IsPointerOverGameObject() == false)
{
// 선택한 타일을 저장
bm.TileforUI = this;
// 선택한 터렛이 없다면
if (bm.GetTurretToInstall() == null)
{
Debug.Log("설치할 터렛이 없습니다");
}
// 선택한 터렛이 있고, 해당 타일 위에 터렛이 없다면
else if (turret == null)
{
// 설치한 터렛의 블루프린트를 할당
tb = bm.GetTurretToInstall();
// 골드가 충분하다면 설치
InstallTurret(turretLV, transform.position);
}
}
}
public void InstallTurret(int LV, Vector3 InstallLocation)
{
// 설치된 터렛이 있다면 터렛블루프린트를 먼저 설정해야 함
if (turret != null)
bm.SetTurretToInstall(tb);
// 설치할 골드가 충분하다면
if (bm.IsGoldEnough(LV))
{
// 이번 유저 입력에 타워를 설치했음을 알리고
bm.turretInstalled = true;
// 설치에 필요한 골드 차감
PlayerStatus.SetGold(PlayerStatus.GetGold() - bm.GetTurretToInstall().price[LV]);
// 타워 설치 이펙트 호출
EffectManager.NeedEffect((int)EffectManager.effectIndex.InstallEffect, InstallLocation + Vector3.up * offset);
// 터렛이 없다면, 설치하고 turret에 할당
if (turret == null)
{
turret = Instantiate(bm.GetTurretToInstall().prefab, InstallLocation + Vector3.up * offset, Quaternion.identity);
turret.transform.SetParent(transform);
}
// 터렛을 업그레이드했다면
else
// 타워 레벨 변경을 알림 -> 타워, 불릿의 외형, 스테이터스 변경
turret.GetComponent<Turret>().isUpgraded = true;
// 최대 레벨 미만인 경우 터렛의 레벨을 올려줌
if (turretLV < Shop.MaxUpgrade)
++turretLV;
// 설치 후 선택중인 타워 해제
bm.SetTurretToInstall(null);
}
else
{
bm.SetTurretToInstall(null);
Debug.Log("Gold 부족");
}
}
// 현재 터렛에 지금까지 들어간 골드 계산 -> 절반이 판매 가격
public int SellPrice()
{
int sum = 0;
for (int i = 0; i < turretLV; i++)
{
sum += tb.price[i];
}
return sum/2;
}
public void SellTurret()
{
// 반값 돌려줌
PlayerStatus.SetGold(PlayerStatus.GetGold() + SellPrice());
// 터렛 파괴
Destroy(turret);
// 다음 터렛 설치를 위해 Tiles.cs 의 변수 초기화
turret = null;
tb = null;
turretLV = 0;
}
public int GetTurretLV() => turretLV;
}
설치 가능/불가 판정 알림을 위한 타일 색상 교체
해당 타일 위에 터렛 설치
타일을 터치하여 설치되어 있던 터렛 판매 / 업그레이드
3) 설치 매니저
설치 과정에 관여
using UnityEngine;
public class BuildManager : MonoBehaviour
{
// 자기 자신을 메모리에 올려주기 위해 타입 선언
public static BuildManager BMInstance;
// 설치할 터렛 : 버튼 클릭으로 바꿈
private TurretBlueprint TurretToInstall = null;
// 타일을 선택했을 때 그 타일 오브젝트를 저장
Tiles SelectedTile;
bool isturretInstalled;
// 각각 투명, 반투명에 쓰이는 쉐이더의 경로 및 명칭
string shaderTransparent = "UI/Lit/Transparent";
// 모든 터렛 구성 메터리얼 색상의 알파값
float transparentAlpha = 0.5f;
GameObject preInstall;
private void Awake()
{
// BuildManager 자신을 메모리에 올려주기
if(BMInstance != null)
{
Destroy(gameObject);
return;
}
BMInstance = this;
DontDestroyOnLoad(gameObject);
// 변수들 초기화
SelectedTile = null;
TileforUI = null;
turretInstalled = false;
preInstall = null;
}
private void Update()
{
// 반투명하게 생성한 타워가 있다면
if(preInstall!=null)
{
// 반투명 타워 오브젝트가 마우스 커서 위치를 따라다니게
Vector3 mousePos = new Vector3(Input.mousePosition.x, Input.mousePosition.y, -Camera.main.transform.position.z-10f);
preInstall.transform.position = Camera.main.ScreenToWorldPoint(mousePos);
}
// 마우스 우클릭 시, 선택한 타워 해제
if (Input.GetKeyDown(KeyCode.Mouse1))
SetTurretToInstall(null);
}
public TurretBlueprint GetTurretToInstall()
{
return TurretToInstall;
}
// 기존에는 상점에서 터렛 버튼 클릭 시, 설치할 터렛의 정보만 넘겨주었지만
// 터렛의 프리팹을 미리 반투명하게 생성하고 정보도 넘겨주고 이후 해당 반투명 오브젝트가 마우스를 따라다니게끔 위치 변경
// 플레이어가 좀 더 직관적으로 게임 상황을 파악할 수 있음
public void SetTurretToInstall(TurretBlueprint turret)
{
TurretToInstall = turret;
// 상점에서 터렛을 선택했을 때
if (TurretToInstall != null)
{
// 해당 터렛 생성
preInstall = Instantiate(TurretToInstall.prefab);
// 생성한 터렛의 Turret 스크립트를 막고
preInstall.GetComponent<Turret>().enabled = false;
// 생성한 터렛의 자식 오브젝트을 받아옴
Transform[] turretComponents = preInstall.GetComponentsInChildren<Transform>();
float colorOffset = 0.1f;
// 모든 자식 오브젝트들에 대해
for (int i = 0; i < turretComponents.Length; i++)
{
// MeshRenderer가 있는 오브젝트만
if (turretComponents[i].TryGetComponent(out MeshRenderer MR))
{
// 모든 메테리얼을 받아와서 색상, 투명도 조절
Material[] MRmaterials = MR.materials;
for (int j = 0; j < MRmaterials.Length; j++)
{
// colorOffset을 이용하여 조금 더 어둡게, transparentAlpha를 이용하여 반투명하게
MRmaterials[j].color = new Color(MRmaterials[j].color.r - colorOffset, MRmaterials[j].color.g - colorOffset, MRmaterials[j].color.b - colorOffset, transparentAlpha);
MRmaterials[j].shader = Shader.Find(shaderTransparent);
}
}
}
}
// TurretToInstall이 null이 들어오는 때는 선택 해제 및 반투명 터렛 파괴
else
Destroy(preInstall);
}
public bool IsGoldEnough(int Lv)
{
return PlayerStatus.GetGold() >= TurretToInstall.price[Lv];
}
// 타일이 선택될 때 해당 타일을 가져와서 UI표시에 씀
public Tiles TileforUI
{
get => SelectedTile;
set => SelectedTile = value;
}
// 터렛을 설치했을 때 true -> TileUI에서 true일 경우 UI를 표시하지 않고 false로 만듦
public bool turretInstalled
{
get => isturretInstalled;
set => isturretInstalled = value;
}
}