Arcing Projectiles in Unity

A frequently asked question in both the Unity forums and on Unity Answers is: How do I make a projectile arc to its target, like an arrow shot from a bow?  I've seen (and given) lots of different answers to this question, and honestly, most of them are unjustified hacks.

The right (and easy!) way to do this is: just add a bit of arc to your standard movement.  Objects in freefall (ignoring air resistance) follow a parabolic arc, and the equation for a parabola is very simple.  So, we can just use that equation to compute how must extra height we should have, and simply add it to our Y position, and the job is done.


To demonstrate this, let's make a simple projectile class which, for this first version, ignores the whole arcing problem and just flies straight at its target.

 

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

public class Projectile : MonoBehaviour {
	
	[Tooltip("Position we want to hit")]
	public Vector3 targetPos;
	
	[Tooltip("Horizontal speed, in units/sec")]
	public float speed = 10;

	Vector3 startPos;
	
	void Start() {
		// Cache our start position, which is really the only thing we need
		// (in addition to our current position, and the target).
		startPos = transform.position;
	}
	
	void Update() {
		// Compute the next position -- straight flight
		Vector3 nextPos = Vector3.MoveTowards(transform.position, targetPos, speed * Time.deltaTime);

		// Rotate to face the next position, and then move there
		transform.rotation = LookAt2D(nextPos - transform.position);
		transform.position = nextPos;
		
		// Do something when we reach the target
		if (nextPos == targetPos) Arrived();
	}
	
	void Arrived() {
		Destroy(gameObject);
	}
	
	/// 
	/// This is a 2D version of Quaternion.LookAt; it returns a quaternion
	/// that makes the local +X axis point in the given forward direction.
	/// 
	/// forward direction
	/// Quaternion that rotates +X to align with forward
	static Quaternion LookAt2D(Vector2 forward) {
		return Quaternion.Euler(0, 0, Mathf.Atan2(forward.y, forward.x) * Mathf.Rad2Deg);
	}
}

There's nothing terribly tricky going on here.  We just use MoveTowards to compute the next position at each step.  To make the projectile sprite actually point at its target, I'm using a handy little function called LookAt2D, which returns the rotation needed to make a sprite point in a given forward direction.  This simple straight-line version could have just done that once in Start, but since we're about to make a nice arc, I went ahead and did it on every frame in Update.

The result, not surprisingly, looks like this.

OK, so how would we add a pretty parabolic arc to this motion?  We need one more public property, to define how high we want the arc to be; and we need to change the code in the Update method, that computes the next position.  The rest of the script is unchanged, but here's the whole thing anyway.

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

public class Projectile : MonoBehaviour {
	
	[Tooltip("Position we want to hit")]
	public Vector3 targetPos;
	
	[Tooltip("Horizontal speed, in units/sec")]
	public float speed = 10;
	
	[Tooltip("How high the arc should be, in units")]
	public float arcHeight = 1;
	
	
	Vector3 startPos;
	
	void Start() {
		// Cache our start position, which is really the only thing we need
		// (in addition to our current position, and the target).
		startPos = transform.position;
	}
	
	void Update() {
		// Compute the next position, with arc added in
		float x0 = startPos.x;
		float x1 = targetPos.x;
		float dist = x1 - x0;
		float nextX = Mathf.MoveTowards(transform.position.x, x1, speed * Time.deltaTime);
		float baseY = Mathf.Lerp(startPos.y, targetPos.y, (nextX - x0) / dist);
		float arc = arcHeight * (nextX - x0) * (nextX - x1) / (-0.25f * dist * dist);
		nextPos = new Vector3(nextX, baseY + arc, transform.position.z);
		
		// Rotate to face the next position, and then move there
		transform.rotation = LookAt2D(nextPos - transform.position);
		transform.position = nextPos;
		
		// Do something when we reach the target
		if (nextPos == targetPos) Arrived();
	}
	
	void Arrived() {
		Destroy(gameObject);
	}
	
	/// 
	/// This is a 2D version of Quaternion.LookAt; it returns a quaternion
	/// that makes the local +X axis point in the given forward direction.
	/// 
	/// forward direction
	/// Quaternion that rotates +X to align with forward
	static Quaternion LookAt2D(Vector2 forward) {
		return Quaternion.Euler(0, 0, Mathf.Atan2(forward.y, forward.x) * Mathf.Rad2Deg);
	}
}

Notice that we're now using MoveTowards only on the X value; we don't want to confuse things by getting Y in there as well. Instead, we compute a base Y by just interpolating from the start Y to the end Y, and then compute how much arc to add in. That (-0.25f * dist * dist) expression is just a scaling factor needed to make the maximum arc work out to whatever arc height you specified.

The result looks like this.

And of course it works for targets that aren't at the same level as the source, arcing up or down appropriately.

So!  The next time you have arrows, cannon balls, or other projectiles to rain down on your enemies, I hope you find this code useful.