Unity Spaceship Controller
I have always been excited about making my own games and sharing them with others. This year, I took on learning Unity. Simulating rigid body physics is quite fun in Unity, but I wished to go further. So, in December, I took on trying to create a spaceship controller - with all assets of my own. I used Unity’s scripting system in C# to write the code for this controller, and Blender for creating the 3D models, for the main player, the enemies and some other assets.
I started with making simple controls for a cube to simulate the player, which was quickly replaced with a model I made in Blender, which was based on a swallow for it’s overall shape. The model is very low – poly, so it’s easier on the framerate, as well as making a collider for it easier.
To control the movement of the player’s spaceship, I declare the following variables, which will hold values for the thrust, pitching torque, yaw torque, roll torque and maximum velocity of the player.
public bool pressingThrottle = false;
public bool pressingBack;
public float pitchPower, rollPower, yawPower, enginePower, backthrust;
private float activeRoll, activePitch, activeYaw;
public float maxVelocity;
private Rigidbody rb;
In reality, a spaceship travelling through space should not have a cap on its velocity if it continues accelerating (except, of course, the speed of light); I believe that I want to continue to develop this simple controller into a game, having a cap on the speed limit would make it just that little bit easier for players to navigate.
In the start() function, which gets executed as soon as the script is executed, and not after that, I set up some constraints on the movement of the player, to make it more like actually moving in space. So, I removed angular drag and linear drag. I also removed Unity’s default gravitational acceleration.
private void Start()
{
// Ensure a Rigidbody is attached
rb = GetComponent();
rb.useGravity = false; // No gravity in space
rb.drag = 0f; // No linear drag
rb.angularDrag = 0f; // No angular drag
}
The player has basic AddForce statements in the Update() function, which simulate thrust acceleration, as well as pitch, yaw and roll by the thrusters. These were all given separate axes in the unity engine, which makes adding these forces and torques more intuitive, and easy. I also wrote a script to instantiate a particle system to simulate the thruster flames when the corresponding user inputs are given.
I felt that the player did not have a good sense of orientation while manoeuvring the spaceship. Which is why I added a sun as a source of reference for the player to orient themselves. It also doubles as a light source for the environment, as well as adding to the space aesthetic. The sun uses a Universal Render Pipeline shader – emitter material, which gives it the bloom. I have also made another particle system for the sun, to make the glowing bits of gas escaping its surface. These also use a URP shader. Currently the sun does no damage to the player.
I have also implemented laser bolts and homing missiles, as weapons for the player. I used volumetric lines for the laser bolts with a parented capsule collider. But, I did model the homing missile in Blender. The code for the laser bolt is quite simple. It instantiates a prefab at the position of the laser blaster in the spaceship model (an empty object which is a child of the main player), and it is given an initial impulse.
The homing missiles are more interesting to have a look at. They get instantiated two at a time, from twin points on either “wing” on the player, which are again empty objects which are children of the player model. These homing missiles are spawned in a similar fashion to the laser bolts, but instead of just receiving an initial impulse and going in that direction, these seek out all objects tagged “Enemy”, which are visible in the main camera, and then target the closest one to the player at the time zero. If no such targets are found, then the missile shoots off in the direction the player was facing. If there is such a target, then the missile calculates the distance to its target, and based on that, either chases it smoothly using a Slerp() function, or if the distance is less than a fixed parameter called “accuracyDistance”, the missile moves directly towards the object. There were problems with this approach wherein the missile would essentially just keep orbiting the target until it expired. So, to combat this, there is a final “Hail Mary” where the missile just moves to the position of the target, if it is close enough that it should have hit the target.
private void Start()
{
private Transform targetEnemy;
private Rigidbody rb;
public float initialSpeed = 600f; // Initial forward speed
public float homingSpeed = 600f; // Constant forward speed
public float turnSpeed = 5f; // Smooth turn speed
public float accuracyDistance = 10f; // Distance to switch to LookAt for accuracy
void Start()
{
rb = GetComponent();
if (rb == null)
{
Debug.LogError("Rigidbody component not found!");
return;
}
// Set initial forward velocity
rb.velocity = transform.forward * initialSpeed;
// Acquire target
AcquireTarget();
}
void AcquireTarget()
{
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
Camera mainCamera = Camera.main;
if (mainCamera == null)
{
Debug.LogError("Main Camera is not tagged correctly!");
return;
}
float closestDistance = Mathf.Infinity;
Transform closestEnemy = null;
foreach (GameObject enemy in enemies)
{
Transform enemyTransform = enemy.transform;
// Check visibility in the main camera
Vector3 viewportPoint = mainCamera.WorldToViewportPoint(enemyTransform.position);
bool isVisible = viewportPoint.z > 0 && viewportPoint.x > 0 && viewportPoint.x < 1 && viewportPoint.y > 0 && viewportPoint.y < 1;
if (isVisible)
{
float distance = Vector3.Distance(transform.position, enemyTransform.position);
if (distance < closestDistance)
{
closestDistance = distance;
closestEnemy = enemyTransform;
}
}
}
if (closestEnemy != null)
{
targetEnemy = closestEnemy;
Debug.Log("Target acquired: " + targetEnemy.name);
}
else
{
Debug.Log("No visible targets found.");
}
}
private void FixedUpdate()
{
if (targetEnemy == null)
return;
// Calculate the distance to the target
float distanceToTarget = Vector3.Distance(transform.position, targetEnemy.position);
if (distanceToTarget > accuracyDistance)
{
// Smoothly curve towards the target
Vector3 directionToTarget = (targetEnemy.position - transform.position).normalized;
Vector3 currentVelocity = rb.velocity.normalized;
Vector3 desiredVelocity = directionToTarget;
// Smoothly adjust the direction using Slerp
Vector3 newVelocity = Vector3.Slerp(currentVelocity, desiredVelocity, turnSpeed * Time.fixedDeltaTime);
// Update velocity based on homing speed
rb.velocity = newVelocity * homingSpeed;
}
if (distanceToTarget 10)
{
// Use LookAt for precision
transform.LookAt(targetEnemy);
rb.velocity = transform.forward * homingSpeed;
}
else
{
//Hail Mary
transform.position = targetEnemy.transform.position;
}
}
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.tag == "Enemy")
{
Destroy(gameObject);
}
}
}
The missile summons an explosion prefab when it collides with a target tagged “Enemy”, and the missile entity is destroyed.
I wanted to try the homing missiles and the laser blaster on moving enemies, so I made another spaceship in Blender, this time taking inspiration from a bee. This is because I gave it a very basic player-following algorithm, and it just rams into the player, destroying itself in the process. This “Kamikaze” tactic is similar to how bees usually kill themselves when they sting, so it felt like an opt design choice.

