나 개발자 진짜 되냐?

[ Unity 뱀서라이크 게임을 만들어보자 7 ] 투사체를 날려보자! 본문

유니티를 공부해봐요!/초급이에요!

[ Unity 뱀서라이크 게임을 만들어보자 7 ] 투사체를 날려보자!

Snow Rabbit 2024. 10. 10. 16:04

흐흐

드디어 화살을 발사할 수 있다!

 

그래그래

내 큐피드

야무지게 발사해 볼까!?

 

그전에 늘 하는

문법 정리!

 

이번에는 좀 많다.

오늘은 비트연산자와 레이어마스크

쿼터니언과 벡터의 곱셈

네 가지에 대해 알아보자!


레이어는 오브젝트 간의 상호작용을

관리하기 위해 사용되는데

 

이 레이어가 비트 필드로 표현된다.

 

비트 연산자는 레이어의 비트 필드를 조작하기 위해 사용되는데..

연산자라는 이름에 맞춰

연산할 수 있는 기호들이 있다.

 

AND ( & )

두 비트 필드 모두에서 해당 비트가 설정되어 있을 때 결과 비트를 설정

특정 레이어의 존재 여부를 확인

 

( 여기서 중요한 건 0 이랑 0은 1일까? 0일까?인데

답은 0이다. 무조건 1 1 일 때만 1이다.)

 

 

OR ( | )

두 비트 필드 중 하나라도 해당 비트가 설정되어 있으면 결과 비트를 설정

새로운 레이어를 추가할 때 유용

 

 

XOR ( & )

두 비트 필드에서 해당 비트가 서로 다를 때 결과 비트를 설정

두 레이어의 차이를 찾을 때 사용

둘이 달라야지만 1이 된다. 같으면 0

 

 

NOT  ( & )

모든 비트를 반전

특정 레이어를 제외시킬 때 유용

 

 

또 비트연산자는

재미난 능력이 있다

바로 시프트 연산이 가능하다는 것!

 

시프트 연산은 

1 << n

이라고 쓰고

1을 n번째 비트위치로 당겨라!라는 뜻이 됩니다.

 

비트는 오른쪽에다가 글을 쓰며 왼쪽으로 밀리기 때문에

1이 맨뒤에 있었다면

3 시프트를 해주면

1000이 되겠지요

시프트해준다음에 그곳에는 0을 넣어주는데

 

이 친구를 활용하는 이유는

뭐 지금은 3이지라서

1000이지만

사실 1000으로 하드코딩하는 거보다

1 << 3이라고 하면 훨씬 편하다

거기에 만약에 시프트 10이면..

0이 10개..

총코드에 10000000000

라고 써주어야 하는데.. 너무 번거롭다

그래서 시프트연산이 좋고 활용도가 크다고 볼 수 있다.

 

 

이 친구들은 어디에 사용되느냐!

 

물리적 충돌, 카메라 레터링. 레이캐스팅 제어 

에 사용된다.

 

충돌검사

비트마스킹을 사용하여 특정 레이어에 속한 오브젝트만을

대상으로 충돌 검사를 수행

 

만약에 나무뒤에 몬스터가 있다.

그러면 나무는 공격이 되면 안 되니까 그런 것들

조절에 필요

 

 

레이캐스팅

레이캐스트가 특정 레이어의 오브젝트에만 반응하도록 비트마스크를 설정

 

 

카메라 렌더링 설정

카메라가 특정 레이어의 오브젝트만을 렌더링 하도록 설정

시각적 요소를 세밀하게 제어

 

우리는 지난 시간에

쿼터니언에 대해 잠시 이야기했었다.

https://qua28.tistory.com/166

 

[ Unity 뱀서라이크 게임을 만들어보자 4 ] 삼각함수를 이용하여 조준 구현

