Skip to content

Custom entities

Status: Implemented (API 1.8). Custom entity types, spawning and removing, motion, rotation, size, pose and a per-type tick event. Models and animations for custom types: Guide: Entity Models and Animation (API 1.9). Queries, attributes and effects at runtime, synced data, combat, spawn rules and passengers: Guide: Entity Gameplay (API 1.10).

This guide builds a small creature from start to finish: a type, a way to spawn it, and a tick handler that moves it. The same calls work on vanilla entities, so the second half shows how to push, turn and resize any mob.

Reference: Entities, function tables entity, registry, level.

1. How a custom entity works

A custom entity type has two faces:

  • The server simulates the real type: its own size, health, attributes and your code. It saves the entity under your id (my_mod:wisp).
  • Clients are told a vanilla fallback type (minecraft:zombie, minecraft:item_display, …). Vanilla clients and Celestial clients without your mod draw and animate the fallback. Celestial clients with your mod also learn the real type and size, so e:type(), picking and hitboxes match the server.

If your mod is removed, the saved entities stay in the world file untouched (the server does not simulate them) and come back when the mod returns.

2. Register the type

Types are registered in lifecycle.register, like blocks and items.

cl.on("lifecycle.register", function()
  cl.entities.register{
    id = "wisp",                       -- my_mod:wisp
    kind = "mob",                      -- "mob", "misc" or "projectile"
    fallback = "minecraft:vex",        -- what other clients see
    category = "monster",
    width = 0.4, height = 0.8,         -- eye_height defaults to 0.85 x height
    max_health = 14,
    attributes = { movement_speed = 0.35, attack_damage = 3 },
    no_gravity = true,                 -- flags: fire_immune, not_summonable, no_gravity, silent, persistent
  }
end)
Kind What the server runs Default fallback
mob A living mob: health, attributes, gravity, the mob tick. It has no AI goals of its own: your code moves it. The fallback must be a mob. minecraft:zombie
misc A plain entity that falls and slides, no health. minecraft:item_display (invisible)
projectile Flies with gravity and drag, faces its motion, stops when it hits a block. minecraft:snowball

The fallback’s attributes are the base; max_health and your attributes replace single values. Ids without a namespace mean minecraft:. A mob plays its fallback’s sounds unless you set silent = true. Custom types work with /summon my_mod:wisp too (unless not_summonable).

3. Spawn it

cl.on("player.join", function(event)
  local p = event.player
  local wisp = p.world:spawn("my_mod:wisp", p.pos + cl.vec(0, 2, 0), { yaw = 180 })
  wisp:set_velocity(0, 0.2, 0)
end)

level:spawn(type, pos, opts) works for every type, vanilla or yours, in a loaded chunk. The options are yaw, pitch, velocity, nbt (an SNBT tag merged onto the new entity like /summon: "{NoAI:1b,CustomName:'Bob'}") and finalize (mobs pick their spawn equipment like /summon; default on, skipped when a tag is given). Players can’t be spawned.

e:remove("discarded") removes an entity without a trace; e:remove("killed") kills it like /kill (loot, experience, entity.death). The handle is stale afterwards: calls on it raise stale_handle.

4. Move it every tick

entity.tick runs after an entity’s tick. Always give it a filter: the game then never looks at types nobody listens to, so it costs nothing for the thousands of other entities.

cl.on("entity.tick", function(event)
  if event.client then return end         -- the event also runs on clients for their copies
  local w = event.entity
  local target = cl.entity.server_players()[1]
  if not target then return end
  local d = target.pos - w.pos
  local len = math.sqrt(d.x * d.x + d.y * d.y + d.z * d.z)
  if len > 1.5 then
    w:set_velocity(d.x / len * 0.15, d.y / len * 0.15, d.z / len * 0.15)
    w:set_rotation(math.deg(math.atan(-d.x, d.z)), 0)   -- face the player
  end
end, { filter = "my_mod:wisp" })

The payload has entity, level, type and client (true on a client). On the server it runs for every entity ticking in a loaded chunk, players included (filter = "minecraft:player").

5. Motion, rotation, size and pose (any entity)

Lua C Notes
e:velocity(), e:set_velocity(v) cl_entity_velocity, cl_entity_set_velocity Blocks per tick. Players get it like a knockback.
e:rotation(), e:set_rotation(yaw, pitch) cl_entity_rotation, cl_entity_set_rotation Degrees; yaw 0 = south, 90 = west. Players turn like /rotate.
e:head_rotation(), e:set_head_rotation(yaw, pitch) cl_entity_head_rotation, … Living entities turn the head apart from the body.
e:on_ground() cl_entity_on_ground
e:eye_position(), e:look_vector() cl_entity_eye_position, cl_entity_look_vector The look vector uses the pitch and head yaw.
e:dimensions(), e:set_dimensions(w, h, eye), e:reset_dimensions() cl_entity_dimensions, … Collisions, hitbox and pathfinding use it; saved with the entity; synced to Celestial clients. Not for players.
e:pose(), e:set_pose("crouching") cl_entity_pose, cl_entity_set_pose The synced animation pose. It doesn’t resize by itself: pair it with set_dimensions. Not for players.
e:is_custom() cl_entity_is_custom A mod type.
cl.registry.entity_type_fallback(id) cl_entity_type_fallback The vanilla type others see.

A zombie that crawls through one-block gaps:

z:set_pose("swimming")          -- the crawl animation
z:set_dimensions(0.6, 0.6)      -- and a box that fits

Setters are server-side. On a client the getters read the client’s copy of the entity (interpolated, so it can lag a tick behind the server).

6. The same in C and C++

static void on_register(void* payload, void* user) {
    (void)payload, (void)user;
    cl_entity_type_desc d = CL_ENTITY_TYPE_DESC_INIT;
    d.id = CL_STR("my_mod:wisp");
    d.kind = CL_ENTITY_KIND_MOB;
    d.vanilla_fallback = CL_STR("minecraft:vex");
    d.width = 0.4f;
    d.height = 0.8f;
    d.attributes = CL_STR("movement_speed=0.35, attack_damage=3");
    cl_entity_type_register(&d, NULL);
}
cl_spawn_opts o = CL_SPAWN_OPTS_INIT;
o.yaw = 180.0F;
if (auto wisp = player.level().spawn("my_mod:wisp", pos, &o)) {
    wisp->set_velocity({0.0, 0.2, 0.0});
}

7. Feature checks

cl.has("entity.custom_types"), "entity.spawn", "entity.motion", "entity.dimensions", "entity.pose", "entity.tick" are true from API 1.8. "entity.models" is true from API 1.9 (Guide: Entity Models and Animation); players without your mod still see the fallback, so pick one that looks close.