The script for the Kamikaze Enemy to follow the player is quite simple. The object checks for the position of the player, determines the normalised vector towards the player, and moves in that direction at a fixed velocity. A quaternion keeps information about its rotation, so the enemy can keep looking in the direction of its velocity. It gets destroyed when it collides with anything tagged “player” or “bullet”.
private GameObject player;
public float maxVelocity = 10f;
public float avoidanceRadius = 5f; // Radius to check for nearby enemies
private Rigidbody rb;
void Start()
{
player = GameObject.FindGameObjectWithTag("Player");
rb = GetComponent();
rb.useGravity = false;
rb.drag = 0f;
rb.angularDrag = 0f;
}
void Update()
{
// Step 1: Calculate velocity towards the player
Vector3 toPlayer = (player.transform.position - transform.position).normalized * maxVelocity;
// Step 2: Calculate avoidance velocity
Vector3 avoidanceVelocity = Vector3.zero;
Collider[] nearbyEnemies = Physics.OverlapSphere(transform.position, avoidanceRadius);
foreach (var collider in nearbyEnemies)
{
// Check if the collider is an enemy and not this spaceship
if (collider.gameObject != gameObject && collider.CompareTag("Enemy"))
{
Vector3 directionAway = transform.position - collider.transform.position;
float distance = directionAway.magnitude;
// Avoidance force is inversely proportional to distance
if (distance > 0) // Prevent division by zero
{
directionAway.Normalize();
avoidanceVelocity += directionAway * maxVelocity;
}
}
}
// Step 3: Combine player pursuit velocity with avoidance
Vector3 finalVelocity = toPlayer + avoidanceVelocity;
// Step 4: Apply velocity and limit it
rb.velocity = Vector3.ClampMagnitude(finalVelocity, maxVelocity);
// Step 5: Rotate to face the direction of velocity
if (rb.velocity.sqrMagnitude > 0.01f) // Avoid jitter when velocity is very low
{
Quaternion targetRotation = Quaternion.LookRotation(rb.velocity, Vector3.up);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 5f);
}
}
This project is still far, far from finished, and I do wish to turn it into a full-fledged game in the near future, with volumetric lighting, much better enemy AIs, planets, asteroids, UI and gizmos, HUDs and, of course, a story to tell. It cannot be overstated how crucial a story is for a game. I would like to train Ais for specific types of enemies, create boss battles, and improve the atmosphere of the game. One of the things I am still pondering about is the sound design. Of course, space doesn’t have a medium for sound to propagate, so realistically, there should not be much, if any, sound in this game. But adding sound to any game can do wonders for its immersion. I will have to work that out.