Humility

아무리 노력해도 최고가 되지 못할 수 있다⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀그럼에도 노력하는자가 가장 겸손한 것 아닌가

공부하는 블로그

유니티/기능구현

[Unity] 유니티 C#) FPS 주무기를 어떻게 구현할까? ( 샷건 )

새벽_글쓴이 2024. 10. 23. 03:18
반응형

이번엔 샷건을 구현해보자.

샷건은 다른 총들과 달리 생각해야할 점이 2가지가 있다.

1번째: 총 발사시, 쉘이 퍼져 나간다.

보통의 총들은 발사시에 한발씩 나가지만 샷건은 한번 발사시에 여러개의 쉘로 퍼져서 나간다

오버워치 로드호그 총 발사

 

2번째: 장전 시 한발씩 장전이 된다.

보통의 총들은 장전시에 탄창을 교환하는 방식으로 한번에 장전되지만 샷건은 한발씩 장전되어야 한다

펌프액션 샷건 장전

 

그럼 이제 샷건을 구현해보자.

 

1. 변수 설정

public class ShotGun : MainWeapon
{
    private bool canReset = true;  // 처음에만 총알 넣어주기 위해
    private float nextFireTime;  // 다음 발사 주기
    public TextMeshProUGUI ammoTxt;  // 탄약 UI 표시
    private int shell = 10;  // 발사되는 셸
    private float spreadAngle = 30f;  // 퍼지는각도

    protected override void Awake()
    {
        base.Awake();
        initializeAmmo = 50;  // 총기 최대 탄약
        maxLoadedAmmo = 5;  // 장전될 수 있는 탄약
        damage = 6;  // 데미지
        bulletRange = 5f;  // 총알 발사 거리
        fireRate = 1.26f;  // 총알 발사 주기
        recoilX = 0.5f;  // 좌우 반동
        recoilY = 10f;  // 수직 반동
        recoilRecoverySpeed = 5f;  // 반동 회복 속도
        reloadTime = 1.3f;  // 장전 시간  //0.18f + 1.12f + 0.5f+2.12f
        adsSpeed = 5;  // 정조준 속도
        adsFOV = 45;  // 정조준시 CameraFOV
        ResetAmmo(initializeAmmo);  // 탄약 세팅
    }

    // 시작할 때, 탄약 세팅 함수
    public void ResetAmmo(int _totalAmmo)
    {
        if (canReset)
        {
            loadedAmmo = maxLoadedAmmo;
            remainAmmo = _totalAmmo - loadedAmmo;
            canReset = false;
        }
    }
}

 

기존 라이플과 동일하게 설정을 해주고 추가적으로 쉘과 쉘이 퍼지는 각도 변수를 만들어 줬다.

탄약 세팅까지는 동일하다

 

2.슈팅과 장전

    // 슈팅 함수
    public override void Shoot(Transform _firePos)
    {
        if (Time.time >= nextFireTime)
        {
            nextFireTime = Time.time + fireRate;
            base.Shoot(_firePos);

            if (loadedAmmo <= 0)
            {
                Debug.Log("장전된 탄약 없음");
                Reload();
                canShoot = false;                 // 총알 없으면 슈팅 불가능
            }
        }

        // 장전 중이면 장전 끝
        if (isReloading && loadedAmmo > 0)
        {
            isReloading = false;
            return;
        }
    }

    // 장전 함수
    public override void Reload()
    {
        if (isReloading)
        {
            Debug.Log("재장전중");
            return;
        }

        if (loadedAmmo == maxLoadedAmmo)
        {
            Debug.Log("탄창 꽉참");
            return;
        }

        if (remainAmmo <= 0)
        {
            Debug.Log("남은 탄약 없음");
            return;
        }
        
        isReloading = true;
        loadedAmmo++;
        remainAmmo--;
        canShoot = true;
        isReloading = false;
    }

 

처음에 말했던 샷건은 장전이 한발씩 되야 하기 때문에, 부모 클래스에서 받아오지 않고 재정의를 해줬다.

 

또한, 한발씩 장전이 되다 보니까 장전 중 적이 나타나면 장전을 그만두고 싸워야 하니 장전을 멈출 수 있는 로직도 만들어줬다. 장전 중 마우스 왼쪽키를 누르면 장전이 끊기게 설계했다.

 

3. 발사 함수

샷건의 가장 큰 특성은 가까이서 쏘면 굉장한  강한 데미지를, 멀리서 쏘면  굉장히 약한 데미지를 입히게 된다.

