Skip to content

These docs were made completely by AI, so they might be right, or wrong, you'll need to test them yourself. This was made for a easier understanding of everything. So use at your own risk. If anything is wrong, please don't hurt to make a PR on the page you have a problem with. ON GITHUB

Template: Custom Mob

This template walks you through adding a completely new hostile mob to LCE from scratch. We are building a Shadow Walker, a tall dark creature that spawns in caves, hunts players, and drops a custom item when killed. By the end you will have:

  • A ShadowWalker entity class extending Monster with custom health and damage
  • A Goal-based AI that targets players, attacks in melee, and wanders around
  • A custom model built from ModelPart cubes (tall humanoid with long arms)
  • A custom renderer that draws the model with your texture
  • Entity registration so the game knows it exists (save/load, spawn eggs, networking)
  • Natural spawning in dark areas and specific biomes
  • Custom loot drops on death
  • Custom sounds for ambient, hurt, and death

That is a lot of systems, but each piece is small. Let’s go.

SystemWhat It DoesReference
Entity hierarchyHow Entity -> Mob -> PathfinderMob -> Monster worksAdding Entities
GoalSelector AIPriority-based behavior goalsCustom AI
ModelPart modelsBox-based 3D models with animationEntity Models
EntityRendererDrawing entities in the worldAdding Entities
EntityIORegistration, save/load, spawn eggsAdding Entities
Biome spawningNatural mob spawning rulesAdding Entities
Mob dropsDeath loot systemCustom Loot
Sound eventsAmbient, hurt, death soundsCustom Sounds

Make sure you can build the project. See Getting Started if you have not done that yet. You should also read Adding Entities first, since this guide builds on that foundation.

Every entity needs a few unique identifiers. Check your codebase to make sure these are not already taken.

ThingTypeValue
Entity type enumeINSTANCEOFeTYPE_SHADOW_WALKER
Entity numeric IDEntityIO64 (next open hostile slot after EnderDragon at 63)
Texture constantTextures.hTN_MOB_SHADOW_WALKER
Sound enum entriesSoundTypes.heSoundType_MOB_SHADOWWALKER_AMBIENT, _HURT, _DEATH

Open Minecraft.World/Definitions.h and add a new entry to the eINSTANCEOF enum:

// In the eINSTANCEOF enum, after eTYPE_ENDERDRAGON
eTYPE_SHADOW_WALKER,

This is how the engine identifies entity types at runtime without using dynamic_cast.

This is the core of your mob. The Shadow Walker extends Monster, which gives us hostile mob behavior, dark-spawn checks, and burning in daylight for free.

Create Minecraft.World/ShadowWalker.h:

#pragma once
#include "Monster.h"
class ShadowWalker : public Monster
{
public:
eINSTANCEOF GetType() { return eTYPE_SHADOW_WALKER; }
static Entity *create(Level *level) { return new ShadowWalker(level); }
ShadowWalker(Level *level);
virtual int getMaxHealth();
protected:
virtual bool useNewAi();
virtual void defineSynchedData();
virtual int getAmbientSound();
virtual int getHurtSound();
virtual int getDeathSound();
virtual int getDeathLoot();
virtual void dropDeathLoot(bool wasKilledByPlayer, int playerBonusLevel);
virtual MobType getMobType();
public:
virtual void addAdditonalSaveData(CompoundTag *tag);
virtual void readAdditionalSaveData(CompoundTag *tag);
};

Two important things here:

  • The static create function is the factory that EntityIO calls when loading from saves or creating from spawn eggs.
  • useNewAi() must return true to enable the GoalSelector AI system.

Create Minecraft.World/ShadowWalker.cpp:

