Skip to main content

Custom Entities

Entities are server-controlled objects that sync to clients - mobs, NPCs, projectiles, or any game object that needs authoritative state.

Example: Bot Entity

This example creates a bot that follows players around the world.

Server: Define the Entity Loader

Server Entity Definition
use voxelize::*;
use specs::Component;
use nanoid::nanoid;
use std::time::Duration;

#[derive(Component, Default)]
#[storage(NullStorage<BotFlag>)]
struct BotFlag;

world.set_entity_loader("bot", |world, metadata| {
let body = RigidBody::new(
&AABB::new().scale_x(0.6).scale_y(1.8).scale_z(0.6).build()
);
let interactor = world.physics_mut().register(&body);

world
.create_entity(&nanoid!(), "bot")
.with(BotFlag)
.with(PositionComp::default())
.with(DirectionComp::default())
.with(RigidBodyComp::new(&body))
.with(InteractorComp::new(&interactor))
.with(BrainComp::new(BrainOptions {
max_speed: 4.0,
jump_impulse: 8.0,
..Default::default()
}))
.with(TargetComp::players())
.with(PathComp::new(
100,
24.0,
10000,
Duration::from_millis(50),
))
});

Server: Spawn via Method

Spawn Method Handler
#[derive(Serialize, Deserialize)]
struct SpawnBotPayload {
position: [f32; 3],
}

world.set_method_handle("spawn-bot", |world, _client_id, payload| {
let data: SpawnBotPayload = serde_json::from_value(payload).unwrap();
let [x, y, z] = data.position;

world.spawn_entity_at("bot", &Vec3(x, y, z));
});

Client: Define the Entity Class

Client Entity Class
import * as VOXELIZE from "@voxelize/core";
import * as THREE from "three";

type BotData = {
position: VOXELIZE.Coords3;
direction: number[];
};

class Bot extends VOXELIZE.Entity<BotData> {
character: VOXELIZE.Character;

constructor(id: string) {
super(id);

this.character = new VOXELIZE.Character({
nameTagOptions: {
fontFace: "monospace",
},
});
this.character.username = "Bot";
this.character.head.paint("all", new THREE.Color("#4a90d9"));

this.add(this.character);
}

onCreate = (data: BotData) => {
this.character.set(data.position, data.direction);
};

onUpdate = (data: BotData) => {
this.character.set(data.position, data.direction);
};

onDelete = () => {
this.character.dispose();
};

update = () => {
this.character.update();
};
}

Client: Register and Use

Client Entity Registration
const entities = new VOXELIZE.Entities();
const method = new VOXELIZE.Method();

network.register(entities);
network.register(method);

entities.setClass("bot", Bot);
world.add(entities);

inputs.bind("KeyZ", () => {
method.call("spawn-bot", {
position: controls.object.position.toArray(),
});
});

function animate() {
if (world.isInitialized) {
entities.update();
}
}

Entity Lifecycle

ServerClient
set_entity_loader("type", ...)entities.setClass("type", Class)
spawn_entity_at("type", pos)onCreate(data) called
ECS systems update componentsonUpdate(data) called
despawn_entity(id)onDelete() called

Full Client Implementation

main.ts
import * as VOXELIZE from "@voxelize/core";
import * as THREE from "three";

type BotData = {
position: VOXELIZE.Coords3;
direction: number[];
};

class Bot extends VOXELIZE.Entity<BotData> {
character: VOXELIZE.Character;

constructor(id: string) {
super(id);
this.character = new VOXELIZE.Character();
this.character.username = "Bot";
this.add(this.character);
}

onCreate = (data: BotData) => {
this.character.set(data.position, data.direction);
};

onUpdate = (data: BotData) => {
this.character.set(data.position, data.direction);
};

update = () => {
this.character.update();
};
}

const network = new VOXELIZE.Network();
const entities = new VOXELIZE.Entities();
const method = new VOXELIZE.Method();

network.register(entities);
network.register(method);

entities.setClass("bot", Bot);

async function start() {
await network.connect("http://localhost:4000");
await network.join("tutorial");

method.call("spawn-bot", { position: [0, 50, 0] });
}

start();

See examples/client/src/main.ts for a complete implementation with path visualization and multiple entity types.