ECS Skill
Use this skill when working with the Entity Component System (ECS), including:
- Finding or modifying components, systems, or worlds
- Understanding ECS architecture and data flow
- Debugging ECS-related issues
- Adding new components or systems
- Working with entity serialization and network synchronization
Overview
This project uses bytenetc, a custom sparse-set ECS framework with typed array storage. Both server (packet_router) and client (game) maintain parallel ECS instances synchronized via Socket.io packets.
Key Locations
ECS Core Framework:
packages/bytenetc/src/lib/ - Core ECS implementation
Components:
packages/network-world-entities/src/lib/components/ - All component definitions
- Common components: Position, Sprite, Velocity, Collider, Player, Building, Resource, Tile
Systems:
apps/packet_router/src/systems/ - Server-side systems (20 TPS tick loop)
apps/game/src/systems/ - Client-side systems (60 FPS render loop)
Entities:
packages/network-world-entities/src/lib/entities/ - Entity factory functions
Serialization:
packages/network-world-entities/src/lib/serializeConfig.ts - Component serialization config
packages/network-world-entities/src/lib/SerializationIDs.ts - Serialization ID definitions
Network Sync:
apps/packet_router/src/main.ts - Server tick loop and packet broadcasting
apps/game/src/scenes/Game.ts - Client packet handling and deserialization
apps/game/src/networking/Network.ts - Socket.io client wrapper
Quick Reference
Component Definition
typescript
1import { defineComponent, Types } from '@virtcon2/bytenetc';
2
3export const MyComponent = defineComponent('myComponent', {
4 value: Types.i32,
5 position: Types.f32,
6 data: [Types.ui8, 32], // Fixed-size array
7});
Supported types: i8, i16, i32, f32, f64, ui8, ui16, ui32
World Management
typescript
1import { createWorld, deleteWorld, registerComponents } from '@virtcon2/bytenetc';
2
3const world = createWorld('world-id');
4registerComponents(world, [Position, Sprite, MyComponent]);
5deleteWorld(world); // Cleanup
Entity Operations
typescript
1import { addEntity, addComponent, removeEntity, removeComponent } from '@virtcon2/bytenetc';
2
3const eid = addEntity(world);
4addComponent(world, Position, eid);
5
6// Set values (direct array access)
7Position.x[eid] = 100;
8Position.y[eid] = 200;
9
10removeComponent(world, Sprite, eid);
11removeEntity(world, eid);
Queries
typescript
1import { defineQuery, Has, Not, Changed, enterQuery, exitQuery } from '@virtcon2/bytenetc';
2
3// Basic query - entities with both components
4const query = defineQuery(Position, Sprite);
5const entities = query(world); // Returns entity ID array
6
7// Modifiers
8const playerQuery = defineQuery(Position, Has(Player), Not(NPC));
9const changedQuery = defineQuery(Changed(Position));
10
11// Enter/Exit queries
12const newEntities = enterQuery(query)(world);
13const removedEntities = exitQuery(query)(world);
System Definition (Server)
typescript
1import { defineSystem } from '@virtcon2/bytenetc';
2import type { SyncEntities } from '@virtcon2/network-packet';
3
4export const createMySystem = (world: World) => {
5 const query = defineQuery(Position, MyComponent);
6
7 return defineSystem<SyncEntities>(({ worldData }) => {
8 const entitiesToSync: number[] = [];
9 const entitiesToRemove: number[] = [];
10
11 // Process entities
12 for (const eid of query(world)) {
13 Position.x[eid] += 1;
14 entitiesToSync.push(eid);
15 }
16
17 // Serialize for network
18 const serialized = defineSerializer(
19 getSerializeConfig(world)[SerializationID.MY_ENTITY]
20 )(world, entitiesToSync);
21
22 return {
23 sync: [{ serializationId: SerializationID.MY_ENTITY, data: serialized }],
24 removeEntities: entitiesToRemove,
25 worldData,
26 };
27 });
28};
System Definition (Client)
typescript
1export const createMyClientSystem = (world: World) => {
2 const query = defineQuery(Position, Sprite);
3
4 return (state: State) => {
5 for (const eid of query(world)) {
6 const sprite = state.spritesById[eid];
7 if (sprite) {
8 sprite.x = Position.x[eid];
9 sprite.y = Position.y[eid];
10 }
11 }
12 return state;
13 };
14};
Network Synchronization Flow
-
Server (packet_router):
- Runs tick loop at 20 TPS (
apps/packet_router/src/main.ts)
- Systems process entities and return serialized data
- Broadcasts
SYNC_SERVER_ENTITY and REMOVE_ENTITY packets via Socket.io
-
Client (game):
- Receives packets in
Game.update() (apps/game/src/scenes/Game.ts)
- Deserializes entities into local ECS world
- Runs client systems at 60 FPS to update Phaser sprites
-
Serialization:
- Defined in
serializeConfig.ts - maps SerializationID to component lists
- Entities serialize to arrays:
[componentName, fieldName, value]
- Only specified components are synced per entity type
Common Tasks
Adding a New Component
- Create component definition in
packages/network-world-entities/src/lib/components/
- Export from
packages/network-world-entities/src/lib/components/index.ts
- Add to serialization config in
serializeConfig.ts if network-synced
- Register in world initialization (both server and client if synced)
Adding a New System
Server:
- Create in
apps/packet_router/src/systems/
- Import and add to
tickSystems() in apps/packet_router/src/main.ts
Client:
- Create in
apps/game/src/systems/
- Import and call in
Game.update() in apps/game/src/scenes/Game.ts
Adding a New Entity Type
- Create factory function in
packages/network-world-entities/src/lib/entities/
- Add SerializationID in
SerializationIDs.ts
- Add serialization config in
serializeConfig.ts
- Create spawning logic in appropriate system
Debugging
Check which entities have a component:
typescript
1const query = defineQuery(MyComponent);
2console.log('Entities with MyComponent:', query(world));
Check component values:
typescript
1console.log('Position:', { x: Position.x[eid], y: Position.y[eid] });
Track component changes:
typescript
1const changedQuery = defineQuery(Changed(Position));
2console.log('Entities with changed Position:', changedQuery(world));
Important Notes
- Maximum 3,000 entities per world
- Component data stored in typed arrays for performance
- Direct array access (e.g.,
Position.x[eid]) for reading/writing values
- Server systems run at 20 TPS, client systems at 60 FPS
- Not all components need network sync (client-only rendering components)
- Use enter/exit queries to detect new/removed entities efficiently
Documentation
Full details: /docs/ECS_AND_NETWORK_SYNC.md