본문 바로가기
Unity/Undead Survivor

범위 감지(Scanner) / 무기(Weapon)와 총알(Bullet)

by hwan91 2025. 3. 18.

Scanner.cs

플레이어 주변의 목표(적, 아이템 등)를 탐색하는 스크립트로, 가장 가까운 적을 찾아 자동 조준하는 역할을 한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Scanner : MonoBehaviour
{
    public float scanRange; // 스캔 반경(범위)
    public LayerMask targetLayer; // 스캔 대상 레이어(감지 대상이 속한)
    public RaycastHit2D[] targets; // 스캔 결과 리스트(감지된 적들을 저장할 배열)
    public Transform nearstTarget; // 가장 가까운 대상(적)

    // 주기적으로 주변을 탐색
    void FixedUpdate()
    {
        /* .CircleCastAll = 원형 탐색, 원형의 Cast를 쏘고 모든 결과를 반환하는 함수
         * 속성 설명
         * 1. 캐스팅 시작 위치
         * 2. 원의 반지름
         * 3. 캐스팅 방향(Vector2.zero = 특정 방향 없이 정적인 원형 탐색)
         * 4. 캐스팅 거리(0 = 추가 거리 없이 현재 위치에서 즉시 스캔)
         * 5. 대상 레이어 */
        
        // 현재 위치에서 반지름 scanRange 크기의 원을 생성하여 해당 범위 안의 targetLayer에 속한 오브젝트를 찾아 리스트에 저장
        targets = Physics2D.CircleCastAll(transform.position, scanRange, Vector2.zero, 0, targetLayer);
        nearstTarget = GetNearest(); // 가장 가까운 적 찾기
    }

    // 가장 가까운 적 찾기
    Transform GetNearest()
    {
        Transform result = null; // 가장 가까운 적
        float diff = 100; // 최소한의 거리, 큰 값을 넣어 초기 설정

        // RaycastHit = 충돌한 오브젝트 대한 2D 물리 충돌 정보를 담는 구조체
        foreach (RaycastHit2D target in targets) // 스캔 결과 오브젝트를 하나씩 접근
        {
            Vector3 myPos = transform.position; // 플레이어 위치
            Vector3 targetPos = target.transform.position; // 감지된 적 위치
            // .Distance = 둘의 거리를 자동 계산해주는 함수
            float curDiff = Vector3.Distance(myPos, targetPos);// 위 둘의 거리 계산

            // 가장 가까운 적 갱신
            if (curDiff < diff) // 현재 적과의 거리가 기존 최소 거리보다 가까우면
            {
                diff = curDiff; // 더 짧은 거리로 최소 거리 업데이트
                result = target.transform; // 해당 적을 가장 가까운 적으로 설정
            }
        }

        return result; // 가장 가까운 적 반환
    }
}

Bullet.cs

총알을 관리하는 스크립트로, 플레이어가 발사하는 원거리 공격(투사체, 예: 총알, 마법탄 등)을 관리하는 역할을 한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    public float damage; // 데미지
    public int per; // 관통력
    /* per > 0 -> 일정 횟수까지만 관통
     * per == -1 -> 무한 관통(예: 레이저 같은 무기)
     * per == -100 -> 근접 무기 판정(투사체가 아니라 타격 효과만 존재) */

    Rigidbody2D rigid; // 원거리 공격의 물리적 움직임 설정을 위해 가져옴

    void Awake()
    {
        rigid = GetComponent<Rigidbody2D>();
    }

    // 총알의 초기 설정
    public void Init(float damage, int per, Vector3 dir) // dir = 원거리 공격이 날아갈 방향
    {
        // this = 해당 Class의 변수로 접근
        this.damage = damage;
        this.per = per;

        if (per >= 0) // 관통력이 -1(무한)이 아닌 경우에만
        {
            // velocity = 속도
            rigid.velocity = dir * 15f; // 특정 방향(dir)으로 15의 속도로 이동
        }
    }

    // 적과 충돌 시 처리(관통력 설정)
    void OnTriggerEnter2D(Collider2D collision)
    {
        // || = or
        if (!collision.CompareTag("Enemy") || per == -100) // 충돌한 오브젝트가 적이 아니라면 or 근접 무기라면
            return;
        per--;

        if (per < 0) // 관통력이 사라지면
        {
            rigid.velocity = Vector2.zero; // 재활용을 위해 속도 초기화
            gameObject.SetActive(false); // 오브젝트 풀링으로 재사용하기 위해 오브젝트 비활성화
        }
    }

    // 화면을 벗어나면(일정 거리를 벗어난) 투사체 비활성화
    void OnTriggerExit2D(Collider2D collision)
    {
        if (!collision.CompareTag("Area") || per == -100) // 총알이 화면 밖으로 나가면 or 근접 무기라면
            return;

        gameObject.SetActive(false); // 비활성화하여 리소스 절약
    }
}

