
시간 제한을 구현한 이유는 보스 별로 회전을 패턴화 시키고 싶은데 시간 제한이 없으면 계속 대기하다가 편할 때만 진행하는 그런 플레이 방식을 제한하고 싶었다.
그래서 보스한테 시간제한이 있는게 좋지 않을까 하는 생각에 시간제한을 구현했다.

굳이 Canvas > Image로 Time Circle로 구현한 이유는 Image 속성 중에
Filled Type을 이용하면 쉽게 시간 표현을 해줄 수 있다.
일단 간단히 작성해놓고 한 번 어떻게 작동하는지 보자.
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using Sirenix.OdinInspector;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;
public class Boss : Target
{
[TabGroup("Boss","Settings")] public string bossName;
[TabGroup("Boss","Settings")] public float timeLimitSeconds;
[TabGroup("Boss","Settings")] public GameObject timeCircle;
private void OnEnable() {
Events.OnBossSpawn += OnBossSpawn;
Events.OnBossDestroy += OnBossDestroy;
}
private void OnDisable() {
Events.OnBossSpawn -= OnBossSpawn;
Events.OnBossDestroy -= OnBossDestroy;
}
private void OnBossSpawn() {
StartCoroutine(StartTimeCircle());
}
private void OnBossDestroy() {
DOTween.Kill(timeCircle);
StopCoroutine(StartTimeCircle());
timeCircle.SetActive(false);
}
private IEnumerator StartTimeCircle() {
yield return new WaitForSeconds(20f);
timeCircle.gameObject.SetActive(true);
timeCircle.GetComponent<Image>().DOFillAmount(0f, timeLimitSeconds).SetEase(Ease.Linear)
.OnComplete(()=> {
Debug.Log("Time Over!");
});
}
}
근데 회전이 작동을 안 한다.. 생각해보니깐 Target을 상속받아서 Boss 스크립트에서 사용하는데 Boss에서 Start 문을 써버리니깐 Target의 Start문은 호출되지 않는다. 그래서 명시적으로 부모 클래스의 Start문을 호출해줘야 한다.
private void Start() {
base.Start();
timeCircle.SetActive(false);
}
이렇게 해서 쉽게 고쳐질 줄 알았으나..

당연히 쉽게 갈 일이 없다. Boss 클래스에서 Start를 작성하면 기본 클래스인 Target의 Start와 이름이 동일하기 때문에 메서드를 숨긴다고 한다. 이름을 바꾸고 싶어도 유니티 메소드인 Start를 내가 바꿀 순 없다!
그래서 해결한 방법이 override 키워드를 사용했다. Target 클래스의 start를 virtual로 선언하고 Boss 클래스의 Start를 override키워드로 다시 오버라이드 해줬다. 이 방법을 통해 Boss 클래스에서 Start 메소드를 명시적으로 재정의 할 수 있다.
protected virtual void Start() {
rotateCoroutine = StartCoroutine(RotateTargetObject());
StartTargetAnimation();
}
protected override void Start() {
base.Start();
timeCircle.SetActive(false);
}
자식 클래스에서 메소드를 오버라이드를 하고 싶으면 해당 메소드를
abstract또는virtual로 선언해줘야 한다. 물론 두 키워드를 붙이지 않아도 재정의하여 사용할 수 있지만, 명시적으로 키워드를 사용하는 것과 그 동작이 다르다. 자세한 내용은 TIL(12)를 참고하자.
실행하고 TimeCircle이 자동으로 Target 생성 위치로 초기화 되도록 해보자. UI의 캔버스 상의 오브젝트를 게임 오브젝트의 월드 좌표 상으로 옮기는 작업은 아래와 같다.
// 월드 좌표를 화면 좌표로 변환
Vector2 screenPoint = Camera.main.WorldToScreenPoint(worldPosition);
// 화면 좌표를 캔버스의 로컬 좌표로 변환
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.GetComponent<RectTransform>(), // 캔버스의 RectTransform
screenPoint,
canvas.worldCamera, // 화면 좌표계와 일치하도록 카메라 설정
out Vector2 localPoint);
// RectTransform의 위치를 설정
timeCircle.anchoredPosition = localPoint + screenPoint;
따라서 기존 target에서의 초기 위치 설정을 GameManager로 옮기고 UIManager를 아래와 같이 수정했다.
private void Start() {
gameOverUI.SetActive(false);
NewBestUI.SetActive(false);
bossTimeCircle.SetActive(false);
Vector2 screenPoint = Camera.main.WorldToScreenPoint(GameManager.Instance.targetSpawnPosition);
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.GetComponent<RectTransform>(),
screenPoint,
canvas.worldCamera,
out Vector2 localPoint);
bossTimeCircle.GetComponent<RectTransform>().anchoredPosition = screenPoint + localPoint;
foreach (GameObject stageIcon in stageIcons) {
stageIconsImage.Add(stageIcon.GetComponent<Image>());
}
InitializeStageIcons();
}

이제 잘 회전한다.
완성된 최종 Boss.cs는 아래와 같다.
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.UI;
public class Boss : Target
{
[TabGroup("Boss","Settings")] public string bossName;
[TabGroup("Boss","Settings")] public float timeLimitSeconds = 10f;
[TabGroup("Boss","Settings")] public GameObject timeCircle;
private Tween timeCircleTween;
private void OnEnable() {
Events.OnBossSpawn += OnBossSpawn;
Events.OnBossDestroy += OnBossDestroy;
}
private void OnDisable() {
Events.OnBossSpawn -= OnBossSpawn;
Events.OnBossDestroy -= OnBossDestroy;
}
protected override void Start() {
timeCircle = UIManager.Instance.bossTimeCircle;
base.Start();
timeCircle.SetActive(false);
}
private void OnBossSpawn() {
StartCoroutine(StartTimeCircle());
}
private void OnBossDestroy() {
timeCircleTween.Kill();
StopCoroutine(StartTimeCircle());
timeCircle.SetActive(false);
}
private IEnumerator StartTimeCircle() {
yield return new WaitForSeconds(5f);
timeCircle.gameObject.SetActive(true);
timeCircleTween = timeCircle.GetComponent<Image>().DOFillAmount(0f, timeLimitSeconds).SetEase(Ease.Linear)
.OnComplete(()=> {
GameManager.Instance.GameOver();
});
}
}
Continue 버튼을 누르면 해당 스테이지부터 한 번까지 다시 시도가 가능하도록 구현했다.
public void ContinueGame() {
UIManager.Instance.Initialize();
UIManager.Instance.InitialzieForContinue();
StartStage();
}
public void OnContinueButton() {
Debug.Log("[GameManager.cs] OnContinueButton");
if (canContinue) {
canContinue = false;
ContinueGame();
}
else {
Debug.Log("[GameManager.cs] OnContinueButton - Can't Continue!");
}
}
public void InitialzieForContinue() {
gameOverUI.SetActive(false);
NewBestUI.SetActive(false);
bossTimeCircle.SetActive(false);
}
이게 Boss와 Target을 별도의 스크립트로 분할하고 Boss가 Target을 상속받아서 쓰도록 하고 난 이후에 발생하는 자잘한 오류를 수정해줬다.