Building a Cannonball Shooter Game: Part 1 — Cannon Controls

Simon Pham
10 min readAug 15, 2024

--

In this series, we’ll be building a simple cannonball shooter game that incorporates some math and physics knowledge in Unity. In this first blog post of the series, we’ll focus on the cannon controls.

Disclaimer: This series is inspired by a tutorial from Tarodev. You can check out his excellent tutorial and download the assets here.

Basic setups and properties

First, let’s create a script called Cannon and attach it to the cannon. In this script, I’ll get references to each of the cannon’s parts, including the two wheels and the barrel pivot, and assign the correct GameObjects to their slots in the Inspector.

[Header("Cannon Parts")]
[SerializeField] private Transform _barrelPivot;
[SerializeField] private Transform _leftWheel;
[SerializeField] private Transform _rightWheel;

Here’s a screenshot of its hierarchy:

Next, I’m going to create a variable for the rotation speed and give it a value of 80 in the Inspector.

[Header("Cannon Stats")]
[SerializeField] private float _rotSpeed;

Here’s a screenshot of the script component.

Vertical Control

Before diving into the coding part, we first need to determine which of the three axes is needed to control the barrel. When I manually rotate the barrel pivot, I notice that the rotation changes its value only on the x-axis.

Rotation changes its value on the x axis only

So, I’m thinking that we might be able to rotate the barrel using the Transform.Rotate method and pass in a rotation as a parameter. For this rotation parameter, we can use Vector3.right (1, 0, 0) and Vector3.left (-1, 0, 0). To make the rotation smooth, I’ll multiply the rotation by the rotation speed and Time.deltaTime.

For the key bindings, I’ll use the S key for downward rotation and the W key for upward rotation.

void Update()
{
VerticalControl();
}

void VerticalControl()
{
if (Input.GetKey(KeyCode.S))
{
_barrelPivot.Rotate(Vector3.right * _rotSpeed * Time.deltaTime);
}
else if (Input.GetKey(KeyCode.W))
{
_barrelPivot.Rotate(Vector3.left * _rotSpeed * Time.deltaTime);
}
}

And here’s what we have:

Horizontal Control

Similarly to the vertical control, we can rotate the entire cannon using the Transform.Rotate method and pass in a rotation parameter with Vector3.up (0, 1, 0) and Vector3.down (0, -1, 0) unit vectors. I’ll bind the left and right rotations to the A and D keys, respectively.

void Update()
{
HorizontalControl();
}

void HorizontalControl()
{
if (Input.GetKey(KeyCode.A))
{
transform.Rotate(Vector3.down * _rotSpeed * Time.deltaTime);
}
else if (Input.GetKey(KeyCode.D))
{
transform.Rotate(Vector3.up * _rotSpeed * Time.deltaTime);
}
}

To make things look a bit more interesting, we can rotate the wheels when the cannon rotates left and right. We can achieve this by using Vector3.forward (0, 0, 1) and Vector3.back (0, 0, -1).

Here’s the updated code:

void HorizontalControl()
{
if (Input.GetKey(KeyCode.A))
{
transform.Rotate(Vector3.down * _rotSpeed * Time.deltaTime);
_leftWheel.Rotate(Vector3.forward * _rotSpeed * 1.5f * Time.deltaTime);
_rightWheel.Rotate(Vector3.back * _rotSpeed * 1.5f * Time.deltaTime);
}
else if (Input.GetKey(KeyCode.D))
{
transform.Rotate(Vector3.up * _rotSpeed * Time.deltaTime);
_leftWheel.Rotate(Vector3.back * _rotSpeed * 1.5f * Time.deltaTime);
_rightWheel.Rotate(Vector3.forward * _rotSpeed * 1.5f * Time.deltaTime);
}
}

I also added a 1.5f speed modifier so the wheels spin a bit faster. Here’s the result:

Motion Limits

We can now rotate our cannon and its barrel in different directions, which is great. However, players can currently spin the cannon indefinitely or rotate its barrel 360 degrees, like this:

To prevent this from happening, I’ll implement limits on the vertical and horizontal motions.

Vertical limits

First, I’ll draw two straight lines: one from the pivot point of the barrel pointing upward (blue) and one from the pivot point of the cannon pointing forward (yellow). Additionally, I’ll draw a vector pointing forward from the barrel (green).

We need to calculate the angles formed between the green line and the blue and yellow lines, and limit these angles to between 0 and 90 degrees. That’s the explanation covered. Now, let’s get to the coding.

Variable declaration

First, I’ll create some variables:

// Limits
private bool _canRotateUpward;
private bool _canRotateDownward;

