눈팅하는 게임개발자 블로그

개발 기록 - Turret 본문

Project/Tactical Architect Tower Defense

개발 기록 - Turret

Palamore 2020. 9. 14. 19:38

이 타워 디펜스에서 타워 역할을 하는 Turret이다.

 

관련 코드는 Turret_Parent, Turret_Base, Turret_Laser, Turret_Missile

github.com/Palamore/TATD_Codes/blob/master/Turret_Parent.cs

github.com/Palamore/TATD_Codes/blob/master/Turret_Base.cs

github.com/Palamore/TATD_Codes/blob/master/Turret_Missile.cs

github.com/Palamore/TATD_Codes/blob/master/Turret_Laser.cs

 

기본적으로 터렛이 해야할 동작인 공격할 적을 찾기(타겟팅), 공격하기, 버프 받기 등의 동작을 뼈대로

기본, 머신건, 미니 유탄, 탱크, 버프, 레일건, 레이저, 미사일의 8가지 터렛을 만들었다.

유탄, 탱크 터렛은 광역 범위 공격 특성, 레이저 터렛은 적의 이동속도를 늦추는 특성,

레일건 터렛은 탄환이 적을 뚫고 지나가는 특성 등이 있어 

플레이어는 각 특성을 잘 활용하여 터렛을 배치할 필요가 있다.

 

8가지 터렛이 각 LV1 ~ LV5까지 5가지로

총 40개의 터렛 종류가 존재한다.

버프 터렛 LV1 ~ LV5

같은 LV, 같은 종류의 터렛 2개가 모이면 다음 LV의 터렛으로 진화하는 방식으로

LV5의 터렛을 만드려면 LV1의 터렛이 16번 지어져야 한다.

 

using UnityEngine;

public class Turret_Parent : MonoBehaviour
{
    protected Transform target;
    protected UpgradeShopManager USM;
    protected SoundManager SDM;
    private Node BaseNode;

    [Header("Related Objects Field")]
    //from Scene
    public GameObject TagTail;
    public GameObject TagObject;
    //from Asset
    public GameObject RangeDisplay;
    public GameObject TurretNextLV;

    [Header("Turret Information Field")]
    public string turretName;
    public string turretDescription;
    public int turretIndex;
    public int synergyCode1;
    public int synergyCode2;

    public int turretLV;
    public float range;
    public float baseDamage;
    public float damage;
    public float baseFireRate;
    public float fireRate;
    public float turnSpeed;
    public float rangeScale;
    public Transform PartToRotate;

    public bool bIsBuffed;              // 버프타워 버프 플래그
    public bool bEnemySkillTriggered;   // Mage enemy skill 플래그 
    public bool bIsLV5;                 // LV5 turret 플래그

    public string enemyTag = "Enemy";   // 버프타워에서만 "BuffTail"

    [Header("Model issue Init Field")]   // 모델에서 틀어진 만큼의 Transform 값을 조정해주는 필드
    public Vector3 InitialMove;           // 생성시 틀어진 위치만큼 이동해줘야할때 사용함.
    public float InitialRotateNumber;     // 생성시 틀어진 위치만큼 움직여줘야하는 각도 > 주로 x축
    public int InitialRotateAxis;         // 생성시 틀어진 위치만큼 움직여줘야하는 축   > 주로 z축, 가끔y축


    void Start()
    {
        bIsLV5 = false;
        bEnemySkillTriggered = false;
        baseFireRate = fireRate;
 
        InvokeRepeating("UpdateTarget", 0f, 0.5f);
        transform.position = transform.position + InitialMove;

        rangeScale = range * 0.2f;
        UpgradeConfirm();
    }


    void Awake()
    {
        USM = UpgradeShopManager.Instance();
        SDM = SoundManager.Instance();
        gameObject.AddComponent<AudioSource>().clip = SDM.TurretFire[turretIndex].clip;
        gameObject.GetComponent<AudioSource>().playOnAwake = false;
    }

    public void SetBaseNode(Node node)
    {
        BaseNode = node;
        node.turret = gameObject;
    }
    public Node GetBaseNode()
    {
        return BaseNode;
    }

    public void SetNodeTurretNull() //터렛이 노드에서 없어질 때 후처리
    {
        BaseNode.turret = null;
        BaseNode.BuildableEffect.GetComponent<SpriteRenderer>().sprite = BaseNode.NodeCoverSprites[8];
        BaseNode.BuildableEffect.transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
    }
    
    void UpdateTarget()
    {
        GameObject[] enemies = GameObject.FindGameObjectsWithTag(enemyTag);
        if (enemies.Length == 0) return;
        float shortestDistance = Mathf.Infinity;
        GameObject nearestEnemy = null;
        foreach (GameObject enemy in enemies)
        {
            float distanceToEnemy = Vector3.Distance(transform.position, enemy.transform.position);
            if (distanceToEnemy < shortestDistance)
            {
                shortestDistance = distanceToEnemy;
                nearestEnemy = enemy;
            }
        }

        if (nearestEnemy != null && shortestDistance <= range)
        {
            target = nearestEnemy.transform;
        }
        else
        {
            target = null;
        }
    }
 