#include "ShadowWalker.h"
#include "net.minecraft.world.entity.ai.goal.h"
#include "net.minecraft.world.entity.ai.goal.target.h"
#include "net.minecraft.world.entity.ai.navigation.h"
#include "Player.h"
#include "Item.h"
#include "ItemInstance.h"
ShadowWalker::ShadowWalker(Level *level) : Monster(level)
{
// IMPORTANT: Call defineSynchedData() here because virtual
// dispatch is not available in the Entity base constructor.
this->defineSynchedData();
// Set health after synched data is ready
health = getMaxHealth();
// Texture (we will register this constant in Step 8)
this->textureIdx = TN_MOB_SHADOW_WALKER;
// Movement and combat stats
runSpeed = 0.3f; // Slightly faster than a zombie (0.23)
attackDamage = 6; // 3 hearts of damage per hit
// Bounding box: tall and narrow
setSize(0.6f, 2.4f);
// Navigation
getNavigation()->setCanOpenDoors(false);
getNavigation()->setAvoidWater(true);
// --- Behavior Goals (lower number = higher priority) ---
goalSelector.addGoal(0, new FloatGoal(this));
goalSelector.addGoal(1, new MeleeAttackGoal(this, eTYPE_PLAYER, runSpeed, false));
goalSelector.addGoal(2, new MoveTowardsRestrictionGoal(this, runSpeed));
goalSelector.addGoal(3, new RandomStrollGoal(this, 0.8f));
goalSelector.addGoal(4, new LookAtPlayerGoal(this, typeid(Player), 12.0f));
goalSelector.addGoal(5, new RandomLookAroundGoal(this));
// --- Targeting Goals ---
targetSelector.addGoal(1, new HurtByTargetGoal(this, false));
targetSelector.addGoal(2, new NearestAttackableTargetGoal(
this, typeid(Player), 16, 0, true));
}

Let’s break down the AI setup:

PriorityGoalWhat It Does
0FloatGoalSwim when in water so it does not drown
1MeleeAttackGoalWalk to target and hit them. The false means it does not need line of sight to start.
2MoveTowardsRestrictionGoalStay near its spawn area
3RandomStrollGoalWander around at 0.8x speed when idle
4LookAtPlayerGoalTurn to face any player within 12 blocks
5RandomLookAroundGoalLook in random directions when nothing else is happening

And the targeting goals:

PriorityGoalWhat It Does
1HurtByTargetGoalTarget whatever just hit us. The false means allies do not join in.
2NearestAttackableTargetGoalActively hunt the nearest player within 16 blocks

The targeting goals control who to attack. The behavior goals control how to attack and what to do otherwise. See Custom AI for the full breakdown of how priorities and control flags work.

Still in ShadowWalker.cpp, add the remaining methods:

bool ShadowWalker::useNewAi()
{
return true;
}
int ShadowWalker::getMaxHealth()
{
return 30; // 15 hearts
}
MobType ShadowWalker::getMobType()
{
// UNDEAD makes it vulnerable to Smite enchantment.
// Use UNDEFINED if you don't want any enchantment bonus.
return MobType::UNDEAD;
}
void ShadowWalker::defineSynchedData()
{
Monster::defineSynchedData();
// Add custom synched data here if needed.
// For a basic mob, the parent's data is enough.
}
void ShadowWalker::addAdditonalSaveData(CompoundTag *tag)
{
Monster::addAdditonalSaveData(tag);
// Save custom fields here if you add any later
}
void ShadowWalker::readAdditionalSaveData(CompoundTag *tag)
{
Monster::readAdditionalSaveData(tag);
// Load custom fields here if you add any later
}

The save/load methods do not have any custom data right now, but always call the parent class. You will need these hooks later if you add custom state like a powered mode or rage timer.

We need three sounds: ambient (idle groaning), hurt, and death. This involves two files.

Add three entries to the eSOUND_TYPE enum in Minecraft.World/SoundTypes.h:

// After the last mob sound entry
eSoundType_MOB_SHADOWWALKER_AMBIENT,
eSoundType_MOB_SHADOWWALKER_HURT,
eSoundType_MOB_SHADOWWALKER_DEATH,

Add the matching string names in Minecraft.Client/Common/Audio/SoundNames.cpp. These must be in the exact same order as the enum entries:

// In the wchSoundNames[] array, at the same indices:
L"mob.shadowwalker", // eSoundType_MOB_SHADOWWALKER_AMBIENT
L"mob.shadowwalkerhurt", // eSoundType_MOB_SHADOWWALKER_HURT
L"mob.shadowwalkerdeath", // eSoundType_MOB_SHADOWWALKER_DEATH

