본문 바로가기

내일 배움 캠프/따봉Ib

[따봉Ib 👍] ASTROWARD (1)

[타워 레벨/업그레이드 시스템]

디펜스 게임에서는 타워의 레벨 업그레이드와 속성 변화가 핵심 콘텐츠 중 하나이다. 

ScriptableObject를 이용하여 유지보수성이 높고 확장하기 쉬운 타워 업그레이드 시스템을 구현했다.

 

UpgradeCost: 업그레이드 비용 정보

    ● 레벨마다 필요한 자원의 종류와 수량을 정의

[System.Serializable]
public class UpgradeCost
{
    public ItemType itemType; // 필요한 자원 종류
    public int amount;        // 필요한 자원 수량
}

 

TowerLevelData: 타워의 레벨별 세부 데이터

    ● 공격력, 사거리, 공격 속도, 발사체 수 등의 정보를 담음

    ● upgradeCosts를 통해 해당 레벨에서 필요한 업그레이드 비용도 관리

    ● 설치된 오브젝트의 외형(Prefab)과 발사체도 레벨에 따라 변경 가능

[CreateAssetMenu(menuName = "Tower/LevelData")]
public class TowerLevelData : ScriptableObject
{
    public GameObject towerPrefab;
    public GameObject projectilePrefab;
    public int atk;
    public float attackRange;
    public float attackSpeed;
    public int projectilesNumber;
    public float projectilesAngle;
    public List<UpgradeCost> upgradeCosts;
}

 

TowerData: 하나의 타워가 가진 모든 레벨의 데이터

    ● 한 종류의 타워가 가질 수 있는 모든 레벨 데이터 리스트

    ● GetLevelData() 함수로 특정 레벨의 세부 데이터를 쉽게 참조 가능

    ● 예시: 유도 미사일 타워는 레벨 0~4까지 각각 다른 속성과 외형을 가질 수 있음

[CreateAssetMenu(menuName = "Tower/TowerData")]
public class TowerData : ScriptableObject
{
    public List<TowerLevelData> towerLevelDatas;

    public TowerLevelData GetLevelData(int level)
    {
        return (level >= 0 && level < towerLevelDatas.Count) ? towerLevelDatas[level] : null;
    }
}

 

ScriptableObject를 사용한 이유

    ● 데이터 중심 설계: 게임 밸런싱과 레벨 디자인을 코드 수정 없이 조정 가능

    ● 에디터 친화적: 인스펙터에서 바로 값 수정, 프리팹/발사체 교체 가능

    ● 유지보수 용이: 레벨 추가/수정 시 코드 변경 없이 SO 데이터만 갱신

    ● 메모리 효율: 다수의 타워 인스턴스가 같은 SO를 참조해 데이터 중복 없음

 

적용 예시

    ● 게임 중에는 해당 데이터를 참조하여 UI에 표시하거나 실제 타워 기능에 반영

TowerData towerData = ... // 예: GuidedTowerData
TowerLevelData currentLevelData = towerData.GetLevelData(currentLevel);
float atkSpeed = currentLevelData.attackSpeed;
List<UpgradeCost> costs = currentLevelData.upgradeCosts;

 


 

[타워]

타워들은 다양한 공격 방식과 업그레이드 시스템을 공통적으로 갖추고 있기 때문에 모든 타워의 부모 클래스인 Tower를 설계

 

설계 목적

    ● 타워마다 공격 방식이나 특수 기능은 다르지만 공통 동작이 존재

        ○ 일정 주기로 공격

        ○ 레벨에 따라 스탯 변화

        ○ 업그레이드 자원 체크

        ○ 프로젝타일 발사 등

    ● 이를 모두 묶어 상속 기반의 구조로 구현

 

Tower

    ● 모든 타워의 기반 클래스

    ● 내부에서 공격 주기 관리, 스탯 적용, 레벨업 처리 등을 담당

    ● 자식 클래스는 Attack()만 오버라이드하면 자신만의 공격 방식을 정의할 수 있음

 

