Unity 입문 TopDown Shooting - 적 구현

Amberjack·2024년 1월 19일
0

Unity

목록 보기
16/44
post-custom-banner

🔍 FindGameObjectWithTag

  • "FindGameObjectWithTag"는 지정된 태그와 일치하는 첫 번째 활성 GameObject를 반환한다. 이 메서드는 특정 태그를 가진 오브젝트를 빠르게 찾을 수 있도록 해준다.

  • 태그를 사용하면 씬 내에서 특정 유형의 오브젝트를 쉽게 찾을 수 있으며, 코드에서 게임 오브젝트를 참조할 때 유용하다.

  • 그러나 "FindGameObjectWithTag"는 매우 비싼 연산입니다. 즉, 이 메서드를 사용하면 CPU를 많이 사용하게 됩니다. 따라서 이 메서드를 매 프레임마다 호출하면 게임 성능에 심각한 영향을 미칠 수 있다.

  • 따라서 일반적으로는 이 메서드를 Start나 Awake와 같은 초기화 메서드에서 한 번만 호출하는 것이 권장된다. 오브젝트를 찾은 후에는 참조를 저장하고 나중에 재사용해야 한다.

  • 게임 오브젝트가 많은 큰 씬에서는 태그 대신 레이어나 다른 메서드를 사용하는 것이 더 효율적일 수 있다.

💥 Physics.Raycast 또는 Physics2D.Raycast

  • Raycasting은 일련의 콜라이더와 교차하는지를 감지하는 데 사용되는 기술이다. 이는 레이저 포인터나 총알을 쏘는 효과를 만들거나, 플레이어의 시야를 계산하는 등 다양한 방식으로 사용된다.

  • Unity에서는 Physics.Raycast 또는 Physics2D.Raycast를 사용하여 Raycast를 수행할 수 있다. 이들 메서드는 시작점, 방향, 최대 거리, 그리고 선택적으로 레이어 마스크를 매개변수로 받는다.

  • Raycast는 hit 정보를 반환한다. 이 정보에는 충돌한 객체, 충돌 지점, 충돌 지점의 정규화된 벡터 등이 포함된다.

  • Raycast는 충돌 검사에 비해 계산 비용이 많이 든다. 따라서 불필요한 Raycast를 줄이고, 가능한 경우 레이어 마스크를 사용하여 검사 범위를 제한하는 것이 성능에 좋다.

  • Raycast는 비주얼 디버깅을 위해 Debug.DrawRay와 함께 사용할 수 있다. 이를 통해 Scene 뷰에서 Raycast의 경로를 시각적으로 확인할 수 있다.

🧌 고블린 만들기

하이라키에서 Create Empty → 이름을 Goblin으로 변경하고 그 밑에 Sprite를 생성한다.

그 후, Controller 폴더 밑에 TopDownEnemyController.cs를 생성한다.

▪️ TopDownEnemyController.cs

public class TopDownEnemyController : TopDownCharacterController
{
    protected Transform ClosestTarget { get; private set; }     // 플레이어 찾기

    protected override void Awake()
    {
        base.Awake();
    }

    protected virtual void Start()
    {

    }

    protected virtual void FixedUpdate()
    {

    }

    protected float DistanceToTarget()
    {
        return Vector3.Distance(transform.position, ClosestTarget.position);
    }

    protected Vector2 DirectionToTarget()
    {
        return (ClosestTarget.position - transform.position).normalized;     // 현재 위치에서 ClosetTarget에 대한 방향을 return 한다.
    }
}

TopDownEnemyController의 코드 구조이다. 여기서 한 가지 문제가 있는데, ClosestTarget을 통해 플레이어의 위치를 찾으려 할 때, Awake()에서 찾기에는 몬스터가 많이 생성되면 낭비가 심해진다. 때문에 GameManager를 통해서 플레이어를 찾도록 코드를 작성해보자!

🎮 GameManager 만들기!

Global 폴더 밑에 GameManager.cs를 생성하자!

이후, 생성되어 있는 GameManager에 스크립트를 추가한다.

public class GameManager : MonoBehaviour
{
    public static GameManager Instance;

    public Transform Player {  get; private set; }

    [SerializeField] private string playerTag = "Player";       // 플레이어를 가져가거나 가져올 수 있도록.

    private void Awake()
    {
        Instance = this;

        // GameObject.Find()를 쓰는 경우, 굉장히 무거운 코드이기 때문에 Awake나 Start 같은 곳에서 한 번만 찾을 때 사용해야 한다.
        Player = GameObject.FindGameObjectWithTag(playerTag).transform;
    }

}

⌨️ 다시 EnemyController 로...

// TopDownEnemyController.cs
...

GameManager gameManager;

...

protected virtual void Start()
{
    gameManager = GameManager.Instance;
    ClosestTarget = gameManager.Player;		// 플레이어 가져오기
}
...

⌨️ TopDownContactEnemyController.cs

근접 공격을 하는 적을 컨트롤하는 TopDownContactEnemyController를 작성해보자.

public class TopDownContactEnemyController : TopDownEnemyController
{
    [SerializeField][Range(0f, 100f)] private float followRange;
    [SerializeField] private string targetTag = "Player";   // 태그 검사

    private bool _isCollidingWithTarget;

    [SerializeField] private SpriteRenderer characterRenderer;

    // Start is called before the first frame update
    protected override void Start()
    {
        base.Start();   
    }

    protected override void FixedUpdate()
    {
        base.FixedUpdate();

        Vector2 direction = Vector2.zero;

        // 플레이어 좌표 가져오기
        if(DistanceToTarget() < followRange)
        {
            direction = DirectionToTarget();
        }

        CallMoveEvent(direction);
        Rotate(direction);
    }

