ENFRKOantony langlois
work

The Creepy Walker

visitotherJavaMinecraftForge

Minecraft horror mob · a terrain-aware monster that stands, sneaks and crawls to chase you through any space · one per world, enforced through the save file · Java · Forge

Architecture

Forge client
Client · rendering
Entity renderer
entity/client · MobRenderer
Draws the walker each frame; skips body rotation during death so the corpse stays exactly where it fell
Blockbench
Client · geometry
Hierarchical model
entity/client · 18 bones
Skeleton exported from Blockbench; head yaw and pitch are clamped so the stare keeps tracking the player
Java keyframes
Client · animation
Keyframe clips
entity/animations
14 clips: walk, sneak, crawl, idles, attacks, spawn, death and stance transitions
server tick
Core · server brain
TheCreepyWalkerEntity
entity/custom · Monster subclass
Probes the blocks above and ahead every tick and picks a stance: stand, sneak or crawl
stance state machinehitbox resizingmelee goaldamage rulesspawn check
Minecraft
External · game runtime
Forge + vanilla engine
Event bus, AI goal scheduler and natural-spawn engine the mod hooks into
Forge events
Server · spawn gate
Spawn gate
event · SpawnPlacements
canSpawnHere: underground, on solid ground, no sky above, and no other walker alive
SavedData
Storage · world save
Singleton world data
worlddata · NBT
One boolean exists persisted in the world save; survives restarts and chunk unloads
JSON
Storage · datapack
Biome modifier
data/forge/biome_modifier
Adds the walker to the spawn pool of every overworld biome at weight 100

The vanilla spawn engine picks the walker from the biome modifier spawn pool and asks the spawn gate, whose canSpawnHere reads the singleton world data: if a walker already exists anywhere in the save, the spawn is refused. Once alive, TheCreepyWalkerEntity probes the terrain every server tick, switches between stand, sneak and crawl, resizes its hitbox, and ships the stance flags to clients through SynchedEntityData. The entity renderer reads those flags each frame and the hierarchical model samples the matching keyframe clips, including two-step choreography for crawl-to-stand transitions.

Deployed: Mod jar → Forge client + server · Spawn rules → datapack JSON, no code reload needed

Outcomes

  • 3 stances · hitbox shrinks 2.7 to 1.7 to 0.7 blocks
  • 1 per world · singleton enforced through the save file
  • 14 clips · hand-keyframed Blockbench animations
  • 0 packets · custom netcode · all sync rides vanilla entity data

Skills demonstrated: server-client state sync · finite-state machines · hitbox + collision handling · event-driven registration · NBT world persistence · game AI goal design

Problems and solutions

One constraint drives everything: a single monster that must feel alive and be able to follow the player through any space.

█ Terrain-aware stance machine

Problem: A 2.7-block-tall monster cannot chase players into 1-block crawlspaces. Vanilla mobs simply path around low openings or get stuck at the entrance, which kills the horror premise of a thing that follows you anywhere.

Every server tick the entity probes the blocks above it and ahead of it (including one step up when facing a wall) and picks a stance. Stand, sneak and crawl each carry their own hitbox: 2.7, 1.7 and 0.7 blocks tall, applied through getDimensions and refreshDimensions.

  • Rejected vanilla Pose swimming/crouching: it only covers player-shaped hitboxes and gives no control over when the switch happens
  • Accepted up to 8 block-state reads per tick; a 20-tick cooldown after each switch stops stance thrashing at ledges
  • Bought a mob that physically fits wherever the player can go, which no pathfinding tweak alone provides

▚ Stance sync without custom packets

Problem: Stance is decided on the server, but animations only exist on the client. Plain Java fields silently desync in multiplayer: each client would guess its own stance and play the wrong clip.

Current stance, previous stance and a numeric stance-change code all live in SynchedEntityData accessors, Minecraft's replicated entity-state channel. The client reads the change code and starts the matching transition clip, then the code resets to zero.

  • Rejected a custom network channel: more code, versioning and ordering concerns for data that fits in booleans and one int
  • Accepted ten synced accessors on one entity, chattier than a hand-rolled packet but replicated and race-free for free
  • Bought correct animations for every observer, including players who walk into render distance mid-transition

▞ Two-step transition choreography

Problem: Snapping straight from crawl to standing looks like teleportation. Transition clips were only authored between adjacent stances, so crawl-to-stand had no animation at all.

Skipping a stance queues two clips: crawl-to-sneak plays first, a waiting flag arms the follow-up, and sneak-to-stand starts the tick the first clip's timeout hits zero. Tick-counted timeouts on the client drive the whole sequence.

  • Rejected authoring dedicated skip-level clips: two extra Blockbench animations to build and maintain for a rare case
  • Accepted roughly 16 ticks of transition latency before the new stance moves at full speed
  • Bought smooth motion through every stance path with only four authored transition clips

▓ One walker per world, enforced twice

Problem: A horror mob loses all menace if the spawn engine produces dozens of copies. Vanilla spawn caps work per-category and per-chunk, so they cannot express 'exactly one, ever, across the whole world'.

A SavedData boolean in the world save is the source of truth: canSpawnHere refuses any spawn while it is set, and Forge join/leave events flip it. Spawn weight 100 in every overworld biome makes the single allowed spawn happen fast.

  • Rejected counting loaded entities: unloaded chunks hide the walker and the count would allow a duplicate
  • Accepted a whole-world flag over per-dimension bookkeeping, one walker total is the design, not a limitation
  • Bought singleton behavior that survives server restarts because the flag persists as NBT

▒ Melee timed to the animation, not the tick

Problem: Vanilla MeleeAttackGoal applies damage the instant the target is in range, but the lunge animation takes several ticks to reach the target. Players get hit by a monster whose arms have not moved yet.

A custom attack goal runs a windup counter: the attacking flag flips early so the client starts the swing, and doHurtTarget only fires when the counter reaches the frame where the arms land.

  • Rejected the vanilla goal plus client-side guessing: latency makes the guess wrong exactly when it matters
  • Accepted a slower effective attack rate (12-tick cooldown) in exchange for readable, dodgeable swings
  • Bought hits that land on the animation frame in all three stances, each with its own attack clip

▧ Scripted spawn and death, no ragdoll

Problem: The vanilla death sequence (red flash, 90-degree keel-over, instant despawn) reads as a game system, not a creature dying, and the standard pop-in spawn wastes the first encounter.

Death starts a keyframed collapse clip while the renderer suppresses the vanilla corpse rotation; the body stays 60 ticks and is unpickable meanwhile. Spawning plays a 108-tick emergence clip during which the server brain holds still.

  • Rejected the default LivingEntity death handling: overriding tickDeath and setupRotations was the only way to keep the pose
  • Accepted pausing AI for 5 seconds on spawn, a vulnerable window, so the emergence is never cut short
  • Bought entrances and exits that stay in character, the point of a horror mod