유니티2D 입문 정리 9 - 투사체 구현하기

woollim·2024년 11월 9일
0

0. 핵심 내용

○ 비트 연산자, 레이어 마스크

<< 1은 왼쪽으로 1만큼 보내라는 의미에요. 빈칸은 0으로 채워요.

  • Unity에서 각 게임 오브젝트는 특정 레이어에 속할 수 있으며, 이는 주로 오브젝트 간의 상호작용을 관리하기 위해 사용됩니다. 레이어는 비트 필드로 표현되어, 각 비트가 다른 레이어를 나타내게 됩니다.

  • 비트 연산자는 레이어의 비트 필드를 조작하기 위해 사용됩니다. 여기에는 주로 다음과 같은 연산자가 포함됩니다

    • AND (&) : 두 비트 필드 모두에서 해당 비트가 설정되어 있을 때만 결과 비트를 설정합니다. 이를 통해 특정 레이어의 존재 여부를 확인할 수 있습니다.
    • OR (|) : 두 비트 필드 중 하나라도 해당 비트가 설정되어 있으면 결과 비트를 설정합니다. 이는 새로운 레이어를 추가할 때 유용합니다.
    • XOR (^) : 두 비트 필드에서 해당 비트가 서로 다를 때만 결과 비트를 설정합니다. 이는 두 레이어의 차이를 찾을 때 사용됩니다.
    • NOT (~) : 모든 비트를 반전시킵니다. 이는 특정 레이어를 제외시킬 때 유용합니다.
  • 비트 시프트 연산: 1 << n은 1을 n번째 비트 위치로 시프트합니다. 이는 n번째 레이어를 나타내는 비트마스크를 생성하는 데 사용됩니다. 이를 통해 특정 레이어에 대한 연산을 쉽게 수행할 수 있습니다.

  • 이 방법은 물리적 충돌, 레이캐스팅, 카메라 렌더링 등을 제어하는 데 사용됩니다.

    • 충돌 검사 : 비트마스킹을 사용하여 특정 레이어에 속한 오브젝트만을 대상으로 충돌 검사를 수행할 수 있습니다.
    • 레이캐스팅 제어 : 레이캐스트가 특정 레이어의 오브젝트에만 반응하도록 비트마스크를 설정할 수 있습니다.
    • 카메라 렌더링 설정 : 카메라가 특정 레이어의 오브젝트만을 렌더링하도록 설정하여, 게임의 시각적 요소를 세밀하게 제어할 수 있습니다.

○ 쿼터니언과 벡터의 곱셈

  • 쿼터니언을 활용해 벡터도 회전할 수 있어요.
  • 예를 들어, 캐릭터의 오른쪽에 있다고 할 때, 캐릭터가 회전하면 그에 맞게 벡터도 회전해야겠죠?
  • Unity에서는 Quaternion과 Vector3의 곱셈을 지원하기 때문에 순서만 맞춰서 곱하면 돼요! Q*V!


1. 투사체 구현하기

○ Layer 추가

  • 아무 오브젝트의 Layer → Add Layer 클릭
  • Layer 추가


○ Player Layer 변경

  • Player의 Layer → Player 변경
  • 다음과 같은 확인 창에서 Yes, Change Children 클릭


○ Tileamap Layer 변경

  • 빈 오브젝트 추가 → Level 이름 변경
  • Grid를 하위 오브젝트로 넣기
  • Level의 Layer → Level 변경
  • Yes, Change Children 클릭

○ TopDownController 수정

using System;
using UnityEngine;

public class TopDownController : MonoBehaviour
{
    // Action은 void형 메소드를 등록할 수 있는데 Vector2를 인자로 받는 메소드를 등록하도록 Action<Vector2>를 정의
    // event 키워드를 입력하면 public이더라도 나만 Invoke가능.
    public event Action<Vector2> OnMoveEvent;
    public event Action<Vector2> OnLookEvent;
    
