2026-09-05 · MALCONVERGENCE

Three systems that were written and never ran

MALCONVERGENCE went from a 786-line prototype to a playable first level, and most of the work was discovering that the weapons, the combat effects and the whole material pipeline were finished, compiled and never reached. Plus a blind comparison against the reference art that the game lost, on purpose and in public.

MALCONVERGENCE is a first-person shooter built in Unity, with dual-wield gunplay, a fusion system that pairs weapons, and a boss at Elden Ring scale. This is its first dev update.

The project went from a 786-line prototype to a playable vertical slice of its first chapter: 32,105 lines across 55 source files. It boots to a title screen, plays with mouse or gamepad, and runs clean. A stress test drives every weapon, all 28 fusion pairings, every enemy variant, the boss, pause and death-retry with zero compile errors and zero runtime exceptions.

Most of the work was not new systems. It was finding the ones already written and quietly not running. Three layers were complete, compiled, and never reached. A build succeeding proves nothing about whether a system is reachable.

The player could not shoot

Firing.Shoot was fully implemented: hitscan, projectiles, beams, chaining. Nothing in the project called it, and no system owned magazines or reload. The weapon layer was an island. It now has a driver that owns fire cadence, the ammo economy, reload and fusion input, for mouse and gamepad.

Gunfire and impacts were invisible

The effects layer (muzzle flashes, material-aware impacts, bullet decals, brass ejection, camera shake) was written, pooled and correct. Firing did the ballistics and called none of it, so shots landed with no discharge at the barrel and no reaction at the target. It is wired now, including a four-point star flash with a real light that lands on the gloves and the ground.

Every PBR map was being thrown away

The texture synthesiser writes its roughness map already in Unity's _MetallicGlossMap layout. The material library read the red channel as roughness and re-derived both outputs from it, discarding the baked data. Every non-metal surface collapsed to one uniform gloss: wet ground became a mirror across its whole area, and rust read exactly inverted.

A second defect in the same function made it worse. It did CPU readbacks on textures whose CPU copy is freed outside the editor. In a player build the readback threw, the catch disabled the metallic-gloss map, and every surface lost its metallic and smoothness data. It worked in the editor and silently broke in the shipped game, which is the worst failure mode there is: nothing in play mode could have caught it.

Smaller faults with big effects

what was wrongwhat was actually happening
rendering in gamma, not linearThe project setting was m_ActiveColorSpace: 0, so every PBR calculation ran in the wrong colour space. This is the single largest reason the build read as flat and washed regardless of how the materials were authored. Switched to linear before any art work started.
Shader.Find returned null in player buildsThe shipped build threw 35 exceptions per run, because every muzzle flash looked up a shader nothing referenced and Unity strips those. The shader is now pinned by a material in Resources and added to Always Included Shaders, which fixes the same latent bug in five other files at once.
19,418 particle errors in a 20-second runUnity requires all three axes of velocity-over-lifetime to share one curve mode. Rain, embers and dust each set Y as a range and X and Z as constants. The per-frame wind refresh had the same defect, which turned a one-off warning into a per-frame one, each dragging a full stack trace with it. That was frame cost, not just log noise.
gamepad axes bound one index offUnity's InputManager axis fields are zero-based, so the "9th axis" is axis 8. The triggers and D-pad were declared at 9, 10, 6 and 7, which bound L2 to the right trigger and R2 to nothing. All ten joystick axes are now declared generically and mapped semantically in code.

How the work was judged

Judging this project by looking at it was not going to scale, so two pieces of test infrastructure came early.

An offscreen capture path renders authored beauty shots at 1920x1080 from framings chosen to match specific reference images, so renders compare like for like. It composites the viewmodel and HUD cameras into the same target, because camera.Render() only renders one camera and neither would otherwise appear.

A blind comparison. Renders are composited side by side with the reference in randomised order, labelled only A and B, with the answer key kept in a separate file the reviewers never see. It is a real blind test rather than a critic being asked to rate something it already knows is ours.

The result: the renders scored 29 to 43 against the reference's 85 to 88, losing all six comparisons decisively. That is up from 22 to 31 at the first measurement, but it is a loss, and the remaining gap is real rather than a matter of taste. The recurring criticisms were consistent: surfaces carry one tiling texture at one scale with no material breaks, grime or edge wear; the ground tiles visibly to the horizon; and large parts of the near field receive almost no key light. A surface-detail and shadow pass has since landed but has not yet been re-measured.

The "translucent panel" was a lighting failure

A hard-edged pale quad in the left foreground was reported unprompted by three separate reviewers as looking like a broken render. Isolation tests ruled out rain, god rays and near-camera geometry in turn. It is an oversized opaque debris chunk receiving almost zero key light, measured at RGB 5/5/28, which is exactly the post-process lift value over pure black. An unlit bevelled slab with no silhouette is indistinguishable from a haze card. The same comparison showed a cover block, a kerb and cobbled relief present in the depth buffer and absent from the shaded image.

Isolation beat iteration here. Three cycles went into tuning "rain" that was not rain; turning one system off entirely answered it in a single cycle, and the same approach then settled the god-ray smear.

The stress test

Malconvergence.exe -stresstest drives every weapon in both hands, all 28 fusion pairings, every mech variant through spawn, damage and death, the boss, pause and resume, and death and retry, while watching Unity's own log stream and counting errors itself. It exits non-zero on failure and takes about 90 seconds. Current result: 0 errors, 0 exceptions, 28 of 28 fusions. It matters because several of those systems bind to each other reflectively, which is exactly where silent breakage hides.

Audio is synthesised procedurally, with no audio assets in the project. It is wired and survives the stress test without throwing, but nobody has yet confirmed that a sound actually reaches the output device.