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
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.