aio3d-core
v0.0.8
Published
A simple 3D game engine using Three.js
Readme
AIO3D Engine Core Package
A modular Entity Component System (ECS) game engine for building 3D web applications and games.
Overview
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.
Key Features
- 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
Installation
npm install aio3d-corePublic API
ECS Framework
The foundation of the engine:
World: Manages entities and systems, provides the event busEntity: Container for components with unique IDComponent: Base class for all component typesSystem: Base class for all system logic
Components
Built-in components include:
TransformComponent: Position, rotation, scale dataMeshComponent: 3D visual representation using Three.jsCameraComponent: Camera properties and configurationPersistentComponent: Marks entity to survive level transitionsDOMComponent: Connects to DOM elementsOrbitControlComponent: Orbit camera controlsModelComponent: 3D model data with animation supportAnimationControllerComponent: Animation state management for modelsRigidBodyComponent: Physics body properties (dynamic, static, kinematic)ColliderComponent: Physics shape and collision properties
Systems
Built-in systems include:
SceneSystem: Manages Three.js scene and renderer setupRenderSystem: Handles rendering the sceneMeshRegistrationSystem: Adds meshes to the sceneWindowSystem: Handles window events and resizingInputSystem: Manages keyboard, mouse, and touch inputsOrbitCameraSystem: Implements orbit camera controlsModelSystem: Manages 3D model loading and animation setupModelRegistrationSystem: Registers models with the sceneAnimationControllerSystem: Controls model animation states and transitionsPhysicsSystem: Manages Rapier physics simulation and synchronization
Usage Examples
Basic Setup
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);Prefab System
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");Level Management
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");Event System
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);Physics System
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}`);
});Animation System
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,
});Best Practices
Component Design
- 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();
}
}System Implementation
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;
}
}Physics Best Practices
Component Setup
- Keep
TransformComponentandMeshComponentsynchronized with the physics simulation - Set initial positions in
TransformComponentbefore 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
gravityScalefor faster falling - Lower
linearDampingfor more persistent motion - Lower
angularDampingto allow rotation
- Higher
Physics Events
- Use the event system for physics interactions:
physics_apply_impulsefor immediate force applicationphysics_apply_torque_impulsefor rotational forces- Listen for
physics_collision_start/endfor 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
Working With Obfuscated Code
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
WebGL Management Best Practices
- 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
Examples
For complete implementation examples, check out the game-template package which demonstrates practical usage of all core engine features.
Support
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
License
MIT
Local Development with Dependent Packages
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 installin the root. This script usesnpm linkto make your localaio3d-corebuild available to other local packages (likegame-template) as if it were installed normally. - Build Watching: You need to continuously build
aio3d-coreas you make changes. Runnpm run watch:core(ornpm run dev:core) in a separate terminal from the root directory. This watches thecoresource 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'spathsin theirtsconfig.jsonto point directly to theaio3d-coresource (../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).
