Building a Cannonball Shooter Game: Part 3 — Trajectory Line
In this third blog post of the series, we’ll create a trajectory line to help us aim better.
Disclaimer: This series is inspired by a tutorial from Tarodev. You can check out his excellent tutorial and download the assets here.
Setting up targets
First, I’ll create a target prefab that has a platform and a bunch of cubes.
Next, I’ll set up three of this in the scene.
Each cube is also a prefab that has a Rigidbody and a Box Collider component.
Additionally, they have a simple script called Box that destroys them after one second of touching the ground.
using UnityEngine;
public class Box : MonoBehaviour
{
private void OnCollisionEnter(Collision collision)
{
if (collision.transform.CompareTag("Ground"))
Destroy(gameObject, 1f);
}
}
For the cannon, I’ll adjust the Force and Rotation Speed properties a little bit.
Now we can destroy the cubes with our cannon.
This looks great; however, I want to increase the power of our cannonball so that it can knock down more cubes. To do this, we can use the Physics.OverlapSphere method to cast a sphere at the contact point, and then use the AddExplosionForce method to create a push.
[SerializeField] private float _explosionRadius;
[SerializeField] private float _explosionForce;
[SerializeField] float _upwardModifier;
private Vector3 _hitPoint;
private void OnCollisionEnter(Collision collision)
{
CreateSmallPush(collision);
if (collision.transform.CompareTag("Ground"))
Destroy(gameObject, 0.5f);
}
private void CreateSmallPush(Collision collision)
{
if (collision.transform.CompareTag("Target"))
{
ContactPoint contact = collision.contacts[0];
_hitPoint = contact.point;
Collider[] colliders = Physics.OverlapSphere(_hitPoint, _explosionRadius);
foreach (Collider collider in colliders)
{
if (collider.TryGetComponent<Rigidbody>(out Rigidbody rb))
{
Vector3 dir = collider.transform.position - _hitPoint;
rb.AddExplosionForce(_explosionForce, _hitPoint, _explosionRadius, _upwardModifier, ForceMode.Impulse);
}
}
}
}
Here’s the result.
Creating a trajectory line
Creating a simulated physics scene
We will create a new, separate scene from the main scene to simulate the process of firing projectiles every frame. This will provide us with the positions of the projectiles, which will form an arc. We can then use a Line Renderer component to turn the arc into a trajectory line.
First, I’ll create a new script called Projection and attach it to the cannon. The obstacle parent object contains all the objects that we want to duplicate into the simulated scene so let’s create reference for it, and assign it to its slot.
[Header("Scene Objects")]
[SerializeField] private Transform _obstaclesParent;
Next, let’s declare a variable for the simulation scene and its physics scene.
[Header("Scene Objects")]
private Scene _simulationScene;
private PhysicsScene _physicsScene;
To create a new scene, we’ll need to use the CreateScene method of the SceneManager class. And to access this class, we’ll need to include its namespace (UnityEngine.SceneManagement). After that, we can get the physics scene using the GetPhysicsScene method.
using UnityEngine;
using UnityEngine.SceneManagement;
void Start()
{
CreatePhysicsScene();
}
private void CreatePhysicsScene()
{
_simulationScene = SceneManager.CreateScene("Simulation", new CreateSceneParameters(LocalPhysicsMode.Physics3D));
_physicsScene = _simulationScene.GetPhysicsScene();
}
When I hit the Play button, I could see that a new scene called Simulation was created in the Hierarchy window.
Moving objects to the new scene and disabling their Renderer components
Now that we've successfully created a new scene, it's time to duplicate and move the obstacle object and its child objects to this scene. For the duplicated objects, since we only need their Collider and Rigidbody components for collision, we can disable their meshes to avoid seeing identical objects in the main scene.
I’ll create a new function to duplicate and move objects to a new scene.
private GameObject InstantiateAndMoveObjectsToSimulationScene(GameObject prefab, Vector3 pos, Quaternion rotation)
{
var obj = Instantiate(prefab, pos, rotation);
SceneManager.MoveGameObjectToScene(obj, _simulationScene);
return obj;
}
Because the obstacle parent object contains a hierarchical structure with three levels of nested children, I’ll create a function to disable the objects’ Renderer components (meshes) in all nested child objects.
private void DisableRenderers(Transform parentObj)
{
// disable renderer component in parent object (level 1)
if (parentObj.TryGetComponent<Renderer>(out Renderer renderer))
renderer.enabled = false;
// disable renderer components in child objects (level 2)
if (parentObj.childCount > 0)
{
foreach (Transform childObj_1 in parentObj.transform)
{
if(childObj_1.TryGetComponent<Renderer>(out Renderer renderer_child_1))
renderer_child_1.enabled = false;
// disable renderer components in child objects (level 3)
if (childObj_1.childCount > 0)
{
foreach (Transform childObj_2 in childObj_1.transform)
{
if(childObj_2.TryGetComponent<Renderer>(out Renderer renderer_child_2))
renderer_child_2.enabled = false;
}
}
}
}
}
Let’s call these functions inside the CreatePhysicsScene method.
private void CreatePhysicsScene()
{
_simulationScene = SceneManager.CreateScene("Simulation", new CreateSceneParameters(LocalPhysicsMode.Physics3D));
_physicsScene = _simulationScene.GetPhysicsScene();
foreach (Transform obj in _obstaclesParent)
{
GameObject ghostObj = InstantiateAndMoveObjectsToSimulationScene(obj.gameObject, obj.position, obj.rotation);
DisableRenderers(ghostObj.transform);
}
}
In Play mode, you can see that all nested child objects have been moved to the new scene and their meshes have been disabled.
Simulate a trajectory line
Setting up Line Renderer component
In the Inspector, let’s add a Line Renderer to the cannon.
I set the Color property to a green gradient and Texture Mode to Tile (since I wanted the line to be dotted instead of solid). For the line material, I created a new one called Trajectory Line where I used a dot sprite for the Albedo property.
Next, in the Projection script, I’ll declare a couple new variables and assign values to them in the Inspector.
[Header("Trajectory Line")]
[SerializeField] private int _maxPhysicsFrameIterations;
[SerializeField] private LineRenderer _lineRenderer;
[SerializeField] private float _lineWidth;
Preventing trajectory line from intereacting with the targets
Before we create a function to simulate a trajectory line, I want to modify the Init function in the Cannonball script. I’ll create a new boolean variable called _isGhost, and set it to true to prevent the trajectory line from interacting with objects in the environment. When we actually launch cannonballs, we will set the variable to false.
private bool _isGhost;
public void Init(Vector3 velocity, bool isGhost)
{
_isGhost = isGhost;
_rb.AddForce(velocity, ForceMode.Impulse);
}
Now we need to modify the Fire function (in the Cannon script) since it includes the Init method. I’ll set _isGhost to false because we use this function to actually launch cannonballs.
void Fire()
{
var ball = Instantiate(_cannonballPrefab, _cannonBallSpawn.position, _cannonBallSpawn.rotation);
ball.Init(_cannonBallSpawn.forward * _force, false);
}
Next, we can use the _isGhost boolean in the OnCollisionEnter method (in the Cannonball script) to prevent the collision between the trajectory line and the targets.
private void OnCollisionEnter(Collision collision)
{
if (_isGhost) return;
CreateSmallPush(collision);
if (collision.transform.CompareTag("Ground"))
Destroy(gameObject, 0.5f);
}
Simulate trajectory line
In the Projection script, I’ll create a new function called SimulateTrajectoryLine to visualize the line in our scene.
public void SimulateTrajectoryLine(Cannonball cannonballPrefab, Vector3 pos, Vector3 velocity)
{
GameObject ghostObj = InstantiateAndMoveObjectsToSimulationScene(cannonballPrefab.gameObject, pos, Quaternion.identity);
ghostObj.GetComponent<Cannonball>().Init(velocity, true);
_lineRenderer.positionCount = _maxPhysicsFrameIterations;
_lineRenderer.startWidth = _lineWidth;
for (var i = 0; i < _maxPhysicsFrameIterations; i++)
{
_physicsScene.Simulate(Time.fixedDeltaTime);
_lineRenderer.SetPosition(i, ghostObj.transform.position);
}
Destroy(ghostObj.gameObject);
}
Next, we can call this function in the Cannon script.
private void FixedUpdate()
{
_projection.SimulateTrajectoryLine(_cannonballPrefab, _cannonBallSpawn.position, _cannonBallSpawn.forward * _force );
}
Notes: When dealing with physics in Unity, it’s recommended to use Time.fixedDeltaTime (instead of Time.deltaTime) and FixedUpdate (instead of Update) to maintain stability and consistency.
Now, let’s see the result.
You can see that the trajectory line looks a bit odd. This is because we set the Texture Mode property of the Line Renderer to Tile, and the value of the Titling property of the material is only 1 which doesn’t allow the texture to repeat frequently enough.
To fix this, you can increase the Titling value and I’ll go with 20. Here’s the result.
If you want the dots to be bigger or smaller, you can experiment with the Line Width property in the Projection script and the Tilting property to get a desired result.
Animate trajectory line
To animate the trajectory line, we first need to create a new Animator Controller and attach it to the cannon. Next, I’ll create a new Animation called Idle and check the Loop Time checkbox to ensure it repeats constantly.
Inside the Animator, make sure to connect the Entry node to the Idle node.
Open the Idle animation and add the Material_Main Tex_ST property. Set it to start at 3 on the z-axis and end at 0 after 60 frames.
To prevent the animation from starting and stopping periodically, select the Curves button, press Ctrl + A to select all the curves, right-click on one of the dots, and choose Both Tangents > Linear.
Here’s the result.