투사체 구현하기

CharacterController에서 Attack을 불러올 때 스탯을 불러오도록 한다.

 

Call이 일어나고 Attack에 대한 데미지를 전달하는 코드들을 수정한다.

 

전달 받은 코드인 OnShoot 메서드에서 Attack을 받도록 처리한다. 투사체가 부채꼴로 퍼지도록 만들어줬다.

private void OnShoot(AttackSO attackSO) 
{
    if (attackSO is not RangedAttackData rangedAttackData) return;
    float projectilesAngleSpace = rangedAttackData.multipleProjectilesAngel;
    int numberOfProjectilesPerShot = rangedAttackData.numberofProjectilesPerShot;

    float minAngle = -(numberOfProjectilesPerShot / 2f) * projectilesAngleSpace + 0.5f * rangedAttackData.multipleProjectilesAngel;

    for (int i = 0; i < numberOfProjectilesPerShot; i++)
    {
        float angle = minAngle + projectilesAngleSpace * i;
        float randomSpread = Random.Range(-rangedAttackData.spread, rangedAttackData.spread);
        CreateProjecrile(rangedAttackData, angle);
    }
}

as는 형변환으로 attackSO를 RangedAttackData 참조형식으로 바꿔주겠다는 뜻이다.

 

형변환 실패 시 예외처리를 해주는 것이 안전하기 때문에 null 처리를 해준다.

RangedAttackData rangedAttackData = attackSO as RangedAttackData;
if (rangedAttackData == null) return;

 

아니면 is 연산자를 이용하여 간결하게 처리할 수도 있다.

if (attackSO is not RangedAttackData rangedAttackData) return;

 

투사체가 공격 데이터와 각도를 받아오도록 한다.

private void CreateProjecrile(RangedAttackData rangedAttackData, float angle) 
{
    Instantiate(ArrowPrefab, projectileSpawnPosition.position, Quaternion.identity);
}

 

투사체를 관리해주는 ProjectileManager 스크립트를 만들어준다.

쉬운 접근성을 위해 싱글톤 패턴을 사용한다.

static은 정적 메모리로, 정적 메모리들끼리 공간을 공유한다. 정적 메모리 공간을 할당 받은 후, 오브젝트들이 바라보게 한다. 즉, 오브젝트들이 모두 한 공간을 바라보는 것이다.

[SerializeField] private ParticleSystem _impactParticleSystem;

public static ProjectileManager instance;

private void Awake()
{
    instance = this;
}

public void ShootBullet(Vector2 startPosition, Vector2 direction, RangeAttribute attackData)
{
	//TODO
}

 

instance가 정적 메모리에 들어있기 때문에 클래스명으로 접근이 가능해진다. 클래스명으로 인스턴스를 접근하면 자기 자신이 참조이므로 만들어져 있는 객체에 접근할 수 있게 된다. 그러나 가장 마지막에 들어온 오브젝트만 접근이 가능하고 처리할 수 있다.

그렇기 때문에 싱글톤 패턴을 사용할 때는 단일 객체로 만들어 하나만 접근할 수 있도록 한다. 보통 매니저에 자주 사용한다.

 

Shooting 스크립트에서 싱글톤이 설정된 이후 사용하기 위해 Start에서 ProjectileManager를 접근할 수 있도록 한다.

private ProjectileManager _projectileManager;

private void Start()
{
    _projectileManager = ProjectileManager.instance;
}

 

총알을 생성해준다. 현재 캐릭터 위치에서 어느 각도로 쏴야하는지만 있고 방향이 없기 때문에 벡터로 구해준다.

private void CreateProjecrile(RangedAttackData rangedAttackData, float angle) 
{
	// (발사위치, 회전각, 공격정보)
    _projectileManager.ShootBullet(projectileSpawnPosition.position, RotateVector2(_aimDirection, angle), rangedAttackData);
}

private static Vector2 RotateVector2(Vector2 v, float degree)
{
    return Quaternion.Euler(0, 0, degree) * v;
}

Quaternion과 Vector 둘다 횡렬이기 때문에 곱하기가 가능한데 Verctor * Quaternion은 원소수가 맞지 않기 때문에 곱할 수 없고 Quaternion * Vector(Vector를 이 각도로 회전시켜라)를 해야한다.

 

다시 매니저로 돌아와서 실제로 생성되는 부분을 구현한다. 아직 Prefab을 불러오는 방법이 없기 때문에 임시로 처리한다.

[SerializeField] private GameObject testObj;

public void ShootBullet(Vector2 startPosition, Vector2 direction, RangedAttackData attackData)
{
    GameObject obj = Instantiate(testObj);

    obj.transform.position = startPosition;
    //TODO
}

 

 

