Post

[Jem Clash] 개발일지 #5 - Meteor, Gravity, Blover 능력 구현

[Jem Clash] 개발일지 #5 - Meteor, Gravity, Blover 능력 구현

Meteor 능력

UnitControl 능력에 이어 플레이어가 지정한 영역에 운석을 떨어뜨려 데미지를 입히는 Meteor 능력을 구현했습니다

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;

public class Meteor : MonoBehaviour
{
    public GameObject rangePrefab;
    public LayerMask targetLayer;

    [Header("Values")] public float radius;

    public int damageAmount;
    public float meteorDropDelay;

    private bool _isActive;
    private GameObject _rangeIndicator;

    private void Update()
    {
        Function();
    }

    private void OnDrawGizmosSelected()
    {
        if (_isActive)
        {
            Gizmos.color = Color.green;
            Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            Gizmos.DrawWireSphere(new Vector3(mousePos.x, mousePos.y, 0), radius);
        }
    }

    private void Function()
    {
        if (_isActive && _rangeIndicator != null) FollowMouse();

        if (_isActive && Input.GetMouseButtonDown(0))
        {
            if (EventSystem.current.IsPointerOverGameObject())
            {
                Debug.Log("UI 클릭");
                return;
            }

            Vector2 mousePos = GetMousePos();
            StartCoroutine(DeactivateAbility(meteorDropDelay, mousePos));
        }
    }

    public void ActivateAbility()
    {
        _isActive = true;

        if (_rangeIndicator == null)
        {
            _rangeIndicator = Instantiate(rangePrefab);
            _rangeIndicator.transform.localPosition = new Vector3(radius, radius, 1);
        }

        FollowMouse();
    }

    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);
    }

    private void FollowMouse()
    {
        Vector3 mousePos = GetMousePos();
        _rangeIndicator.transform.position = new Vector3(mousePos.x, mousePos.y, 0);
    }

    private void DealDamage(Vector2 position)
    {
        var hitColliders = Physics2D.OverlapCircleAll(position, radius, targetLayer);

        foreach (Collider2D coll in hitColliders)
        {
            UnitStats unitStats = coll.gameObject.GetComponent<UnitStats>();

            if (unitStats != null && unitStats.isAlly == -1) // 적군일 때 
                unitStats.TakeDamage(damageAmount);
        }
    }

    private Vector3 GetMousePos()
    {
        Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);

        return mousePos;
    }
}

플레이어가 능력 버튼을 클릭하면 마우스 위치를 기준으로 범위를 나타내는 프리팹이 생기고, 이후 화면을 클릭하면 meteorDropDelay 이후 범위 안에 있는 적 유닛들에 damageAmount 만큼 데미지를 줍니다

다만 아직 능력을 사용하는 중에 버튼을 다시 클릭하면 코루틴이 이상하게 고장 나는 버그가 있어서, 나중에 능력 쿨타임을 적용하거나 추가적인 조치를 해야 할 것 같네요

Image

이런 식으로 화면을 클릭하면 범위가 빨간색으로 변하고, 범위가 사라지는 순간 데미지를 입히는 식으로 구현해 주었습니다

이후에 애니메이션을 추가해주면 그럴싸한 능력이 될 것 같습니다

Gravity 능력

다음으로 적 유닛을 특정 범위 안에 가두는 Gravity 능력을 만들어주었습니다

원래 기획했던 능력의 컨셉은 자유롭게 이동하는 적 유닛을 1초에 한번씩 범위의 중심으로 끌어당기도록 하는 것이었는데, 구현하려고 하니까 너무 복잡해지는 것 같더라고요

그래서 그냥 범위에 들어오는 유닛들이 연속적으로 범위의 중심을 향해 이동하도록 하고, 범위가 끝나면 정상적으로 이동하도록 만들어주었습니다

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;

public class Gravity : MonoBehaviour
{
    public GameObject rangePrefab;
    public LayerMask targetLayer;

    [Header("Values")] public float radius;
    public float controlTime;
    public float gravityInterval;
    public float gravityForce;

    private bool _isActive;
    private bool _isGravity;
    private GameObject _rangeIndicator;

    private void Update()
    {
        Function();
    }

    private void OnDrawGizmosSelected()
    {
        if (_isActive)
        {
            Gizmos.color = Color.green;
            Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            Gizmos.DrawWireSphere(new Vector3(mousePos.x, mousePos.y, 0), radius);
        }
    }

