Skip to content

Entities

Lesson 12.5

Mobs, arrows, boats, dropped items: everything in the world that moves is an entity. This lesson shows how to spawn entities, move and turn them, change their size, react to every tick of one type, and register an entity type of your own.

Spawning

world:spawn(type, pos, options) puts a new entity into the world and returns it:

local pig = player.world:spawn("minecraft:pig", player.pos + cl.vec(2, 0, 0))
print(pig:type())   --> minecraft:pig

The options are all optional:

Option Means
yaw, pitch Which way it faces, in degrees. Yaw 0 looks south, 90 west, 180 north, 270 east.
velocity Its first motion, a cl.vec in blocks per tick.
nbt Extra data like /summon’s tag: "{NoAI:1b,CustomName:'Bob'}".

Spawning happens on the server: in server events, commands and server timers. The position must be in a loaded chunk, near players.

To get rid of an entity, e:remove("discarded") makes it vanish, and e:remove("killed") kills it like /kill, with loot. After that the variable is stale: using it raises a stale_handle error.

Moving and turning

pig:set_velocity(0, 0.6, 0)          -- a hop: 0.6 blocks per tick upwards
pig:set_rotation(90, 0)              -- face west
print(pig:on_ground())               --> false (it is in the air now)

e:velocity() reads the motion back and e:rotation() gives { yaw = ..., pitch = ... }. Living entities also turn their head on its own: e:head_rotation() and e:set_head_rotation(yaw, pitch).

Two helpers save you the trigonometry: e:eye_position() is where its eyes are, and e:look_vector() is a vector of length 1 pointing where it looks.

-- a block 5 blocks in front of the player's eyes
local spot = player:eye_position() + player:look_vector() * 5

Size and pose

Every entity has a box used for collisions and hits. You can change it at runtime, and the mob then fits through gaps, gets stuck or gets harder to hit:

local z = player.world:spawn("minecraft:zombie", player.pos + cl.vec(3, 0, 0))
z:set_dimensions(0.6, 0.6)     -- width, height (eye height: 0.85 x height)
z:set_pose("swimming")         -- the crawl animation
print(z:dimensions().height)   --> 0.6

The pose is the animation clients show ("standing", "crouching", "swimming", "sleeping", "sitting", …). It doesn’t change the size by itself, so pair the two. z:reset_dimensions() goes back to the normal size. Players are the exception: their own game decides their size and pose.

Every tick of one type

entity.tick runs after an entity has moved for the tick. Always give it a filter, so only that type calls you:

cl.on("entity.tick", function(event)
  if event.client then return end   -- it runs on clients too
  local cow = event.entity
  if cow:on_ground() and math.random() < 0.02 then
    cow:set_velocity(0, 0.5, 0)       -- now and then a cow jumps
  end
end, { filter = "minecraft:cow" })

Without a filter your function would run for every entity in the world, every tick. With one, the game skips all the other types without even calling your mod.

Your own entity type

Register a type in lifecycle.register, like an item:

cl.on("lifecycle.register", function()
  cl.entities.register{
    id = "slime_ball",
    kind = "mob",                    -- "mob", "misc" or "projectile"
    fallback = "minecraft:slime",    -- what players without your mod see
    width = 0.5, height = 0.5,
    max_health = 6,
    attributes = { movement_speed = 0.3 },
  }
end)

The server simulates your type with your size and health; other clients see the fallback. Players without your mod see the fallback, so pick one that looks close; with your mod they see your own model (Models and Animation). A "mob" has no AI of its own: you move it from entity.tick. "misc" entities just fall and slide, and "projectile" ones fly until they hit a block.

Spawn it like any other: world:spawn("my_mod:slime_ball", pos), or /summon my_mod:slime_ball.

Try it

Make every spawned my_mod:slime_ball hop towards the nearest… well, the first player, once a second (every 20 ticks).

Show a solution
local timer = {}
cl.on("entity.tick", function(event)
  if event.client then return end
  local ball = event.entity
  local n = (timer[ball] or 0) + 1
  timer[ball] = n
  if n % 20 ~= 0 or not ball:on_ground() then return end
  local player = cl.entity.server_players()[1]
  if not player then return end
  local d = player.pos - ball.pos
  local len = math.sqrt(d.x * d.x + d.z * d.z)
  if len < 1 then return end
  ball:set_velocity(d.x / len * 0.3, 0.45, d.z / len * 0.3)
end, { filter = "my_mod:slime_ball" })

Entity values are the same object every time, so they work as table keys.

Recap: world:spawn makes entities, set_velocity / set_rotation move and turn them, set_dimensions and set_pose change their size and animation, entity.tick with a filter runs your code every tick, and cl.entities.register adds your own type with a vanilla fallback.