Weapon.cs

플레이어가 사용하는 무기를 관리하는 스크립트로, 장착하는 무기가 공격 방식에 맞게 동작하도록 설정하는 역할을 한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Weapon : MonoBehaviour
{
    public int id; // 무기의 고유 ID
    public int prefabId; // Object Pool에서 무기 프리팹을 가져오기 위한 ID
    public float damage; // 무기 데미지
    public int count; // 무기 개수
    public float speed; // 회전 속도(근거리), 연사 속도(원거리)

    float timer; // 발사 시간 간격
    Player player; // 플레이어 캐릭터 정보

    void Awake()
    {
        // 이전 코드의 단점 = 무기가 플레이어 오브젝트의 자식이어야만 작동
        // player = GetComponentInParent<Player>(); // 부모 오브젝트에 있는 플레이어 컴포넌트 가져오기

        // 현재 코드의 장점 = 무기가 어디에 있든지 플레이어 정보를 가져올 수 있음
        // 게임 내 플레이어를 전역적으로 가져올 수 있어 더 안정적
        player = GameManager.instance.player;

        /* 기존 방식은 부모-자식 관계를 강제하는 구조였기 때문에 유연성이 떨어짐
         * 새로운 방식은 언제 어디서든 플레이어를 가져올 수 있어 유지보수하기 쉽고, 코드 안정성이 증가함
         * 플레이어가 무조건 하나만 존재하는 게임이라면 GameManager를 통해 접근하는 게 더 나은 선택 */
    }

    void Update()
    {
        if (!GameManager.instance.isLive) // 게임이 끝나면 무기 작동 종료
            return;

        switch (id)
        {
            case 0: // 근거리 무기
                // Vector3.back = (0, 0, 1) -> Z축을 기준으로 회전
                // speed값을 곱해 회전 속도 조절
                transform.Rotate(Vector3.back * speed * Time.deltaTime);
                break;
            default: // 원거리 무기
                timer += Time.deltaTime;

                if (timer > speed) // 일정 시간이 지나면 실행(speed 만큼 연사 속도 설정)
                {
                    timer = 0f; // 초기화해서 다시 0부터 카운트
                    Fire();
                }
                break;
        }
    }

    // 무기 레벨업
    public void LevelUp(float damage, int count)
    {
        this.damage = damage * Character.Damage; // 캐릭터의 능력치에 따라 데미지 조정
        this.count += count; // 무기 개수 증가

        /* 근거리 무기는 플레이어 주변을 회전하며 공격하는 방식이기 때문에
         * 무기의 개수가 늘어나거나 줄어들면 균등한 간격으로 재배치 필요 */
        if (id == 0)
            Batch();

        /* BroadcastMessage() 함수란?
         * 특정 함수를 자신과 모든 자식 오브젝트에게 전달하는 함수
         * 현재 오브젝트와 그 아래 계층(Hierarchy) 모든 오브젝트가 해당 함수를 실행하려고 시도함
         * 사용 방법 = gameObject.BroadcastMessage("함수명", 매개변수(생략가능), 옵션);
         
         * SendMessageOptions.DontRequireReceiver 옵션은?
         * 객채에 해당 함수가 존재하지 않아도 오류 없이 무시
         
         * 무기뿐만 아니라 다른 장비에도 능력치 변화가 반영되어야 하므로
         * 한 번에 모든 장비의 ApplyGear() 함수를 실행시키기 위해 사용 */

        // 플레이어의 모든 자식 오브젝트(무기, 장비 등)에 ApplyGear() 함수가 있다면 실행
        player.BroadcastMessage("ApplyGear", SendMessageOptions.DontRequireReceiver);
    }

    // 무기 초기화
    public void Init(ItemData data)
    {
        // 기본 설정
        name = "Weapon " + data.itemId; // itemId를 가져와 무기의 이름을 "Weapon [ID]" 형태로 설정
        transform.parent = player.transform; // 현재 무기의 부모를 플레이어로 설정
        transform.localPosition = Vector3.zero; // 무기의 위치를 플레이어 기준 (0, 0, 0)으로 설정

        // 속성 설정, data에 저장된 무기의 기본 공격력과 개수를 적용
        id = data.itemId;
        damage = data.baseDamage * Character.Damage;
        count = data.baseCount + Character.Count;

        // 프리팹 ID 찾기
        // Object Pool에서 해당 무기의 발사체(projectile) 프리팹을 찾아 그 index를 prefebId에 저장
        for (int index = 0; index < GameManager.instance.pool.prefabs.Length; index++)
        {
            if (data.projectile == GameManager.instance.pool.prefabs[index])
            {
                prefabId = index;
                break;
            }
        }

        // id에 따라 다른 초기화 방식(무기를 다르게 설정)
        switch (id)
        {
            case 0: // 근거리 무기
                speed = 150 * Character.WeaponSpeed; // 회전 속도 설정
                Batch(); // 무기 생성
                break;
            default: // 원거리 무기
                speed = 0.5f * Character.WeaponRate; // 연사 속도(총알 생성 속도) 설정
                break;
        }

        // 무기에 맞는 손 설정
        Hand hand = player.hands[(int)data.itemType]; // data.itemType이 enum값 이므로 (int)로 강제 형변환
        hand.spriter.sprite = data.hand; // 현재 장착한 무기의 손 이미지(Sprite 타입)
        hand.gameObject.SetActive(true); // 오브젝트 활성화

        player.BroadcastMessage("ApplyGear", SendMessageOptions.DontRequireReceiver);
    }

    // 근거리 무기 배치
    void Batch()
    {
        for (int index=0; index < count; index++) // count만큼 무기 생성
        {            
            Transform bullet; // 무기를 저장할 변수 선언

            // 기존 오브젝트를 먼저 재사용하고, 부족하면 새로 생성
            // childCount = 자신의 자식 오브젝트 개수 확인하는 속성
            if (index < transform.childCount)
            {
                // 기존 무기가 있으면 GetChild(index)로 가져오기
                bullet = transform.GetChild(index);
            }
            else
            {
                bullet = GameManager.instance.pool.Get(prefabId).transform; // 부족하면 Object Pool에서 새로운 무기를 가져와 생성
                // .parent = 현재 오브젝트의 자식으로 설정, 플레이어를 따라 움직이게 만듦
                bullet.parent = transform; // 현재 무기를 부모로 설정
            }

            // Vector3.zero == (0, 0, 0)
            bullet.localPosition = Vector3.zero; // 위치를 플레이어와 동일한 위치로 초기화
            bullet.localRotation = Quaternion.identity; // 회전값 초기화(무기 방향 재설정)

            // count = 4일 때 -> index 0 = 0도, index 1 = 90도, index 2 = 180도, index 3 = 270도
            Vector3 rotVec = Vector3.forward * 360 * index / count; // 360도를 count로 나눠 각도를 계산
            bullet.Rotate(rotVec); // 해당 각도만큼 회전, 이게 없으면 모든 무기가 같은 방향을 보게 됨

            // bullet.up == (0, 1, 0)
            /* Space.World = 월드 좌표계, 전체 게임 씬(혹은 세계)에서의 절대적인 위치
             * Space.Self = 부모 객체를 기준으로 오브젝트의 위치, 회전, 크기를 정의
             * Self 사용 시 무기가 현재 회전 상태에 따라 이동 방향이 달라질 수 있어
             * World를 사용하여 회전 상태에 관계없이 고정된 방향으로 이동하게 함 */
            bullet.Translate(bullet.up * 1.5f, Space.World); // 무기와 플레이어가 겹치지 않게 일정 거리만큼 떨어지도록 설정

            // 방향이 필요 없는 근거리 무기이므로 Vector3.zero로 설정
            bullet.GetComponent<Bullet>().Init(damage, -100, Vector3.zero); // 생성된 무기의 데미지와 관통력 설정
        }
    }

    // 원거리 공격 발사
    void Fire()
    {
        if (!player.scanner.nearstTarget) // 공격할 적이 없으면 함수 종료
            return;

        Vector3 targetPos = player.scanner.nearstTarget.position; // 가장 가까운 적 위치
        // 이 벡터는 거리에 대한 정보(크기)도 포함하므로, normalized(현재 Vector의 방향은 유지하고 크기를 1로 변환) 사용
        Vector3 dir = (targetPos - transform.position).normalized; // 플레이어에서 적 위치 까지의 방향

        Transform bullet = GameManager.instance.pool.Get(prefabId).transform; // Object Pool에서 미리 생성된 총알 가져옴
        bullet.position = transform.position; // 총알이 플레이어 위치에서 시작하도록 설정
        
        // FromToRotation(A, B) = A 방향을 B 방향으로 회전시키는 함수\
        // 총알의 기본 방향이 Vector.up(위쪽 (0, 1, 0))이므로 dir(적 방향)으로 회전
        bullet.rotation = Quaternion.FromToRotation(Vector3.up, dir); // 지정된 축을 중심으로 target을 향해 회전하는 함수
        bullet.GetComponent<Bullet>().Init(damage, count, dir); // 총알의 속성 설정

        AudioManager.instance.PlaySfx(AudioManager.Sfx.Range);
    }
}