These string names map to events in the Miles soundbank. You will need to add actual audio files to the soundbank as well. See Custom Sounds for the full soundbank pipeline.

Back in ShadowWalker.cpp, add the sound overrides:

int ShadowWalker::getAmbientSound()
{
return eSoundType_MOB_SHADOWWALKER_AMBIENT;
}
int ShadowWalker::getHurtSound()
{
return eSoundType_MOB_SHADOWWALKER_HURT;
}
int ShadowWalker::getDeathSound()
{
return eSoundType_MOB_SHADOWWALKER_DEATH;
}

The engine calls getAmbientSound() on a random timer. getHurtSound() fires whenever the mob takes damage, and getDeathSound() fires once on death. Simple as that.

The Shadow Walker drops 1 to 3 Ender Pearls, plus a bonus from the Looting enchantment. If you wanted it to drop a custom item, you would create that item first (see Adding Items).

Add these to ShadowWalker.cpp:

int ShadowWalker::getDeathLoot()
{
// Fallback drop item ID. Used by the base class if you
// don't override dropDeathLoot.
return Item::enderPearl_Id;
}
void ShadowWalker::dropDeathLoot(bool wasKilledByPlayer, int playerBonusLevel)
{
// Drop 1-3 ender pearls, plus up to 1 extra per Looting level
int count = 1 + random->nextInt(3) + random->nextInt(1 + playerBonusLevel);
for (int i = 0; i < count; i++)
{
spawnAtLocation(Item::enderPearl_Id, 1);
}
// 50% chance to drop a bone (flavor drop)
if (random->nextInt(2) == 0)
{
spawnAtLocation(Item::bone_Id, 1);
}
}

The dropDeathLoot method is called by Mob::die() on the server side only. The wasKilledByPlayer flag tells you whether loot should be player-quality, and playerBonusLevel is the Looting enchantment level from the killing weapon. See Custom Loot for the full drop pipeline.

The Shadow Walker model is a tall humanoid with long arms and a narrow body. We build it from ModelPart cubes just like every other mob in the game.

Create Minecraft.Client/ShadowWalkerModel.h:

#pragma once
#include "Model.h"
class ModelPart;
class ShadowWalkerModel : public Model
{
public:
ModelPart *head;
ModelPart *body;
ModelPart *rightArm;
ModelPart *leftArm;
ModelPart *rightLeg;
ModelPart *leftLeg;
ShadowWalkerModel();
virtual void render(float f, float f1, float f2,
float f3, float f4, float scale);
virtual void setupAnim(float limbSwing, float limbSwingAmount,
float ageInTicks, float headYaw,
float headPitch, float scale);
};

Create Minecraft.Client/ShadowWalkerModel.cpp:

#include "ShadowWalkerModel.h"
#include "ModelPart.h"
#include "Mth.h"
ShadowWalkerModel::ShadowWalkerModel() : Model()
{
// Use a 64x64 texture sheet
texWidth = 64;
texHeight = 64;
// Head: 8x8x8 cube, positioned at the top
head = new ModelPart(this, 0, 0);
head->addBox(-4.0f, -8.0f, -4.0f, 8, 8, 8, 0.0f);
head->setPos(0.0f, -4.0f, 0.0f);
// Body: 8x16x4, narrow and tall
body = new ModelPart(this, 0, 16);
body->addBox(-4.0f, -2.0f, -2.0f, 8, 16, 4, 0.0f);
body->setPos(0.0f, -2.0f, 0.0f);
// Right arm: 4x16x4, long and thin
rightArm = new ModelPart(this, 32, 0);
rightArm->addBox(-3.0f, -2.0f, -2.0f, 4, 16, 4, 0.0f);
rightArm->setPos(-5.0f, -2.0f, 0.0f);
// Left arm: mirrored
leftArm = new ModelPart(this, 32, 0);
leftArm->bMirror = true;
leftArm->addBox(-1.0f, -2.0f, -2.0f, 4, 16, 4, 0.0f);
leftArm->setPos(5.0f, -2.0f, 0.0f);
// Right leg: 4x14x4
rightLeg = new ModelPart(this, 0, 36);
rightLeg->addBox(-2.0f, 0.0f, -2.0f, 4, 14, 4, 0.0f);
rightLeg->setPos(-2.0f, 14.0f, 0.0f);
// Left leg: mirrored
leftLeg = new ModelPart(this, 0, 36);
leftLeg->bMirror = true;
leftLeg->addBox(-2.0f, 0.0f, -2.0f, 4, 14, 4, 0.0f);
leftLeg->setPos(2.0f, 14.0f, 0.0f);
// Compile all parts into GPU display lists
float s = 1.0f / 16.0f;
head->compile(s);
body->compile(s);
rightArm->compile(s);
leftArm->compile(s);
rightLeg->compile(s);
leftLeg->compile(s);
}