타워의 전투 주기

    ● Update()를 통해 일정 시간마다 공격 실행

    ● 실제 공격 로직은 Attack() 메서드에 정의되어 있으며 자식 클래스가 오버라이드할 수 있도록 virtaul로 선언

private void Update()
{
    attackTimer += Time.deltaTime;
    if (attackTimer >= attackSpeed)
    {
        Attack();
        attackTimer = 0f;
    }
}

 

virtaul Attack(): 공격 로직의 분리

    ● 부모 클래스 Tower는 타이머 기반 공격 흐름만 관리

    ● 실제 공격 방식은 각 자식 클래스에서 오버라이드한 Attack()에서 정의

public class PulseTower : Tower
{
    protected override void Attack()
    {
        // 범위 내 적 탐지 후 데미지 부여
    }
}

 

스탯 적용 및 레벨업

    ● ScriptableObject로 구성된 TowerData 및 TowerLevelData를 기반으로 현재 레벨에 해당하는 타워의 공격력, 사거리, 속도, 발사체 수 등을 반영

    ● 외형도 towerPrefab에 따라 교체 가능

    ● 레벨업 절차 요약

        ○ CheckLevelUp(): 업그레이드 가능한지 확인

        ○ SpendResource(): 자원 소모

        ○ LevelUp(): 레벨 증가 후 스탯 적용

 

Projectile 생성

    ● Object Pool을 통해 재사용하며 퍼포먼스를 고려

protected void GenerateProjectile(GameObject bulletPrefab, Vector3 startPos, Vector3 dir, int damage)
{
    //Object Pool에서 꺼내고 정보 설정
    GameObject bullet = TowerPoolManager.Instance.Pop(bulletPrefab);
    bullet.GetComponent<TowerBullet>().SetInfo(startPos, dir, damage);
}

 

설계 장점

    ● 코드 재사용: 공통 로직은 부모에 두고, 자식은 핵심만 구현

    ● 확장 용이: 새로운 타워 추가 시 Attack()만 구현하면 됨

    ● 유지보수 효율: 레벨업, 스탯 관리 등의 공통 기능은 일괄 수정 가능

    ● 성능 최적화: Object Pool과 데이터 중심 설계(SO) 병행

 


 

[비유도 타워]

비유도 사격 방식을 가지고 있으며 여러 개의 발사체를 전방으로 퍼지게 발사

 

기본 구조

    ● Tower의 자식 클래스로 Attack()을 오버라이드하여 비유도 사격 방식을 구현함

    ● 하나의 축에서 퍼지는 공격 패턴을 가지며 앞 방향(firePivot.forward) 기준으로 발사됨

 

발사체 퍼짐 로직

    ● 발사체의 개수와 각도에 따라 좌우로 퍼지는 중앙 기준 분산 각도를 계산

    ● 예시: 발사체가 3개이고 간격이 10도일 경우 →  -10°, 0°, 10°로 분산

float projectileAngleSpace = projectilesAngle;
int projectileNumber = projectilesNumber;
float minAngle = -(projectileNumber - 1) / 2f * projectileAngleSpace;

 

살짝 아래로 기울어진 방향

    ● 수직 방향으로 살짝 아래를 향하도록 조정하여 발사체가 완전히 수평이 아니라 대각선 아래로 날아가게 함

    ● inclination 값으로 기울기 강도 조절 가능

dir = (dir + Vector3.down * inclination).normalized;

 

최종 공격 처리

    ● Tower 부모 클래스에 정의된 GenerateProjectile() 메서드를 통해 실제 발사체 생성

    ● Object Pool을 사용해 성능 최적화

GenerateProjectile(projectilePrefab, startPos, dir.normalized, atk);

 

설계 포인트

    ● 유도 없이 직진하는 투사체를 다루는 단순한 로직이지만 퍼짐 각도와 기울기를 조절함으로써 다양한 시각적 연출과 전략적 배치 가능

    ● 데이터 기반으로 조절 가능한 값들(projectileAngle, inclination, projectileNumber)은 게임 밸런싱에 매우 유용


 

