TIL) 1:1 전용 AutoAimShooter 리팩토링 — 직선/포물선 발사 정리

김보근·2025년 8월 27일

Unity

목록 보기
113/113
post-thumbnail

TIL) 1:1 전용 AutoAimShooter 리팩토링 — 직선/포물선 발사 정리

오늘 한 일: 1:1 게임인 걸 감안해서 자동 조준/발사를 가볍게 리팩토링했다.
핵심은 타깃 탐색 제거(=TargetRef에 단일 적 캐시), 직선/포물선 두 가지 발사 모드, 리드샷 옵션 제거.
나중에 봐도 바로 이해할 수 있게 수식/의도/셋업 체크리스트까지 정리.

  • 1:1이면 매 프레임 적을 다시 찾을 필요가 없다 → TargetRef에 적 Transform만 들고 쏜다.

  • 발사 방식은 두 가지:

  • 직선: velocity = dir * speed (총알 gravityScale = 0)

  • 포물선: 정점(arcHeight) 을 기준으로 초기속도 v0 = (v0x, v0y) 계산 (총알 gravityScale > 0)

  • 쿨타임/사거리/방향 연출은 AutoAimShooter 안에서 해결.

코드

TargetRef

using UnityEngine;

public class TargetRef : MonoBehaviour
{
    [SerializeField] private Transform target;
    public Transform Target => target;

    public void Bind(Transform t) => target = t;
    public void Clear() => target = null;

    void Start()
    {
        if (!target)
        {
            var go = GameObject.FindWithTag("Enemy");
            if (go) target = go.transform; // 1:1이면 이것만으로 충분
        }
    }
}

AutoAimShooter

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

public class AutoAimShooter : MonoBehaviour
{
    [Header("Refs")]
    [SerializeField] private TargetRef targetRef;   // 1:1 대상 보관소(적 Transform)
    [SerializeField] private Transform firePoint;   // 총알이 나갈 위치

    [Header("Fire")]
    [SerializeField] private float fireRate = 3f;   // 초당 발사 수
    [SerializeField] private float range = 0f;      // 0이면 사거리 무제한
    [SerializeField] private bool useBallistic = true; // true: 포물선, false: 직선

    [Header("Straight")]
    [SerializeField] private float straightSpeed = 16f; // 직선 발사 속도

    [Header("Ballistic")]
    [SerializeField] private float speedHint = 12f;     // 비행시간 추정용 속도 힌트
    [SerializeField] private float minT = 0.25f, maxT = 0.9f; // 비행시간 최소/최대
    [SerializeField] private float arcHeight = 1.5f;    // 정점(꼭대기) 높이

    float shootTimer;

    void Awake()
    {
        if (!firePoint) firePoint = transform;
        if (!targetRef) targetRef = FindObjectOfType<TargetRef>();
    }

    void Update()
    {
        // 1) 타깃 없거나 비활성이면 아무것도 안 함
        var tgt = targetRef ? targetRef.Target : null;
        if (!tgt || !tgt.gameObject.activeInHierarchy) return;

        // 2) 사거리 제한(옵션)
        if (range > 0f)
        {
            float sq = ((Vector2)tgt.position - (Vector2)transform.position).sqrMagnitude;
            if (sq > range * range) return;
        }

        // 3) 발사 타이밍
        shootTimer -= Time.deltaTime;
        if (shootTimer <= 0f)
        {
            Shoot(tgt);
            shootTimer = 1f / fireRate;
        }
    }

    void Shoot(Transform tgt)
    {
        if (useBallistic) ShootBallistic(tgt);
        else              ShootStraight(tgt);
    }

    // ───────────────────────── 직선 발사 ─────────────────────────
    void ShootStraight(Transform tgt)
    {
        Vector2 origin = firePoint.position;
        Vector2 dir = ((Vector2)tgt.position - origin).normalized;

        FireVelocity(dir * straightSpeed);
    }

