Chapter 12-2. 기타 : 포탑(Defense Tower) 만들기

Date:     Updated:

카테고리:

태그:

인프런에 있는 케이디님의 [유니티 3D] 실전! 생존게임 만들기 - Advanced 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click

🚀 포탑 오브젝트

image

평소에는 360도로 혼자 회전하다가 포신이 향하는 방향과 포탑에서 플레이어를 향하는 방향이 일치하면 플레이어를 쏜다.

image

TopGun은 포신이다. Foundation은 포탑의 몸체이다. Foundation은 플레이어가 관통하지 못하도록 Collider를 붙여주었다.

image

총의 메시가 되는 SubMachineGun2은 원래 땅에 눕혀져있던 모양이 디폴트였어서 x 축 중심으로 -90도 회전해주어 일으켰다. 그래서 이렇게 포신의 앞(Z축)이 하늘을 향하는 모양새가 되었다. 포신이 향하는 방향과 포탑에서 플레이어를 향하는 방향이 일치하면 플레이어를 쏘게 할 것이기 때문에 포신이 Z 축과 일치했으면 좋겠다.

image

그래서 SubMachineGun2을 빈 오브젝트 GunHolder의 자식으로 넣어주었다. 이로써 X = -90 회전값을 가지는 SubMachineGun2가 부모인 GunHolder의 X 축을 중심으로 -90도 회전한 셈이 되었기 때문에 GunHolder의 회전축은 다음과 같이 된다. 아직도 Z축이 포신과 일치하지 않았다. 여기서 회전 축 자체가 Y 축 중심으로 90도 더 회전되야 Z 축이 포신과 일치한다. 따라서 Y 축을 중심으로 -90도 회전해주고 TopGun 빈 오브젝트의 자식으로 이를 또 넣어준다.

image

그러면 Y = -90 회전값을 가지는 GunHolder가 부모인 TopGun의 Y 축을 중심으로 -90도 회전한 셈이 되었기 때문에 TopGun의 회전축은 위와 같이 된다. 이제 포신의 방향과 Z 축이 일치하게 되었다.


🚀 포탑 애니메이션

✈ 애니메이션 클립

image

발포할 때 애니메이션은 6프레임 동안 살짝 총이 뒤로 밀려나왔다가 다시 제자리로 오게끔

image

정지 상태 애니메이션


✈ 애니메이션 컨트롤러

image

  • 플레이어를 발견하면 발사 애니메이션 재생
    • “Fire” Trigger 발동
  • 발사 애니메이션 재생이 끝나면 자연스레 정지 애니메이션 재생
    • 전이 조건 X, Has Exit Time 체크


🚀 포탑 작동시키기

