Post

[Jem Clash] 개발일지 #3 - 유닛 업그레이드 + Firework 능력 구현

[Jem Clash] 개발일지 #3 - 유닛 업그레이드 + Firework 능력 구현

유닛 체력 업그레이드

저번 포스팅에서 유닛 개수, 유닛 소환 자동화까지 구현해 주었는데, 이제 유닛 체력을 업그레이드하는 기능을 만들어주겠습니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void OnClick()
{
    switch (upgradeData.type)
    {
        case UpgradeData.UpgradeType.UnitAuto:
            ToggleAutoSpawn();
            break;
    }
}

private void IncreaseUnitHealth()
{
    GameManager.Instance.unitSpawner.allyData.health += upgradeData.counts[level];
}

이전에 구현해 주던 대로 switch 문 내부에 새로운 함수를 추가해 주었고, 테스트를 진행했는데 문제가 발생했습니다 🤔

Scriptable Object인 allyData에 직접 업그레이드 수치를 더해주는 식으로 구현했는데, 플레이 모드를 종료해도 플레이 도중 더해준 값이 남아있더라고요

찾아보니 값이 게임 종료 후에도 변경된 상태로 남는 이유는 ScriptableObject가 에셋 파일로 저장되기 때문이었습니다

이게 장점이 될 수도 있는 특징인데, 현재 제가 만들고 싶은 기능과 맞지 않기 때문에 안 그러도록 바꿔야겠죠?

그래서 런타임 중 변경된 값을 메모리에서만 유지하고, 게임 종료 시 초깃값으로 복구하도록 코드를 추가해 줬습니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UnitDataBackup
{
    public int health;
    public int attackDamage;
    public float moveSpeed;

    public UnitDataBackup(UnitData data)
    {
        health = data.health;
        attackDamage = data.attackDamage;
        moveSpeed = data.moveSpeed;
    }

    public void Restore(UnitData data)
    {
        data.health = health;
        data.attackDamage = attackDamage;
        data.moveSpeed = moveSpeed;
    }
}

먼저 UnitDataBackup이라는 새로운 클래스를 만들어주고, 데이터를 백업해 두고 복구하는 함수를 추가해 줍니다

1
2
3
4
5
6
7
8
9
10
11
private void Awake()
{
    _allyDataBackup = new UnitDataBackup(allyData);
    _enemyDataBackup = new UnitDataBackup(enemyData);
}

private void OnDestroy()
{
    _allyDataBackup.Restore(allyData);
    _enemyDataBackup.Restore(enemyData);
}

다음으로 UnitSpawner 클래스의 Awake와 OnDestroy 메서드에 함수를 추가해 주면, allyData 내부의 값이 변경되어도 플레이 모드 종료 시 초기화됩니다

테스트해 보니까 원하는 대로 잘 작동하네요 👍

에너지 사용

이전에 물자를 사용하는 것까지만 구현해 주었는데, 이어서 에너지 사용도 구현해 주겠습니다

에너지는 능력을 업그레이드할 때 주로 사용될 것이기에, Upgrade 스크립트에서 현재 에너지양을 체크하고 사용하는 기능을 넣어주는 것이 좋을 것 같네요

그전에 지금 Upgrade 스크립트 내의 switch 문이 너무 지저분해서, 한번 정리해 주고 구현하겠습니다

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
private void HandleUpgrade()
{
    switch (upgradeData.type)
    {
        case UpgradeData.UpgradeType.UnitSpawn:
            SpawnAllyUnit();
            break;

        case UpgradeData.UpgradeType.SupplyUp:
            IncreaseSupply();
            break;
    }
}

private static void SpawnAllyUnit()
{
    GameManager.Instance.unitSpawner.SpawnAllyUnit(
        GameManager.Instance.unitSpawner.spawnCount);
}

private void IncreaseSupply()
{
    if (!EnoughEnergy()) return;

    SpendEnergy();
    GameManager.Instance.resourceManager.SupplyAmountUp(
        upgradeData.counts[level]);
    IncrementLevel();
}

이런 식으로 함수 안에 에너지가 충분한지 체크하는 EnoughEnergy, 에너지를 사용하는 SpendEnergy, 레벨을 증가시키는 IncrementLevel 함수를 나눠주었더니 훨씬 보기 편하네요

액티브 능력 구현

게임의 기본적인 시스템을 만들었으니, 이제 액티브 능력을 만들 차례입니다

현재 플레이어가 상호작용할 수 있는 버튼의 개수를 20개로 설정했고, 그중에서 7개는 기본적인 업그레이드, 13개는 액티브 능력으로 배분해 둔 상태입니다

13개를 각각 어떤 능력으로 할진 모두 확정된 것은 아니지만, 우선 초기 기획 단계부터 만들고 싶었던 불꽃놀이 능력을 구현할 거에요

플레이어가 능력을 클릭하면 게임화면 내 세 군데에서 폭죽이 터지는 것처럼 유닛이 소환되는, 단순하지만 눈에 띄는 능력입니다