    private void Function()
    {
        if (_isActive && _rangeIndicator != null) FollowMouse();

        if (_isActive && Input.GetMouseButtonDown(0))
        {
            if (EventSystem.current.IsPointerOverGameObject())
            {
                Debug.Log("UI 클릭");
                return;
            }

            Vector2 mousePos = GetMousePos();
            StartCoroutine(DeactivateAbility(controlTime, mousePos));
        }
    }

    public void ActivateAbility()
    {
        _isActive = true;

        if (_rangeIndicator == null)
        {
            _rangeIndicator = Instantiate(rangePrefab);
            _rangeIndicator.transform.localPosition = new Vector3(radius, radius, 1);
        }

        FollowMouse();
    }

    private IEnumerator DeactivateAbility(float delay, Vector2 targetPos)
    {
        _isActive = false; // 비활성화해서 이후 클릭을 방지
        _isGravity = true;

        SpriteRenderer sr = _rangeIndicator.GetComponent<SpriteRenderer>();
        sr.color = Color.red;

        StartCoroutine(GravityPull(targetPos));

        yield return new WaitForSeconds(delay);

        _isGravity = false;

        if (_rangeIndicator != null)
            Destroy(_rangeIndicator);
    }

    private void FollowMouse()
    {
        Vector3 mousePos = GetMousePos();
        _rangeIndicator.transform.position = new Vector3(mousePos.x, mousePos.y, 0);
    }

    private IEnumerator GravityPull(Vector2 targetPos)
    {
        while (_isGravity)
        {
            ApplyGravityPull(targetPos);
            yield return new WaitForSeconds(gravityInterval);
        }

        ResetVelocity(targetPos);
    }

    private void ApplyGravityPull(Vector2 targetPos)
    {
        var hitColliders = Physics2D.OverlapCircleAll(targetPos, radius, targetLayer);

        foreach (Collider2D coll in hitColliders)
        {
            UnitStats unitStats = coll.gameObject.GetComponent<UnitStats>();

            if (unitStats != null && unitStats.isAlly == -1) // 적군일 때
            {
                Debug.Log("Pull Enemy");
                coll.gameObject.GetComponent<UnitMovement>().GravityPull(targetPos);
            }
        }
    }

    private void ResetVelocity(Vector2 targetPos)
    {
        var hitColl = Physics2D.OverlapCircleAll(targetPos, radius, targetLayer);

        foreach (Collider2D coll in hitColl)
        {
            UnitStats unitStats = coll.gameObject.GetComponent<UnitStats>();
            UnitMovement unitMovement = coll.gameObject.GetComponent<UnitMovement>();

            if (unitMovement != null && unitStats.isAlly == -1) // 적군일 때
                unitMovement.rb.velocity = unitMovement.rb.velocity.normalized * unitStats.moveSpeed;
        }
    }

    private Vector3 GetMousePos()
    {
        Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);

        return mousePos;
    }
}

마우스를 클릭해서 범위를 지정해 주는 부분의 코드는 Meteor 능력의 것을 그대로 가져왔기에, 중력으로 끌어당기는 부분과 속도를 초기화시키는 부분만 추가해 줬습니다

1
2
3
4
5
6
7
8
// UnitMovement
public void GravityPull(Vector2 targetPos)
{
    Vector2 dirVec = targetPos - new Vector2(gameObject.transform.position.x, gameObject.transform.position.y);

    rb.AddForce(dirVec * (GameManager.Instance.abilityManager.GetComponent<Gravity>().gravityForce * 0.1f),
        ForceMode2D.Impulse);
}

UnitMovement 클래스의 GravityPull 메서드입니다

단순하게 targetPos와 유닛의 위치를 계산해서 이동 방향을 바꿔주는 코드입니다

이 메서드를 GravityPull 코루틴에서 계속 실행시키면 유닛이 마치 범위 안에서 갇힌 듯한 느낌을 줄 수 있습니다

Image

실제 게임에서 실행시켜 보면 다음과 같이 작동합니다

gravityForce 변수를 이용해 끌어당기는 힘을 조절하고 있는데, 유닛이 범위에서 탈출하지 않고 범위 안쪽에 계속 있게 하려면 생각보다 큰 값을 넣어줘야 하더라고요

Image