이유는, 샷건의 탄 퍼짐은 쉽게 말해 원뿔 모양의 형태로 총구에서 부터 점점 퍼져나가기 때문이다.

아래 모양을 보면 조금 더 생각하기 쉬울 것이다.

원뿔모양

 

그럼 구현한 코드를 보여드리겠다.

public override void PlayerFireBullet()
{
    for (int i = 0; i < shell; i++)
    {
        Vector3 spreadDirection = CalculateSpreadDirection(spreadAngle, cam.transform);
        
        RaycastHit hit;

        if (canShoot)
        {
            Debug.DrawRay(cam.transform.position, spreadDirection * bulletRange, Color.red, 1f);
            if (Physics.Raycast(cam.transform.position, spreadDirection, out hit, bulletRange))
            {
                
                if ((canAttackMask.value & (1 << hit.transform.gameObject.layer)) == 0)
                {
                    continue;
                }
            }
        }
    }
}

// 탄퍼짐 계산 함수
private Vector3 CalculateSpreadDirection(float _speadAngle, Transform _firePos)
{
    // 균일한 원 내의 랜덤한 점 생성
    float randomRadius = Random.Range(0f, 1f);  // 원의 중심으로부터 거리
    float randomAngle = Random.Range(0f, 2f * Mathf.PI);  // 360도 사이의 무작위 각도

    // 원뿔모양의 표면의 값을 점으로 변환
    float x = Mathf.Cos(randomAngle) * randomRadius;
    float y = Mathf.Sin(randomAngle) * randomRadius;
    float z = Mathf.Sqrt(1f - randomRadius * randomRadius);  // z좌표는 점이 단위 구의 표면 위에 위치

    // 퍼짐 각도 적용
    Vector3 spreadVector = new Vector3(x, y, z);  // Vector3 값으로 변환
    // 각도를 정규화
    spreadVector = Vector3.Slerp(Vector3.forward, spreadVector, _speadAngle / 180f);

    // 총알 나가는 곳을 기준으로 확산
    return _firePos.rotation * spreadVector;
}

 

코드의 아래쪽 CalculateSpreadDirection 함수는 탄퍼짐을 계산해주는 함수이다.

 

1. 일단 원의 중심에서 랜덤한 거리( 0~1 )의 랜덤한 각도( 0 ~ 360도 ) 를 선언해주었다.

그림으로 말하자면 아래 사진 부분에 찍힐 점의 위치이다.

 // 원의 중심으로부터 거리
 float randomRadius = Random.Range(0f, 1f);                  

 // 360도 사이의 무작위 각도
float randomAngle = Random.Range(0f, 2f * Mathf.PI);


2. 그 후에 원뿔 표면상의 점을 계산해준다.

쉽게 말하자면 위에서 해준 작업은 2차원의 원 위에 찍힐 점의 위치를 정해준 것이고

이번에 할 작업은 3차원으로 바꿔준다고 생각하면 쉽다.

 

float x,y는 원 위의 점을 극좌표계에서 직교좌표계로 변환하고

float z는 피타고라스의 정리 함수인 sqrt를 이용해 단위구 표면위의 z좌표를 계산한다

이렇게 하면 xyz는 단위구 표면 위의 한 점이 된다.

float x = Mathf.Cos(randomAngle) * randomRadius;            
float y = Mathf.Sin(randomAngle) * randomRadius;
float z = Mathf.Sqrt(1f - randomRadius * randomRadius);

 

3. 이제 점의 위치를 구했으니 vector3로 변환하고 선형보간을 이용해 각도를 정규화 하면

지정된 퍼짐 각도 내에서 랜덤한 방향이 생성된다.

Vector3 spreadVector = new Vector3(x, y, z);
spreadVector = Vector3.Slerp(Vector3.forward, spreadVector, spreadAngle / 180f);

 

4. 마지막으로 총알이 발사될 위치에 곱해 리턴으로 넘겨주면 샷건의 탄퍼짐 계산이 끝난다.

return _firePos.rotation * spreadVector;

 

5. 계산된 값을 가져와 라이플처럼 대입만 해주면 샷건의 발사가 완성된다.

Vector3 spreadDirection = CalculateSpreadDirection(spreadAngle, cam.transform);

 

 

내가 구현한 샷건의 단점이라면 점이 찍히는 위치가 완전 랜덤이라 편차가 너무 크다는 점이다.

이부분은 레벨디자인 혹은 수정을 통해 보완해야할 것이다.

결과

반응형