A modular Entity Component System (ECS) game engine for building 3D web applications and games.
The AIO3D Engine Core Package provides a robust Entity Component System (ECS) architecture for building 3D applications and games. It seamlessly integrates with Three.js for rendering and implements a modular design to promote clean, maintainable code.
Note: This package is distributed in minified form, but the public API remains fully accessible as documented here.
- Entity Component System (ECS) - Clean separation of data and behavior
- Three.js Integration - First-class support for Three.js rendering
- Prefab System - Data-driven entity creation
- Level Management - Structured level transitions
- Event System - Decoupled communication between systems
- DOM Integration - Seamless DOM and WebGL integration
- Component Factory System - Flexible component instantiation
- Physics System - Rapier-powered 3D physics with collision detection
npm install aio3d-core
The foundation of the engine:
-
World
: Manages entities and systems, provides the event bus -
Entity
: Container for components with unique ID -
Component
: Base class for all component types -
System
: Base class for all system logic
Built-in components include:
-
TransformComponent
: Position, rotation, scale data -
MeshComponent
: 3D visual representation using Three.js -
CameraComponent
: Camera properties and configuration -
PersistentComponent
: Marks entity to survive level transitions -
DOMComponent
: Connects to DOM elements -
OrbitControlComponent
: Orbit camera controls -
ModelComponent
: 3D model data with animation support -
AnimationControllerComponent
: Animation state management for models -
RigidBodyComponent
: Physics body properties (dynamic, static, kinematic) -
ColliderComponent
: Physics shape and collision properties
Built-in systems include:
-
SceneSystem
: Manages Three.js scene and renderer setup -
RenderSystem
: Handles rendering the scene -
MeshRegistrationSystem
: Adds meshes to the scene -
WindowSystem
: Handles window events and resizing -
InputSystem
: Manages keyboard, mouse, and touch inputs -
OrbitCameraSystem
: Implements orbit camera controls -
ModelSystem
: Manages 3D model loading and animation setup -
ModelRegistrationSystem
: Registers models with the scene -
AnimationControllerSystem
: Controls model animation states and transitions -
PhysicsSystem
: Manages Rapier physics simulation and synchronization
import {
World,
Entity,
SceneSystem,
RenderSystem,
ComponentTypes,
prefabRegistry,
PrefabService,
} from "aio3d-core";
// Create world
const world = new World();
// Add core systems
world.addSystem(new SceneSystem());
world.addSystem(new RenderSystem());
// Create entity from prefab
const prefabService = new PrefabService(world, factoryRegistry);
const cubeEntity = prefabService.createEntityFromPrefab("CubePrefab");
world.addEntity(cubeEntity);
// Game loop
function gameLoop(time) {
const deltaTime = time - lastTime;
lastTime = time;
// Update all systems
world.update(deltaTime / 1000);
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
Template-based entity creation:
// Define prefab
const cubePrefab = {
name: "CubePrefab",
components: [
{
type: ComponentTypes.TRANSFORM,
data: { position: new THREE.Vector3(0, 1, 0) },
},
{
type: ComponentTypes.MESH,
data: {
geometryType: "BoxGeometry",
geometryArgs: [1, 1, 1],
materialType: "MeshStandardMaterial",
materialArgs: { color: 0x3080ff },
},
},
],
};
// Register prefab
prefabRegistry.registerPrefab(cubePrefab);
// Create entity from prefab
const entity = prefabService.createEntityFromPrefab("CubePrefab");
Structure game into distinct levels:
// Register level
LevelRegistry.getInstance().registerLevel(
"MAIN_LEVEL",
(container, world, prefabService, levelService) =>
new MainLevel(container, world, prefabService, levelService)
);
// Change level
levelService.changeLevel("MAIN_LEVEL");
Communication between systems:
// Subscribe to event
world.eventBus.on("entityCreatedFromPrefab", handleEntityCreated);
// Emit event
world.eventBus.emit("input_action", { type: "JUMP", value: 1 });
// Unsubscribe
world.eventBus.off("entityCreatedFromPrefab", handleEntityCreated);
Creating physics-enabled entities:
// Create an entity with physics
const box = new Entity();
// Add transform component (position/rotation will be managed by physics)
const transform = new TransformComponent();
transform.position.set(0, 5, 0); // Initial position before physics takes over
box.addComponent(ComponentTypes.TRANSFORM, transform);
// Add mesh component (visual representation)
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const meshComp = new MeshComponent(geometry, material);
box.addComponent(ComponentTypes.MESH, meshComp);
// Add rigid body component (physics properties)
box.addComponent(ComponentTypes.RIGID_BODY, {
bodyType: "dynamic", // Options: "dynamic", "static", "kinematicPosition", "kinematicVelocity"
mass: 1.0,
gravityScale: 1.0,
linearDamping: 0.01,
angularDamping: 0.05,
});
// Add collider component (collision shape)
box.addComponent(ComponentTypes.COLLIDER, {
shape: "box", // Options: "box", "sphere", "capsule"
size: [1, 1, 1], // Dimensions based on shape type
isSensor: false, // true for trigger volumes
restitution: 0.3, // Bounciness (0-1)
friction: 0.8, // Surface friction
});
// Add to world
world.addEntity(box);
// Apply physics forces via events
world.eventBus.emit("physics_apply_impulse", {
entityId: box.id,
impulse: { x: 0, y: -5, z: 0 },
});
// Apply rotational forces
world.eventBus.emit("physics_apply_torque_impulse", {
entityId: box.id,
torque: { x: 0, y: 1, z: 0 },
});
// Listen for collision events
world.eventBus.on("physics_collision_start", (event) => {
console.log(`Collision between entities ${event.bodyA} and ${event.bodyB}`);
});
Manage model animations with state-based controllers:
// Create animation controller component
const animController = new AnimationControllerComponent();
// Map states to animation names
animController.states.set("idle", "Idle");
animController.states.set("walk", "Walk");
animController.states.set("run", "Run");
// Support model swapping for different animations
animController.useModelSwapping = true;
animController.modelUrlMap.set("idle", "assets/models/character/idle.glb");
animController.modelUrlMap.set("walk", "assets/models/character/walk.glb");
animController.modelUrlMap.set("run", "assets/models/character/run.glb");
// Change animation state
world.eventBus.emit("animation_state_change", {
entityId: entity.id,
state: "walk",
loop: true,
});
- Components should be pure data containers
- Implement
validate()
for constraint enforcement - Implement
cleanup()
for resource disposal
export class CustomComponent extends Component {
public readonly type = ComponentTypes.CUSTOM;
public value: number;
constructor(data?: { value?: number }) {
super();
this.value = data?.value ?? 0;
}
public validate(): void {
// Enforce constraints
this.value = Math.max(0, Math.min(100, this.value));
}
public cleanup(): void {
// Dispose resources
// e.g., this.texture?.dispose();
}
}
export class CustomSystem extends System {
private boundHandler: (event: any) => void;
constructor() {
super();
this.boundHandler = this.handleEvent.bind(this);
}
public initialize(world: World): void {
this.world = world;
world.eventBus.on("someEvent", this.boundHandler);
}
public update(world: World, deltaTime: number): void {
try {
const entities = world.queryComponents([
ComponentTypes.TRANSFORM,
ComponentTypes.CUSTOM,
]);
for (const entity of entities) {
const transform = entity.getComponent<TransformComponent>(
ComponentTypes.TRANSFORM
);
const custom = entity.getComponent<CustomComponent>(
ComponentTypes.CUSTOM
);
if (!transform || !custom) continue;
// Process entity
}
} catch (error) {
console.error("Error in CustomSystem update:", error);
}
}
private handleEvent(data: any): void {
// Handle event
}
public cleanup(): void {
if (this.world?.eventBus) {
this.world.eventBus.off("someEvent", this.boundHandler);
}
this.world = null;
}
}
-
Component Setup
- Keep
TransformComponent
andMeshComponent
synchronized with the physics simulation - Set initial positions in
TransformComponent
before adding the entity to the world - Understand that physics will take control of positioning after initialization
- Keep
-
Performance Optimization
- Use appropriate collider shapes (prefer primitive shapes like box/sphere over complex ones)
- Adjust physics parameters based on simulation needs:
- Higher
gravityScale
for faster falling - Lower
linearDamping
for more persistent motion - Lower
angularDamping
to allow rotation
- Higher
-
Physics Events
- Use the event system for physics interactions:
-
physics_apply_impulse
for immediate force application -
physics_apply_torque_impulse
for rotational forces - Listen for
physics_collision_start/end
for gameplay logic
-
- Use the event system for physics interactions:
-
TransformComponent Synchronization
- During initialization: TransformComponent → RigidBody (one-time)
- During gameplay: RigidBody → TransformComponent (every frame)
- Manual updates to TransformComponent won't affect physics simulation after initialization
Since this package is distributed in obfuscated form, the following tips will help you use it effectively:
- Use TypeScript: TypeScript definitions are provided and unobfuscated, giving you autocomplete and type checking
- Error Handling: Add proper try/catch blocks when working with the API
-
Console Debugging: Enable verbose logging with
loggingService.setGlobalLevel(LogLevel.DEBUG);
- Sample Reference: Refer to the game-template package for implementation examples
- Initialize WebGL context asynchronously
- Handle context loss and restoration using provided events
- Always check renderer state before performing operations
- Properly dispose Three.js resources using cleanup methods
For complete implementation examples, check out the game-template
package which demonstrates practical usage of all core engine features.
If you encounter issues when using the API:
- Verify your implementation against the examples in
game-template
- Enable debug logging to get more diagnostic information
- Check for updated documentation or package versions
MIT
If you are developing aio3d-core
concurrently with a package that depends on it locally (like game-template
within this monorepo), you'll need to link the packages.
-
Linking: The monorepo root includes a script
npm run link:setup
. Run this once afternpm install
in the root. This script usesnpm link
to make your localaio3d-core
build available to other local packages (likegame-template
) as if it were installed normally. -
Build Watching: You need to continuously build
aio3d-core
as you make changes. Runnpm run watch:core
(ornpm run dev:core
) in a separate terminal from the root directory. This watches thecore
source files and rebuilds the output inpackages/core/dist
, which is what the linked dependents will use. -
TypeScript Paths: Dependent packages (like
game-template
) use TypeScript'spaths
in theirtsconfig.json
to point directly to theaio3d-core
source (../core/src
). This enables better IDE integration (like Go to Definition) and allows tools like Vite to leverage the source code for faster development builds (HMR).
This combination ensures that dependent packages can resolve aio3d-core
correctly both at build/runtime (via the linked built artifacts) and during development (via TypeScript paths to the source).