Each ModelPart gets a texture offset (the first two arguments to the constructor), then one or more cubes via addBox. The setPos call places the part’s pivot point in model space. Parts are compiled once at construction time into GPU display lists.

The addBox parameters are: x offset, y offset, z offset, width, height, depth, grow. The grow value of 0.0f means no inflation. Armor layers use grow values like 0.5f to sit on top of the body without clipping.

See Entity Models for the full details on UV mapping, cube geometry, and the faceMask system.

Add the animation and render methods:

void ShadowWalkerModel::setupAnim(float limbSwing, float limbSwingAmount,
float ageInTicks, float headYaw,
float headPitch, float scale)
{
// Head follows where the mob is looking
head->yRot = headYaw / (180.0f / Mth::PI);
head->xRot = headPitch / (180.0f / Mth::PI);
// Arms swing opposite to legs
rightArm->xRot = Mth::cos(limbSwing * 0.6662f + Mth::PI) * 2.0f * limbSwingAmount * 0.5f;
leftArm->xRot = Mth::cos(limbSwing * 0.6662f) * 2.0f * limbSwingAmount * 0.5f;
rightArm->zRot = 0.0f;
leftArm->zRot = 0.0f;
// Legs walk
rightLeg->xRot = Mth::cos(limbSwing * 0.6662f) * 1.4f * limbSwingAmount;
leftLeg->xRot = Mth::cos(limbSwing * 0.6662f + Mth::PI) * 1.4f * limbSwingAmount;
// Subtle idle arm sway when standing still
rightArm->zRot += Mth::cos(ageInTicks * 0.09f) * 0.05f + 0.05f;
leftArm->zRot -= Mth::cos(ageInTicks * 0.09f) * 0.05f + 0.05f;
rightArm->xRot += Mth::sin(ageInTicks * 0.067f) * 0.05f;
leftArm->xRot -= Mth::sin(ageInTicks * 0.067f) * 0.05f;
// Attack animation
if (attackTime > 0.0f)
{
rightArm->xRot = rightArm->xRot * (1.0f - attackTime) +
(-Mth::PI / 2.0f) * attackTime;
}
}
void ShadowWalkerModel::render(float f, float f1, float f2,
float f3, float f4, float scale)
{
setupAnim(f, f1, f2, f3, f4, scale);
head->render(scale, true);
body->render(scale, true);
rightArm->render(scale, true);
leftArm->render(scale, true);
rightLeg->render(scale, true);
leftLeg->render(scale, true);
}

The animation system passes in limbSwing (distance walked) and limbSwingAmount (how fast). We use sine and cosine waves to swing the arms and legs back and forth. The ageInTicks value increases every tick, so we use it for subtle idle movements. The attackTime field goes from 0 to 1 during an attack swing.

The renderer connects your model to the rendering pipeline. It tells the engine what texture to use and how to draw the entity.

Create Minecraft.Client/ShadowWalkerRenderer.h:

#pragma once
#include "MobRenderer.h"
class ShadowWalkerModel;
class ShadowWalkerRenderer : public MobRenderer
{
public:
ShadowWalkerRenderer(ShadowWalkerModel *model, float shadowSize);
virtual float getScale(shared_ptr<Mob> mob, float partialTick);
};