먼저 랜덤한 스폰 포인트 세 군데를 잡고, 스폰 포인트마다 기본 6개의 유닛을 소환하도록 구현해 주었어요

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
private void SpawnFirework(Transform spawnPoint)
{
    for (int i = 0; i < unitCount; i++)
    {
        GameObject pooledObject = GameManager.Instance.poolManager.Get(0);
        Transform spawnUnit = pooledObject.transform;
        spawnUnit.SetParent(spawnPoint);

        // 위치, 회전 초기화
        spawnUnit.localPosition = Vector3.zero;
        spawnUnit.localRotation = Quaternion.identity;

        // 원형으로 배치 후 간격 살짝 벌림
        Vector3 rotVec = Vector3.forward * 360 * i / unitCount;
        spawnUnit.Rotate(rotVec);
        spawnUnit.Translate(spawnUnit.up * 0.2f, Space.World);
        spawnUnit.Rotate(-rotVec);

        // 지정한 방향으로 이동 시작
        Vector2 dirVec = (spawnUnit.position - spawnPoint.position).normalized;
        UnitStats stats = spawnUnit.GetComponent<UnitStats>();
        spawnUnit.GetComponent<Rigidbody2D>().velocity = dirVec * stats.moveSpeed;

        spawnUnit.SetParent(GameManager.Instance.poolManager.transform);
    }
}

한 개의 스폰 포인트에서 unitCount만큼의 유닛을 소환하는 코드입니다

코드를 짜면서 2가지 문제에 부딪혔는데, 첫 번째는 한번 소환되고 발사된 유닛들이 다음 번에 능력을 썼을 때 갑자기 위치가 텔레포트 되더라고요

알고 보니 SetParent를 이용해서 spawnPoint를 부모 오브젝트로 설정하고 해제하지 않아서였는데, 이후에 poolManager에서 활용할 수 있도록 발사 후 poolManager를 부모로 설정해 주었습니다

두 번째는 unitCount만큼 유닛을 발사하기 전 배치하는 과정에서 오브젝트가 회전하여, 스프라이트가 이상하게 보이는 문제가 있었습니다

현재 픽셀 스프라이트를 사용하고 있는데, 90도가 아닌 각도로 회전할 경우 Pixel Perfect Camera에서 이상하게 보였어요

그래서 위치 초기화 후 회전을 시켜준 다음, 간격을 벌리고 다시 역방향으로 회전시켜 주는 간단한 코드를 넣어 해결했습니다

어차피 발사 방향은 유닛에서 spawnPoint의 위치를 뺀 벡터를 이용하기 때문에, 회전 방향을 초기화해도 문제가 없죠

Image

결과적으로 이런 느낌으로 사용하는 능력입니다


저번 포스팅과 비교했을 때 이미지들이 조금 달라졌는데, 동그라미랑 네모만 보면서 개발하니까 이게 게임인지 시뮬레이션인지 구분이 안 되고 그래서 잠깐 숨도 돌릴 겸 조잡하지만 간단하게 스프라이트 몇 개 만들고 적용해 줬습니다

메인 캐릭터는 음… 그냥 귀여운 세포 느낌으로 일단 만들어줬습니다

아직 게임의 세계관이나 컨셉이 뚜렷하게 잡힌 건 아니어서, 개발하면서 천천히 생각해보려구요

Image

지금까지 만든 능력에 해당하는 버튼들도 스프라이트를 추가해 줬습니다

물론 아직 초기 디자인이고, 앞으로 계속 발전시켜 나가야죠

UI 패널이랑 버튼의 픽셀 사이즈 맞추느라 많이 애먹었는데, 잘 적용된 걸 보니 마음이 편안합니다

Grid Layout Group을 이용해서 판넬 안에 버튼들을 넣어주었는데, 이게 Cell Size에 따라서 버튼의 픽셀 크기가 왔다 갔다 하더라고요

조절해 가면서 적절한 사이즈를 찾아줬더니 픽셀이 딱 맞습니다

이제 1층은 거의 다 만들어줬고, 2층에 들어갈 액티브 능력들도 고민해 봐야 할 것 같네요

중간 점검

다음으로 구현하게 될 것은 적 오브젝트의 행동 방식일 것 같습니다

지금은 테스트용으로 적 스폰 포인트에서 n초에 한 번씩 유닛을 소환하도록 해놨는데, 당연히 이렇게 단조로우면 재미없으니까 패턴이나 능력 변화를 추가해야겠죠?

덤으로 개발하는 과정에서 조금씩 밸런싱도 하고 그러려면 미리 만들어두는 게 좋을 것 같습니다

다만 아직 적 오브젝트도 플레이어 캐릭터처럼 하나 만들어서 맵을 돌아다니게 할 건지, 아니면 넥서스에서 관리하는 식으로 구현할지는 고민 중이에요

캐릭터를 하나 더 만들면 물자와 에너지를 획득하는 구역을 추가하고, 오브젝트 이동과 행동을 구현하고, 액티브 능력을 사용하는 것 등 추가적인 작업이 많이 필요할 거 같아서 아마 넥서스에서 관리하는 식으로 할 것 같긴 합니다

그 이후엔 액티브 능력들 추가로 구현하고, 액티브 능력 간에 상호작용도 있으면 좋겠다는 생각도 했습니다

예를 들어서 얼음 능력으로 언 유닛들을 불 능력으로 녹일 수 있고… 이런 식으로 말이죠

그거랑 밋밋한 배경을 어떻게 디자인해 줄지, 게임의 세계관은 어떻게 잡을지, 다른 씬도 추가로 만들어줘야 하고… 할 게 태산이네요 😅

그래도 글 열심히 쓰면서 하나씩 해치워 나가보겠습니다 👍

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