📜DefenseTower

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DefenseTower : MonoBehaviour
{
    [SerializeField] private string towerName; // 방워타워의 이름
    [SerializeField] private float range; // 방어타워의 사정거리
    [SerializeField] private int damage; // 방어 타워의 공격력
    [SerializeField] private float rateOfAccuracy; // 정확도(0에 가까울 수록 정확도 높음)
    [SerializeField] private float rateOfFire; // 연사속도(rateOfFire초마다 발사)
    private float currentRateOfFire; // 연사속도 계산(갱신됨)
    [SerializeField] private float viewAngle; // 시야각
    [SerializeField] private float spinSpeed; // 포신 회전 속도
    [SerializeField] private LayerMask layerMask; // 움직이는 대상만 타겟으로 지정(플레이어 혹은 동물)
    [SerializeField] private Transform tf_TopGun; // 포신
    [SerializeField] private ParticleSystem particle_MuzzleFlash; // 총구 섬광
    [SerializeField] private GameObject go_HitEffect_Prefab; // 적중 효과 이펙트

    private RaycastHit hitInfo;
    private Animator anim;
    private AudioSource theAudio;

    private bool isFindTarget = false; // 적 타겟 발견시 True
    private bool isAttack = false; // 정확히 타겟을 향해 포신 회전 완료시 True (총구 방향과 적 방향이 일치할 때)

    private Transform tf_Target; // 현재 설정된 타겟의 트랜스폼

    [SerializeField] private AudioClip sound_Fire;


    void Start()
    {
        theAudio = GetComponent<AudioSource>();
        theAudio.clip = sound_Fire;
        anim = GetComponent<Animator>();
    }

    void FixedUpdate() // 정해진 초마다 (!= 프레임마다. update보다 좀 느리고 정확)
    {
        Spin();
        SearchEnemy();
        LookTarget();
        Attack();
    }
  • rateOfFire 👉 연사 속도
    • rateOfFire 시간에 한번씩 쏜다. 이게 바로 연사 속도가 된다. 이 값이 작을 수록 연사가 같은 시간 안에 더 많이 이루어진다는 의미이므로 연사 속도가 빠름
      • currentRateOfFire을 프레임마다 증가시켜서 이게 rateOfFire에 도달할 때 발사하고 currentRateOfFire은 0 으로 갱신하고 다시 증가. 이를 반복
  • rateOfAccuracy 👉 정확도
    • 발사 방향을 플레이어가 정확히 위치한 방향으로부터 rateOfAccuracy만큼 벡터의 X, Y 방향값에 더해줄 것이다.
      • 즉 수직, 좌우로 좀 더 튀게!
      • Z 값엔 더해주지 않는다. Z 값에 더해주면 총알이 뒤로 날아갈 수 있음..
    • 따라서 높을수록 정확도가 떨어진다.
  • FixedUpdate()
    • 어떤 매 시간마다
      • 1️⃣ Spin() 👉 360도로 계속 뺑글뺑글 회전하다가
      • 2️⃣ SearchEnemy() 👉 사정 거리내에서 타겟을 찾았다면
      • 3️⃣ LookTarget() 👉 타겟을 향해 회전한다.
      • 4️⃣ Attack() 👉 그리고 발사한다.

FixedUpdateUpdate의 차이점

  • FixedUpdate
    • 프레임마다 호출되지 않는다. 독립적인 타이머가 존재하여 정해진, 고정적인 시간 간격으로 호출된다.
      • 프레임과 관계없이 규칙적으로 호출되므로 물리적인 연산을 할 때 이 곳에서 하는게 좋다.
        • 프레임은 시스템 환경을 따라가므로 컴퓨터 환경이 좋지 않으면 느리고 불규칙적으로 변할 수 있기 때문에 Rigidbody 같은 어떤 물리 효과가 적용된 움직임 처리를 Update 안에 구현하는건 좋지 않다.
    • TimeSCale에 의존하기 때문에 Time.timeScale = 0;이 될 때 실행되지 않는다.
      • Time.fixedDeltaTime마다 실행된다. 이는 0.02초로 고정되어 있다.
  • Update
    • 프레임마다 호출된다.
    • TimeSCale에 의존하지 않기 때문에 Time.timeScale = 0;이 될 때도 Update 함수 자체는 실행이 된다.
      • 다만 이 안에서 deltaTime을 사용하여 움직임을 제어한게 있었다면 멈추겠지!

Except for realtimeSinceStartup and fixedDeltaTime, timeScale affects all the time and delta time measuring variables of the Time class. If you lower timeScale it is recommended to also lower Time.fixedDeltaTime by the same amount. FixedUpdate functions will not be called when timeScale is set to zero. https://docs.unity3d.com/ScriptReference/Time-timeScale.html


✈ 평소엔 360도 회전하기

    private void Spin()
    {
        if (!isFindTarget && !isAttack)
        {
            Quaternion _spin = Quaternion.Euler(0f, tf_TopGun.eulerAngles.y + (1f * spinSpeed * Time.deltaTime), 0f);
            tf_TopGun.rotation = _spin;
        }
    }

타겟을 찾은 상태도 아니고 공격해야하는 상태도 아니라면 FixedUpdate 안에서 Y 축을 중심으로 현재 회전 값에서 spinSpeed * Time.deltaTime만큼 더 회전한다. Z 축과 포신 방향을 일치해준 상태기 때문에 하늘을 향하는 축은 Y 축임. 따라서 Y 축을 중심으로 회전하면 된다.


✈ 타겟 찾기

    private void SearchEnemy()
    {
        Collider[] _target = Physics.OverlapSphere(tf_TopGun.position, range, layerMask);

        for (int i = 0; i < _target.Length; i++)
        {
            Transform _targetTf = _target[i].transform; // 이게 더 빠르다

            if (_targetTf.name == "Player")
            {
                Vector3 _direction = (_targetTf.position - tf_TopGun.position).normalized;
                float _angle = Vector3.Angle(_direction, tf_TopGun.forward);
                
                if (_angle < viewAngle * 0.5f)
                {
                    tf_Target = _targetTf;
                    isFindTarget = true;

                    if (_angle < 5f) // 거의 차이 안나면
                        isAttack = true;
                    else
                        isAttack = false;
                    
                    return;
                }
            }
        }
        // 플레이어 못 찾음
        tf_Target = null;
        isAttack = false;
        isFindTarget = false;
    }
  • 매번 _target[i].transform로 접근 하는 것보다 미리 _target[i].transform_targetTf Transform _targetTf = _target[i].transform 이렇게 변수에 대입 시켜놓고 _targetTf를 사용하는 것이 더 빠르다. 매번 _target[i].transform로 사용하면 그 원소를 접근하는데 드는 시간도 무시할 수 없을 것이다. 배열 크기가 아무 크고 잦은 접근을 한다면..
  • 사정거리 range 내에 있는 모든 layerMask 레이어를 가진 충돌체들을 _target 배열에 담는다.
    • _target 배열을 매번 순회해 검사하여 “Player” 플레이어가 담긴 것을 알게 된다면 👉 플레이어가 사정 거리내에 들어왔다는 뜻
      • 그 이후 시야각 안에 들어오는지를 체크한다.
        • 플레이어와 포탑 사이각이 시야각의 절반 보다 작다면 이제 플레이어를 향해 포탑 방향을 틀어야 하므로 isFindTarget을 True 시킨다.(LookTarget()에서 할 것)
        • 시야각과 5도 미만으로 차이가 나면 그냥 바로 공격 때리게 Attack()
        • 그리고 리턴해버린다.
    • for문을 빠져나왔다면 충돌 범위에 플레이어가 없어서 for문 안에서 return 을 못만난 것이므로 못찾은것이다.


✈ 타겟을 향해 회전하기

    private void LookTarget()
    {
        if (isFindTarget)
        {
            Vector3 _direction = (tf_Target.position - tf_TopGun.position).normalized;
            Quaternion _lookRotation = Quaternion.LookRotation(_direction);
            Quaternion _rotation = Quaternion.Lerp(tf_TopGun.rotation, _lookRotation, 0.2f);
            tf_TopGun.rotation = _rotation;
        }
    }

SearchEnemy() 에서 플레이어를 시야각내에서 찾았다면 isFindTarget가 True인 상태가 된다. 즉, 타겟을 향해 이제 회전시켜야 한다. 포탑이 플레이어를 향하는 방향을 바라보게 회전해야 한다. 즉, _lookRotation만큼 회전. Lerp 를 사용하여 부드럽게 회전시킨다.

  • 플레이어쪽으로 회전이 완료되어 시야각과 플레이어와 포탑 사이의 각도가 5 도 미만으로 차이나면 SearchEnemy() 에서 isAttack = true가 되어 공격할 수 있는 상태가 된다.


✈ 발사

    private void Attack()
    {
        if (isAttack)
        {
            currentRateOfFire += Time.deltaTime;
            if(currentRateOfFire >= rateOfFire)
            {
                currentRateOfFire = 0;
                anim.SetTrigger("Fire");
                theAudio.Play();
                particle_MuzzleFlash.Play();

                if (Physics.Raycast(tf_TopGun.position,
                                    tf_TopGun.forward + new Vector3(Random.Range(-1, 1f) * rateOfAccuracy, Random.Range(-1, 1f) * rateOfAccuracy, 0f),
                                    out hitInfo,
                                    range,
                                    layerMask)){
                    GameObject _HitEffect = Instantiate(go_HitEffect_Prefab, hitInfo.point, Quaternion.LookRotation(hitInfo.normal));
                    Destroy(_HitEffect, 1f);

                    if (hitInfo.transform.name == "Player")
                    {
                        hitInfo.transform.GetComponent<StatusController>().DecreaseHP(damage);
                    }
                }
            }
        }
    }
  • rateOfFire초 간격으로 발사한다. (연사속도)
  • 발사 애니메이션도 재생하고 발사 효과음도 재생한다.
  • 플레이어가 있는 방향으로 회전을 완료한 후 (거의 5도 미만 차이) 앞으로 (Z축. 포신과 일치하는 방향) Raycast 를 쏜다.
    • Raycast 를 쏘되 좌우 수직으로 총알이 좀 튀게 한다.
      • foward 앞 방향으로부터 랜덤하게 [-rateOfAccuracy, rateOfAccuracy) 범위의 값으로 X, Y 방향으로 더 움직인 그 방향으로 쏜다.
        • Z 방향은.. 앞뒤로 얼마나 나가느냐의 문제니까 뒤로도 총알이 튈 수 있게 됨. 총알이 앞으로 나가는건 당연하고! 그래서 Z 방향은 보정하지 않는다.
    • 맞은 상대에게 피격효과 파티클 시스템 생성
    • Raycast 충돌한게 플레이어라면 HP 깎음

image



🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Unity Lesson 3 카테고리 내 다른 글 보러가기

댓글 남기기