MultiLoader 1.21+ · Part 19

Custom Entities (MultiLoader 1.21+)

ADVANCED MULTILOADER 1.21-1.21.1 30 min read · Sep 14, 2026
MultiLoader 1.21+ · 28 parts
1 Getting Started with MultiLoader 1.21+ 2 Setting Up RegistrationUtils 3 Creating Items (MultiLoader 1.21+) 4 Creating Blocks (MultiLoader 1.21+) 5 Data Generation: Block & Item Models (MultiLoader 1.21+) 6 Data Generation: Block Loot Tables (MultiLoader 1.21+) 7 Data Generation: Crafting Recipes (MultiLoader 1.21+) 8 Data Generation: Block & Item Tags (MultiLoader 1.21+) 9 Custom Food Items (MultiLoader 1.21+) 10 Custom Tools (MultiLoader 1.21+) 11 Custom Armour (MultiLoader 1.21+) 12 Block Entities (MultiLoader 1.21+) 13 Config Files (MultiLoader 1.21+) 14 Custom Sounds (MultiLoader 1.21+) 15 Events and Listeners (MultiLoader 1.21+) 16 Networking and Custom Packets (MultiLoader 1.21+) 17 Data Generation: Advancements (MultiLoader 1.21+) 18 Data Generation: Language Files (MultiLoader 1.21+) 19 Custom Entities (MultiLoader 1.21+) 20 Ore Generation (MultiLoader 1.21+) 21 Introduction to Mixins (MultiLoader 1.21+) 22 Custom Particles (MultiLoader 1.21+) 23 Menus & Screens (MultiLoader 1.21+) 24 Key Bindings (MultiLoader 1.21+) 25 Custom Potion Effects (MultiLoader 1.21+) 26 Custom Enchantments (MultiLoader 1.21+) 27 Custom Commands (MultiLoader 1.21+) 28 Custom Biomes (MultiLoader 1.21+)

Custom entities involve three distinct layers: a shared EntityType registration and logic class in common, a client-only renderer that draws the model, and a spawn egg item so the entity can be summoned in creative mode. This tutorial creates a simple passive mob that wanders and looks at nearby players using vanilla AI goals.

NOTE
The renderer and model classes must only be referenced from client-side code. Registering them on the server will cause a crash. This tutorial shows the correct client-only registration pattern for both loaders.

Entity Registry

Create an EntityRegistry class in your common registry package:

java
public class EntityRegistry {
public static final RegistrationProvider<EntityType<?>> ENTITY_TYPES =
RegistrationProvider.get(Registries.ENTITY_TYPE, Constants.MOD_ID);
public static final RegistryObject<EntityType<?>, EntityType<ExampleEntity>> EXAMPLE_ENTITY =
ENTITY_TYPES.register("example_entity", () ->
EntityType.Builder.<ExampleEntity>of(ExampleEntity::new, MobCategory.CREATURE)
.sized(0.6f, 1.8f)
.clientTrackingRange(8)
.build(ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "example_entity")));
public static void init() {}
}

The sized call sets the entity's hitbox width and height in blocks. clientTrackingRange is the chunk radius within which the server sends tracking updates to clients. Call EntityRegistry.init() from CommonClass.init().

Entity Class

Create ExampleEntity in a common entity package. Extend Animal for a passive mob that breeds, or PathfinderMob for a passive mob without breeding:

java
public class ExampleEntity extends PathfinderMob {
public ExampleEntity(EntityType<? extends ExampleEntity> type, Level level) {
super(type, level);
}
public static AttributeSupplier.Builder createAttributes() {
return PathfinderMob.createMobAttributes()
.add(Attributes.MAX_HEALTH, 20.0)
.add(Attributes.MOVEMENT_SPEED, 0.25);
}
@Override
protected void registerGoals() {
goalSelector.addGoal(0, new FloatGoal(this));
goalSelector.addGoal(1, new WaterAvoidingRandomStrollGoal(this, 1.0));
goalSelector.addGoal(2, new LookAtPlayerGoal(this, Player.class, 6.0f));
goalSelector.addGoal(3, new RandomLookAroundGoal(this));
}
}