    // ───────────────────────── 포물선 발사 ─────────────────────────
    void ShootBallistic(Transform tgt)
    {
        Vector2 origin = firePoint.position;
        Vector2 dest   = tgt.position;

        var sample   = ObjectPoolManager.Instance.Get(PoolKey.PlayerBullet);
        var sampleRB = sample.GetComponent<Rigidbody2D>();
        float g      = Physics2D.gravity.y * (sampleRB ? sampleRB.gravityScale : 1f); // 보통 음수
        float gAbs   = Mathf.Abs(g);
        sample.SetActive(false);

        // 1) 목표 정점(꼭대기) 높이
        Vector2 dp   = dest - origin; // (Δx, Δy)
        float  yApex = Mathf.Max(origin.y, dest.y) + Mathf.Max(0.01f, arcHeight);

        // 2) 올라가는 구간
        float hUp = Mathf.Max(0.001f, yApex - origin.y);
        float v0y = Mathf.Sqrt(2f * gAbs * hUp); // 위(+y)로 가는 초기 y속도
        float tUp = v0y / gAbs;                  // 올라가는 시간 t = v/g

        // 3) 내려가는 구간
        float hDown = Mathf.Max(0f, yApex - dest.y);
        float tDown = Mathf.Sqrt(2f * hDown / gAbs);

        // 4) 총 비행시간 T
        float T   = Mathf.Clamp(tUp + tDown, minT, maxT);

        // 5) 수평 속도 v0x
        float v0x = dp.x / Mathf.Max(0.001f, T);

        // 6) 초기 속도 벡터 v0
        Vector2 v0 = new Vector2(v0x, v0y);

        FireVelocity(v0, orient: true);
    }

    // ───────────────────────── 총알 발사 공통 처리 ─────────────────────────
    void FireVelocity(Vector2 v0, bool orient = true)
    {
        var go = ObjectPoolManager.Instance.Get(PoolKey.PlayerBullet);
        go.transform.position = firePoint.position;

        if (orient) go.transform.up = v0.normalized; // 스프라이트 '앞'을 진행 방향으로

        var b  = go.GetComponent<Bullet>();
        var rb = go.GetComponent<Rigidbody2D>();

        if (b != null)
        {
            // Bullet이 내부에서 rb.velocity를 세팅하고 활성화까지 처리
            b.Initialize(BulletTeam.Player, v0);
        }
        else if (rb)
        {
            rb.velocity = v0;
            go.SetActive(true);
        }
    }
}

전제: Bullet.Initialize(BulletTeam, Vector2 velocity)는 내부에서Rigidbody2D.velocity = velocity 및 활성화/수명 리셋을 처리한다.

동작 원리(의도와 이유)

왜 TargetRef?

  • 1:1이면 매 프레임 OverlapCircle로 적 탐색은 낭비.

  • 적 리스폰/교체 시에만 TargetRef.Bind(newEnemyTransform) 한 번 호출하면 끝.

  • 발사 컴포넌트는 항상 같은 타깃 참조를 사용 → 가볍고 예측 가능.

Update의 3단계

  • 타깃 유효성: 적이 비활성/사망이면 조준/발사 중단.

  • 사거리 체크(옵션): range == 0이면 무제한. 그 외에는 제곱거리로 빠르게 검사.

  • 발사 타이밍: shootTimerfireRate에 맞춰 일정한 간격으로 Shoot() 호출.

직선 발사

방향만 정규화해서 v = dir * straightSpeed.

직선 탄은 총알 Rigidbody2D.gravityScale = 0이 안정적.

튜닝 포인트

  • 발사 간격: fireRate 증가 → 더 자주 발사

  • 사거리: range = 0 무제한 / 값 주면 제한

  • 포물선 높이: arcHeight ↑ → 탄도가 더 높고 부드러움

  • 비행 시간: minT/maxT로 너무 느린/빠른 탄 방지

  • 직선 속도: straightSpeed로 명중까지 시간 조절

profile
게임개발자꿈나무

0개의 댓글