지금까지 투사체를 정확하게 발사하는 것까지 구현했다. 이제 투사체가 혼자 날아갈 수 있도록 해주어야한다.

투사체 Prefab에 충돌처리를 위해 Box Collider와 RigidBody를 준다.

 

그리고 Trail Renderer를 추가해주는데 날아가는 투사체의 잔상이라고 보면 된다.

 

투사체를 컨트롤할 스크립트를 만들어준다.

GetComponent는 자신이 포함되어있는 오브젝트에서만 찾기 때문에 자신을 포함한 하위까지 찾는 GetComponentInChildren을 사용한다. 자신에게 있으면 자신의 것을 사용하고 없다면 자식을 검사한다.

[SerializeField] private LayerMask levelCollisionLayer;

    private RangedAttackData _attackData;
    private float _currentDuration;
    private Vector2 _direction;
    private bool _isReady;

    private Rigidbody2D _rigidbody;
    private SpriteRenderer _spriteRenderer;
    private TrailRenderer _trailRenderer;
    private ProjectileManager _projectileManager;

    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;
    }

    public void InitializeAttack(Vector2 direction, RangedAttackData attackData, ProjectileManager projectileManager)
    {
        _projectileManager = projectileManager;
        _attackData = attackData;
        _direction = direction;

        UpdateProjectilSprite();
        _trailRenderer.Clear();
        _currentDuration = 0;
        _spriteRenderer.color = attackData.projectileColor;

        transform.right = _direction;

        _isReady = true;
    }

    private void UpdateProjectilSprite()
    {
        transform.localScale = Vector3.one * _attackData.size;
    }

    private void DestroyProjectile(Vector3 position, bool createFx)
    {
        if(createFx)
        {

        }
        gameObject.SetActive(false);
    }

 

다시 매니저로 돌아와서 투사체를 날리는 것을 테스트하기 위해 구현한다.

public void ShootBullet(Vector2 startPosition, Vector2 direction, RangedAttackData attackData)
{
    GameObject obj = Instantiate(testObj);

    obj.transform.position = startPosition;
    RangedAttackController attackController = obj.GetComponent<RangedAttackController>();
    attackController.InitializeAttack(direction, attackData, this);

    obj.SetActive(true);
}

 

투사체에 투사체를 컨트롤하는 스크립트를 달아주고 GameManager를 만들어 매니저 스크립트를 달아준다.

 

작동 순서는 Shooting에서 Shoot을 하면 ProjecticleManager에 투사체를 만들어달라고 요청한다. ProjecticleManager는 총알을 만들고 투사체 컨트롤러로 요청하여 초기화를 진행한 뒤, 총알을 발사한다.

 

그런데 투사체가 안보이는 오류가 있다. Player_RangedAttackData의 Projecticle Color 알파값이 0이었다.

 

최대로 바꿔준 뒤, 실행해보면 화살이 보인다.

 

현재 투사체가 Trigger 충돌로 되어있기 때문에 충돌 감지는 되지만 물리적인 충돌은 되지 않기 때문에 벽 밖으로 발사된다.

 

벽과 충돌했을 때, 투사체가 사라지도록 처리하기 위해 세 가지 레이어를 추가한다.

 

Player와 하위 오브젝트들의 레이어를 Player로 바꿔주고 Level 오브젝트를 만들어 Grid를 넣어준 뒤, Level 레이어로 바꿔준다.

 

그리고 투사체 Prefab을 컨트롤 하는 스크립트에서 레이어를 Level로 바꿔준다.

 

스크립트에서 누구와 충돌처리를 할 것인지 추가한다.

private void OnTriggerEnter2D(Collider2D collision)
    {
        if(levelCollisionLayer.value == (levelCollisionLayer.value | (1 << collision.gameObject.layer)))
        {
            DestroyProjectile(collision.ClosestPoint(transform.position) - _direction * .2f, fxOnDestory);
        }
    }

Layer 검사나 Name 검사, Tag 검사도 가능하지만 비트 연산이 빠르게 처리하기 때문에 사용한다.

'부트캠프 > Study' 카테고리의 다른 글

<Unity> 숙련 - 2D 게임 개발(6)  (1) 2023.12.12
ToString() 메서드  (1) 2023.12.11
<Unity> 숙련 - 2D 게임 개발(4)  (0) 2023.12.08
<프로젝트> 팀과제 Unity 2D 게임 - Space Survival(2)  (0) 2023.12.01
벡터(Vecter)  (0) 2023.11.29

+ Recent posts