    // OnAttackEvent는 눌렸을 때 공격 기준정보(AttackSO)를 들고 옴
    public event Action<AttackSO> OnAttackEvent;

    private float timeSinceLastAttack = float.MaxValue;
    protected bool isAttacking;

    protected CharacterStatHandler stats { get; private set; }

    protected virtual void Awake()
    {
        stats = GetComponent<CharacterStatHandler>();
    }

    protected virtual void Update()
    {
        HandleAttackDelay();
    }

    private void HandleAttackDelay()
    {
        if (timeSinceLastAttack <= stats.CurrentStat.attackSO.delay)
        {
            timeSinceLastAttack += Time.deltaTime;
        }

        if (isAttacking && timeSinceLastAttack > stats.CurrentStat.attackSO.delay)
        {
            timeSinceLastAttack = 0;
            // 현재 장착된 무기의 attackSO전달
            CallAttackEvent(stats.CurrentStat.attackSO);
        }
    }

    public void CallMoveEvent(Vector2 direction)
    {
        // onMoveEvent는 public이어서 TopDownMovement에서 메소드들을 등록해놨음(a.k.a. 구독)
        OnMoveEvent?.Invoke(direction);
    }

    public void CallLookEvent(Vector2 direction)
    {
        // 조준 시스템에서 등록했음.
        OnLookEvent?.Invoke(direction);
    }

    public void CallAttackEvent(AttackSO attackSO)
    {
        // TopDownShooting에서 등록한 OnShoot 메소드가 구독되어 있음.
        OnAttackEvent?.Invoke(attackSO);
    }
}

○ TopDownShooting 수정

using UnityEngine;

public class TopDownShooting : MonoBehaviour
{
    private TopDownController contoller;

    [SerializeField] private Transform projectileSpawnPosition;
    private Vector2 aimDirection = Vector2.right;

    public GameObject testPrefab;

    private void Awake()
    {
        contoller = GetComponent<TopDownController>();
    }

    void Start()
    {
        contoller.OnAttackEvent += OnShoot;
        // OnLookEvent에 이제 두개가 등록되는 것(하나는 지난 시간에 등록했었죠? TopDownAimRotation.OnAim(Vec2)
        // 한 개의 델리게이트에 여러 개의 함수가 등록되어있는 것을 multicast delegate라고 함.
        // Action이나 Func도 델리게이트의 일종인 것 기억하시죠..?
        contoller.OnLookEvent += OnAim;
    }

    private void OnAim(Vector2 newAimDirection)
    {
        aimDirection = newAimDirection;
    }

    private void OnShoot(AttackSO attackSO)
    {
        RangedAttackSO RangedAttackSO = attackSO as RangedAttackSO;
        float projectilesAngleSpace = RangedAttackSO.multipleProjectilesAngel;
        int numberOfProjectilesPerShot = RangedAttackSO.numberofProjectilesPerShot;

        // 중간부터 펼쳐지는게 아니라 minangle부터 커지면서 쏘는 것으로 설계했어요! 
        float minAngle = -(numberOfProjectilesPerShot / 2f) * projectilesAngleSpace + 0.5f * RangedAttackSO.multipleProjectilesAngel;


        for (int i = 0; i < numberOfProjectilesPerShot; i++)
        {
            float angle = minAngle + projectilesAngleSpace * i;
            // 그냥 올라가면 재미없으니 랜덤으로 변하는 randomSpread를 넣었어요!
            float randomSpread = Random.Range(-RangedAttackSO.spread, RangedAttackSO.spread);
            angle += randomSpread;
            CreateProjectile(RangedAttackSO, angle);
        }
    }

    private void CreateProjectile(RangedAttackSO RangedAttackSO, float angle)
    {
        // 화살 생성 -> 다음강에서 구조개선을 위해 잠시 흉물스러운 이름 참아주세요!
        GameObject obj = Instantiate(testPrefab);
        
        // 발사체 기본 세팅
        obj.transform.position = projectileSpawnPosition.position;
        ProjectileController attackController = obj.GetComponent<ProjectileController>();
        attackController.InitializeAttack(RotateVector2(aimDirection, angle), RangedAttackSO);

        // 다음강에서 개선 시 활용할 코드
        // obj.SetActive(true);
    }

