Skip to content

Entities

Lesson 7.5

Mobs, arrows, boats, dropped items: everything in the world that moves is an entity, a cl_entity handle. 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

cl_level_spawn puts a new entity into the world:

static cl_entity spawn_pig(cl_entity player) {
    cl_vec3 pos;
    cl_entity pig = {0};
    cl_entity_position(player, &pos);
    pos.x += 2;
    cl_spawn_opts o = CL_SPAWN_OPTS_INIT;
    o.yaw = 90.0f;                              // face west
    if (cl_level_spawn(cl_entity_level(player), CL_STR("minecraft:pig"), pos, &o, &pig) != CL_OK) {
        CL_LOGF(CL_LOG_WARN, "no pig: %d", (int)cl_last_error());
    }
    return pig;
}

cl_spawn_opts is a descriptor, so it starts from CL_SPAWN_OPTS_INIT. Its fields: yaw and pitch in degrees (yaw 0 looks south, 90 west), velocity (a cl_vec3, blocks per tick), nbt (a tag like /summon’s: CL_STR("{NoAI:1b}")) and finalize (spawn equipment like /summon, on by default). Pass NULL for all defaults.

Spawning runs on the server tick, at a loaded position. cl_entity_remove(e, CL_REMOVE_DISCARDED) makes an entity vanish, CL_REMOVE_KILLED kills it like /kill. Afterwards the handle is stale and calls return CL_E_STALE_HANDLE.

Moving and turning

cl_entity_set_velocity(pig, (cl_vec3){0, 0.6, 0});     // a hop
cl_entity_set_rotation(pig, 180.0f, 0.0f);            // face north

cl_rotation r;
cl_entity_rotation(pig, &r);                          // r.yaw, r.pitch
bool flying = !cl_entity_on_ground(pig);

Living entities also turn their head: cl_entity_head_rotation / cl_entity_set_head_rotation. cl_entity_eye_position gives the eyes, and cl_entity_look_vector a vector of length 1 along the view.

Size and pose

cl_entity_set_dimensions(zombie, 0.6f, 0.6f, 0.0f);   // width, height, eye height (0: 0.85 x height)
cl_entity_set_pose(zombie, CL_POSE_SWIMMING);         // the crawl animation

cl_dimensions d;
cl_entity_dimensions(zombie, &d);                     // d.width, d.height, d.eye_height

The size is used for collisions, hits and pathfinding, and it is saved with the entity. The pose is only the animation, so pair the two. cl_entity_reset_dimensions goes back to normal. Players return CL_E_UNSUPPORTED: their own game owns their size and pose.

Every tick of one type

CL_EV_ENTITY_TICK runs after an entity’s tick. Give it a filter, so only that type calls your mod:

static void on_cow_tick(void* payload, void* user) {
    (void)user;
    cl_ev_entity_tick* e = CL_EVENT(cl_ev_entity_tick, payload);
    if (e->client) return;                               // it runs on clients too
    if (cl_entity_on_ground(e->entity) && (cl_time_now_ns() & 63) == 0)
        cl_entity_set_velocity(e->entity, (cl_vec3){0, 0.5, 0});
}

static cl_result subscribe_cows(void) {
    cl_subscribe_opts o = CL_SUBSCRIBE_OPTS_INIT;
    o.filter = cl_ident_intern(CL_STR("minecraft:cow"));
    return cl_event_subscribe(CL_EV_ENTITY_TICK, &o, on_cow_tick, NULL, NULL);
}

With the filter the game skips every other type without calling into your mod, so it costs nothing for them.

Your own entity type

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:slime_ball");
    d.kind = CL_ENTITY_KIND_MOB;                    // or CL_ENTITY_KIND_MISC / CL_ENTITY_KIND_PROJECTILE
    d.vanilla_fallback = CL_STR("minecraft:slime"); // what players without your mod see
    d.width = 0.5f;
    d.height = 0.5f;
    d.max_health = 6.0f;
    d.attributes = CL_STR("movement_speed=0.3");
    if (cl_entity_type_register(&d, NULL) != CL_OK) CL_LOGF(CL_LOG_ERROR, "slime_ball failed to register");
}

Register in CL_EV_LIFECYCLE_REGISTER, like items. The server simulates your type with your size and health; other clients see the fallback. Celestial clients with your mod can draw a model of your own instead: Models and Animation. A mob has no AI of its own: move it from CL_EV_ENTITY_TICK. More in the guide Custom Entities.

Try it

Spawn a minecraft:zombie next to every player who joins, shrink it to 0.6 x 0.6 and make it crawl.

Show a solution
static void on_join(void* payload, void* user) {
    (void)user;
    cl_ev_player* e = CL_EVENT(cl_ev_player, payload);
    cl_vec3 pos;
    cl_entity z = {0};
    cl_entity_position(e->player, &pos);
    pos.x += 3;
    if (cl_level_spawn(cl_entity_level(e->player), CL_STR("minecraft:zombie"), pos, NULL, &z) != CL_OK) return;
    cl_entity_set_dimensions(z, 0.6f, 0.6f, 0.0f);
    cl_entity_set_pose(z, CL_POSE_SWIMMING);
}

Recap: cl_level_spawn makes entities, cl_entity_set_velocity / set_rotation move and turn them, set_dimensions and set_pose change size and animation, CL_EV_ENTITY_TICK with a filter runs every tick, and cl_entity_type_register adds your own type with a vanilla fallback.