(❁´◡`❁) 수학적인 부분이 많이 나와서당황했다..하지만 나이과생이었다 잊지말자 몇년전인지는 모르지만.. 삼각함수에 대해 공부하고활을 쥐어줘서 활을 쏘게해보자!!  삼각함수를 알기

qua28.tistory.com

쿼터니언과 벡터의 곱셈에 대해 잠시 짚고 가면

 

쿼터니언을 쓸 이유는

짐벌락 때문이었다.

즉 회전 때문이었는데

이것을 활용하여 벡터도 회전시킬 수 있다고 한다.

 

캐릭터의 오른쪽에 있다고 할 때,

캐릭터가 회전하면 그에 맞게 벡터도 회전해야겠죠?

Unity에서는 Quaternion과 Vector3의 곱셈을 지원하기 때문에

순서만 맞춰서 곱하면 돼요!

 

Q*V!

쿼터 곱하기 벡터!

 

벡터를

 쿼터니어만큼 회전해라 라는 뜻을 가지게 된다.


먼저

Layer을 추가해주자

 

아무 오브젝트에서

인스펙터창을 열면 오른쪽에 Layer가 있다.

눌러줘서 add 

 

player에는 palyer이를

우리가 저번에 만들었던 파일은 Grip에 있는데

그 친구는 Level에 넣어주면 된다.

 

기존에는 공격에 layer개념을 추가하기위해

코드를 수정해보자.

 

1.

TopDownController.cs 수정

 

원래의 경우 우리는 OnAttackEvent를 그냥 누르면 공격!

이었는데

이제는 제대로 공격기준을 정하려고하기때문에

 


    public event Action OnAttackEvent;


    public event Action<AttackSO> OnAttackEvent;

로 수정한다.

즉 어떤공격을 할 것이다. 를 만든다.

 

자, 그 다음에

우리는 이 변수한테로 가서!

채워넣어주고,

 

그럼 또 빨간줄이 생긴다

여기안에는 현재 내가 들고있는 무기에 달아줘야해서

 CallAttackEvent(stats.CurrentStat.attackSO);

라고 수정해주어야한다.

 

빨간줄을 없앴지만

또다른 스크립트의 빨간줄을 찾아 떠난다.

 

2. TopDownShooting

 

자 마우스를 가져다 대면 

우리 아까 <AttactSO>를 만들어주어서 그거에 대한 오류같다.

 

수정해보자!

 

그냥 괄호에 AttackSO넣어주면 끝

 

자 우리는 여기에 이제 투사체에 대해 좀더 넣어봐야한다.

 

먼저 변수 두개 만들기

( 변수 이름 괴랄함 주의 )

 

        float projectilesAngleSpace = rangedAttackSO.multipleProjectilesAngel;
        int numberOfProjectilesPerShot = rangedAttackSO.numberofProjectilesPerShot;

 

..너무 길지만

이래야 헷갈리지 않지요

 

우리는 이제 발사를 해야하는데

발사하려는 위치에서 아무곳에서 튀어나오면 매우 곤란하다

그래서 우리는 

아래에서 위로 발사하는 느낌을 줄 예정이다.

 

 

 

for문을 돌려서 발사에 생동감을 부여

 

전시간에 비워두었던 함수도 채워봅시다

 

그리고 중요한게

우리는 벡터를 몇도만큼 움직일 것인지에 대해 함수를 적어주어야한다.

아까 Q*V였기때문에

쿼터니언을 구해주어야하는데

오일러화를통해 각도를 구하고 곱하기 벡터해준다.

 

그다음 바로위에 새로운 클래스가 생겨져있다

그의 이름은..ProjectileController

 

새롭게 만들어 보자

 

ProjectileController.cs

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()
    {
        //평소 awake랑 다른점은 GetComponentInChildren이다.
        //스프라이트 랜더러는 무기 아래에서 가져오다보니..자식이라는것을 표시
        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);
    }
}

 

주석에 다 넣어서 문제가 없다고 생각하는데

가장 중요한부분이 

OnTriggerEnter이다.

 

트리거..뭐였지?

싶은데

충돌이랑 비슷한 느낌이지만 약간 다르다.

충돌은 부딪히는거라면

트리거는 닿았을때? 라고생각해주면 좋다.

 

벽에 부딪히면 충돌

해리포터처럼 9와3/4를 통과하는건 트리거

 

if문을 보시면..

IsLayerMatched라는 친구가있다.

이친구가 이제 레이어마스크가 일치하는지 보는 친구이다.

 

만약에

벨류값이 2, 레이어값이 1이면

100이고 / 010이다.

여기서 | or을 해주면

110 즉 3이된다.

즉 새로운 친구가 된다.

그래서 얘는 안들어간다.

 

즉 뭐 간단하게 말하면

새로운 비트가 들어오게되면 여기서 막힐 것이다.

 

자 이렇게 코드를 다 적었다면

유니티로 돌아와서

프리펩에 화살로 온다.

 

박스콜라이더와 리지드바디를 추가하고 설정

 

그다음!

우리는 

Trail Renderer을 추가해준다.

왼쪽끝에 빨간선을 잡고

y축을 0.07로 잡은 후

빨간선 오른쪽 끝으로 가서

점을 눌러주고

다시0으로 바꿔주면 저렇게 주르륵 내려간다.

 

다음 값을 설정해주고

중요한건 컬러

 

없어지게하는 느낌을 주게하기위해

오른쪽 위에 점을 눌러줘서

알파를 0으로 설정해줘서 점차 사라지게 만들어줘야한다.

 

그리고 밑에 materials 값을 디폴트라인으로 설정!

 

그리고 아까 추가했던 스크립트 넣기!

 

이제 실행해보면..!!

 

아주 잘된다!

 

신난다!!

 

다음에는 

투사체가 너무 많아지면..

어떡하지?!

그 해답을 찾으러 가보자!!