// Vectors
private Vector3 _initialForwardVector; //yellow line
private Vector3 _upwardVector; //blue line
private Vector3 _barrelForwardVector; //green line

// Angles
private float _verticalAngleUpward; //angle created by green and blue lines
private float _verticalAngleDownward; //angle created by green and yellow lines

Angles calculation

Let’s create a function to calculate the vertical angles.

void Start()
{
_initialForwardVector = transform.forward;
_upwardVector = transform.up;
}

void CalculateVerticalAngle()
{
_barrelForwardVector = _barrelPivot.forward;
_verticalAngleUpward = Vector3.Angle(_barrelForwardVector, _upwardVector);
_verticalAngleDownward = Vector3.Angle(_barrelForwardVector, _forwardVector);
}

The reason I define the upward and initial forward vectors in the Start method is that I want them to be fixed, unlike the barrel’s forward vector. In Unity, you can use the Vector3.Angle method to calculate the angle between two vectors in degrees.

Angles limits

I’ll create a function that enables or disables the corresponding motion when one of the angles reaches 90 degrees.

void VerticalLimits()
{
if (_verticalAngleUpward >= 90)
{
_canRotateDownward = false;
_canRotateUpward = true;
}
else if (_verticalAngleDownward >= 90)
{
_canRotateUpward = false;
_canRotateDownward = true;
}
else
{
_canRotateUpward = true;
_canRotateDownward = true;
}
}

Now, we can use the boolean variables in the VerticalControl function.

void VerticalControl()
{
if (Input.GetKey(KeyCode.S) && _canRotateDownward)
{
_barrelPivot.Rotate(Vector3.right * _rotSpeed * Time.deltaTime);
}
else if (Input.GetKey(KeyCode.W) && _canRotateUpward)
{
_barrelPivot.Rotate(Vector3.left * _rotSpeed * Time.deltaTime);
}
}

Since we’re making quite a few changes to our code, I’ll refactor it by isolating each part of the logic into its own function.

Note: this code snippet relates only to the vertical control and limits, and does not include the full code.

void Start()
{
_initialForwardVector = transform.forward;
_upwardVector = transform.up;
}

void Update()
{
HandleControls();
}


private void HandleControls()
{
VerticalControl();
LimitCannonAngle();
}

void VerticalControl()
{
if (Input.GetKey(KeyCode.S) && _canRotateDownward)
{
_barrelPivot.Rotate(Vector3.right * _rotSpeed * Time.deltaTime);
}
else if (Input.GetKey(KeyCode.W) && _canRotateUpward)
{
_barrelPivot.Rotate(Vector3.left * _rotSpeed * Time.deltaTime);
}
}

void LimitCannonAngle()
{
CalculateVerticalAngle();
VerticalLimits();
}

void VerticalLimits()
{
if (_verticalAngleUpward >= 90)
{
_canRotateDownward = false;
_canRotateUpward = true;
}
else if (_verticalAngleDownward >= 90)
{
_canRotateUpward = false;
_canRotateDownward = true;
}
else
{
_canRotateUpward = true;
_canRotateDownward = true;
}
}

void CalculateVerticalAngle()
{
_barrelForwardVector = _barrelPivot.forward;
_verticalAngleUpward = Vector3.Angle(_barrelForwardVector, _upwardVector);
_verticalAngleDownward = Vector3.Angle(_barrelForwardVector, _forwardVector);
}

Now, the barrel’s motion is limited to between 0 and 90 degrees.

Horizontal limits

The logic for horizontal limits is quite similar to that for vertical limits. I’ll reuse the initial forward vector (yellow) and introduce a new vector called the forward vector (red). The only difference is that the direction of the forward vector will be updated as the cannon rotates.

We’ll calculate the angle between these two vectors and limit it to between 0 and 60 degrees (though you can adjust the range to your preference).

Variables declaration

Let’s declare some variables that we’ll use.

// Limits
private bool _canRotateLeft;
private bool _canRotateRight;

// Vectors
private Vector3 _initialForwardVector; //yellow line
private Vector3 _forwardVector; //red line

// Angles
private float _horizontalAngle; //angle created by the two lines
private float _horizontalDir; //direction to which the cannon is rotating

Angle calculation

Let’s create a function to calculate the angle between the two vectors.

void Start()
{
_initialForwardVector = transform.forward;
}

void CalculateHorizontalAngle()
{
_forwardVector = transform.forward;
_horizontalAngle = Vector3.Angle(_initialForwardVector, _forwardVector);

// Calculate direction of the rotation
Vector3 crossProduct = Vector3.Cross(_initialForwardVector, _forwardVector);
_horizontalDir = Vector3.Dot(transform.up, crossProduct);
}