    private static Vector2 RotateVector2(Vector2 v, float degree)
    {
        // 벡터 회전하기 : 쿼터니언 * 벡터 순
        return Quaternion.Euler(0, 0, degree) * v;
    }
}

○ ProjectileController 만들기

using UnityEngine;

public class ProjectileController : MonoBehaviour
{
    // 벽에 부딪혔을 때 사라지면서 이펙트 나오게 해야돼서 레이어를 알고 있어야 해요!
    [SerializeField] private LayerMask levelCollisionLayer;

    private RangedAttackSO attackData;
    private float currentDuration;
    private Vector2 direction;
    private bool isReady;

    private Rigidbody2D rigidbody;
    private SpriteRenderer spriteRenderer;
    private TrailRenderer trailRenderer;

    public bool fxOnDestory = true;

    private void Awake()
    {
        spriteRenderer = GetComponentInChildren<SpriteRenderer>();
        rigidbody = GetComponent<Rigidbody2D>();
        trailRenderer = GetComponent<TrailRenderer>();
    }

    private void Update()
    {
        if (!isReady)
        {
            return;
        }

        currentDuration += Time.deltaTime;

        if (currentDuration > attackData.duration)
        {
            DestroyProjectile(transform.position, false);
        }

        rigidbody.velocity = direction * attackData.speed;
    }
    
    private void OnTriggerEnter2D(Collider2D collision)
    {
        // levelCollisionLayer에 포함되는 레이어인지 확인합니다.
        if (IsLayerMatched(levelCollisionLayer.value, collision.gameObject.layer))
        {
            // 벽에서는 충돌한 지점으로부터 약간 앞 쪽에서 발사체를 파괴합니다.
            Vector2 destroyPosition = collision.ClosestPoint(transform.position) - direction * .2f;
            DestroyProjectile(destroyPosition, fxOnDestory);
        }
        // _attackData.target에 포함되는 레이어인지 확인합니다.
        else if (IsLayerMatched(attackData.target.value, collision.gameObject.layer))
        {
			      // 아야! 피격 구현에서 추가 예정
            // 충돌한 지점에서 발사체를 파괴합니다.
            DestroyProjectile(collision.ClosestPoint(transform.position), fxOnDestory);
        }
    }

    // 레이어가 일치하는지 확인하는 메소드입니다.
    private bool IsLayerMatched(int layerMask, int objectLayer)
    {
        return layerMask == (layerMask | (1 << objectLayer));
    }

    public void InitializeAttack(Vector2 direction, RangedAttackSO attackData)
    {
        this.attackData = attackData;
        this.direction = direction;

        UpdateProjectileSprite();
        trailRenderer.Clear();
        currentDuration = 0;
        spriteRenderer.color = attackData.projectileColor;

        transform.right = this.direction;

        isReady = true;
    }

    private void UpdateProjectileSprite()
    {
        transform.localScale = Vector3.one * attackData.size;
    }

    private void DestroyProjectile(Vector3 position, bool createFx)
    {
        if (createFx)
        {
            // TODO : ParticleSystem에 대해서 배우고, 무기 NameTag로 해당하는 FX가져오기
        }
        gameObject.SetActive(false);
    }
}

○ Arrow 수정하기

  • BoxCollider 2D 추가

  • Rigidbody 2D 추가

  • Trail Renderer 추가
    물체가 이동하면서 뒤에 "잔상"이나 "자취"를 남기는 효과를 만드는 데 사용됩니다

  • 커브 조정을 잘 못하겠어요!
    각 지점의 우클릭을 하면 Edit Key를 통해 값을 바꿀 수 있어요.
    이렇게 하면 강의와 더 가까운 값을 얻을 수 있어요!
  • Projectile Controller 추가

0개의 댓글