The createAttributes static method must be registered with the attribute event on NeoForge and the entity attribute registry on Fabric. Create a new event class called ExampleModEvents where we will keep MOD bus events in. Annotate it using the MOD eventbus - @EventBusSubscriber(modid = Constants.MOD_ID, bus = EventBusSubscriber.Bus.MOD) - then add the event:

java
// NeoForge (in ExampleModEvents.java)
@SubscribeEvent
public static void onEntityAttributeCreation(EntityAttributeCreationEvent event) {
event.put(EntityRegistry.EXAMPLE_ENTITY.get(), ExampleEntity.createAttributes().build());
}
java
// Fabric (in ExampleModFabric.java, inside onInitialize)
FabricDefaultAttributeRegistry.register(
EntityRegistry.EXAMPLE_ENTITY.get(), ExampleEntity.createAttributes());

Entity Renderer

Create a renderer in the common project's client package. For a quick start, reuse the vanilla pig model to confirm everything is wired up before writing a custom model:

java
public class ExampleEntityRenderer extends MobRenderer<ExampleEntity, PigModel<ExampleEntity>> {
private static final ResourceLocation TEXTURE =
ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "textures/entity/example_entity.png");
public ExampleEntityRenderer(EntityRendererProvider.Context context) {
super(context, new PigModel<>(context.bakeLayer(ModelLayers.PIG)), 0.7f);
}
@Override
public ResourceLocation getTextureLocation(ExampleEntity entity) {
return TEXTURE;
}
}

Create a 64×32 PNG texture at src/main/resources/assets/examplemod/textures/entity/example_entity.png (it can be a recolour of the vanilla pig texture to start). The third argument to the super constructor is the shadow radius.

Client-Side Registration

Register the renderer from client-only code. On NeoForge, use EntityRenderersEvent.RegisterRenderers on the mod bus:

java
// NeoForge: add to a @EventBusSubscriber(bus = Bus.MOD, value = Dist.CLIENT) class
@SubscribeEvent
public static void onRegisterRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerEntityRenderer(EntityRegistry.EXAMPLE_ENTITY.get(),
ExampleEntityRenderer::new);
}
java
// Fabric: inside ExampleModFabricClient.onInitializeClient()
EntityRendererRegistry.register(EntityRegistry.EXAMPLE_ENTITY.get(),
ExampleEntityRenderer::new);

In Fabric, you will need to create ExampleModFabricClient. Create this within a client package in the fabric project, and use the following code:

java
public class ExampleModClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
EntityRendererRegistry.register(EntityRegistry.EXAMPLE_ENTITY.get(),
ExampleEntityRenderer::new);
}
}

Then, in your fabric.mod.json, add the client entrypoint:

json
"entrypoints": {
"main": [
"com.example.examplemod.ExampleMod"
],
"client": [
"com.example.examplemod.client.ExampleModClient"
]
},
WARNING
On NeoForge, the class containing the renderer registration must be annotated with @EventBusSubscriber(value = Dist.CLIENT) so it is only loaded on the client. Referencing renderer or model classes outside a Dist.CLIENT context will cause a server-side classloading crash.

Spawn Egg

Register a spawn egg item in ItemRegistry. The two integer arguments are the shell colour and spot colour in hexadecimal - these are applied at render time, so no texture file is needed:

java
public static final RegistryObject<Item, Item> EXAMPLE_ENTITY_SPAWN_EGG =
ITEMS.register("example_entity_spawn_egg", () ->
new SpawnEggItem(EntityRegistry.EXAMPLE_ENTITY.get(), 0x8B4513, 0x228B22,
getItemProperties()));

Because spawn eggs use colour tinting rather than a dedicated texture, their datagen model must use minecraft:item/template_spawn_egg as the parent - not basicItem. Add this to your ExampleItemModelProvider:

java
withExistingParent(
ItemRegistry.EXAMPLE_ENTITY_SPAWN_EGG.getId().getPath(),
mcLoc("item/template_spawn_egg"));

Spawn eggs belong in both your own custom creative tab and the vanilla SPAWN_EGGS tab so players can find them alongside all other eggs. Add to your custom tab the same way you added other items. For the vanilla tab, use BuildCreativeModeTabContentsEvent on NeoForge:

java
// NeoForge - add to ExampleModEvents.java (MOD bus)
@SubscribeEvent
public static void onBuildCreativeTabContents(BuildCreativeModeTabContentsEvent event) {
if (event.getTabKey() == CreativeModeTabs.SPAWN_EGGS) {
event.accept(ItemRegistry.EXAMPLE_ENTITY_SPAWN_EGG.get());
}
}
java
// Fabric - inside ExampleMod.onInitialize()
ItemGroupEvents.modifyEntriesEvent(CreativeModeTabs.SPAWN_EGGS).register(entries ->
entries.accept(ItemRegistry.EXAMPLE_ENTITY_SPAWN_EGG.get()));

Add a translation for the egg in your ExampleLangProvider:

java
addItem(ItemRegistry.EXAMPLE_ENTITY_SPAWN_EGG, "Example Entity Spawn Egg");

Entity Loot Table

Entity loot tables define what the mob drops when killed. In your NeoForge data package create ExampleEntityLootProvider extending EntityLootSubProvider - the same pattern used for BlockLootSubProvider in the loot tables tutorial:

java
public class ExampleEntityLootProvider extends EntityLootSubProvider {
public ExampleEntityLootProvider(HolderLookup.Provider registries) {
super(FeatureFlags.REGISTRY.allFlags(), registries);
}
@Override
public void generate() {
add(EntityRegistry.EXAMPLE_ENTITY.get(),
LootTable.lootTable()
.withPool(LootPool.lootPool()
.setRolls(ConstantValue.exactly(1))
.add(LootItem.lootTableItem(Items.STRING)
.apply(SetItemCountFunction.setCount(UniformGenerator.between(1, 3))))
.when(LootItemKilledByPlayerCondition.killedByPlayer())));
}
@Override
protected Stream<EntityType<?>> getKnownEntityTypes() {
return EntityRegistry.ENTITY_TYPES.getEntries().stream()
.map(RegistryObject::get);
}
}

getKnownEntityTypes() works the same way as getKnownBlocks() - it lists every entity type this provider is responsible for, so datagen will error if you forget to add a loot table entry for one.

TIP
To add a looting enchantment bonus, chain .apply(LootingEnchantFunction.lootingMultiplier(this.registries, UniformGenerator.between(0, 1))) after SetItemCountFunction. The this.registries field is inherited from EntityLootSubProvider.

Register the entity loot provider alongside the existing block loot provider inside gatherData. Add a second SubProviderEntry with LootContextParamSets.ENTITY:

java
generator.addProvider(true,
new LootTableProvider(output, Set.of(),
List.of(
new LootTableProvider.SubProviderEntry(
ExampleBlockLootTableProvider::new,
LootContextParamSets.BLOCK),
new LootTableProvider.SubProviderEntry(
ExampleEntityLootProvider::new,
LootContextParamSets.ENTITY)
), registries));

After running datagen the generated file appears at common/src/generated/resources/data/examplemod/loot_table/entities/example_entity.json.

Testing

Run NeoForge Data, then launch the client and verify:

  • The spawn egg appears in your custom creative tab and in the vanilla Spawn Eggs tab, with the correct shell and spot colours.
  • Using the spawn egg summons the entity and it renders with your texture.
  • The entity wanders and looks at the player as the goals dictate.
  • Killing the entity drops 1–3 String (only when killed by a player).
  • No client crash occurs when the entity enters and exits render range.
TIP
To use a fully custom model instead of the borrowed pig model, create a class extending HierarchicalModel<ExampleEntity>, define the cube parts in the constructor using CubeListBuilder, and register a LayerDefinition via the model layer events. That topic is covered in a future custom model tutorial.

You can find the source for this tutorial here:

View Source on GitHub
NEXT IN SERIES

Ore Generation (MultiLoader 1.21+)

Define ConfiguredFeature and PlacedFeature resource keys in common, generate the JSON files with DatapackBuiltinEntriesProvider and a NeoForge BiomeModifier, and apply them on Fabric using BiomeModifications.

Continue →