The reason I define the initial forward vector (yellow) in the Start method is that I want it to be fixed, unlike the forward vector (red).

To determine the direction in which the cannon is rotating, we use both the cross product and the dot product:

  • Cross Product: the cross product of two vectors results in a third vector that is perpendicular to both of them (visualized as a white line).
  • Dot Product: we then calculate the dot product of this perpendicular vector with the upward vector (visualized as a blue line). The result tells us whether the perpendicular vector is pointing more upward or downward relative to the upward vector. This helps determine if the cannon is rotating to the left or right.

By serializing the _horizontalDir variable, we can see that it is negative when the cannon rotates to the left and positive when it rotates to the right.

Angle limits

I’ll create a function that enables or disables the corresponding motion when one of the angles reaches 60 degrees in either direction.

void HorizontalLimits()
{
if (_horizontalAngle >= 60)
{
if (_horizontalDir < 0)
{
_canRotateLeft = false;
_canRotateRight = true;
}
else if(_horizontalDir > 0)
{
_canRotateRight = false;
_canRotateLeft = true;
}
}
else
{
_canRotateLeft = true;
_canRotateRight = true;
}
}

And now let’s update the HorizontalControl function.

void HorizontalControl()
{
if (Input.GetKey(KeyCode.A) && _canRotateLeft)
{
transform.Rotate(Vector3.down * _rotSpeed * Time.deltaTime);
_leftWheel.Rotate(Vector3.forward * _rotSpeed * 1.5f * Time.deltaTime);
_rightWheel.Rotate(Vector3.back * _rotSpeed * 1.5f * Time.deltaTime);
}
else if (Input.GetKey(KeyCode.D) && _canRotateRight)
{
transform.Rotate(Vector3.up * _rotSpeed * Time.deltaTime);
_leftWheel.Rotate(Vector3.back * _rotSpeed * 1.5f * Time.deltaTime);
_rightWheel.Rotate(Vector3.forward * _rotSpeed * 1.5f * Time.deltaTime);
}
}

Also, let’s refactor our code a little bit:

Note: this code snippet relates only to the horizontal control and limits, and does not include the full code.

void Start()
{
_initialForwardVector = transform.forward;
}

void Update()
{
HandleControls();
}

private void HandleControls()
{
HorizontalControl();
LimitCannonAngle();
}

void HorizontalControl()
{
if (Input.GetKey(KeyCode.A) && _canRotateLeft)
{
transform.Rotate(Vector3.down * _rotSpeed * Time.deltaTime);
_leftWheel.Rotate(Vector3.forward * _rotSpeed * 1.5f * Time.deltaTime);
_rightWheel.Rotate(Vector3.back * _rotSpeed * 1.5f * Time.deltaTime);
}
else if (Input.GetKey(KeyCode.D) && _canRotateRight)
{
transform.Rotate(Vector3.up * _rotSpeed * Time.deltaTime);
_leftWheel.Rotate(Vector3.back * _rotSpeed * 1.5f * Time.deltaTime);
_rightWheel.Rotate(Vector3.forward * _rotSpeed * 1.5f * Time.deltaTime);
}
}

void LimitCannonAngle()
{
CalculateHorizontalAngle();
HorizontalLimits();
}

void HorizontalLimits()
{
if (_horizontalAngle >= 60)
{
if (_horizontalDir < 0)
{
_canRotateLeft = false;
_canRotateRight = true;
}
else if(_horizontalDir > 0)
{
_canRotateRight = false;
_canRotateLeft = true;
}
}
else
{
_canRotateLeft = true;
_canRotateRight = true;
}
}

void CalculateHorizontalAngle()
{
_forwardVector = transform.forward;
_horizontalAngle = Vector3.Angle(_initialForwardVector, _forwardVector);

// Calculate direction of the rotation
Vector3 crossProduct = Vector3.Cross(_initialForwardVector, _forwardVector);
_horizontalDir = Vector3.Dot(transform.up, crossProduct);
}

With all that implemented, the cannon’s motion should now be limited both vertically and horizontally. That was quite a lot of work just to implement the motion limits, and you don’t have to do this if you prefer to rotate the cannon freely.

Here’s the full code, including the debug code used to draw the vectors.

using UnityEngine;
using Vector3 = UnityEngine.Vector3;

