Devlog / Unobserved
Unobserved: The Nuke Artifact
A dedicated look at the Nuke artifact: how it deploys, moves, detonates, traces ownership, synchronizes across the network, and renders its mushroom-cloud shader.
The artifact that deserved its own post
The Nuke is one of those features that immediately changes the personality of Unobserved. It is not a passive upgrade and it is not just another projectile. It is an active artifact with a visible launch, a committed target, a traveling bomb, a huge blast area, camera feedback, fog-aware presentation, and a custom mushroom-cloud shader.
I wanted this one isolated because the full shape matters. The fun is not only the explosion. The fun is unlocking the button, arming the shot, choosing the place, watching the bomb leave the ship, seeing it bend toward the target, and then having the whole screen understand that something serious just happened.
Deployment
The Nuke enters the game as an item kind called nuke, with its own pickup scene, inventory presentation, artifact script, runtime bomb scene, explosion scene, icon textures, and shader. When the player picks it, it becomes part of the artifact HUD alongside Wormhole Dash and Vortex.
Activating the HUD button arms placement. The next click in the universe becomes the target position, clamped through the same artifact world-position path used by the other active powers. The upgrade preview tells the player what matters before choosing it: a 700 px blast radius, 30 damage, and a 55 second cooldown in the current tuning (this might change in the future when balance time comes).
- Unlock path: item pickup to inventory level to artifact ownership.
- Control path: Artifact HUD button to armed placement to clicked world target.
- Runtime path: NukeArtifact spawns NukeBomb into the artifact effects layer.
- Feedback path: the ship pulses on activation, the HUD starts cooldown, and the bomb becomes visible if fog rules allow it.
Movement
The bomb movement has a little ceremony before the real flight starts. It spawns at the ship launch origin, faces the ship artifact direction, deploys forward for a short 0.18 second burst, pauses for a tiny settle window, then ignites. That makes it feel like an object leaving the ship instead of a sprite appearing at speed.
After ignition, the bomb behaves like a guided heavy projectile. It starts with a launch speed, accelerates toward the configured travel speed, turns toward the clicked target with a capped turn rate, leaves a fading trail, and detonates when it reaches a distance threshold based on the blast radius. If it somehow never reaches the target, its lifetime forces detonation instead of leaving a forgotten node in the universe.
- deploy_duration gives the launch a visible push out of the ship.
- deploy_settle_duration creates a tiny beat before ignition.
- launch_speed, acceleration, travel_speed, and turn_speed shape the guided flight.
- trail points make the path readable and fade with fog presentation alpha.
- detonation distance scales from the blast radius, capped so the impact remains reliable.
The blast
On impact, the bomb instantiates NukeExplosionEffect and passes the final position, radius, damage, duration, damage flag, and owner peer. The explosion creates a white texture at runtime, assigns the mushroom-cloud shader to it, scales it to the blast radius, starts smoke particles, requests camera shake, and damages ships once.
Damage is applied to both enemies and player ships through group lookup. Each body is checked once, destroyed bodies are ignored, touch radius is included, and the outer edge of the blast uses a falloff band. The center takes the strongest hit; the edge still hurts, but it drops toward a lower damage ratio instead of feeling like a hard binary circle.
The final version also moved away from noisy fire and ember sparkles inside the explosion scene. The effect now leans harder on the shader and smoke puffs, which makes the result feel more like a single connected nuclear bloom instead of a pile of unrelated particle systems.
The shader
The mushroom cloud is a canvas_item shader driven by three runtime parameters: progress, global_fade, and seed. Progress moves from 0 to 1 over the explosion lifetime. Global fade connects the effect to fog visibility. Seed gives every detonation a slightly different noise field so repeated nukes do not feel stamped out of the same template.
The shader layers procedural noise, expanding radius masks, early hot bloom, lingering smoke plumes, short-lived fissures, and a shock ring. The early frames are hot and sharp. As progress advances, smoke takes over and the whole shape fades out through the effect lifetime.
shader_type canvas_item;
render_mode blend_mix;
uniform float progress : hint_range(0.0, 1.0) = 0.0;
uniform float global_fade : hint_range(0.0, 1.0) = 1.0;
uniform float seed = 0.0;
uniform vec4 core_color : source_color = vec4(1.0, 0.9, 0.42, 1.0);
uniform vec4 fire_color : source_color = vec4(1.0, 0.32, 0.08, 1.0);
uniform vec4 smoke_color : source_color = vec4(0.2, 0.18, 0.24, 1.0);
uniform vec4 rim_color : source_color = vec4(0.72, 0.9, 1.0, 1.0);
float hash(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32 + seed);
return fract(p.x * p.y);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash(i + vec2(0.0, 0.0)), hash(i + vec2(1.0, 0.0)), u.x),
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x),
u.y
);
}
float fbm(vec2 p) {
float value = 0.0;
float amplitude = 0.5;
for (int i = 0; i < 5; i++) {
value += noise(p) * amplitude;
p = p * 2.03 + vec2(17.7, 9.2);
amplitude *= 0.5;
}
return value;
}
void fragment() {
vec2 p = (UV - vec2(0.5)) * 2.0;
float t = clamp(progress, 0.0, 1.0);
float fade_in = smoothstep(0.0, 0.045, t);
float fade_out = 1.0 - smoothstep(0.82, 1.0, t);
float life = fade_in * fade_out * global_fade;
float expansion = smoothstep(0.0, 0.36, t);
vec2 warp_a = vec2(
fbm(p * 2.1 + vec2(seed * 0.013 + t * 0.08, 5.1)),
fbm(p * 2.1 + vec2(7.4, seed * 0.019 - t * 0.06))
) - vec2(0.5);
vec2 warp_b = vec2(
fbm(p * 4.7 + vec2(2.0, seed * 0.031)),
fbm(p * 4.7 + vec2(seed * 0.027, 8.0))
) - vec2(0.5);
vec2 q = p + warp_a * (0.28 + t * 0.16) + warp_b * 0.08;
float r = length(q);
float n = fbm(q * (3.0 - expansion * 0.9) + vec2(seed * 0.017, t * 0.1));
float fine = fbm(q * 9.0 + vec2(seed * 0.011, -t * 0.18));
float cloud_radius = mix(0.08, 0.62, expansion);
float edge_noise = (n - 0.5) * 0.36 + (fine - 0.5) * 0.09;
float hot_bloom = (1.0 - smoothstep(0.0, 0.36, r + edge_noise * 0.22)) * (1.0 - smoothstep(0.1, 0.32, t));
float dense_core = (1.0 - smoothstep(0.0, cloud_radius * 0.56, r + edge_noise * 0.12)) * smoothstep(0.04, 0.22, t);
float cloud = smoothstep(cloud_radius + 0.24, cloud_radius - 0.1, r + edge_noise);
vec2 offset_one = q - vec2(0.14, -0.1);
vec2 offset_two = q - vec2(-0.18, 0.12);
vec2 offset_three = q - vec2(0.02, 0.2);
float plume_one = smoothstep(0.42, 0.04, length(offset_one) + (fbm(offset_one * 6.0 + seed) - 0.5) * 0.18);
float plume_two = smoothstep(0.46, 0.05, length(offset_two) + (fbm(offset_two * 5.3 + seed * 0.7) - 0.5) * 0.2);
float plume_three = smoothstep(0.38, 0.03, length(offset_three) + (fbm(offset_three * 7.1 - seed * 0.3) - 0.5) * 0.16);
float smoke_age = smoothstep(0.22, 0.76, t);
float lingering_smoke = max(max(plume_one, plume_two), plume_three) * smoke_age;
cloud = max(cloud, lingering_smoke * 0.8);
cloud = max(cloud, dense_core * 0.7);
float fissure_noise = fbm(q * 12.0 + vec2(seed * 0.021, 4.0));
float fissures = smoothstep(0.72, 0.92, fissure_noise) * smoothstep(0.08, 0.42, r);
fissures *= 1.0 - smoothstep(0.2, 0.4, t);
fissures *= smoothstep(0.16, 0.34, cloud);
float shock_radius = mix(0.05, 0.72, smoothstep(0.0, 0.24, t));
float shock = 1.0 - smoothstep(0.0, 0.038, abs(r - shock_radius));
shock *= 1.0 - smoothstep(0.1, 0.34, t);
float connected_mask = 1.0 - smoothstep(0.72, 0.86, length(p));
cloud *= connected_mask;
fissures *= connected_mask;
hot_bloom *= connected_mask;
shock *= connected_mask;
float hot_core = clamp(hot_bloom * 1.35 + fissures * 0.9, 0.0, 1.0);
float rim = clamp(shock + fissures * 0.55 + smoothstep(0.5, 0.82, cloud) * (1.0 - smoothstep(0.3, 0.58, t)) * 0.18, 0.0, 1.0);
float smoke_mix = smoothstep(0.12, 0.82, t) * clamp(cloud + fine * 0.28, 0.0, 1.0);
vec3 color = mix(fire_color.rgb, smoke_color.rgb, smoke_mix);
color = mix(color, core_color.rgb, hot_core);
color = mix(color, rim_color.rgb, rim * 0.62);
color += fire_color.rgb * fissures * 0.45;
float alpha = clamp(cloud * 0.8 + shock * 0.48 + hot_core * 0.45 + fissures * 0.25, 0.0, 1.0);
alpha *= life;
COLOR = vec4(color, alpha);
} Why it feels different
The best part of the Nuke is that it combines several systems that have been growing separately. Active artifacts provide the decision. Placement preview provides intent. Ship launch direction gives it a physical origin. The bomb movement gives it anticipation. The network payload gives it traceability. The fog presentation keeps it honest. The shader gives it identity.
It is exactly the kind of feature I want more of in Unobserved: powerful, readable, a little ridiculous, but still built through real game systems instead of being a one-off visual trick.
Thanks for reading,
Hack the planet!
Arliax