    void Update()
    {
        if (target != null)
        {
            if (bEnemySkillTriggered)
            {
                StopFire();
                return;
            }
            LockOnTarget();
            Fire();
        }
        else
        {
            StopFire();
        }
    }

    void LockOnTarget()
    {
            Vector3 dir = target.position - transform.position;
            Quaternion lookRotation = Quaternion.LookRotation(dir);
            Vector3 rotation = Quaternion.Lerp(PartToRotate.rotation, lookRotation, Time.deltaTime * turnSpeed).eulerAngles;
            if (InitialRotateAxis == 3)     // 회전해야하는 모델이 회전할 때 z축이 돌아가는 경우
                PartToRotate.rotation = Quaternion.Euler(InitialRotateNumber, 0f, rotation.y);
            else if (InitialRotateAxis == 2)// y축이 돌아가는 경우
                PartToRotate.rotation = Quaternion.Euler(InitialRotateNumber, rotation.y, 0f);
            else if (InitialRotateAxis == 1)// x축이 돌아가는 경우
                PartToRotate.rotation = Quaternion.Euler(rotation.y, 0f, 0f);
    }

    public void Buff(int lev)
    {
        if (bIsBuffed) return;
        bIsBuffed = true;
        switch (lev)
        {
            case 1:
                damage = damage * 1.1f;
                break;
            case 2:
                damage = damage * 1.2f;
                break;
            case 3:
                damage = damage * 1.3f;
                break;
            case 4:
                damage = damage * 1.4f;
                break;
            case 5:
                damage = damage * 1.5f;
                break;
            default:
                break;
        }
    }
    public void ResetBuff()
    {
        damage = baseDamage;
        fireRate = baseFireRate;
    }

    protected virtual void UpgradeConfirm(){ }

    protected virtual void StopFire()
    {
        GetComponent<AudioSource>().Stop();
    }

    protected virtual void Fire() { }


}

모든 터렛의 뼈대가 되는 상위 클래스인 Turret_Parent 코드.

함수를 하나하나 살펴보자.

    public void SetBaseNode(Node node)
    {
        BaseNode = node;
        node.turret = gameObject;
    }
    public Node GetBaseNode()
    {
        return BaseNode;
    }
    public void SetNodeTurretNull() //터렛이 노드에서 없어질 때 후처리
    {
        BaseNode.turret = null;
        BaseNode.BuildableEffect.GetComponent<SpriteRenderer>().sprite = BaseNode.NodeCoverSprites[8];
        BaseNode.BuildableEffect.transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
    }

Node는 터렛이 설치되는 위치, 박스 모양의 지형인데

turret이 설치되는 위치인 Node를 해당 터렛의 내부변수로 세팅한다.

turret 아래의 이펙트 설정이나 node 클릭 시 range를 보여준다거나 turret에 관한 정보를 보여주는 등의 기능을 할 때 

Node가 자신의 위치에 존재하는 turret이 무엇인지에 대한 정보가 필요하다.

SetNodeTurretNull은 터렛이 진화했을 때 Node 위에서 사라지는 경우 Node가 가지고 있던 turret에 대한 포인터를 없애는 함수다. turret 아래의 이펙트도 없앤다.

 

    void UpdateTarget()
    {
        GameObject[] enemies = GameObject.FindGameObjectsWithTag(enemyTag);
        if (enemies.Length == 0) return;
        float shortestDistance = Mathf.Infinity;
        GameObject nearestEnemy = null;
        foreach (GameObject enemy in enemies)
        {
            float distanceToEnemy = Vector3.Distance(transform.position, enemy.transform.position);
            if (distanceToEnemy < shortestDistance)
            {
                shortestDistance = distanceToEnemy;
                nearestEnemy = enemy;
            }
        }

        if (nearestEnemy != null && shortestDistance <= range)
        {
            target = nearestEnemy.transform;
        }
        else
        {
            target = null;
        }
    }

Start()함수에서 InvokeRepeat로 이 함수를 0.5초마다 반복해서 실행한다.

0.5초마다 조건에 맞는(사정 범위 안, 가장 가까운)target을 설정한다.

    void Update()
    {
        if (target != null)
        {
            if (bEnemySkillTriggered)
            {
                StopFire();
                return;
            }
            LockOnTarget();
            Fire();
        }
        else
        {
            StopFire();
        }
    }

Update()함수, target이 존재하면 LockOnTarget(), 타겟 방향을 바라보고.

Fire() 쏜다. (Fire()은 각 터렛마다 어떤 투사체를 쏘는가, 어떤 방식으로 쏘는가가 다르기 때문에 상속받는 클래스에서 구현한다.)

bEnemySkillTriggered는 Enemy의 Skill 공격에 맞았는가 아닌가에 대한 플래그인데

