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:
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:
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:
// NeoForge (in ExampleModEvents.java)
@SubscribeEvent
public static void onEntityAttributeCreation(EntityAttributeCreationEvent event) {
event.put(EntityRegistry.EXAMPLE_ENTITY.get(), ExampleEntity.createAttributes().build());
}
// 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:
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:
// 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);
}
// 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:
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:
"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:
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:
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:
// 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());
}
}
// 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:
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:
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:
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 →