Create Minecraft.Client/ShadowWalkerRenderer.cpp:

#include "ShadowWalkerRenderer.h"
#include "ShadowWalkerModel.h"
ShadowWalkerRenderer::ShadowWalkerRenderer(ShadowWalkerModel *model,
float shadowSize)
: MobRenderer(model, shadowSize)
{
}
float ShadowWalkerRenderer::getScale(shared_ptr<Mob> mob, float partialTick)
{
// The Shadow Walker is 1.2x normal scale (makes it look taller)
return 1.2f;
}

MobRenderer handles all the heavy lifting: model transformation, texture binding, hurt flash, death animation, name tags, and shadow rendering. All we need to provide is the model and optionally a scale override. The shadowSize parameter controls how big the shadow circle is on the ground (0.5 to 0.7 is typical).

Open Minecraft.Client/EntityRenderDispatcher.cpp and add the renderer to the map:

#include "ShadowWalkerRenderer.h"
#include "ShadowWalkerModel.h"
// In EntityRenderDispatcher::EntityRenderDispatcher(), after existing renderers:
renderers[eTYPE_SHADOW_WALKER] = new ShadowWalkerRenderer(
new ShadowWalkerModel(), 0.6f);

The renderers map uses eINSTANCEOF as the key. Every entity type that can appear in the world needs an entry here. There is no fallback renderer, so forgetting this will crash the game when the mob tries to render.

Register a texture constant in Minecraft.Client/Textures.h:

// After the last TN_MOB entry
#define TN_MOB_SHADOW_WALKER /* next available texture index */

Then load the actual texture file in the client’s texture loading system. Your texture should be a 64x64 PNG (matching the texWidth/texHeight in the model). See Block Textures for the general texture pipeline. Entity textures work similarly, they just go through a different loading path.

For the UV layout, refer to Entity Models. Each ModelPart cube maps its six faces onto the texture sheet based on the texture offset you set in the constructor. The head at offset (0, 0) uses the top-left area, the body at (0, 16) uses the area below that, and so on.

This is what makes the game actually know your mob exists. Open Minecraft.World/EntityIO.cpp and add the registration inside EntityIO::staticCtor():

#include "ShadowWalker.h"
// In EntityIO::staticCtor(), after existing registrations:
setId(ShadowWalker::create, eTYPE_SHADOW_WALKER, L"ShadowWalker", 64,
0x1A1A2E, // Spawn egg primary color (dark blue-black)
0x6C3483, // Spawn egg secondary color (purple)
IDS_SHADOW_WALKER);

The seven-argument setId call registers the mob in all five internal maps (string ID, numeric ID, factory function, type enum) and adds it to the spawn egg list for creative mode. If you do not want a spawn egg, use the four-argument version instead.

The numeric ID 64 is the next open slot after EnderDragon at 63. The two hex colors control the spawn egg appearance. IDS_SHADOW_WALKER is a string table entry for the localized mob name.

You also need to add the IDS_SHADOW_WALKER string to the localization table. The exact location depends on your platform, but it is usually in a string resource file or a language .lang file.

We want the Shadow Walker to spawn naturally in dark areas, just like zombies and skeletons. The spawning system is biome-based.

Open the biome files where you want the Shadow Walker to appear. For example, to add it to plains and forest biomes:

// In PlainsBiome constructor (Minecraft.World/PlainsBiome.cpp)
enemies.push_back(new MobSpawnerData(eTYPE_SHADOW_WALKER, 6, 1, 2));
// In ForestBiome constructor (Minecraft.World/ForestBiome.cpp)
enemies.push_back(new MobSpawnerData(eTYPE_SHADOW_WALKER, 8, 1, 3));

The MobSpawnerData parameters:

ParameterValueMeaning
mobClasseTYPE_SHADOW_WALKERWhich mob to spawn
probabilityWeight6 or 8Relative spawn chance (zombies use 10)
minCount1Minimum group size
maxCount2 or 3Maximum group size

