
















Overview
// Tank controller — in-engine overview
GDEV50004 Advanced Mechanics Design is a solo Level 5 module at Staffordshire University. The challenge: build a convincing, fully-featured tank controller in Unity 6 without using Unity's WheelCollider at all. Every aspect of suspension, traction, gear behaviour, and terrain traversal had to be constructed from scratch in C#.
The result is a full-featured tank prototype on a custom test range: ramped terrain, slope seams, and bumpy obstacles. Spring-damper forces are calculated per suspension arm and applied at the exact hit point in world space. Turret and barrel aiming use Quaternion math directly from lecture material — ProjectOnPlane, LookRotation, and Slerp — with live debug gizmos in scene view.
A "Pimp My Tank" easter egg mode (P key) drops the tank into hydraulic bounce mode with gold body paint, gangster chains, money stacks flying out of the barrel, and a custom BGM playlist swap. The UI is built entirely in UI Toolkit (UXML/USS) — no legacy Canvas — including a live debug panel, auto-swapping input glyphs (keyboard vs gamepad), a crosshair with cooldown ring, and a controls panel.
Physics
Each suspension arm fires a downward raycast every FixedUpdate. If grounded, it calculates compression as a 0–1 float based on how far the ray has penetrated the rest length. A spring force (Hooke's law equivalent) and a damper force (velocity component along the surface normal) are combined and applied at the ray hit point via AddForceAtPosition. This means suspension forces correctly torque the rigidbody rather than applying everything at the centre of mass.
The wheel visual GameObject is placed at springBasedDistance from the arm origin each frame, smoothed via Lerp so it never snaps. When airborne, a reduced fallback force prevents the nose from pitching down aggressively. Traction force is also computed per arm — lateral slip is cancelled via the Track component's TractionPercent, so rough terrain and slopes authentically reduce grip.
Suspension parameters (rest length, spring stiffness, damper coefficient, rest force) are all stored in a TankSuspensionConfig ScriptableObject, making terrain tuning a pure data operation rather than a code change.
Gearbox
// HUD — gear indicator at 50% speed
The gearbox runs in a coroutine at 20 Hz — an intentional performance decision. Gear state evaluation does not need to happen every frame; running it every 50 ms keeps it responsive while freeing the main thread. Four gear states are defined:
Each gear state has MaxSpeed and Acceleration defined in a TankGearConfig ScriptableObject. The engine model (ArcadeEngine or RealisticEngine ScriptableObject) can be swapped at runtime — arcade gives sharp per-gear acceleration curves with immediate torque feel, realistic gives smoother torque simulation that builds over RPM. Both are tunable in the Inspector without touching code.
Aiming
The turret yaws by projecting the camera's look direction onto the turret's yaw plane via Vector3.ProjectOnPlane, then constructing the target rotation with Quaternion.LookRotation and interpolating to it with Quaternion.Slerp. The ProjectOnPlane guard is what prevents gimbal lock at extreme camera pitch — without it, aiming steeply up or down causes the turret to spin erratically as the look direction becomes nearly parallel to the yaw axis.
The barrel pitches independently from the turret yaw. A target pitch angle is extracted by projecting the camera forward onto the turret's local vertical plane, then an AngleAxis rotation is applied around the turret's local right axis. This separation means the turret and barrel can run at different Slerp speeds for more natural feel.
Debug gizmos render during play in the Scene view: a filled-arc wedge from hull-forward to turret-forward showing the yaw delta, the barrel's forward vector as a coloured ray, the turret's right axis as the pitch pivot indicator, and a trajectory arc from the barrel tip (LineRenderer with physics simulation). These were invaluable for diagnosing the gimbal-lock issue and validating the aiming pipeline visually.
Easter Egg
// Pimp My Tank — 10% customisation
// Pimp My Tank — 50% customisation
// Pimp My Tank — 90% customisation
Pressing P toggles Pimp My Tank — a ScriptableObject-controlled chaos mode that reimagines the tank as a gold-plated hydraulic lowrider. The visual body (separated from the physics hull) gets a sine-wave vertical bounce driven by the average suspension compression across all arms. A hang-time bias makes the body linger at the apex of each bounce before slamming down, exaggerated further by a compression boost multiplier.
Gamepad players receive rumble pulses timed to the rear/front hop cycle — the rear pads fire first on lift, front pads fire on the slam, creating a rocking sensation that matches the visual. The hull swaps to gold paint, gangster chains spawn as physics props at attachment points, and money stacks fire from the barrel instead of rockets. The background music playlist swaps to a custom BGM track.
m_VisualBody.localPosition is hard-reset to m_BodyBaseLocalPos before the sine offset is applied — ensuring the base is always known-good regardless of what the previous frame left behind.
HUD
The entire HUD is built in UI Toolkit (UXML/USS) — Unity's modern retained-mode UI system. No legacy Canvas or uGUI components anywhere in the project. The HUD is composed of several independent panels, each wired via UnityEvents (the Observer pattern) so the physics system raises events and the HUD responds — neither side holds a reference to the other.
Challenges
Suspension arms fighting each other — Early iterations applied suspension force at the rigidbody centre, causing arms to compete. Each arm's response to compression overwrote its neighbour's contribution. Fixed by computing force strictly per arm from the exact raycast hit point — AddForceAtPosition distributes the torque correctly from that world-space location, so arms cooperate rather than interfere.
Slope seams causing the hull to catch and halt — The tank would snag on terrain mesh edge seams at speed, killing all momentum abruptly. Fixed by the SlopeTransitionAssist component: a forward sphere-cast constantly probes for upcoming seams and applies a pre-emptive lift force plus a forward nudge to carry the hull over cleanly before it contacts the geometry edge.
Turret gimbal lock at extreme pitch — Aiming steeply caused the turret to spin erratically. Root cause: the camera's forward vector was nearly parallel to the yaw axis at steep angles, making LookRotation degenerate. Resolved by always projecting onto the yaw plane first — ProjectOnPlane ensures the look vector is always perpendicular to the up axis before it reaches LookRotation.
HUD DebugPanel chunks not showing — An identical quirk to other projects: UI Toolkit VisualElement children not rendering because the parent panel reference was wrong in the UXML binding. The DebugPanel container required an explicit reference to its parent panel element — the same UMG Canvas Panel equivalent issue documented in the C++ project.
Pimp My Tank body drifting off — After multiple Pimp mode toggle cycles, the visual body drifted progressively off its attachment point. Residual offset accumulation from incomplete bounce frames. Fixed by hard-resetting localPosition to m_BodyBaseLocalPos at the start of each bounce calculation — base is always authoritative.