Unity Physics: Predicting Trajectories with Physics Simulations
It is commonplace these days to see a trajectory UI when the player is getting read to throw an object. This article covers how to accomplish this by using a physics scene for running the simulation. Let’s get started!
Setting up the Projectile
I am going to use this sci-fi box prefab variant as the projectile to be shot out from a turret.
The prefab has rigidbody and box collider components as well as a Flying Box script.
The Flying Box class inherits from this ILaunchable interface, which takes parameters for direction and force.
public interface ILaunchable
{
//use for applying force to rigidbodys
public void Launch(Vector3 direction, float force);
}
This behavior uses physics based movement on the rigidbody, so the Flying Box class requires a rigidbody component. If your object does not have a rigidbody already, this will create one for you when you add the script to the projectile.
If you are not requiring a rigidbody at the top of the script, the Init function will try to get a rigidbody component, and then create one if there isn’t one already.
The Launch method multiplies the direction and the force to get a velocity value, and then adds force and torque to the rigidbody. Because I am accessing this through an interface, the Flying Box is not done initializing by the time I called the Launch method, which is why I have an additional check for the rigidbody to initialize it before applying the force.
[RequireComponent(typeof(Rigidbody))]
public class FlyingBox : MonoBehaviour, ILaunchable
{
[SerializeField] private Rigidbody _rb;
private void Start()
{
Init();
}
public void Launch(Vector3 direction, float force)
{
Debug.Log("Launching");
Vector3 velocity = direction * force;
Debug.Log("Velocity is " + velocity);
if (_rb == null)
Init();
_rb.AddForce(velocity, ForceMode.Impulse);
_rb.AddRelativeTorque(transform.right * force, ForceMode.Impulse);
}
private void Init()
{
if (_rb == null & TryGetComponent(out Rigidbody rigidbody))
{
_rb = rigidbody;
}
else if (_rb == null)
{
Rigidbody rb = gameObject.AddComponent<Rigidbody>();
_rb = rb;
}
}
}
Instantiating the Projectile
This plasma cannon will act as the holder for the Launcher script. The goal here is to launch the package through the cutout in the glass wall, and let the player see the trajectory before firing.
The Launcher is a child of the main turret body, and the script has references to the Flying Box projectile, the force and direction to apply to it, a Lab Complete script that you can ignore for the purposes of this article, and a Physics Simulation script.
The Update method checks for the mouse left-click button before instantiating the package and applying force to it via the ILaunchable interface.
The Fixed Update method contacts the Physics Simulation script and passes in the package prefab, a position, a direction and a force value. More on this shortly. The important takeaway here is, running this in update will become out of sync with the physics simulation and result in an inconsistent and shaky looking line render. Using Fixed Update ensures that the physics time-step is in sync.
public class Launcher : MonoBehaviour
{
[SerializeField] private float _force = 10f;
[SerializeField] private Vector3 _direction;
[SerializeField] private GameObject _package;
[SerializeField] private LabComplete _labComplete;
//ref to simulated physics script
[SerializeField] private PhysicsSimulation _physicsSimulation;
private void Start()
{
_physicsSimulation = FindObjectOfType<PhysicsSimulation>();
if (_physicsSimulation == null)
Debug.LogWarning("The Physics Simulation script could not be found.");
if (_package == null)
Debug.LogWarning("You need to assign the package prefab to this script in the inpector.");
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Mouse0))
{
//instantiate prefab
GameObject package = Instantiate(_package, this.transform.position, this.transform.rotation);
//check for interface
ILaunchable launchable = package.GetComponent<ILaunchable>();
//pass in direction and force
if (launchable != null)
launchable.Launch(transform.forward, _force);
_labComplete.CheckCriteria();
}
}
private void FixedUpdate()
{
//call simulated trajectory from simulated scene script
//pass in prefab and position direction and force
_physicsSimulation.SimulateTrajectory(_package, transform.position, transform.forward, _force);
}
}
Here is the package launching on user input.
Creating the Physics Simulation Scene
This script will instantiate the needed environment and projectile objects into the simulated scene, and then use a line renderer to visualize the trajectory.
The parent object that has all of the physics based colliders will need to be instantiated into the simulated scene for the flying box to bounce off of.
Here are the selected objects in the scene view.
Let’s break down the Physics Simulation script piece by piece. For starters, you will need to use the UnityEngine.SceneManagement namespace for this example. There are two scene references, one for a regular Scene and another for a Physics Scene. There is a Transform variable to store the parent object of the needed environmental objects, a Line Renderer and an int value to use to determine the iterations we have in our line simulation.
using UnityEngine;
using UnityEngine.SceneManagement;
public class PhysicsSimulation : MonoBehaviour
{
//ref to new simulated scene
private Scene _simulatedScene;
//ref to physics scene of simulated scene
private PhysicsScene _physicsScene;
//ref to lab parent
[SerializeField] private Transform _labParent;
[SerializeField] private LineRenderer _lineRenderer;
[SerializeField] private int _maxPhysicsIterations;
}
I am finding the lab parent object by Tag, but you can also just drag it into the inspector to assign it. After the line renderer and parent object are initialized, the Create New Simulated Scene function is called.
private void Start()
{
//set the lab parent reference
_labParent = GameObject.FindGameObjectWithTag("Lab10").transform.root;
if (_lineRenderer == null & TryGetComponent<LineRenderer>(out LineRenderer renderer))
_lineRenderer = renderer;
//call new create simulated physics scene method
CreateNewSimulatedScene();
}
Moving Objects between Scenes
The Create New Simulated Scene function creates a new scene and assigns the scene parameters to LocalPhysicsMode.Physics3D. The GetPhysicsScene method is used to assign the physics scene variable after the new simulated scene is created. The lab parent transform is used to look for child objects with the Obstacle tag, then instantiates the duplicates, turns off the mesh renderer of the original objects, and then moves the duplicates to the physics scene.
//create new method for creating physics simulated scene
private void CreateNewSimulatedScene()
{
//set created scene to simulated scene reference
_simulatedScene = SceneManager.CreateScene("New Physics Simulation", new CreateSceneParameters(LocalPhysicsMode.Physics3D));
//set new physics scene from simulated scene
_physicsScene = _simulatedScene.GetPhysicsScene();
foreach (Transform obstacle in _labParent)
{
if (obstacle.CompareTag("Obstacle"))
{
var simulatedObstacle = Instantiate(obstacle.gameObject, obstacle.position, obstacle.rotation);
if (obstacle.GetComponent<MeshRenderer>() != null)
obstacle.GetComponent<MeshRenderer>().enabled = false;
SceneManager.MoveGameObjectToScene(simulatedObstacle, _simulatedScene);
}
}
}
Instantiating Simulated Objects and setting the Line Renderer
If you remember, this Simulate Trajectory method is being called in Fixed Update in the Launcher script. The Flying Box object prefab is being passed in as a parameter and instantiated at the position of the launcher. It’s mesh renderer is disabled before the object is moved to the physics scene, and the ILaunchable interface triggers the physics force behavior.
The line renderer positions count is assigned the value of the maxPhysicsIterations value from the inspector (20). The for loop simulates the physics scene for each object, as well as sets the line renderers position to the position of the flying box. After the for loop has exceeded the maxPhysicsIteration values, the loop ends and the box is destroyed.
//create new method to simulate trajectory
public void SimulateTrajectory(GameObject obj, Vector3 pos, Vector3 direction, float force)
{
// ref to simulated object / flying box
GameObject objectToLaunch = Instantiate(obj, pos, Quaternion.identity);
//disable simulated object renderer
if (objectToLaunch.GetComponent<MeshRenderer>() != null)
objectToLaunch.GetComponent<MeshRenderer>().enabled = false;
//move simulated object to simulated physics scene
SceneManager.MoveGameObjectToScene(objectToLaunch, _simulatedScene);
//have simulated box apply velocity
ILaunchable launchable = objectToLaunch.GetComponent<ILaunchable>();
if (launchable != null)
launchable.Launch(direction, force);
//set the amount of points in the line renderer
_lineRenderer.positionCount = _maxPhysicsIterations;
//forloop set position of line renderer based on max physics interactions value
for (int i = 0; i < _maxPhysicsIterations; i++)
{
//simulate the physics scene
_physicsScene.Simulate(Time.fixedDeltaTime * 5);
//set position of each point on line rendrer using simulated object position
_lineRenderer.SetPosition(i, objectToLaunch.transform.position);
}
//destroy simulated object
Destroy(objectToLaunch);
}
}
That’s it for this article on creating a trajectory UI for the user. I hope you enjoyed the journey and thanks for reading!