추가로 아군 유닛과 같이 있을 때 능력을 사용해 보면, 범위 안에서 돌고 있는 적군 유닛이 아군 유닛과 충돌해서 상호작용하는 것을 볼 수 있습니다

당장은 큰 문제를 일으키는 현상이 아니기에 일단 그대로 둘 거지만, 만약 밸런스에 문제가 생기거나 예상치 못한 버그가 발생하면 수정해야 할 수도 있겠네요

Blover 능력

마지막으로 화면 내에 있는 적군 유닛을 밀어내는 Blover 능력을 만들었습니다

이름이 생소하실 수도 있는데, 사실 제가 좋아하는 Plants vs Zombies 게임의 한 캐릭터에서 이름을 따왔어요

Image

이렇게 생긴 친구인데, 귀여운 외형과 다르게 화면 내의 공중 유닛을 전부 날려버리는 강력한 성능을 가지고 있습니다

화면 내에 있는 적군 유닛을 밀어내야 하기에, 우선 PoolManager 클래스에 새로운 메서드를 만들어주었습니다

1
2
3
4
5
6
public void BlowEnemies(float magnitude)
{
    foreach (GameObject enemy in _pools[EnemyIndex])
        if (enemy.activeSelf)
            enemy.GetComponent<UnitMovement>().Blover(magnitude);
}

이렇게 현재 풀매니저 안에 활성화된 적군 유닛들에 적용시켜 줄 거에요

다음으로 유닛의 움직임을 관리하는 UnitMovement 클래스에 Blover 메서드를 추가해 줍니다

1
2
3
4
public void Blover(float magnitude)
{
    rb.AddForce(new Vector2(magnitude * 0.1f, 0), ForceMode2D.Impulse);
}

그냥 magnitude만큼 AddForce를 해주는 간단한 함수입니다

마지막으로 Blover 스크립트를 만들어 코루틴을 추가해 줍니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System.Collections;
using UnityEngine;

public class Blover : MonoBehaviour
{
    public float blowMagnitude;

    public void ActivateAbility()
    {
        StartCoroutine(BlowEnemies());
    }

    private IEnumerator BlowEnemies()
    {
        for (int i = 0; i < 10; i++)
        {
            GameManager.Instance.poolManager.BlowEnemies(blowMagnitude);
            yield return new WaitForSeconds(0.1f);
        }
    }
}

0.1초마다 blowMagnitude만큼 힘을 주면 바람이 부는 것처럼 유닛들이 이동할 겁니다

Image

1레벨로 실험했더니 눈에 띄진 않아서, 만렙으로 만들어주고 능력을 사용해 보았습니다

글로는 잘 안 와닿을 수도 있는데 이런 느낌으로 작동하는 능력이에요

아군 넥서스에 적군 유닛이 많을 경우 방어하기 위해 사용될 것 같습니다


우선 액티브 능력 구현은 여기서 잠시 멈추고, 다음으로 능력 쿨타임을 만들어준 다음 타이틀 씬 및 설정 씬 등 다른 씬들을 추가해 줄 것 같습니다

기획을 하면서 어떻게 하면 사람들이 게임을 질리지 않고 오랫동안 재밌게 할 수 있을까 고민하다가, 로그라이트 게임의 형식을 취하면 좋겠다고 생각했어요

액티브 능력을 여러 개 구현해서 플레이어가 어떤 능력들을 가지고 게임을 진행할지 선택할 수 있고, 능력을 강화하거나 다른 능력과 조합해서 더욱 강력하게 사용할 수 있도록 만들면 재밌을 것 같습니다

또한 사용할 수 있는 액티브 능력의 개수를 줄이는 대신 강력한 패시브 능력을 획득하거나 패시브 능력을 따로 구매할 수 있으면 좋을 것 같은데, 여기까지 만들려면 너무 스케일도 커지고 시간이 오래 걸릴 것 같아서 먼저 로그라이트 시스템부터 간단하게 구현하려고 해요

현재 능력 쿨타임을 구현해 보려고 애쓰는 중인데, 구글링해 가면서 해도 생각보다 난도가 높네요

다음 포스팅은 쿨타임을 구현한 이후에 올라올 것 같습니다


게임 이미지 출처

Blover : https://imgur.com/gallery/trebolina-UzRaF

This post is licensed under CC BY 4.0 by the author.