Dubhe ECS (Entity Component System)
The Dubhe ECS library provides a powerful Entity Component System for building complex game logic and data management. It integrates seamlessly with the GraphQL client to provide real-time data synchronization and efficient querying capabilities.
Installation
pnpm install @0xobelisk/ecs @0xobelisk/graphql-client
Getting Started
Prerequisites
Before using the ECS library, ensure you have:
- A running GraphQL server with indexed data
- DubheMetadata configuration from your contract deployment
- A configured GraphQL client instance
Basic Setup
import { createECSWorld, DubheECSWorld } from '@0xobelisk/ecs';
import { createDubheGraphqlClient } from '@0xobelisk/graphql-client';
// Create GraphQL client
const graphqlClient = createDubheGraphqlClient({
endpoint: 'http://localhost:4000/graphql',
subscriptionEndpoint: 'ws://localhost:4000/graphql',
dubheMetadata: yourDubheMetadata, // From contract deployment
});
// Create ECS World
const ecsWorld = createECSWorld(graphqlClient, {
dubheMetadata: yourDubheMetadata,
queryConfig: {
defaultCacheTimeout: 5 * 60 * 1000, // 5 minutes
maxConcurrentQueries: 10,
enableBatchOptimization: true,
},
subscriptionConfig: {
defaultDebounceMs: 100,
maxSubscriptions: 50,
reconnectOnError: true,
},
});
Core Concepts
Entities and Components
In the ECS pattern, entities are unique identifiers that have components attached to them. Components are data containers that define the properties of entities.
// Check if entity exists
const entityExists = await ecsWorld.hasEntity('player_123');
// Get all components for an entity
const entity = await ecsWorld.getEntity('player_123');
console.log(entity.components); // { position: {...}, health: {...} }
// Get specific component data
const position = await ecsWorld.getComponent('player_123', 'position');
console.log(position); // { x: 100, y: 200 }
Component Discovery
The ECS system automatically discovers available components from your DubheMetadata:
// Get all available component types
const componentTypes = ecsWorld.getAvailableComponents();
console.log(componentTypes); // ['position', 'health', 'inventory']
// Get component metadata
const positionMeta = ecsWorld.getComponentMetadata('position');
console.log(positionMeta.fields); // [{ name: 'x', type: 'Int' }, { name: 'y', type: 'Int' }]
Querying Entities
Basic Queries
// Query entities with a specific component
const entitiesWithPosition = await ecsWorld.queryWith('position', {
first: 10,
orderBy: [{ field: 'updatedAt', direction: 'DESC' }],
});
console.log(entitiesWithPosition.entityIds); // ['player_1', 'player_2', ...]
console.log(entitiesWithPosition.items); // [{ x: 100, y: 200 }, ...]
Advanced Queries
// Query entities with ALL specified components (intersection)
const playersWithHealthAndPosition = await ecsWorld.queryWithAll([
'position',
'health'
], {
first: 20,
});
// Query entities with ANY of the specified components (union)
const combatEntities = await ecsWorld.queryWithAny([
'weapon',
'armor',
'spell'
]);
// Query entities with include/exclude logic
const alivePlayersWithWeapons = await ecsWorld.queryWithout(
['player', 'weapon'], // Must have these
['dead'], // Must not have these
);
Conditional Queries
// Query with conditions
const nearbyPlayers = await ecsWorld.queryWhere('position', {
x: { greaterThan: 50, lessThan: 150 },
y: { greaterThan: 100, lessThan: 200 },
}, {
first: 10,
orderBy: [{ field: 'updatedAt', direction: 'DESC' }],
});
Real-time Subscriptions
Component Change Subscriptions
// Listen to component additions
const addSubscription = ecsWorld.onComponentAdded('position', {
fields: ['x', 'y', 'updatedAt'],
debounceMs: 100,
}).subscribe({
next: (result) => {
console.log(`Entity ${result.entityId} added position:`, result.data);
},
error: (error) => console.error('Subscription error:', error),
});
// Listen to component changes
const changeSubscription = ecsWorld.onComponentChanged('health', {
initialEvent: true,
}).subscribe({
next: (result) => {
console.log(`Entity ${result.entityId} health changed:`, result.data);
},
});
// Listen to specific entity component changes
const entitySubscription = ecsWorld.onEntityComponent('health', 'player_123')
.subscribe({
next: (result) => {
console.log(`Player 123 health update:`, result.data);
},
});
Query Result Watching
// Watch query results for changes
const queryWatcher = ecsWorld.watchQuery(['position', 'velocity'], {
debounceMs: 200,
}).subscribe({
next: (result) => {
console.log('Query results changed:', result.changes);
console.log('Added entities:', result.changes.added);
console.log('Removed entities:', result.changes.removed);
console.log('Current entities:', result.changes.current);
},
});
Resource Management
Resources are global data that doesn’t belong to specific entities:
// Get available resource types
const resourceTypes = ecsWorld.getAvailableResources();
// Query resource data
const gameConfig = await ecsWorld.getResource('game_config', {
first: 1,
});
// Subscribe to resource changes
const resourceSubscription = ecsWorld.subscribeToResourceChanges('game_config')
.subscribe({
next: (data) => {
console.log('Game config updated:', data);
},
});
Performance Optimization
Batch Operations
// Query multiple component data efficiently
const [positions, healths] = await ecsWorld.queryMultiComponentData([
'position',
'health'
], {
first: 100,
});
Caching
// Configure caching for better performance
const ecsWorld = createECSWorld(graphqlClient, {
queryConfig: {
defaultCacheTimeout: 10 * 60 * 1000, // 10 minutes
enableBatchOptimization: true,
},
});
// Clear cache when needed
ecsWorld.clearCache();
Error Handling
try {
const entity = await ecsWorld.getEntity('invalid_entity');
} catch (error) {
console.error('Failed to get entity:', error);
}
// Subscription error handling
const subscription = ecsWorld.onComponentChanged('position')
.subscribe({
next: (result) => {
// Handle successful updates
},
error: (error) => {
console.error('Subscription failed:', error);
// Implement retry logic if needed
},
});
Cleanup
// Unsubscribe from all subscriptions
ecsWorld.unsubscribeAll();
// Dispose of the ECS world
ecsWorld.dispose();
Integration with Dubhe Client
The ECS system works seamlessly with the Dubhe client for contract interactions:
import { Dubhe } from '@0xobelisk/sui-client';
// Use ECS for reading game state
const playerPosition = await ecsWorld.getComponent('player_123', 'position');
// Use Dubhe client for writing game state
const tx = new Transaction();
await dubhe.tx.movement_system.move({
tx,
params: [playerPosition.x + 10, playerPosition.y],
onSuccess: async (result) => {
// ECS will automatically update via subscriptions
console.log('Move transaction successful:', result.digest);
},
});
Best Practices
- Use subscriptions for real-time updates instead of polling
- Batch queries when possible to reduce network overhead
- Configure appropriate cache timeouts based on your data update frequency
- Handle subscription errors gracefully with retry logic
- Clean up subscriptions when components unmount
- Use specific field selections to minimize data transfer
Common Patterns
Game Loop Integration
class GameEngine {
private ecsWorld: DubheECSWorld;
private subscriptions: Subscription[] = [];
async initialize() {
// Subscribe to relevant component changes
this.subscriptions.push(
this.ecsWorld.onComponentChanged('position').subscribe(this.onPositionUpdate),
this.ecsWorld.onComponentChanged('health').subscribe(this.onHealthUpdate),
);
}
private onPositionUpdate = (result) => {
// Update game rendering
this.renderer.updateEntityPosition(result.entityId, result.data);
};
private onHealthUpdate = (result) => {
// Update UI
this.ui.updateHealthBar(result.entityId, result.data);
};
dispose() {
this.subscriptions.forEach(sub => sub.unsubscribe());
this.ecsWorld.dispose();
}
}
API Reference
DubheECSWorld
The main class for interacting with the ECS system.
Constructor Options
interface ECSWorldConfig {
dubheMetadata?: DubheMetadata;
queryConfig?: {
defaultCacheTimeout?: number;
maxConcurrentQueries?: number;
enableBatchOptimization?: boolean;
};
subscriptionConfig?: {
defaultDebounceMs?: number;
maxSubscriptions?: number;
reconnectOnError?: boolean;
};
}
Methods
hasEntity(entityId: string): Promise<boolean>
- Check if entity existsgetEntity(entityId: string): Promise<any>
- Get complete entity datagetComponent<T>(entityId: string, componentType: string): Promise<T>
- Get specific componentqueryWith<T>(componentType: string, options?: QueryOptions): Promise<PagedQueryResult<T>>
- Query entities with componentqueryWithAll(componentTypes: string[], options?: QueryOptions): Promise<string[]>
- Query entities with all componentsqueryWithAny(componentTypes: string[], options?: QueryOptions): Promise<string[]>
- Query entities with any componentsonComponentAdded<T>(componentType: string, options?: SubscriptionOptions): Observable<ECSSubscriptionResult<T>>
- Subscribe to component additionsonComponentChanged<T>(componentType: string, options?: SubscriptionOptions): Observable<ECSSubscriptionResult<T>>
- Subscribe to component changes
Types
interface PagedQueryResult<T> {
entityIds: string[];
items: T[];
pageInfo: {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor?: string;
endCursor?: string;
};
totalCount: number;
}
interface ECSSubscriptionResult<T> {
entityId: string;
data: T | null;
changeType: 'added' | 'updated' | 'removed';
timestamp: number;
error?: Error;
}