[유도 타워]

정확성과 자동 타겟팅 기능으로 전략적인 의미를 더함

 

기본 구조

    ● Tower의 자식 클래스이며 Attack()을 오버랄이드하여 범위 내에서 가장 가까운 적을 추적해서 발사함

    ● 사거리를 기준으로 타겟을 선정하고 유도형 발사체를 생성

 

타겟 선정 로직

    ● EnemyManager에서 현재 위치(transform)를 기준으로 가장 가까운 적을 가져옴

    ● 가장 가까운 순으로 최대 projectileNumber 개수까지 타겟팅

    ● 탐색 범위는 attackRange로 제한

target = EnemyManager.Instance.GetNearestMonsters(transform, projectilesNumber, attackRange);

 

    ● GerNearestMonsters()

        ○ 정렬: 거리 기준으로 가까운 순으로 정렬(sqrMagnitude 사용으로 최적화)

        ○ 필터링: distanceThreshold 이내의 적만 필터링

        ○ 보정: 목표 수(count)보다 적은 경우 마지막 적을 반복하여 리스트 채움 (무기 수량과 일치 유지)

public List<EnemyController> GetNearestMonsters(Transform tower, int count, float distanceThreshold)

 

유도형 공격 로직

    ● firePivot 기준으로 각 적을 향해 방향 벡터 계산

    ● 해당 방향으로 GenerateProjectile()을 호출하여 발사체 생성

    ● 유도 로직은 간단하지만 실제 타겟팅된 적을 향한 정확한 방향 조준이 핵심

for (int i = 0; i < target.Count; i++)
{
    Vector3 dir = (target[i].transform.position - firePivot.position).normalized;
    Vector3 startPos = firePivot.position;
    GenerateProjectile(projectilePrefab, startPos, dir, atk);
}

 

설계 장점

    ● 범위 기반 탐색 + 자동 타겟팅으로 방치형/자동 전투에 적합

    ● GetNearestMonsters()는 범용적으로 재활용 가능

    ● 발사체 개수가 유동적일 경우에도 로직 안정성 보장

 


 

[범위 타워]

한 번에 여러 적을 처리할 수 있는 핵심 유닛

광범위 데미지 처리와 시각적 퍼짐 이펙트를 구현

 

PulseTower

    ● 기본 Tower 클래스를 상속받아 Attack() 로직을 오버라이드

 

공격 범위 탐지

    ● attackRange 반경 내의 적들을 LayerMask를 기준으로 탐색

    ● EnemyController 컴포넌트를 가진 대상에게만 데미지 적용

Collider[] overlapped = Physics.OverlapSphere(transform.position, attackRange, targetLayer);

    ● 한 번의 공격으로 여러 적에게 동시에 피해를 입힐 수 있음

if (enemy != null)
{
    enemy.OnDamaged(atk);
}

 

퍼지는 이펙트: ExpandEffect

    ● 공격 시 pulseEffectPrefab 이펙트를 Object Pool에서 꺼내 위치 지정

    ● ExpandEffect 스크립트를 통해 시간에 따라 원형으로 커지는 이펙트 연출

GameObject effect = TowerPoolManager.Instance.Pop(pulseEffectPrefab);
effect.transform.position = transform.position;

    ● duration 동안 점점 커지는 방식으로 광범위한 임팩트 표현

    ● 끝나면 Push()로 다시 풀에 반환해 퍼포먼스 최적화

transform.localScale = Vector3.Lerp(Vector3.zero, max, t);

 

설계 장점

    ● OverlapSphere로 간단하게 범위 내 적 모두 타격 (AOE 공격)

    ● Object Pooling을 사용하여 이펙트를 매번 생성하지 않고 재활용

    ● 커지는 이팩트로 공격 타이밍과 범위를 직관적으로 전달해 타격감을 시각화함