public class Cannon : MonoBehaviour
{
[Header("Cannon Parts")]
[SerializeField] private Transform _barrelPivot;
[SerializeField] private Transform _leftWheel;
[SerializeField] private Transform _rightWheel;

[Header("Cannon Stats")]
[SerializeField] private float _rotSpeed;

// Limits
private bool _canRotateUpward;
private bool _canRotateDownward;
private bool _canRotateLeft;
private bool _canRotateRight;

// Vectors
private Vector3 _initialForwardVector;
private Vector3 _forwardVector;
private Vector3 _upwardVector;
private Vector3 _barrelForwardVector;

// Angles
private float _horizontalAngle;
private float _horizontalDir;
private float _verticalAngleUpward;
private float _verticalAngleDownward;

void Start()
{
_initialForwardVector = transform.forward;
_upwardVector = transform.up;
}

void Update()
{
HandleControls();
}

private void HandleControls()
{
VerticalControl();
HorizontalControl();
LimitCannonAngle();
}

void VerticalControl()
{
if (Input.GetKey(KeyCode.S) && _canRotateDownward)
{
_barrelPivot.Rotate(Vector3.right * _rotSpeed * Time.deltaTime);
}
else if (Input.GetKey(KeyCode.W) && _canRotateUpward)
{
_barrelPivot.Rotate(Vector3.left * _rotSpeed * Time.deltaTime);
}
}

void HorizontalControl()
{
if (Input.GetKey(KeyCode.A) && _canRotateLeft) {
transform.Rotate(Vector3.down * _rotSpeed * Time.deltaTime);
_leftWheel.Rotate(Vector3.forward * _rotSpeed * 1.5f * Time.deltaTime);
_rightWheel.Rotate(Vector3.back * _rotSpeed * 1.5f * Time.deltaTime);
}
else if (Input.GetKey(KeyCode.D) && _canRotateRight) {
transform.Rotate(Vector3.up * _rotSpeed * Time.deltaTime);
_leftWheel.Rotate(Vector3.back * _rotSpeed * 1.5f * Time.deltaTime);
_rightWheel.Rotate(Vector3.forward * _rotSpeed * 1.5f * Time.deltaTime);
}
}

void LimitCannonAngle()
{
// Horizontal limits
CalculateHorizontalAngle();
HorizontalLimits();

// Vertical limits
CalculateVerticalAngle();
VerticalLimits();
}

void HorizontalLimits()
{
if (_horizontalAngle >= 60)
{
if (_horizontalDir < 0)
{
_canRotateLeft = false;
_canRotateRight = true;
}
else if(_horizontalDir > 0)
{
_canRotateRight = false;
_canRotateLeft = true;
}
}
else
{
_canRotateLeft = true;
_canRotateRight = true;
}
}

void VerticalLimits()
{
if (_verticalAngleUpward >= 90)
{
_canRotateDownward = false;
_canRotateUpward = true;
}
else if (_verticalAngleDownward >= 90)
{
_canRotateUpward = false;
_canRotateDownward = true;
}
else
{
_canRotateUpward = true;
_canRotateDownward = true;
}
}

void CalculateHorizontalAngle()
{
_forwardVector = transform.forward;
_horizontalAngle = Vector3.Angle(_initialForwardVector, _forwardVector);

// Calculate direction of the rotation
Vector3 crossProduct = Vector3.Cross(_initialForwardVector, _forwardVector);
_horizontalDir = Vector3.Dot(transform.up, crossProduct);
}

void CalculateVerticalAngle()
{
_barrelForwardVector = _barrelPivot.forward;
_verticalAngleUpward = Vector3.Angle(_barrelForwardVector, _upwardVector);
_verticalAngleDownward = Vector3.Angle(_barrelForwardVector, _forwardVector);
}


// Debugging
void OnDrawGizmos()
{
DrawForwardVector();
DrawInitialForwardVector();
DrawBarrelForwardVector();
DrawBarrelUpwardVector();
DrawCrossProductVector();
}

void DrawForwardVector()
{
Debug.DrawLine(transform.position, transform.position + transform.forward, Color.red);
}

void DrawInitialForwardVector()
{
Debug.DrawLine(transform.position, transform.position + _initialForwardVector, Color.yellow);
}

void DrawBarrelForwardVector()
{
Debug.DrawLine(_barrelPivot.position, _barrelPivot.position + _barrelPivot.forward, Color.green);
}

void DrawBarrelUpwardVector()
{
Debug.DrawLine(_barrelPivot.position, _barrelPivot.position + _upwardVector, Color.blue);
}

void DrawCrossProductVector()
{
Debug.DrawLine(transform.position, transform.position + Vector3.Cross(_initialForwardVector, _forwardVector), Color.white);
}

}

--

--