[Jem Clash] 개발일지 #6 - 능력 쿨타임 구현
쿨타임 구현 1단계
지난 포스팅에 이어 이번엔 능력 쿨타임을 구현했습니다
어떤 형태로 구현하면 좋을지 고민하다 매니저를 만들고 안에 딕셔너리를 만들어서 key : 능력 타입, value : 쿨타임 여부로 쿨타임을 구현하기로 했어요
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CooldownManager : MonoBehaviour
{
private readonly Dictionary<UpgradeData.UpgradeType, bool> _cooldowns = new();
public static CooldownManager Instance { get; private set; }
private void Awake()
{
if (Instance == null)
Instance = this;
else
Destroy(gameObject);
}
public bool IsOnCooldown(UpgradeData.UpgradeType upgradeType)
{
if (_cooldowns.ContainsKey(upgradeType))
return _cooldowns[upgradeType];
return false;
}
public IEnumerator StartCoolDown(UpgradeData upgradeData)
{
_cooldowns[upgradeData.type] = true;
yield return new WaitForSeconds(upgradeData.cooldownTime);
_cooldowns[upgradeData.type] = false;
}
}
먼저 _cooldowns 딕셔너리를 만들고 IsOnCooldown 메서드를 추가해 줍니다
IsOnCooldown 메서드는 현재 능력이 쿨다운 중인지 아닌지 bool로 리턴해주는 함수에요
다음으로 쿨다운을 시작하는 StartCoolDown 코루틴을 만들어줍니다
코루틴을 통해 upgradeData의 cooldownTime 데이터를 가져와 그만큼 쿨타임을 가지게 하고, 해당 key 값의 value를 변경합니다
1
2
3
4
5
6
7
8
9
10
11
12
// Upgrade
private void ActivateFirework()
{
GameManager.Instance.abilityManager.GetComponent<Firework>().SetFireworkPoints();
GameManager.Instance.abilityManager.GetComponent<Firework>().SpawnFireworks();
StartCooldown();
}
private void StartCooldown()
{
StartCoroutine(CooldownManager.Instance.StartCoolDown(upgradeData));
}
이후 Upgrade 스크립트에서 StartCooldown 메서드를 다시 만들어주고, 이를 능력을 사용할 때 실행되는 메서드에 각각 추가해 주면 기본적인 쿨타임 기능은 완성입니다
이렇게 마무리가 되었으면 좋겠는데, 지금 구현한 로직은 한 가지 문제가 있습니다
버튼을 누르는 순간 바로 사용되는 능력들도 있지만, 그렇지 않고 버튼을 누른 이후 화면을 다시 클릭해야지 사용되는 능력들이 있죠
앞서 구현한 Meteor와 Gravity 능력이 그 예시입니다
따라서 이 경우에도 쿨타임이 적용되도록 코드를 추가해 줘야 합니다
쿨타임 구현 2단계
함수를 추가해 주기에 앞서, 앞으로 이런 유형의 능력이 또 있을지 모르니 인터페이스를 만들어주면 좋을 것 같습니다
1
2
3
4
5
6
public interface IStrangeAbility
{
bool IsActive { get; }
void CancelAbility();
void ActivateAbility(UpgradeData upgradeData);
}
Upgrade 스크립트 맨 아래쪽에 IStrangeAbility 인터페이스를 추가해 줍니다
IsActive와 ActivateAbility는 능력 쿨타임을 담당하고, CancelAbility는 능력을 취소할 때 사용됩니다
플레이어가 버튼을 눌러서 마우스에 능력 범위가 나타났는데, 생각이 바뀌어서 능력을 사용하고 싶지 않을 수도 있겠죠?
그런 경우를 대비해 버튼을 다시 클릭하면 능력을 취소하는 기능을 담당하는 함수가 CancelAbility입니다
코드를 자세히 들여다보기 전 이전에 구현해 놓은 함수를 살펴볼게요
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Upgrade
private void ActivateMeteor()
{
if (GameManager.Instance.abilityManager.GetComponent<Meteor>().isActive)
GameManager.Instance.abilityManager.GetComponent<Meteor>().CancelAbility();
GameManager.Instance.abilityManager.GetComponent<Meteor>().ActivateAbility(upgradeData);
}
private void ActivateGravity()
{
if (GameManager.Instance.abilityManager.GetComponent<Gravity>().IsActive)
GameManager.Instance.abilityManager.GetComponent<Gravity>().CancelAbility();
GameManager.Instance.abilityManager.GetComponent<Gravity>().ActivateAbility(upgradeData);
}
IsActive를 확인해서 true이면 CancelAbility를, false면 ActivateAbility를 실행하는 메서드들입니다
그런데 위아래 코드가 스크립트 이름을 빼면 동일한 내용이기 때문에 함수를 만들어주면 좋을 것 같네요
아래는 제네릭 메서드를 추가한 코드입니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Upgrade
private void ActivateMeteor()
{
ActivateAbility<Meteor>(upgradeData);
}
private void ActivateGravity()
{
ActivateAbility<Gravity>(upgradeData);
}
private void ActivateAbility<T>(UpgradeData data) where T : MonoBehaviour, IStrangeAbility
{
T ability = GameManager.Instance.abilityManager.GetComponent<T>();
if (ability.IsActive)
ability.CancelAbility();
ability.ActivateAbility(data);
}
이런 식으로 클래스가 MonoBehaviour, IStrangeAbility를 상속받는 경우 ActivateAbility를 실행하도록 만들었습니다
인터페이스 활용도 되고, 전보다 코드가 훨씬 깔끔해진 느낌이네요
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// Meteor
public bool IsActive { get; set; }
public void ActivateAbility(UpgradeData upgradeData)
{
if (IsActive) return;
IsActive = true;
_upgradeData = upgradeData;
if (_rangeIndicator == null)
{
_rangeIndicator = Instantiate(rangePrefab);
_rangeIndicator.transform.localPosition = new Vector3(radius, radius, 1);
}
FollowMouse();
}
public void CancelAbility()
{
IsActive = false;
if (_rangeIndicator != null)
Destroy(_rangeIndicator);
}
private void Function()
{
if (IsActive && _rangeIndicator != null) FollowMouse();
if (IsActive && Input.GetMouseButtonDown(0))
{
if (EventSystem.current.IsPointerOverGameObject()) // UI를 클릭한 경우
return;
Vector2 mousePos = GetMousePos();
StartCoroutine(DeactivateAbility(meteorDropDelay, mousePos));
CooldownManager.Instance.StartCoroutine(CooldownManager.Instance.StartCoolDown(_upgradeData));
}
}
private IEnumerator DeactivateAbility(float delay, Vector2 mousePos)
{
IsActive = false; // 비활성화해서 이후 클릭을 방지
SpriteRenderer sr = _rangeIndicator.GetComponent<SpriteRenderer>();
sr.color = Color.red;
yield return new WaitForSeconds(delay);
DealDamage(mousePos);
if (_rangeIndicator != null)
Destroy(_rangeIndicator);
}
Meteor 스크립트 내부는 이런 식으로 살짝 바뀌었습니다
기존의 _isActive 변수를 IsActive로 이름을 바꾸고, CancelAbility 메서드를 추가해 준 것 말고는 큰 변화가 없네요
Gravity 스크립트도 거의 똑같이 작동하기 때문에 변경 사항을 따로 첨부하진 않았습니다
버그 픽스
그렇게 순조로이 진행되나 싶더니…
테스트 과정에서 에러가 발생했습니다
능력 버튼을 처음 클릭하고 이후에 다시 클릭해서 취소한 다음, 화면을 클릭할 때 에러가 뜨더라고요
이미 파괴된 게임 오브젝트에 접근하려고 해서 그런 것 같은데 살펴보니 파괴된 _rangeIndicator에 접근하려고 한 것이 원인이었습니다
사실 이 변수 관련해서 언젠가 버그가 터질 것 같다고 생각했는데 이참에 고쳐야겠습니다
1
2
3
4
5
6
7
8
9
10
11
// Upgrade
private void ActivateStrangeAbility<T>(UpgradeData data) where T : MonoBehaviour, IStrangeAbility
{
T ability = GameManager.Instance.abilityManager.GetComponent<T>();
if (ability.IsActive)
ability.CancelAbility();
// else 문 추가
else
ability.ActivateAbility(data);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Meteor
private void Function()
{
if (IsActive && _rangeIndicator != null) FollowMouse();
if (IsActive && Input.GetMouseButtonDown(0))
{
if (EventSystem.current.IsPointerOverGameObject()) // UI를 클릭한 경우
return;
Vector2 mousePos = GetMousePos();
StartCoroutine(DeactivateAbility(meteorDropDelay, mousePos));
CooldownManager.Instance.StartCoroutine(CooldownManager.Instance.StartCoolDown(_upgradeData));
}
}
private IEnumerator DeactivateAbility(float delay, Vector2 mousePos)
{
IsActive = false; // 비활성화해서 이후 클릭을 방지
SpriteRenderer sr = _rangeIndicator.GetComponent<SpriteRenderer>();
sr.color = Color.red;
yield return new WaitForSeconds(delay);
DealDamage(mousePos);
if (_rangeIndicator != null)
Destroy(_rangeIndicator);
}
원인은 ActivateStrangeAbility 메서드 안에 있었습니다
디버깅을 해보니
- 능력을 취소하기 위해 버튼을 다시 누름 > CancelAbility 실행
- 이후 화면을 클릭했을 때 ActivateAbility 실행 > IsActive = true
- IsActive와 Input.GetMouseButtonDown(0)가 모두 true > DeactivateAbility 코루틴 시작
- DeactivateAbility 코루틴 안에서 _rangeIndicator 접근 > null이라서 에러 발생
이런 흐름으로 에러가 발생하네요
ActivateAbility 함수를 else 문 안에 넣어주어 IsActive = false일 때만 실행되도록 해주면 문제 해결입니다 👍
[이미지 추가]
쿨타임과 능력 취소 모두 정상적으로 작동하는 것을 확인할 수 있습니다
쿨타임 구현이 더 오래 걸릴 줄 알았는데, 다행히 생각보다 빨리 끝나게 되었네요
한 가지 문제점은 현재 cooldownTime(쿨다운 시간)이 controlTime(능력 시간)보다 짧은 경우 버그가 발생합니다
아직 능력을 실행하는 중인데 쿨다운이 끝나서 다시 실행할 수 있는 상태가 됐을 때 생기는 버그 같네요
일단 임시방편으로 무조건 cooldownTime이 controlTime보다 길도록 데이터를 입력한 상태인데, 지금 상황에서 어떤 버그가 또 발생할지 예측이 안 돼서 이대로 진행할 계획입니다
이어서 씬을 추가로 구현하는 작업을 진행할 것 같습니다
그럼 다음 포스팅에서 뵐게요 😆