오늘 주제는 투사체를 날리는데 필요한 공식과 구현 코드를 남겨 두려고 한다.
여러개를 준비했는데, 직선 이동, 곡선이동, 부메랑 이동, 멀티샷 까지 하나씩 코드를 보도록 하자..
그 전에 공통적 사용가능한 기능들은 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);
}
}
public class LinearProjectile : Projectile
{
protected override void Move()
{
transform.position += direction * speed * Time.deltaTime;
}
}

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;
}
}

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);
}
}
}

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;
}
}
