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.