    private void Rotate(Vector2 direction)
    {
        float rotZ = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
        characterRenderer.flipX = Mathf.Abs(rotZ) > 90f;
    }
}

📚 Goblin에게 컴포넌트 추가하기

고블린에게 Box Collider 2D, Rigidbody 2D를 추가하고 다음과 같이 수정한다.

그리고 CharacterStatsHandler.cs, TopDownMovement.cs, TopDownContactEnemyController.cs, TopDownAnimationController.cs를 추가한다.

그리고 각각의 스크립트에 해당하는 오브젝트들을 추가해준다.

▪️ CharacterStatsHandler.cs

아래와 같이 수정하는데, Attack SO가 없기 때문에 Goblin_DefaultAttackData를 생성한다.

생성은 Assets Menu를 추가 했었기 때문에 TopDownController → Attack → Default로 생성한다.

그리고 아래와 같이 수정한다.

▪️ TopDownContactEnemyController.cs

TopDownContactEnemyController에 Character Renderer에 고블린의 MainSprite를 추가한다.
그리고 Goblin이 플레이어를 쫓도록 Follow Range를 20 정도 준다.

🧌 Goblin 애니메이션 만들기!

Goblin의 MainSprite에 Ctrl + 6을 눌러 애니메이션 레코더를 연다.

애니메이션 폴더에 Goblin을 생성한다. 그 밑에 플레이어와 마찬가지로 애니메이션을 idle, run, hit을 만든다.

나머지는 플레이어와 같으나, 고블린은 attack 애니메이션이 없고, 애니메이션 layer를 나눌 때 Hit라는 layer로 나눈다.

파라미터는 플레이어와 같고, IsHit가 true가 되면 goblin_hit이 재생되고, IsHit이 false가 되면 Exit한다.
그리고 Hit을 생성할 때, 톱니바퀴에서 Override, weight는 1이다.

▪️ Player Tag 설정하기

이제 고블린이 플레이어를 찾을 수 있도록 Player에게 Tag를 설정해주자.

그리고 Goblin의 MainSprite의 Order in Layer를 4정도로 지정해주자.

▪️ 확인하기!

고블린이 정상적으로 동작한다!

▪️ 마지막으로 Goblin을 프리팹을 시켜주면 완성!

🧞 Orc_Shaman 만들기!!!

원거리 공격을 하는 적을 만들어 보자!

Create Empty → Orc_Shaman 만들고 그 밑에 MainSprite 추가.

▪️ TopDownRangeEnemyController.cs

Controller 폴더 밑에 TopDownRangeEnemyController를 만들자.

public class TopDownRangeEnemyController : TopDownEnemyController
{
    [SerializeField] private float followRange = 15f;
    [SerializeField] private float shootRange = 10f;

    protected override void FixedUpdate()
    {
        base.FixedUpdate();

        float distance = DistanceToTarget();
        Vector2 direction = DirectionToTarget();

        IsAttacking = false;

        // 플레이어가 몬스터의 인식 범위에 있을 경우
        if(distance <= followRange)
        {
            // 플레이어가 몬스터의 공격 범위에 있을 경우
            if(distance <= shootRange) 
            {
                int layerMaskTarget = Stats.currentStats.attackSO.target;

                // RaycastHit2D : 안 보이는 레이저를 쏴서 맞는지 확인함.
                // 원거리 공격을 하는 적과 플레이어 사이에 장애물이 있는지 검사하는 코드 -> 현재는 쓸 일이 없다.
                RaycastHit2D hit = Physics2D.Raycast(transform.position, direction, 11f, (1 << LayerMask.NameToLayer("Level")) | layerMaskTarget);

                // 현재 플레이어가 공격 범위에 들어와 있고 앞에 장애물이 없다면
                if(hit.collider != null && layerMaskTarget == (layerMaskTarget | (1 << hit.collider.gameObject.layer)))
                {
                    CallLookEvent(direction);       // 플레이어를 바라보고
                    CallMoveEvent(Vector2.zero);    // 멈춘채
                    IsAttacking=true;       // 공격!
                }
                else
                {
                    // 플레이어가 사거리 밖에 있으므로 다시 이동.
                    CallMoveEvent(direction);
                }
            }
            else
            {
                CallMoveEvent(direction);
            }
        }
        else
        {
            CallMoveEvent(direction);
        }
    }
}

▪️ Orc_Shaman 만들기

Orc_Shaman도 Goblin과 똑같이 애니메이션을 만들고, Rigidbody2D와 Box Collider 2D를 추가한다.

그리고 CharacterStatsHandler를 추가하여 아래와 같이 수정한다.

이후 Player_RangedAttackData를 복제하여 이름을 Orc_Shaman_RangedAttackData로 바꿔준다.
그리고 수정을 조금 해준다.

그리고 아래의 스크립트들을 추가한다.

이제 오크 샤먼도 플레이어처럼 무기를 들 필요가 있다.(공격을 하기 위해)
따라서 플레이어에게 추가했던 것처럼 추가해준다.

WeaponSprite는 대충 오크 샤먼과 위치를 조정해주고, 크기를 조정해준다. 이후, Order in Layer를 5로 변경하여 오크 샤먼 앞으로 나오도록 설정해준다.

BulletSpawnPoint도 대충 맞춰준다.

이후 오크샤먼의 TopDownAimRotation에 추가해준다.

그리고 TopDownShooting을 추가해 BulletSpawnPoint를 추가해준다.

마지막으로 Movement를 추가한 뒤 확인해보자!

확인해보기!

잘 동작한다!!!!

이제 오크 샤먼도 프리팹으로 빼두면 적 구현은 완료했다.

post-custom-banner

0개의 댓글