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
- Server systems update
MetadataCompeach tick EntitiesSendingSystemdetects changes- Changed metadata is sent to interested clients
- Client receives data in entity lifecycle hooks
Setting Metadata
Use metadata.set() to add typed data:
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():
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:
// Automatically adds to all entities with EntityFlag:
metadata.set("position", &position_comp);
PeersMetaSystem
For client entities, syncs position, direction, and name:
// 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:
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:
.with(HealthMetaSystem, "health-meta", &[])
.with(EntitiesSendingSystem, "entities-sending", &["entities-meta", "health-meta"])
Client-Side Data Types
Define matching TypeScript types for your metadata:
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:
// 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:
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:
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:
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:
peers.packInfo(); // Returns position, direction, and custom data
Example: Complete Metadata Flow
Server component and system:
#[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:
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.