[TIL] Projectile 만들기

Dreamer·2024년 11월 11일

1. 오늘 주제

오늘 주제는 투사체를 날리는데 필요한 공식과 구현 코드를 남겨 두려고 한다.
여러개를 준비했는데, 직선 이동, 곡선이동, 부메랑 이동, 멀티샷 까지 하나씩 코드를 보도록 하자..

2. 코드

그 전에 공통적 사용가능한 기능들은 abstract class로 만들어 두자.

[RequireComponent(typeof(SpriteRenderer))]
public abstract class Projectile : MonoBehaviour
{
	// 이동 속도
    public float speed = 2;
    // 일정 시간 후 사라지게
    public float playTime = 2f;
    // Destroy를 할건지 숨길건지!
    public bool shouldDestroyOnTime;
    // 
    public Vector3 direction;

    private float timer = 0;

    public bool IsPlaying { get; protected set; }

    protected Vector3 startPosition;
    protected Transform targetTransform;
    protected SpriteRenderer spriteRenderer;

    protected abstract void Move();

    private void Awake()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
        spriteRenderer.sprite = Resources.Load<Sprite>("Sprites/Circle");
        transform.localScale = new Vector3(0.2f, 0.2f, 0.2f);

    }

    protected virtual void Update()
    {
        if (IsPlaying)
        {
            Move();
            timer += Time.deltaTime;
            if (timer >= playTime)
            {
                IsPlaying = false;
                timer = 0;
                Destroy();
            }
        }
    }

    public virtual void Play(Transform startTransform, Transform targetTransform)
    {
        if (IsPlaying) return;

        IsPlaying = true;
        this.startPosition = startTransform.position;
        this.targetTransform = targetTransform;
        transform.position = this.startPosition;
        direction = (targetTransform.position - startPosition).normalized;
    }

    public virtual void Play(Transform startTransform)
    {
        if (IsPlaying) return;

        IsPlaying = true;
        this.startPosition = startTransform.position;
        transform.position = this.startPosition;
    }

    protected void Destroy()
    {
        if (shouldDestroyOnTime)
            Destroy(gameObject);
        else
            gameObject.SetActive(false);
    }
}
  1. 직선 이동
    직선 이동은 딱히 추가할 만한건 없고 그냥 position만 속도조절하여 변경해주자.
public class LinearProjectile : Projectile
{

    protected override void Move()
    {
        transform.position += direction * speed * Time.deltaTime;
    }
}

  1. 포물선 이동
    포물선 이동은 각도와 순간발사할 힘을 설정해주면 포물선을 그리면서 중력에 의해 낙하 하도록 할 수 있다.
public class ParabolicProjectile : Projectile
{
    [Header("포물선 각도")]
    [Range(0f, 360f)] public float shootAngle = 45;
    [Range(0f, 20f)] public float force = 7;
    [Header("속도")]
    [SerializeField] private Vector3 initVelocity;

    private float elapaedTime = 0;
    private Vector3 gravity = new Vector3(0, -9.81f, 0);

    protected override void Move()
    {
        elapaedTime += speed * Time.deltaTime;
        Vector3 position = startPosition + initVelocity * elapaedTime + 0.5f * gravity * Mathf.Pow(elapaedTime, 2);
        transform.position = position;
    }

    public override void Play(Transform startTransform, Transform targetTransform)
    {
        if (IsPlaying) return;

        IsPlaying = true;
        this.startPosition = startTransform.position;
        this.targetTransform = targetTransform;
        transform.position = this.startPosition;
        direction = (targetTransform.position - startPosition).normalized;

        float angleRad = shootAngle * Mathf.Deg2Rad;
        float fowardVelocity = Mathf.Cos(angleRad) * force;
        float upVelocity = Mathf.Sin(angleRad) * force;

        initVelocity = direction * fowardVelocity + Vector3.up * upVelocity;

        elapaedTime = 0f;
    }
}

  1. 멀티샷
    멀티샷은 직선으로 날아가는 투사체를 방사각도와 갯수를 정해주면 방향과 각도 내에서 방사할 수 있도록 해준다.
public class MultiplyProjectile : Projectile
{
    [Header("멀티샷 설정")]
    public float spreadAngle; // 방사 각도 -spreadAngle ~ spreadAngle 까지
    public int numberOfProjectiles; // 멀티샷 수

    protected override void Move()
    {
        // LinearProjectile을 생성만 해주기때문에 구현하지 않는다.
    }

    public override void Play(Transform startTransform, Transform targetTransform)
    {
        spriteRenderer.enabled = false;

        float angleStep = spreadAngle / (numberOfProjectiles - 1);
        float startAngle = -spreadAngle / 2;

        for (int i = 0; i < numberOfProjectiles; i++)
        {
            GameObject go = new GameObject("MultiplyLinearProjectile");
            LinearProjectile projectile = go.AddComponent<LinearProjectile>();
            projectile.direction = direction;
            float angle = startAngle + (angleStep * i);
            projectile.direction = Quaternion.Euler(0, 0, angle) * direction;
            projectile.speed = speed;
            projectile.shouldDestroyOnTime = true;
            projectile.Play(startTransform);
        }
    }
}

  1. 부메랑 이동
    부메랑처럼 목표지점으로 갔다가 되돌아 오는 이동 방식이다. 필요한 설정값만 지정해주면 목표 지점에 갔다가 돌아오게 된다. 부메랑 이동을 잘 커스텀하면 통통 튀는 투사체도 가능할 것 같다.
public class BoomerangProjectile : Projectile
{
    // 곡선 높이를 외부에서 조절
    public float curveHeightOffset = 2f; // 곡선의 높이
    // 시작 지점 출발한 시간(돌아오는 경우에는 목표지점에서 출발한 시간)
    private float startTime;
    // 돌아오는 중인지
    private bool returning;
    // 시작지점과 목표지점 거리
    private float distance;

    // 목표 지점까지 도달하면 시작, 목표 위치는 바뀌어야 함.
    private Vector3 startPoint; // 시작 위치
    private Vector3 endPoint; // 목표 위치
    private float curveHeight; // 곡선 높이 (목표지점까지 도달하면 반대 쪽 곡선을 그리면서 돌아옴)

    protected override void Update()
    {
        Move();
    }

    protected override void Move()
    {
        float distCovered = (Time.time - startTime) * speed;
        float fractionOfJourney = distCovered / distance;

        if (fractionOfJourney >= 1f && !returning)
        {
            returning = true;
            startTime = Time.time;
            SetDestPoint(endPoint, startPoint);
            curveHeight = -curveHeightOffset;
        }
        else if (fractionOfJourney >= 1f && returning)
        {
            Destroy();
            IsPlaying = false;
        }

        Vector3 currentPos = Vector3.Lerp(startPoint, endPoint, fractionOfJourney);
        currentPos.y += Mathf.Sin(fractionOfJourney * Mathf.PI) * curveHeight;
        transform.position = currentPos;
    }

    public override void Play(Transform startTransform, Transform targetTransform)
    {
        if (IsPlaying) return;
        IsPlaying = true;
        startTime = Time.time;
        transform.position = this.startPosition;
        SetDestPoint(startTransform.position, targetTransform.position);
        distance = Vector3.Distance(startPosition, targetTransform.position);

        returning = false;
        curveHeight = curveHeightOffset;
    }

    private void SetDestPoint(Vector3 start, Vector3 end)
    {
        this.startPoint = start;
        this.endPoint = end;
    }
}

profile
새로운 시작

0개의 댓글