This was one of those days where you build something, throw it away, redesign it, and end up with something much better. Fifteen commits, one revert, and a lot of lessons about composition.
Attempt 1: Monolithic Projectile
We started by building a Projectile component that handled linear, ballistic, and homing movement in one module. It worked, technically. But it was doing too much — trajectory math, collision detection, visual effects, lifetime management, all in one place. Want to add a new projectile type? Edit the giant switch statement. Want a projectile that homes but doesn’t do damage? Hope the flags compose correctly.
We reverted it within an hour. The commit message: “Remove Projectile component — redesigning as multi-component system.”
Attempt 2: Composed turret
Instead of one Projectile-does-everything, we decomposed into single-responsibility components:
- Swivel — Rotates to face a target (yaw or pitch axis)
- Targeter — Acquires and tracks targets within range
- Launcher — Fires projectiles from a muzzle point
A turret is just Swivel + Targeter + Launcher wired together. A stationary cannon is a Launcher without a Swivel. A tracking sensor is a Targeter without a Launcher. Each piece does one thing and composes cleanly. New projectile type? New Launcher config, everything else unchanged.
The physics rabbit hole
Getting projectiles to fly straight was harder than expected. Roblox applies gravity to all BaseParts by default. Our first fix — CustomPhysicalProperties with zero density — didn’t help because gravity applies regardless of mass. Then we tried setting AssemblyGravityModifier = 0, which should cancel gravity on the physics assembly. It didn’t work reliably because of how Roblox resolves multi-part assemblies.
The working solution: a BodyForce that applies a constant upward force exactly equal to gravity times the projectile’s mass. Old-school, but reliable.
Other issues that each took 10 minutes to fix and 30 to debug:
- Turrets firing at angles because the swivel hadn’t finished rotating
- Pitch swivels fighting with the turret base’s CFrame (solved by making the pitch swivel headless — no Head part)
- Projectiles inheriting the turret’s rotational velocity
Signals save everything
The breakthrough was committing fully to signal-driven architecture. Instead of the turret calling launcher:fire() directly, the Targeter emits targetAcquired, the Swivel listens and rotates, then emits aimComplete, and the Launcher listens and fires.
No component directly references another. They communicate entirely through signals. This means you can test each component in isolation, swap implementations without rewiring, and add new behaviors by listening to existing signals.
We also added sync mode to signals — fire a signal and block until all handlers complete. Critical for the “don’t fire until aimed” requirement. Without sync mode, the Launcher would fire the frame after the Swivel started rotating, not after it finished.
The day ended with a declarative control mapping system for InputCapture (so players can aim turrets with gamepad or keyboard) and a shelved shooting gallery demo that’ll come back once the targeting system is more mature.