Skip to main content

Metadata Component

Metadata is how entity state gets synchronized from server to client. The MetadataComp stores a JSON-serializable map that gets sent to clients whenever it changes.

How Metadata Works

  1. Server systems update MetadataComp each tick
  2. EntitiesSendingSystem detects changes
  3. Changed metadata is sent to interested clients
  4. Client receives data in entity lifecycle hooks

Setting Metadata

Use metadata.set() to add typed data:

Setting Metadata
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct HealthData {
current: i32,
max: i32,
}

let mut metadata = MetadataComp::default();
metadata.set("health", &HealthData { current: 80, max: 100 });
metadata.set("position", &PositionComp::new(10.0, 64.0, 10.0));
metadata.set("name", &"Bob".to_string());

Getting Metadata

Retrieve typed data with metadata.get():

Getting Metadata
let health: Option<HealthData> = metadata.get("health");
let position: Option<PositionComp> = metadata.get("position");
let name: Option<String> = metadata.get("name");

Built-in Metadata Systems

Voxelize includes systems that automatically sync common components:

EntitiesMetaSystem

For non-client entities, syncs PositionComp to metadata:

EntitiesMetaSystem Behavior
// Automatically adds to all entities with EntityFlag:
metadata.set("position", &position_comp);

PeersMetaSystem

For client entities, syncs position, direction, and name:

PeersMetaSystem Behavior
// Automatically adds to all entities with ClientFlag:
metadata.set("position", &position_comp);
metadata.set("direction", &direction_comp);
metadata.set("username", &name_comp);

Custom Metadata Systems

Create systems to sync your custom components:

Custom Metadata System
use specs::{System, ReadStorage, WriteStorage, Join};

pub struct HealthMetaSystem;

impl<'a> System<'a> for HealthMetaSystem {
type SystemData = (
ReadStorage<'a, HealthComp>,
WriteStorage<'a, MetadataComp>,
);

fn run(&mut self, (healths, mut metadatas): Self::SystemData) {
use rayon::prelude::*;
use specs::ParJoin;

(&healths, &mut metadatas)
.par_join()
.for_each(|(health, metadata)| {
metadata.set("health", health);
});
}
}

Add to the dispatcher before EntitiesSendingSystem:

Adding to Dispatcher
.with(HealthMetaSystem, "health-meta", &[])
.with(EntitiesSendingSystem, "entities-sending", &["entities-meta", "health-meta"])

Client-Side Data Types

Define matching TypeScript types for your metadata:

Client Data Types
type BotData = {
position: VOXELIZE.Coords3;
direction: number[];
health: {
current: number;
max: number;
};
name: string;
};

class Bot extends VOXELIZE.Entity<BotData> {
onUpdate = (data: BotData) => {
this.position.set(...data.position);
this.updateHealthBar(data.health.current, data.health.max);
this.setName(data.name);
};
}

Partial Updates

Only changed fields are sent. Metadata tracks what changed since last send:

Efficient Updates
// Only sends "health" field, not entire metadata
metadata.set("health", &new_health);

// Position sent separately by EntitiesMetaSystem

Metadata in Entity Loaders

Access metadata passed during spawning:

Metadata in Loaders
world.set_entity_loader("npc", |world, metadata| {
let name = metadata.get::<String>("name").unwrap_or("NPC".to_string());
let health = metadata.get::<HealthComp>("health").unwrap_or(HealthComp(100));

world
.create_entity(&nanoid!(), "npc")
.with(PositionComp::default())
.with(health)
.with(NameComp::new(&name))
.with(metadata)
});

Serialization Requirements

Components synced via metadata must implement Serialize and Deserialize:

Serializable Component
use serde::{Serialize, Deserialize};
use specs::{Component, VecStorage};

#[derive(Component, Default, Serialize, Deserialize, Clone)]
#[storage(VecStorage)]
pub struct StatsComp {
pub kills: u32,
pub deaths: u32,
pub score: i32,
}

Peer Metadata

For client metadata (peers), use the client parser:

Client Parser
world.set_client_parser(|world, metadata_str, client_ent| {
let metadata: PeerUpdate = serde_json::from_str(metadata_str).unwrap();

if let Some(position) = metadata.position {
let mut positions = world.write_component::<PositionComp>();
if let Some(p) = positions.get_mut(client_ent) {
p.0.set(position.0, position.1, position.2);
}
}
});

The client sends metadata via the Peers manager:

Client Peer Metadata
peers.packInfo(); // Returns position, direction, and custom data

Example: Complete Metadata Flow

Server component and system:

Server Side
#[derive(Component, Default, Serialize, Deserialize)]
#[storage(VecStorage)]
pub struct MoodComp(pub String);

pub struct MoodMetaSystem;

impl<'a> System<'a> for MoodMetaSystem {
type SystemData = (
ReadStorage<'a, MoodComp>,
WriteStorage<'a, MetadataComp>,
);

fn run(&mut self, (moods, mut metadatas): Self::SystemData) {
for (mood, metadata) in (&moods, &mut metadatas).join() {
metadata.set("mood", mood);
}
}
}

Client entity:

Client Side
type NPCData = {
position: VOXELIZE.Coords3;
mood: string;
};

class NPC extends VOXELIZE.Entity<NPCData> {
private moodText: VOXELIZE.SpriteText;

constructor(id: string) {
super(id);
this.moodText = new VOXELIZE.SpriteText("...");
this.moodText.position.y = 2;
this.add(this.moodText);
}

onUpdate = (data: NPCData) => {
this.position.set(...data.position);
this.moodText.text = data.mood;
};
}

Read on to learn about customizing the ECS dispatcher.