Smurfs is a licensed Minecraft world developed by Shapescape, where I built the minigame system. This case study covers one of the minigames, Azrael Sneak. You play a Smurf crossing a clearing to free a caged friend while Gargamel’s cat watches the field. The core question for the player is whether the cat can currently see them, and the minigame answers it through the scene itself: the cat’s behaviour, the camera, and a few signals Minecraft already provides. The sections below go through each part and how I built it.

The cat’s field of view
Azrael’s sight is a cone, 75 degrees wide and up to 100 blocks long, rebuilt every time he turns. To keep it cheap, I drop the height and run the whole test in two dimensions.
// The whole sight test runs in 2D: drop the height, keep x and z.
playerLocation.y = playerLocation.z;
if (!fieldOfView.contains(playerLocation)) {
continue;
}
Collapsing to 2D means Azrael does not look up or down, he sweeps a flat slice of the world. The clearing is almost entirely flat, so the player never runs into the limitation, and the check stays cheap enough to run every tick for every player.
A flat cone for a flat field
A full 3D field of view would be more accurate and mostly wasted here, because the play space has no verticality to exploit. I spent the budget instead on a 2D cone that updates smoothly as the cat turns his head. The player feels the turning, and never has any reason to notice the missing dimension.
Line of sight and cover
Being inside the cone is not enough to be seen. I trace the line from Azrael’s eye to the player’s chest, and a single solid block on it is enough to hide them.
// Trace the eye-to-chest line; one solid block on it hides the player.
const eye = this.getHeadLocation();
const target = Vec3.from(player.location).add(0, 1, 0); // body center
const isHidden = isLineOfSightBlocked(dimension, eye, target);
The first version of that trace sampled the line at a fixed half-block step. That is fast but leaky: a thin wall hit at a shallow angle can fall between two samples, so the line reads as clear when it is not, and the same block can get sampled twice. I replaced it with a voxel traversal, the Amanatides and Woo algorithm from 1987, which walks the exact set of blocks the ray passes through, in order, with no gaps and no repeats. It is the same idea a voxel renderer uses, which makes it a natural fit for a block world. Unloaded chunks count as opaque, so Azrael never sees through terrain that is not even loaded.
The setup is the whole trick. Parametrise the ray, then for each axis work out how far along it you travel to cross one block, and how far to the first block boundary:
Each axis then crosses boundaries on its own arithmetic sequence, tMaxₐ + k·tΔₐ for k = 0, 1, 2, and so on. Walking the blocks in order is just a three-way merge: at every step, advance the axis whose next crossing is nearest. That is the entire loop:
// Voxel traversal: step into the next block along the nearest axis boundary.
if (tMaxX <= tMaxY && tMaxX <= tMaxZ) {
x += stepX;
tMaxX += tDeltaX;
} else if (tMaxY <= tMaxZ) {
y += stepY;
tMaxY += tDeltaY;
} else {
z += stepZ;
tMaxZ += tDeltaZ;
}
This check is what makes cover matter: you can stand close to the cat as long as a wall sits between you, which matches the instinct most stealth players arrive with. There is one deliberate exception: within ten blocks the cone stops mattering, because a cat that close detects the player anyway.
// Within ten blocks the cone is skipped: the cat detects the player regardless.
if (Vec3.from(this.getLocation()).distance(playerLocation) < 10) {
result.push(player);
continue;
}
The three rules together, the cone, the line of sight, and the close-range override, are what the diagram below runs. Drag the Smurf around the cat, put the block between you, and rotate the gaze to see when each rule takes over.
The cat sweeps his gaze and the Smurf wanders on its own. Drag the Smurf, the obstacle, or the slider to take over; he is seen inside the cone with a clear line, or within close range regardless of facing.
The cat’s gaze and states
The cone itself is invisible to the player, so the cat’s head movement is what makes it readable. Azrael runs a small state machine, asleep, waking, awake, and his head animates with it. While he is awake his gaze drifts on a slow random sweep, and it speeds up across the one arc a tree already blocks, so he does not spend time looking through something he cannot see past.
// Sweep faster across the arc the tree already blocks: no point lingering.
const rotation = this.getRotation();
if (rotation >= this.treeAngleLow && rotation <= this.treeAngleHigh) {
this.setRotationSpeed(5);
}
Noise feeds into the same loop. Step on a creaky spot and he hears it; do it three times and he stops scanning and turns toward the sound. I aim him deliberately off by forty to sixty degrees, so he turns toward the last noise instead of snapping straight onto the player.
// Three noises in, stop scanning and turn toward the last sound.
if (this.getWarned() >= 3) {
this.setTargetRotation(this.getNoiseRotation());
this.setRotationSpeed(uniform(3.5, 4));
}
Let the enemy telegraph its intent
Each of these is a tell. The head turning, the speed of the sweep, the moment he locks toward a sound: each gives the player a beat to react before anything punishes them. I aimed for a readable enemy rather than a clever one, because readability is what lets a near miss feel earned rather than arbitrary.
Reusing the experience bar as a meter
Rather than build a custom detection meter, I reuse one already on the screen: the experience bar. When Azrael sees you it fills, and when it overflows you are caught and sent back to the cage. It fills about twice as fast as it drains, so a brief sighting is recoverable while a sustained one is not.
// Seen: add XP. Not seen: remove it at half the rate. Overflow means caught.
if (this.seeingPlayers.has(player)) {
player.addExperience(Math.ceil(player.totalXpNeededForNextLevel / (40 * 1.5)));
if (player.level > this.BASE_EXP_LEVEL) {
this.cageSpawn.teleport(player);
}
} else {
player.addExperience(-Math.ceil(player.totalXpNeededForNextLevel / (80 * 1.5)));
}
It reads straight away because every Minecraft player already knows what a filling bar means. Reusing a signal the game has already taught is cheaper and clearer than teaching a new one.
The camera
The last piece is the camera, and it runs during play, not as a cutscene. I hold a third-person camera that keeps the player, the cat, and the current goal in the same frame, biased toward the player so they never lose track of themselves.
// Frame the cat and the goal, bias toward the player, and orbit a widening circle.
let facingLocation = new Segment(
this.azraelEntity.getHeadLocation(),
activePoiPoint,
).getMidPoint();
const playerWeight = 2.0;
const playerLocation = player.getHeadLocation();
facingLocation = new Vec3(
(facingLocation.x + playerLocation.x * playerWeight) / (1 + playerWeight),
(facingLocation.y + playerLocation.y * playerWeight) / (1 + playerWeight),
(facingLocation.z + playerLocation.z * playerWeight) / (1 + playerWeight),
);
const cameraRadius = Math.max(
40,
Vec3.from(this.AZRAEL_LOCATION).distance(player.location) * 1.35,
);
const projectedPoint = LocationUtils.findPointOnCircle(
this.AZRAEL_LOCATION,
cameraRadius,
player.location,
);
The camera orbits a circle around Azrael whose radius grows as the player moves away, so the further you creep, the wider the shot pulls to keep the threat in frame. It is effectively the minigame’s only readout, keeping the threat on screen so the player always knows where it is.
Drag the Smurf around Azrael. Within 5 blocks of the POI the framing segment appears and the midpoint snaps to its centre; otherwise it collapses onto Azrael. The camera does not look at the midpoint itself: it aims 2:1 past it toward you, at the hollow ring, lifted above the ground.
What I would change in a second update
The sight test has one shortcut left, and it is the place I would change first if I came back to this minigame: the cone is a hard binary test, and I have not replaced it yet. Right now you are either inside it or outside it, with a hard edge. Real sight is not uniform: detection is strong in the centre and weak at the edge. A falloff turns the binary test into a weight, strongest along the gaze and fading to zero at the cone’s edge and at maximum range:
// Detection weight: 1 along the gaze, fading to 0 at the cone edge and max range.
const angleFactor = Math.max(0, 1 - Math.abs(offsetFromGaze) / HALF_ANGLE);
const rangeFactor = Math.max(0, 1 - distance / VIEW_DISTANCE);
const detection = angleFactor * rangeFactor;
That single number feeds the experience bar that already exists, so peripheral sightings raise it slowly and a dead-centre line raises it fast. It would turn the edge of the cone into a place you can risk rather than a hard boundary, without adding anything new to the screen.
The diagram below runs that proposed weight. Drag the Smurf through the cone and watch how the detection value, and the rate the experience bar would fill, changes between the edge and the centre.
The cat sweeps his gaze and the Smurf wanders on its own. Drag the Smurf, the obstacle, or the slider to take over. Detection is strongest dead centre and fades to zero at the cone edge and at maximum range, a solid block still hides you, and the close-range ring always notices you.
Smurfs released in 2025.
The Smurfs and all related characters and elements are the intellectual property of Paramount and Peyo. Shapescape developed this Minecraft world under license.