Enemy의 Skill 공격에 맞으면 터렛이 그대로 멈추는 사양인지라 아예 그냥 멈추는 걸로 했다.

    void LockOnTarget()
    {
            Vector3 dir = target.position - transform.position;
            Quaternion lookRotation = Quaternion.LookRotation(dir);
            Vector3 rotation = Quaternion.Lerp(PartToRotate.rotation, lookRotation, Time.deltaTime * turnSpeed).eulerAngles;
            if (InitialRotateAxis == 3)     // 회전해야하는 모델이 회전할 때 z축이 돌아가는 경우
                PartToRotate.rotation = Quaternion.Euler(InitialRotateNumber, 0f, rotation.y);
            else if (InitialRotateAxis == 2)// y축이 돌아가는 경우
                PartToRotate.rotation = Quaternion.Euler(InitialRotateNumber, rotation.y, 0f);
            else if (InitialRotateAxis == 1)// x축이 돌아가는 경우
                PartToRotate.rotation = Quaternion.Euler(rotation.y, 0f, 0f);
    }

터렛의 총구? 방향이 Enemy를 향하도록 하는 LockOnTarget()함수다.

Model Issue가 조금 있어서 방향을 억지로 맞춰주는 코드가 있다.

 

    public void Buff(int lev)
    {
        if (bIsBuffed) return;
        bIsBuffed = true;
        switch (lev)
        {
            case 1:
                damage = damage * 1.1f;
                break;
            case 2:
                damage = damage * 1.2f;
                break;
            case 3:
                damage = damage * 1.3f;
                break;
            case 4:
                damage = damage * 1.4f;
                break;
            case 5:
                damage = damage * 1.5f;
                break;
            default:
                break;
        }
    }
    public void ResetBuff()
    {
        damage = baseDamage;
        fireRate = baseFireRate;
    }

Buff Turret이 버프를 걸면 터렛에서 Buff()함수가 실행된다.

ResetBuff는 웨이브가 끝나면 실행되어 본래의 수치로 돌아온다.

나머지 함수들은 자식 클래스에서 구현.

 

 

위 클래스를 기본으로 다른 터렛들은 구현하는 데 큰 문제가 없었으나 버프 터렛은 상황이 달랐다.

다른 터렛들은 적을 타겟으로 공격을 하지만 버프 터렛은 다른 터렛들의 공격력 buff를 해야하는 것.

이 부분을 다뤄보자.

 

기본적으로 터렛은 적을 타겟으로 하고 공격을 한다. 이게 기본인 상태에서 

버프 터렛을 구현할 수는 없었는데 그냥 버프 터렛만 따로 코드를 짜볼까도 생각해봤지만

버프 터렛만 Turret_Par을 상속받지 않는 것이 마음에 들지 않았다.

 

그래서 이리저리 고민해보다가

어차피 적을 타겟팅하는 방식은 오브젝트에 달린 Tag를 사용하는 것이니 이를 이용해보자 생각했다.

다른 모든 터렛에 Tag를 "BuffTail"으로 붙이고.(이 생각이 났을 땐 이미 모든 터렛이 각각 다른 태그를 달고 있어서 타워에 종속된 고유한 오브젝트를 만들고 이 오브젝트에 태그를 붙여줘야 했다.)

버프 터렛은 enemyTag의 string 값을 "BuffTail"로 바꿔주었다.

웨이브가 시작될 때, 모든 터렛이 "BuffTail"이라는 태그를 가진 무형의 오브젝트를 생성하고

BuffTail Object Inspector

이 오브젝트를 버프 터렛이 공격하여 파괴되면 터렛에 공격력 버프가 들어가게 된다.

BuffTail을 공격하는 버프 터렛

 

 

 

그리고 LockOnTarget()함수에서 잠깐 언급한 Model Issue.

처음에 터렛 모델을 유니티에 올리니까 모델 전체가 90도 돌아가 있는 채로 나타나서

이를 다시 돌려놓기 위해 각 터렛마다 틀어진 방향과 틀어진 각도를 저장할 변수가 필요했다.

 

원인은 터렛들의 모델을 만들어준 친구가 모델링에 블렌더를 사용했는데,

모델링을 할 때 월드 좌표축을 신경쓰지 않고 만들다보니

완성된 모델의 월드 좌표축이 틀어져 있는 경우가 있었던 것이다.

 

어떤건 x축이 틀어져있고 어떤건 z축이 틀어져 있어서

틀어져 있는 것을 각 각 모델마다 돌려놓기 위해서

각 터렛 프리펩마다 하나하나 틀어진 각도를 확인해보는 삽질도 했었다.

 

'Project > Tactical Architect Tower Defense' 카테고리의 다른 글

개발 기록 - 상점 시스템  (0) 2020.09.18
개발 기록 - Enemy  (0) 2020.09.18
개발 기록 - Tutorial Scene  (0) 2020.09.13
개발 기록 - Title Scene  (0) 2020.09.13
개인정보 처리 방침  (0) 2019.11.30