Implementing Parabolic Trajectory Projectiles in Unity
The ProjectileController class manages the parabolic motion of a projectile using a quadratic Bezier curve for path calculation and visualization.
public class ProjectileController
{
private Transform launchPoint, impactPoint, curveControl;
private float velocity, peakAltitude;
private Vector3 movementDirection = Vector3.zero;
private Vector3[] trajectoryPoints;
private int currentPointIndex = 5;
private int curveSegments = 30;
private LineRenderer pathVisualizer = null;
private bool isInFlight = false;
private Vector3 initialPosition = Vector3.zero;
private Vector3 initialRotation = Vector3.zero;
public void Setup(Transform launch, Transform control, Transform target, float projectileVelocity, float maxHeight, LineRenderer visualizer = null)
{
launchPoint = launch;
curveControl = control;
impactPoint = target;
velocity = projectileVelocity;
peakAltitude = maxHeight;
pathVisualizer = visualizer;
initialPosition = launch.position;
initialRotation = launch.eulerAngles;
}
public void LaunchProjectile()
{
if (isInFlight) return;
trajectoryPoints = GenerateBezierPath(launchPoint.position, curveControl.position, impactPoint.position, curveSegments);
isInFlight = true;
}
public void UpdateTrajectory()
{
RenderPath();
if (isInFlight)
{
if (currentPointIndex >= trajectoryPoints.Length)
{
CompleteFlight();
return;
}
movementDirection = (trajectoryPoints[currentPointIndex] - launchPoint.position).normalized;
launchPoint.LookAt(trajectoryPoints[currentPointIndex]);
launchPoint.position += movementDirection * Time.deltaTime * velocity;
if (Vector3.Distance(launchPoint.position, trajectoryPoints[currentPointIndex]) <= 0.5f) currentPointIndex += 1;
}
}
private void CompleteFlight()
{
isInFlight = false;
currentPointIndex = 5;
launchPoint.gameObject.SetActive(false);
launchPoint.position = initialPosition;
launchPoint.eulerAngles = initialRotation;
launchPoint.gameObject.SetActive(true);
}
private void RenderPath()
{
if (pathVisualizer == null) return;
for (int i = 1; i <= curveSegments; i++)
{
float interpolationFactor = i / (float)curveSegments;
Vector3 curvePosition = ComputeBezierPoint(interpolationFactor, initialPosition, curveControl.position, impactPoint.position);
pathVisualizer.positionCount = i;
pathVisualizer.SetPosition(i - 1, curvePosition);
}
}
private Vector3 ComputeBezierPoint(float t, Vector3 startPos, Vector3 controlPos, Vector3 endPos)
{
float oneMinusT = 1 - t;
float tSquared = t * t;
float oneMinusTSquared = oneMinusT * oneMinusT;
Vector3 position = oneMinusTSquared * startPos;
position += 2 * oneMinusT * t * controlPos;
position += tSquared * endPos;
return position;
}
public Vector3[] GenerateBezierPath(Vector3 startPos, Vector3 controlPos, Vector3 endPos, int segments)
{
Vector3[] pathArray = new Vector3[segments];
for (int i = 1; i <= segments; i++)
{
float t = i / (float)segments;
Vector3 point = ComputeBezierPoint(t, startPos, controlPos, endPos);
pathArray[i - 1] = point;
}
return pathArray;
}
}
To utilize this system, create a MonoBehaviour script that initialiezs and controls the proejctile.
public class ProjectileDemo : MonoBehaviour
{
[SerializeField] Transform launchPoint, impactPoint, curveControl;
[SerializeField] float peakAltitude = 40;
[SerializeField] float projectileVelocity = 30;
[SerializeField] LineRenderer pathVisualizer;
private ProjectileController projectile = new ProjectileController();
private void Awake()
{
projectile = new ProjectileController();
projectile.Setup(launchPoint, curveControl, impactPoint, projectileVelocity, peakAltitude, pathVisualizer);
}
private void Update()
{
if (Input.GetMouseButton(0)) { projectile.LaunchProjectile(); }
projectile.UpdateTrajectory();
}
}