Taps can be unreliable inside this app's built-in browser. For the full experience, use its menu to open this page in your browser.
Skip to content

The Ship Was Flying Nose Down

A devlog by Neeraj Kotwani, who builds every game on Elipar.

Somebody told me the spaceship in Space Defender did not look like a spaceship. Fair: at the time it was three bare primitives — a cone for the body, one flat box doing duty as both wings, and a sphere for the engine. From the game's behind-and-above chase camera you were mostly seeing the cone's cross-section, which reads as a blob.

So I rebuilt it properly: a tapered fuselage with a nose cone, a tinted-glass canopy, two-segment swept wings with wingtip fins, a tail fin, and twin rear nacelles with glowing exhausts. Shipped it.

Then the same report came in again. Still not a spaceship.

Two bugs were hiding a model that was already there

This time I measured instead of redesigning. All twelve parts were present and the ship filled about a quarter of the screen width, so the rebuild had landed. Two separate faults were making it unreadable.

A metal with nothing to reflect is black

The hull was metalness: 0.85 and the wings 0.7.

In physically-based shading a metallic surface gets almost all of its colour from reflected environment. This scene has no environment map at all, one weak directional light, and an ambient term — and ambient barely affects metals. So a correctly built model was painted in a material that is physically incapable of showing its own shape, against a black starfield. It rendered essentially black.

The fix is not subtle once you see it: metalness down to 0.2-0.25, lighter base colours, stronger emissive, and a fill light placed behind the camera so the surfaces actually facing the player are lit at all. There are also emissive trim strips along each wing, using a material that ignores lighting entirely, so the silhouette is guaranteed to read even where nothing illuminates it.

The asteroids visibly improved from the fill light as a side effect. They had exactly the same problem and nobody had mentioned it.

The leftover rotation, which is the real lesson

The other fault was one term in one line:

ship.rotation.x = shipVelocity.y * 0.5 - Math.PI/2;

That - Math.PI/2 belonged to the old ship. It was a single cone, and a cone's axis in three.js runs along +Y, so the whole group had to be tipped forward 90 degrees to point it down the track.

Every part of the rebuilt ship orients itself — the fuselage and the nose each carry their own quarter turn. So the group-level rotation was applying a second 90-degree turn on top. I measured the ship's forward vector: [0, -1, 0]. Straight down. Meanwhile its bullets travel along -Z and the asteroids approach from -Z.

The ship had been flying nose-down, pointing away from the things it was shooting at, ever since the rebuild. Deleting the term restores forward to [0, 0, -1].

When you rebuild a model out of primitives, check for a group-level rotation left over from the old one. It survives because nothing breaks. The ship still renders, the game still plays, the rotation is still doing something plausible — it is just ninety degrees wrong, forever, and the only symptom is a player saying it does not look right.

I also confirmed the rotation is only ever written and never read: bullets copy the ship's position and take no rotation from it, so the fix is purely visual with no effect on gameplay. Worth establishing before touching a line in the middle of a game loop.

A baseline that lied

Best testing lesson of this pass, and it nearly sent me in the wrong direction.

A bot that holds the fire key for nine seconds scored 0. That looks exactly like "shooting is broken", and I had just been editing the ship.

So I checked out the pre-change file, served it on its own port, and ran the identical test. Also 0. The bot was never aiming at anything — it held fire while the asteroids drifted past elsewhere. Nothing was broken; my test just did not play the game.

I then proved shooting works by lining the ship up on a known asteroid through a debug hook and firing: score 0 to 30. Establish the baseline on the unmodified build before believing a failure is yours. It takes two minutes and it is the difference between fixing a bug and hunting one that does not exist.

The music was not missing, it was inaudible

Later report: "music is missing". I measured before changing anything — 23 oscillators over three seconds with the audio context running — so the scheduler was working correctly.

The problem was the mix. The bass was scheduled at a gain of 0.035 and the lead at 0.022, against the ordinary sound-effect default of 0.15. Roughly a quarter the volume of a normal sound, on an eight-step loop repeating every 1.8 seconds.

And the louder finding was not what was reported at all: grepping for the tone helper found only the two music calls. The game had no sound effects whatsoever. Shooting, explosions and taking damage were all silent. Nobody had complained about that; they had complained about the thing that was quiet rather than the thing that was absent.

Both fixed: a real effects set wired to the existing explosion and wave-advance call sites, a noise helper added because drums and explosions need filtered noise rather than an oscillator, and the music rebuilt as sixteen steps at 128bpm with peak gain at 0.16.

One more honest note on that round. My first audio verification ran Chromium with the autoplay policy disabled — which switches off exactly the gate that blocks sound in a real browser. It proved the scheduler runs; it did not prove anything is audible. I reported it far more confidently than it deserved. Re-run without the flag, and worth knowing for anyone debugging silence on a phone: the iPhone's physical ring/silent switch mutes Web Audio in Safari outright, and no code change can do anything about that.

Space Defender is free in a browser, no account, no download. The ship points forwards.

We use cookies to keep games running and, once ads are live, to show relevant content. See our Privacy Policy for details.