A weight of 6 to 8 makes the Shadow Walker less common than zombies (weight 10) but still a regular encounter. Tweak these numbers to taste.

If you want the Shadow Walker everywhere, add it to the default enemy list in the base Biome constructor in Minecraft.World/Biome.cpp:

// In Biome::Biome(), alongside zombie and skeleton entries:
enemies.push_back(new MobSpawnerData(eTYPE_SHADOW_WALKER, 5, 1, 2));

Since ShadowWalker extends Monster, it already gets the standard hostile mob spawn checks: light level must be dark enough (isDarkEnoughToSpawn()), and the spawn position must have a solid block below it. If you want stricter rules, override canSpawn():

// In ShadowWalker.h, add to the public section:
virtual bool canSpawn();
// In ShadowWalker.cpp:
bool ShadowWalker::canSpawn()
{
// Only spawn below Y=50 (caves only)
if (y > 50) return false;
// Use the standard Monster spawn checks for everything else
return Monster::canSpawn();
}

This restricts the Shadow Walker to underground areas. Remove the Y check if you want it to spawn on the surface at night too.

At this point you have touched these files:

FileWhat You Changed
Minecraft.World/Definitions.hAdded eTYPE_SHADOW_WALKER to the enum
Minecraft.World/ShadowWalker.hNew file, entity class header
Minecraft.World/ShadowWalker.cppNew file, entity class implementation
Minecraft.World/SoundTypes.hAdded three sound enum entries
Minecraft.Client/Common/Audio/SoundNames.cppAdded three sound name strings
Minecraft.Client/ShadowWalkerModel.hNew file, model header
Minecraft.Client/ShadowWalkerModel.cppNew file, model with animation
Minecraft.Client/ShadowWalkerRenderer.hNew file, renderer header
Minecraft.Client/ShadowWalkerRenderer.cppNew file, renderer implementation
Minecraft.Client/EntityRenderDispatcher.cppRegistered the renderer
Minecraft.Client/Textures.hAdded texture constant
Minecraft.World/EntityIO.cppRegistered with setId and spawn egg
Minecraft.World/PlainsBiome.cppAdded to spawn list (optional)
Minecraft.World/ForestBiome.cppAdded to spawn list (optional)
Minecraft.World/Biome.cppAdded to default spawn list (optional)

Build the project. If it compiles, load a creative world and use the spawn egg to test. Check that:

  1. The mob spawns from the egg and renders correctly
  2. It walks around, looks at you, and attacks
  3. It makes sounds (ambient, hurt, death)
  4. It drops ender pearls and bones when killed
  5. It saves and loads when you quit and reload the world
  6. It spawns naturally in caves (switch to survival and explore)

If something goes wrong, check the most common issues:

  • Crash on spawn: Make sure the renderer is registered in EntityRenderDispatcher. There is no fallback.
  • Invisible mob: Check that textureIdx matches your texture constant and the texture is loaded.
  • No AI: Make sure useNewAi() returns true.
  • No spawning: Check that the biome spawn list has your mob and the weight is high enough to notice.
  • No sound: Make sure the wchSoundNames array is in sync with the eSOUND_TYPE enum. One missing entry will shift everything.

Once you have the basic Shadow Walker working, here are some ideas to build on it:

  • Add a powered variant using synched data. Use defineSynchedData() to add a boolean flag, then change the texture or scale in the renderer based on that flag. See Adding Entities for the synched data system.
  • Write a custom AI goal that makes it teleport short distances, like the Enderman. See Custom AI for how to write your own Goal subclass.
  • Add child parts to the model for horns or a tail. ModelPart::addChild() makes child parts move and rotate relative to their parent. See Entity Models.
  • Add rare drops by overriding dropRareDeathLoot(). The base class gives it a 2.5% chance to fire, increased by Looting. See Custom Loot.
  • Make it burn in sunlight (it already does if Monster handles that). Or override aiStep() to add custom tick behavior like healing in darkness.
  • Add a custom death animation by overriding render behavior in your renderer. You could make it dissolve into particles.
  • Create a custom dimension full of Shadow Walkers. See Custom Dimensions.