안녕하세요.
이번 작업은 크게 5가지를 해주었습니다.
1. 텔레포트 작업
2. 기본 공격 및 스킬의 연출효과
3. 매니저를 이용한 전체 시스템 메시지 및 흐름 관리
4. 플레이어 to 포탑 공격
5. 아이콘 해제
먼저 텔레포트 작업입니다.
PlayerTeleport 스크립트입니다.
using UnityEngine;
using UnityEngine.AI;
public class PlayerTeleport : MonoBehaviour
{
private Vector3 initialPosition;
private Vector3 returnStartPosition;
[SerializeField] private Animator animator;
[SerializeField] private NavMeshAgent agent;
[SerializeField] private GameObject returnParticle;
private bool isReturning = false;
void Start()
{
initialPosition = transform.position;
}
void Update()
{
if (isReturning)
{
float moved = Vector3.Distance(returnStartPosition, transform.position);
if (moved > 0.1f)
{
CancelReturn();
}
}
if (Input.GetKeyDown(KeyCode.B) && !isReturning)
{
returnStartPosition = transform.position;
returnParticle.SetActive(true);
isReturning = true;
animator.SetTrigger("IsReturn");
}
}
public void OnReturnAnimationEnd()
{
if (!isReturning) return;
returnParticle.SetActive(false);
agent.Warp(initialPosition);
isReturning = false;
}
private void CancelReturn()
{
returnParticle.SetActive(false);
isReturning = false;
animator.Play("IdleToRun", 0, 0f);
}
}
핵심 부분을 빠르게 살펴보자면.
void Update()
{
if (isReturning)
{
float moved = Vector3.Distance(returnStartPosition, transform.position);
if (moved > 0.1f)
{
CancelReturn();
}
}
if (Input.GetKeyDown(KeyCode.B) && !isReturning)
{
returnStartPosition = transform.position;
returnParticle.SetActive(true);
isReturning = true;
animator.SetTrigger("IsReturn");
}
}
Update에서 Input.GetKeyDown() 을 이용해 KeyCode.B가 눌리면
파티클 활성화와 애니메이션 등 관련 작업을 실행시켜주었습니다.
또한 Vector3의 Distance를 이용해 B 키를 누른 위치와
플레이어의 현재 위치가 0.1f보다 크다면 이동한 것으로 판단하여
CancleReturn 함수를 실행하게 해 주었습니다.
public void OnReturnAnimationEnd()
{
if (!isReturning) return;
returnParticle.SetActive(false);
agent.Warp(initialPosition);
isReturning = false;
}
private void CancelReturn()
{
returnParticle.SetActive(false);
isReturning = false;
animator.Play("IdleToRun", 0, 0f);
}
귀환이 완료되면 실행될 OnReturnAnimationEnd() 함수는 단순합니다.
NavMeshAgent를 이용하여 플레이어의 이동을 하고 있기에
NavMeshAgent의 Warp 함수를 사용하여 귀환 위치로 이동시켜 주었고
CancleReturn() 함수는 귀환 도중 끊겼을 때 즉시 애니메이션 전환과 파티클을 비활성화해주었습니다.
다음은 기본공격 및 스킬의 연출효과 강화입니다.
기존의 기본 도형으로 사용되던 오브젝트의 Mesh Renderer를 꺼준 뒤
자식으로 파티클을 지정해 주었습니다.
다음은 GameManager 스크립트입니다.
using UnityEngine;
public class GameManager : MonoBehaviour
{
public enum GameState
{
Ready, Play, Pause, Stop
}
public GameState gameState = GameState.Ready;
public static GameManager Instance = null;
void Awake()
{
Instance = this;
}
}
enum을 활용하여 Ready, Play, Pause, Stop을 지정해 주었으며
Enemy가 Spawn 되는 EnemySpawn 스크립트에서
private IEnumerator SpawnEnemies()
{
while (true)
{
if (GameManager.Instance.gameState == GameManager.GameState.Ready)
{
yield return null;
continue;
}
현재 게임의 상태가 Ready 상태이면 생성이 안되게 Continue로 제어를 해주었습니다.
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class SystemMessage : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI textMeshProUGUI;
[SerializeField] private GameObject systemMessageObj;
// 0초
private string messageStep1 = "소환사의 협곡에 오신것을 환영합니다";
// 30초
private string messageStep2 = "미니언 생성까지 30초 남았습니다";
// 60초
private string messageStep3 = "미니언이 생성되었습니다";
private byte currentStep = 0;
[SerializeField] private float messageOffTime = 5.0f;
public void SystemMessagePopup()
{
if (currentStep == 0)
{
textMeshProUGUI.text = messageStep1;
systemMessageObj.SetActive(true);
currentStep++;
StartCoroutine(MessageOff());
}
else if (currentStep == 1)
{
textMeshProUGUI.text = messageStep2;
systemMessageObj.SetActive(true);
currentStep++;
StartCoroutine(MessageOff());
}
else if (currentStep == 2)
{
GameManager.Instance.gameState = GameManager.GameState.Play;
textMeshProUGUI.text = messageStep3;
systemMessageObj.SetActive(true);
currentStep++;
StartCoroutine(MessageOff());
}
}
IEnumerator MessageOff()
{
yield return new WaitForSeconds(messageOffTime);
systemMessageObj.SetActive(false);
}
}
다음은 SystemMessage를 만들어 주었습니다.
string 타입으로 text를 미리 지정해 주었으며,
public void SystemMessagePopup()
{
if (currentStep == 0)
{
textMeshProUGUI.text = messageStep1;
systemMessageObj.SetActive(true);
currentStep++;
StartCoroutine(MessageOff());
}
else if (currentStep == 1)
{
textMeshProUGUI.text = messageStep2;
systemMessageObj.SetActive(true);
currentStep++;
StartCoroutine(MessageOff());
}
else if (currentStep == 2)
{
GameManager.Instance.gameState = GameManager.GameState.Play;
textMeshProUGUI.text = messageStep3;
systemMessageObj.SetActive(true);
currentStep++;
StartCoroutine(MessageOff());
}
}
특이사항으로 GameManager의 gameState를 Play로 변경해 주어 미니언이 Spawn 되게 해주었습니다.
또한
currentStep 변수를 증가시켜 주어
각 다른 조건문이 실행되게 해 주었습니다.
IEnumerator MessageOff()
{
yield return new WaitForSeconds(messageOffTime);
systemMessageObj.SetActive(false);
}
다음 실행되는 코루틴은 지정 시간 지연 후 시스템 메시지를 비활성화해주는 코드입니다.
이 함수가 실행되는 곳은
기존에 게임의 흐른 시간을 나타내는 ElapsedTimeDisplay 스크립트에서 작업을 해주었습니다.
using TMPro;
using UnityEngine;
public class ElapsedTimeDisplay : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI elapsedText;
private float elapsedTime = 0f;
private float nextPopupTime = 0f;
[SerializeField] private SystemMessage systemMessage;
public void ElapsedResult()
{
elapsedTime += Time.deltaTime;
int minutes = Mathf.FloorToInt(elapsedTime / 60f);
int seconds = Mathf.FloorToInt(elapsedTime % 60f);
elapsedText.text = string.Format("{0:00}:{1:00}", minutes, seconds);
if (elapsedTime >= nextPopupTime)
{
systemMessage.SystemMessagePopup();
nextPopupTime += 30f;
}
}
}
현재 흐른 시간인 elapsedTime을 판단하여 주었으며
각 0초, 30초, 60초에 함수가 실행됩니다.
다음으로는 플레이어가 포탑을 공격하는 작업을 해주었습니다.
PlayerSkillQ를 예로 들면
public class PlayerSkillQ : MonoBehaviour
{
[SerializeField] private PlayerSkillController playerSkillController;
[SerializeField] private PlayerStats playerStats;
void OnTriggerEnter(Collider other)
{
if (other.tag == "Enumy")
{
EnemyStats enumyStats = other.gameObject.GetComponent<EnemyStats>();
enumyStats?.TakeDamage(playerStats.playerDamage * playerStats.playerSkillQDamage);
playerSkillController.ResetSkillObject();
}
if (other.tag == "Golem")
{
GolemAI golemAI = other.gameObject.GetComponent<GolemAI>();
golemAI?.TakeDamage(playerStats.playerDamage * playerStats.playerSkillQDamage);
playerSkillController.ResetSkillObject();
}
if (other.tag == "TurretEnemy")
{
TurretStats turretStat = other.gameObject.GetComponent<TurretStats>();
turretStat?.TakeDamage(playerStats.playerDamage * playerStats.playerSkillQDamage);
}
}
}
이 스킬 오브젝트의 TriggerEnter가 된 other의 태그가 "TurretEnemy" 일 때를 추가해 주었습니다.
또한 기본 플레이어가 기본 공격을 할 때에도
private void TryAttack()
{
if (Time.time >= lastAttackTime + attackCooldown)
{
lastAttackTime = Time.time;
if (currentTarget.tag == minionTag)
{
currentTarget.GetComponent<EnemyStats>().TakeDamageEnemy(enemyStats.enemyDamage);
}
if (currentTarget.tag == turretTag)
{
currentTarget.GetComponent<TurretStats>().TakeDamage(enemyStats.enemyDamage);
}
enumyAnim.SetTrigger("IsAttack");
}
}
위와 같이 turret의 태그를 검사해 TakeDamage 함수를 실행시켜
대미지를 주도록 작업했습니다.
마지막으로 포탑이 사라질 때, 아이콘 제거 부분도 추가해 주었습니다.
TurretDestory 스크립트를 만들어 주었으며, 다음과 같이 작업을 했습니다.
using UnityEngine;
using UnityEngine.UI;
public class TurretDestory : MonoBehaviour
{
[SerializeField] private Image enemyTurret01;
[SerializeField] private Image enemyTurret02;
[SerializeField] private Image enemyTurret03;
[SerializeField] private Image Turret01;
[SerializeField] private Image Turret02;
[SerializeField] private Image Turret03;
[SerializeField] private Sprite destoryTurretIcon;
public void EnemyTurretDestoryIcon(int number)
{
if (number == 1)
{
enemyTurret01.sprite = destoryTurretIcon;
}
else if (number == 2)
{
enemyTurret02.sprite = destoryTurretIcon;
}
else if (number == 3)
{
enemyTurret03.sprite = destoryTurretIcon;
}
}
public void TurretDestoryIcon(int number)
{
if (number == 1)
{
Turret01.sprite = destoryTurretIcon;
}
else if (number == 2)
{
Turret02.sprite = destoryTurretIcon;
}
else if (number == 3)
{
Turret03.sprite = destoryTurretIcon;
}
}
}
이 함수가 실행되는 곳은 Turret의 Hp가 0이 되어 Destory로 인해 사라질 때입니다.
using UnityEngine;
public class TurretStats : MonoBehaviour
{
public float damage = 5.0f;
public float turretMaxHp = 200.0f;
public float turretHp = 0f;
[SerializeField] private TurretHpBar turretHpBar;
[SerializeField] private TurretDestory turretDestory;
[SerializeField] private string classification = "";
[SerializeField] private int number;
void Start()
{
turretMaxHp = turretHp;
}
public void TakeDamage(float damage)
{
turretHp -= damage;
turretHpBar.HpVarUpdate(turretHp, turretMaxHp);
if (turretHp <= 0)
{
if (classification == "EnemyTurret")
{
turretDestory.EnemyTurretDestoryIcon(number);
}
else if (classification == "Turret")
{
turretDestory.TurretDestoryIcon(number);
}
Destroy(this.gameObject);
}
}
}
구분은 string이 EnemyTurret / Turret 두 가지로 나눴습니다.
마지막으로 시스템 메시지의 간격을 30초 -> 10초로 변경하고,
아군의 미니니언만 대미지를 올려서
촬영된 영상을 보는 것으로 마무리를 하겠습니다.
맨날 집에 오면 9시~10시 여서 작업을 할 시간이 많이 부족했는데,
드디어 주말입니다!
내일은 이어서 디버깅 및 개선사항들과 함께
플레이어 죽음 관련, 그리고 승리 조건까지를 목표로 잡아보도록 하겠습니다.
감사합니다.😁
모두들 한 주 고생하셨고
즐거운 주말 보내세요!