Maths for Games — Ship Builder • Year 2 • Build + Flight (WIP)

Engine: Unity 2022 LTS  •  Language: C#  •  Focus: Custom maths, grid systems, physics, and gameplay programming

Overview

A two-phase technical project for my Maths for Games module. The Build Phase lets you assemble a ship on a 3D grid using my own math types (vectors/quaternions/TRS). The Flight Phase reconstructs the saved layout into a physical craft, computes COM/inertia, maps thrusters to controls, and flies in a low-gravity asteroid field with spatial-hash broad-phase and swept-sphere collision resolution. Work in progress.

Build Phase

  • Snap-to-grid placement and discrete rotation with a preview ghost; commit to build.
  • Orbit/zoom build camera focused around a point; scroll zooms toward cursor hit.
  • Hotbar selects part data; save pipeline serialises grid position + rotation (TRS) for each part.

Footage

Image Gallery

Build grid and part previews Hotbar and part selection

Flight Phase

The flight scene reconstructs the saved ship, recentres on its computed centre of mass, calculates a diagonalised moment of inertia, and flies using forces/torques from distributed thrusters. The scene also spawns drifting asteroids and runs a spatial-hash broad-phase with per-frame swept sphere checks to resolve collisions.

Rebuild ship from save

A spawner reads serialised part entries (type, grid position, rotation) and instantiates the ship, grouping any disconnected fragments into separate movers.

// FlightShipSpawner — rebuild ship from ShipSaveData, centre on COM, split fragments
ShipSaveData data = LaunchManager.Instance.savedShip;
foreach (var partData in data.parts) {
  // pick prefab by type; compute TRS(partData.gridPosition, partData.rotation, 1)
  // place/rotate, parent under ShipRoot, register into ShipMover
}
mover.CenterOnCOM();
// find disconnected clusters -> keep main cluster on ShipRoot, spawn loose fragments

See the spawner and save format definitions.

Low-G flight, thrust & torque

Each thruster contributes a force and a torque τ = r × F relative to COM; inputs map to canonical directions, and angular velocity integrates into a quaternion delta each frame.

// ShipMover — integrate forces/torques; update rotation via angular velocity
MyVector3 totalForce = 0, totalTorque = 0;
foreach (var t in thrusters) {
  if (Input.GetKey(t.assignedKey)) {
    MyVector3 F = t.thrustDirection * thrustPower;
    totalForce += F;
    totalTorque += MyVector3.Cross(t.localPosition, F);
  }
}
velocity += (totalForce / ComputeTotalMass()) * Time.deltaTime;
position += velocity * Time.deltaTime;

MyVector3 angAcc = new MyVector3(
  totalTorque.x / max(momentOfInertia.x, 0.01f),
  totalTorque.y / max(momentOfInertia.y, 0.01f),
  totalTorque.z / max(momentOfInertia.z, 0.01f)
);
angularVelocity += angAcc * Time.deltaTime;
MyQuaternion dq = ShipMover.FromAngularVelocity(angularVelocity * Time.deltaTime);
transform.rotation = (dq * currentRotation).Normalize().ToUnity();

Thruster mapping, COM recentring, inertia calculation, and update loop.

Camera chase (quaternion orbit)

A chase camera orbits the target using custom Euler→Quaternion math; position lerps for smoothness, then LookAt target.

// FlightCamera — yaw/pitch control, quaternion orbit, smooth follow
yaw += mouseX * sens; pitch = clamp(pitch - mouseY * sens, -80, 80);
MyQuaternion q = EulerToQuaternion(pitch, yaw, 0);
MyVector3 desired = targetPos + q.Rotate(new MyVector3(0, 0, -distance));
currentPosition = Lerp(currentPosition, desired, smooth * Time.deltaTime);

Asteroid field & motion

Procedural asteroid spawning with random scale/velocity; inertia approximated as a solid sphere. Boundary bounce reflects velocity against an outer sphere.

// AsteroidSpawner — random prefab, scale/mass, velocity; add AsteroidMotion if missing
GameObject asteroid = Instantiate(prefab, pos, rot);
motion.velocity = randomDir.Normalize() * Random.Range(30f, 80f);
motion.radius = boundsExtentsMagnitude * scale;
motion.mass = Lerp(100f, 1000f, scaleT);
// AsteroidMotion — integrate position + rotation; boundary reflection
position += velocity * dt;
MyQuaternion dq = ShipMover.FromAngularVelocity(angularVelocity * dt);
rotation = (dq * rotation).Normalize();
// reflect if leaving spherical boundary
if (distFromCenter > boundaryRadius - radius) {
  MyVector3 n = toCenter.Normalize();
  velocity = velocity - n * 2f * MyVector3.Dot(velocity, n);
}

Spatial hashing & swept collisions

Broad-phase uses a spatial hash (voxel grid) to restrict pair checks, then per-cell swept sphere tests for continuous collision with time-of-impact and post-collision integration.

// SpatialHashGrid — hash + neighborhood query
Vector3Int HashPosition(MyVector3 p) => floor(p / cellSize);
Add(collider) -> grid[HashPosition(collider.position)].Add(collider);
GetNearby(p) -> gather 27 neighboring cells
// CollisionManager — for each cell, do swept-sphere tests; resolve at TOI, then finish step
if (MyCollision.SweptSphereCollision(a.pos, a.vel, a.r, b.pos, b.vel, b.r, dt, out float toi)) {
  a.pos += a.vel * toi;
  b.pos += b.vel * toi;
  // resolve (impulses, angular response), then advance remaining dt - toi
}

Extras

  • Base asteroid data wrapper (mass/radius) for runtime sync with motion.
  • Ship data format used by both phases (parts with grid pose + rotation).

Flight Phase Footage

Reflection

Building the toolchain forced me to implement TRS, COM/inertia, quaternion deltas from angular velocity, and continuous collision in a way that plugs straight into gameplay. Next up: damage → detachment → drifting clusters interacting with asteroids; thrust limits, fuel, and better visualisation of force/torque vectors.

Note: Active WIP. Build and Flight phases will continue to evolve through profiling and iteration.