This file is a merged representation of a subset of the codebase, containing specifically included files, combined into a single document by Repomix. ================================================================ File Summary ================================================================ Purpose: -------- This file contains a packed representation of the entire repository's contents. It is designed to be easily consumable by AI systems for analysis, code review, or other automated processes. File Format: ------------ The content is organized as follows: 1. This summary section 2. Repository information 3. Directory structure 4. Multiple file entries, each consisting of: a. A separator line (================) b. The file path (File: path/to/file) c. Another separator line d. The full contents of the file e. A blank line Usage Guidelines: ----------------- - This file should be treated as read-only. Any changes should be made to the original repository files, not this packed version. - When processing this file, use the file path to distinguish between different files in the repository. - Be aware that this file may contain sensitive information. Handle it with the same level of security as you would the original repository. Notes: ------ - Some files may have been excluded based on .gitignore rules and Repomix's configuration - Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files - Only files matching these patterns are included: 4.core-concepts/*.md, 5.addons/*.md - Files matching patterns in .gitignore are excluded - Files matching default ignore patterns are excluded Additional Info: ---------------- ================================================================ Directory Structure ================================================================ 4.core-concepts/ 1.ecs-overview.md 2.entities.md 3.traits.md 4.systems.md 5.components.md 6.actions.md 7.advanced.md 5.addons/ 1.physics.md 2.batch-mesh.md ================================================================ Files ================================================================ ================ File: 4.core-concepts/1.ecs-overview.md ================ --- title: ECS Overview description: Viber3D uses an Entity Component System (ECS) architecture via the koota library. --- ## What is ECS? ECS stands for **Entity Component System**, a data-driven architecture widely used in games: - **Entities** are unique identifiers that represent in-game objects (e.g., players, enemies, bullets). - **Traits** (often called components in other ECSs) hold pure data for those entities. - **Systems** process matching entities to implement logic and behavior. Koota is the ECS-based library that Viber3D uses under the hood. It’s optimized for real-time apps and XR experiences. ## Why ECS? 1. **Flexibility** Entities can have any combination of traits—much more flexible than rigid inheritance trees. 2. **Performance** ECS focuses on data-oriented design, allowing efficient queries and updates to large numbers of entities. 3. **Maintainability** By separating data (traits) from logic (systems), your codebase remains organized and easy to reason about. 4. **Composability** New features or behaviors can be added by creating new traits and systems without modifying existing code. ## ECS in Viber3D Below is a simplified example: ```ts import { trait, createWorld } from 'koota' // 1. Define traits (data) export const Position = trait({ x: 0, y: 0, z: 0 }) export const Health = trait({ amount: 100 }) // 2. Create a world and spawn an entity const world = createWorld() world.spawn(Position({ x: 10, y: 5 }), Health({ amount: 50 })) // 3. Query and update entities world.query(Position, Health).updateEach(([pos, hp]) => { // game logic that modifies position or health }) ``` ## SoA vs. AoS Storage Koota supports two main ways of storing trait data: - **Schema-based (SoA)**: If you define a trait with an object literal, each property is stored in its own array. Ideal for numeric fields updated frequently (e.g., positions, velocities). ```ts const Position = trait({ x: 0, y: 0, z: 0 }) // Internally, x, y, and z each become separate arrays ``` - **Callback-based (AoS)**: If you define a trait with a callback, the entire trait is stored as a single object in an array of structures. Useful when storing complex objects (e.g., `THREE.Mesh`, classes, or vectors). ```ts const Mesh = trait(() => new THREE.Mesh()) // Each entity that has Mesh gets its own THREE.Mesh() instance ``` ## World Traits In Koota, your `World` can also have traits (sometimes called “singleton” traits). These do **not** appear in standard entity queries but can be accessed with `world.get(...)` and `world.set(...)`. Use them for global data like time, input state, or configuration. ```ts import { trait, createWorld } from 'koota' const Time = trait({ delta: 0, current: 0 }) const world = createWorld() world.add(Time) // Now we can store time info globally world.set(Time, (prev) => ({ current: performance.now(), delta: performance.now() - prev.current })) ``` ## Common Patterns - **Composition over inheritance**: Build complex entities by combining small, focused traits. - **Use queries efficiently**: Only query for traits you need. - **Clean up**: Remove or destroy entities when they’re no longer needed (e.g., after a system determines they’re off-screen or at zero health). ## Next Steps - Dive into [Entities](./2.entities.md) to learn how to spawn, remove, and manage them. - Check out [Traits](./3.traits.md) to see how to define data in your ECS world. - See [Systems](./4.systems.md) for writing game logic. - Explore [Components](./5.components.md) for React/Three.js rendering using ECS data. - Learn about [Actions](./6.actions.md) for centralizing world modifications. ================ File: 4.core-concepts/2.entities.md ================ --- title: Entities description: Entities are the building blocks of your Viber3D game. They can have any number of traits that define their data/behavior. --- ## Creating Entities ```ts import { World, trait } from 'koota' import * as THREE from 'three' // Example traits const Transform = trait({ position: () => new THREE.Vector3(), rotation: () => new THREE.Euler() }) const Health = trait({ amount: 100 }) const IsPlayer = trait() // tag trait, no data // Create a world const world = new World() // Spawn a player entity const player = world.spawn( Transform(), Health({ amount: 100 }), IsPlayer() ) ``` ### Entity Lifecycle 1. **Creation** via `world.spawn(...)` 2. **Updates** (via systems or direct calls to `entity.set(...)`) 3. **Removal** when no longer needed (e.g., `entity.destroy()` or queries that remove entities conditionally) --- ## Querying Entities Koota’s query system lets you retrieve entities that have certain traits: ```ts // Single entity (first match) const player = world.queryFirst(IsPlayer) // Multiple entities const enemies = world.query(IsEnemy, Health) ``` ### Filtering Use standard array filters or `.filter()` on the query results: ```ts const lowHealthEnemies = world .query(IsEnemy, Health) .filter(([_, health]) => health.amount < 50) ``` ### Update in Bulk `updateEach()` is a convenient way to iterate and immediately update trait data: ```ts function damageSystem(world: World) { world.query(Health).updateEach(([health]) => { if (health.amount <= 0) { // handle death (destroy entity, spawn effect, etc.) } }) } ``` --- ## Removing Entities ```ts // remove by reference world.remove(entity) // remove in a query world.query(Health).removeIf(([health]) => health.amount <= 0) // remove a single entity directly entity.destroy() ``` --- ## Relationships Koota supports **relationships** between entities with the `relation()` API. This is an alternative to storing entity references directly inside a normal trait. Relationships are powerful for parent-child hierarchies, inventory systems, or targeting logic. ```ts import { relation } from 'koota' const ChildOf = relation() const parent = world.spawn() const child = world.spawn(ChildOf(parent)) // Query all children of `parent` const children = world.query(ChildOf(parent)) ``` ### With Data Relationships can carry extra fields: ```ts const Contains = relation({ store: { amount: 0 } }) const inventory = world.spawn() const gold = world.spawn() inventory.add(Contains(gold)) inventory.set(Contains(gold), { amount: 10 }) ``` ### Exclusive & Auto-Remove ```ts const Targeting = relation({ exclusive: true }) // ensures only one target can be assigned at once const ChildAutoRemove = relation({ autoRemoveTarget: true }) // destroying the parent will also destroy the child ``` ### Querying Relationships ```ts import { Wildcard } from 'koota' // Query all entities containing `gold` const containers = world.query(Contains(gold)) // Query all relationships that target anything (`'*'`) const anything = world.query(Contains('*')) // Inverted or wildcard queries const allRelatedToGold = world.query(Wildcard(gold)) ``` --- ## Advanced Query Modifiers Koota provides several modifiers for advanced use cases: - **Not(...)**: Excludes entities that have certain traits. - **Or(...)**: Finds entities matching any of the listed traits (logical OR). - **Added/Removed/Changed**: Track recent trait additions, removals, or changes since last query. Example: ```ts import { Not, Or, createAdded, createRemoved, createChanged } from 'koota' // Exclude trait const staticEntities = world.query(Transform, Not(Velocity)) // Logical OR const movingOrVisible = world.query(Or(Velocity, Renderable)) // Track newly added const Added = createAdded() const newPositions = world.query(Added(Position)) // Track removed const Removed = createRemoved() const stoppedEntities = world.query(Removed(Velocity)) // Track changed const Changed = createChanged() const movedEntities = world.query(Changed(Position)) ``` --- ## Common Patterns ### Entity Templates ```ts const enemyTemplate = [ Transform(), Health({ amount: 50 }), IsEnemy() ] function spawnEnemy(world: World) { return world.spawn(...enemyTemplate) } ``` ### Direct References vs. Relationships You can store entity references directly in a trait: ```ts import { trait } from 'koota' const TargetRef = trait({ entity: null as Entity | null }) world.spawn(IsProjectile(), TargetRef({ entity: enemy })) ``` However, for more complex parent-child or inventory logic, [Relationships](#relationships) may be a better fit. --- ## Next Steps - Learn more about [Traits](./3.traits.md) to define entity data. - Continue to [Systems](./4.systems.md) for processing queries each frame. - Check out [Actions](./6.actions.md) to see how to centralize entity creation and destruction. ================ File: 4.core-concepts/3.traits.md ================ --- title: Traits description: Traits store your entity’s data. Each trait is a small piece of state in an ECS architecture. --- ## Defining Traits In Koota, traits (analogous to "components") can be defined using either: 1. **Schema-based (SoA)** — for numeric or simpler data fields: ```ts import { trait } from 'koota' export const Health = trait({ amount: 100, max: 100 }) ``` Each property is stored in its own array internally for performance. 2. **Callback-based (AoS)** — for complex objects or classes: ```ts export const Transform = trait(() => ({ position: new THREE.Vector3(), rotation: new THREE.Euler() })) ``` Each entity’s instance is stored in a single array of objects. ### Tag Traits Use empty traits (no data) for simple tagging: ```ts export const IsPlayer = trait() export const IsEnemy = trait() ``` --- ## Initializing Trait Data When adding a trait to an entity, you can override defaults: ```ts entity.add(Health()) // uses defaults entity.add(Health({ amount: 50 })) // custom amount ``` For callback-based traits: ```ts const Model = trait(() => new THREE.Mesh()) const entity = world.spawn(Model()) // Each entity gets its own mesh instance ``` --- ## Typing Traits If your trait is more complex, you can specify the exact TypeScript interface. For schema-based traits: ```ts interface AttackData { continueCombo: boolean | null currentStageIndex: number | null stages: Array | null startedAt: number | null } // Use Pick to avoid TS quirks with interfaces export const Attacker = trait>({ continueCombo: null, currentStageIndex: null, stages: null, startedAt: null, }) ``` --- ## Working with Traits ```ts // Retrieve a trait snapshot const health = entity.get(Health) health.amount -= 10 // Or update with set, which triggers events/changes entity.set(Health, (prev) => ({ ...prev, amount: prev.amount - 10 })) ``` --- ## Adding and Removing Traits ```ts // Add trait entity.add(Health({ amount: 100 })) // Remove trait entity.remove(Health) // Check if a trait is present if (entity.has(Health)) { // ... } ``` --- ## Debugging Traits You can create special traits for debugging: ```ts export const Debug = trait({ label: '', enabled: false }) // Mark an entity for debug entity.add(Debug({ label: 'Player1', enabled: true })) ``` Then your debugging or logging systems can query these and provide extra info. --- ## SoA vs AoS - **Structure of Arrays (SoA)** is best for performance when performing the same operation on large sets of numeric data. - **Array of Structures (AoS)** is more convenient when storing class instances, references, or arbitrary objects like `THREE.Vector3`. Use whichever style suits your data and performance needs. --- ## Next Steps - Go to [Entities](./2.entities.md) to see how to spawn and manage them. - Learn about [Systems](./4.systems.md) for processing trait data. - Check out [Actions](./6.actions.md) for centralized trait updates. ================ File: 4.core-concepts/4.systems.md ================ --- title: Systems description: Systems process entities each frame (or on demand), updating trait data and implementing game logic. --- ## Introduction In ECS, **Systems** are the logic executors. They run over queries of entities that have the required traits, performing operations such as movement, AI, physics, or effects. ```ts import { World } from 'koota' import { Transform, Movement } from '../traits' function movementSystem(world: World) { // Example: let's assume we have a Time trait const time = world.get(Time) if (!time) return const { delta } = time world.query(Transform, Movement).updateEach(([transform, movement]) => { transform.position.add(movement.velocity.clone().multiplyScalar(delta)) }) } ``` --- ## Core Patterns ### Update Systems Most systems run every frame (or every tick). They typically: 1. Retrieve relevant data from the world (e.g., `Time`, `Input`). 2. Query entities matching a specific trait combination. 3. Update trait data as needed. ```ts function healthRegenSystem(world: World) { const time = world.get(Time)! world.query(Health, Regeneration).updateEach(([health, regen]) => { health.amount = Math.min(health.max, health.amount + regen.rate * time.delta) }) } ``` ### Event Systems Sometimes, you only want to process logic when an event happens. Koota has an event system: ```ts world.on('entityDamaged', ({ entity, amount }) => { // maybe spawn a blood effect or play a sound }) ``` You can emit events in your code: ```ts entity.set(Health, (h) => { const newAmount = Math.max(0, h.amount - damage) world.emit('entityDamaged', { entity, amount: damage }) return { ...h, amount: newAmount } }) ``` ### Cleanup Systems Systems that remove entities or reset them: ```ts function cleanupSystem(world: World) { world.query(Health).removeIf(([health]) => health.amount <= 0) } ``` --- ## System Organization Generally, you have a `systems/` directory with one file per system or small system groups. In your main loop (or React’s `useFrame`), you call them in order: ```ts export function GameLoop(world: World) { // Possibly in a requestAnimationFrame or React useFrame timeSystem(world) inputSystem(world) movementSystem(world) collisionSystem(world) cleanupSystem(world) } ``` --- ## Performance Tips 1. **Cache queries**: Repeatedly calling the same query can be more expensive. If you must query the same traits many times, store the result. 2. **Use SoA**: For large numeric datasets, prefer schema-based traits for better performance. 3. **Selective Scheduling**: Not every system needs to run every frame (e.g., AI pathfinding might run less often). --- ## Change Detection Options When using `.updateEach()`, you can specify how Koota handles change events: - `changeDetection: 'never'` — no change events triggered. - `changeDetection: 'always'` — changes are always emitted for mutated traits. - (default) — triggers changes only for traits or queries specifically tracking them. ```ts world.query(Position, Velocity).updateEach( ([pos, vel]) => { // ... }, { changeDetection: 'never' } ) ``` --- ## Testing Systems Because systems are just functions, you can test them easily: ```ts describe('movementSystem', () => { it('updates position based on velocity', () => { const world = createWorld() const ent = world.spawn(Transform(), Movement({ velocity: new Vector3(1, 0, 0) })) world.add(Time({ delta: 1 })) // simulate 1 second movementSystem(world) expect(ent.get(Transform)!.position.x).toBe(1) }) }) ``` --- ## Next Steps - Check out [Entities](./2.entities.md) for more on entity life cycles. - Learn about [Actions](./6.actions.md) to see how systems integrate with centralized function calls. - [Components](./5.components.md) covers how to render ECS data in React + Three.js. ================ File: 4.core-concepts/5.components.md ================ --- title: Components description: Your Viber3D game typically uses React + React Three Fiber. Components can query data from the ECS world and render Three.js objects accordingly. --- ## ECS + React Koota provides React hooks to connect ECS data (traits) to your components. In Viber3D, you’ll often use **React Three Fiber** (`@react-three/fiber`) for rendering 3D scenes, and these hooks to sync ECS state with visuals. ### Common Hooks 1. **`useQuery`** — returns an array of entities matching certain traits. 2. **`useQueryFirst`** — returns the first matching entity (or `undefined`). 3. **`useTrait`** — returns the current value of a given trait on an entity (or `undefined` if missing). 4. **`useTraitEffect`** — runs a side effect whenever the trait is added, removed, or changed. 5. **`useWorld`** — returns the current ECS world from context. 6. **`useActions`** — returns a set of centralized actions for mutating the ECS state. --- ## Example: Rendering Entities ```tsx import { useQuery, useTrait } from 'koota/react' import { Transform, IsEnemy } from '../traits' function EnemyRenderer() { // get all enemies const enemies = useQuery(IsEnemy, Transform) return ( <> {enemies.map(entity => ( ))} ) } function EnemyView({ entity }: { entity: Entity }) { const transform = useTrait(entity, Transform) if (!transform) return null return ( ) } ``` --- ## Using `useTraitEffect` To react to changes in a trait without causing rerenders, use `useTraitEffect`. For example, you might want to update a mesh’s position in a ref: ```tsx function EnemyView({ entity }: { entity: Entity }) { const ref = useRef(null!) // Subscribe to ECS changes outside of React’s render useTraitEffect(entity, Transform, (transform) => { if (!transform || !ref.current) return ref.current.position.copy(transform.position) ref.current.rotation.copy(transform.rotation) }) return ( ) } ``` --- ## React Three Fiber + ECS You can also integrate `useFrame` from R3F for certain updates: ```tsx import { useFrame } from '@react-three/fiber' import { Movement, Transform } from '../traits' function MovementSystem() { // This component acts as a "system" in React const world = useWorld() useFrame(() => { const time = world.get(Time) if (!time) return const { delta } = time world.query(Transform, Movement).updateEach(([t, m]) => { t.position.add(m.velocity.clone().multiplyScalar(delta)) }) }) return null } ``` --- ## Performance Optimizations 1. **Memoize** expensive sub-components if they always render the same geometry. 2. **Instancing** (in R3F) for large numbers of identical objects. 3. **Culling & LOD** for objects not in view or too far away. --- ## Next Steps - Explore [Actions](./6.actions.md) for centralizing ECS changes in your React components. - Check out official [React Three Fiber](https://github.com/pmndrs/react-three-fiber) docs for advanced rendering. - Combine with [Systems](./4.systems.md) for heavy logic outside of React’s render cycle. ================ File: 4.core-concepts/6.actions.md ================ --- title: Actions description: Actions are centralized functions for modifying the game world in a predictable manner. --- ## Understanding Actions Actions in Viber3D (powered by Koota) are **centralized functions** that modify the ECS world. They help avoid scattering spawn/remove/update logic in multiple places. ```ts import { createActions } from 'koota' import { IsPlayer, Health, Transform } from '../traits' export const actions = createActions((world) => ({ spawnPlayer: (position) => { return world.spawn( IsPlayer(), Transform({ position }), Health({ amount: 100 }) ) }, destroyAllPlayers: () => { world.query(IsPlayer).forEach((player) => { player.destroy() }) } })) ``` --- ## Using Actions in React With `useActions`, you get your action functions bound to the current `world`: ```tsx import { useActions } from 'koota/react' import { actions } from '../actions' function GameController() { const { spawnPlayer, destroyAllPlayers } = useActions(actions) useEffect(() => { // Spawn a player on mount spawnPlayer({ x: 0, y: 0, z: 0 }) // Cleanup on unmount return () => destroyAllPlayers() }, []) return null } ``` --- ## Best Practices 1. **Centralize**: Store all your actions in a dedicated file (e.g. `src/actions.ts` or `actions/` folder). 2. **Keep them pure**: Actions should only modify the ECS state; avoid large side effects. 3. **Descriptive names**: Use clear naming like `spawnEnemyWave()`, `applyDamage()`, etc. 4. **Cleanup logic**: Provide actions to remove entities or reset traits as needed. --- ## Action Composition Actions can call one another to build complex flows: ```ts export const actions = createActions((world) => ({ spawnEnemy: (position) => { return world.spawn(IsEnemy(), Transform({ position }), Health({ amount: 50 })) }, spawnEnemyWave: (count) => { for (let i = 0; i < count; i++) { const position = getRandomPosition() actions.spawnEnemy(position) // calling another action } } })) ``` --- ## Conditional Updates ```ts export const actions = createActions((world) => ({ damageEntity: (entity, amount) => { if (entity.has(Health)) { entity.set(Health, (h) => { const newAmount = Math.max(0, h.amount - amount) if (newAmount <= 0) { actions.destroyEntity(entity) } return { ...h, amount: newAmount } }) } }, destroyEntity: (entity) => entity.destroy() })) ``` --- ## Testing Actions Actions are straightforward to test. You can create a test world, apply your action, and inspect the result: ```ts describe('playerActions', () => { let world: World let boundActions: ReturnType beforeEach(() => { world = createWorld() boundActions = actions.bindTo(world) }) it('spawnPlayer creates a player entity', () => { const player = boundActions.spawnPlayer({ x: 0, y: 0, z: 0 }) expect(player.has(IsPlayer)).toBe(true) expect(player.has(Health)).toBe(true) }) it('destroyAllPlayers removes all player entities', () => { boundActions.spawnPlayer({ x: 0, y: 0, z: 0 }) boundActions.spawnPlayer({ x: 1, y: 0, z: 0 }) boundActions.destroyAllPlayers() expect(world.query(IsPlayer).length).toBe(0) }) }) ``` --- ## Summary Actions let you organize all your ECS modifications into a single, testable location—helping maintain clarity and consistency throughout your codebase. - Integrate [Actions in React](#using-actions-in-react) via `useActions`. - Combine with [Systems](./4.systems.md) for runtime logic and [Traits](./3.traits.md) for the data definition. - Keep your ECS usage clean, predictable, and well-structured. ================ File: 4.core-concepts/7.advanced.md ================ --- title: Advanced ECS Topics description: Dive deeper into Koota’s advanced features for optimization, low-level control, and special ECS workflows. --- This section covers advanced features in Koota. While not always required, they can significantly improve performance, enable custom workflows, or simplify complex scenarios. --- ## 1. Direct Store Updates with `useStore()` In most cases, you’ll read and update trait data via `.updateEach()`, `entity.set()`, or the React hooks. However, for **performance-critical** operations on many entities, you can **directly modify** the underlying store arrays. ```ts world.query(Position, Velocity).useStore(([pos, vel], entities) => { // pos, vel are SoA (Structure of Arrays) stores if you used a schema-based trait // If it's AoS (callback-based trait), they’ll be arrays of objects instead. for (let i = 0; i < entities.length; i++) { const e = entities[i] // an Entity handle const eid = e.id() // numeric ID to index into SoA arrays pos.x[eid] += vel.x[eid] * delta pos.y[eid] += vel.y[eid] * delta } }) ``` ### Notes on `useStore()` - **Bypasses** typical event triggers and change detection. - If you rely on `onChange(...)` or the `Changed(...)` query modifier, direct store writes might not trigger those events. - Best used in performance-critical “hot” loops. --- ## 2. Partial Selection of Traits with `select()` You can limit which traits to retrieve or update during a query. If your system only needs to modify `Health` (but still wants the entity to have `Transform`, `Velocity`, etc.), you can do: ```ts world.query(Transform, Velocity, Health) .select(Health) .updateEach(([health]) => { health.amount -= 1 }) ``` **Why use it**: - Performance: Minimizes overhead by only pulling the data you actually need. - Clarity: Emphasizes which traits you’re reading/updating. --- ## 3. Trait Lifecycle Events Subscribe to trait additions, removals, or changes at the **world** level: ```ts const unsubAdd = world.onAdd(Position, (entity) => { console.log('Entity added Position:', entity.id()) }) const unsubRemove = world.onRemove(Health, (entity) => { console.log('Entity removed Health:', entity.id()) }) const unsubChange = world.onChange(Health, (entity) => { console.log('Entity changed Health:', entity.id(), entity.get(Health)) }) // Later, to stop listening: unsubAdd() unsubRemove() unsubChange() ``` ### Multi-Trait Events You can also pass an array or query-like structure: ```ts // Fired when an entity gains or loses BOTH Position & Velocity world.onAdd([Position, Velocity], (entity) => { /* ... */ }) world.onRemove([Position, Velocity], (entity) => { /* ... */ }) ``` --- ## 4. Querying All Entities If you call `world.query()` **without any traits**, you get **all** _queryable_ entities in the world: ```ts const everything = world.query() everything.forEach((entity) => { console.log('Entity:', entity.id()) }) ``` *Koota excludes certain internal or “excluded” entities by default, so you won’t see world-level entities or other hidden items. If you want literally every single entity (including system-level ones), you can do `world.entities`, but that’s rarely needed.* --- ## 5. Conditional System Scheduling Not all systems need to run every frame. In many games, you want different subsets of systems to run in different game states (e.g. “paused,” “main menu,” “combat,” etc.): ```ts function updateWorld(world: World) { const state = world.get(GameState) if (!state) return if (state.phase === 'Menu') { menuSystems.forEach(sys => sys(world)) } else if (state.phase === 'Combat') { combatSystems.forEach(sys => sys(world)) } else { normalSystems.forEach(sys => sys(world)) } } ``` **Benefits**: - Save performance by not running irrelevant systems. - Keep logic organized for different states or phases of the game. --- ## 6. Additional Relationship Patterns You’ve seen the basics of `relation()`, including `exclusive: true` or `autoRemoveTarget: true`. Consider these patterns: 1. **Multi-level Parenting**: A parent can also have a parent, forming a hierarchical tree. If you set `autoRemoveTarget: true`, destroying a top-level parent **cascades** removal down the chain. 2. **Bidirectional or “Friend/Ally”**: If each entity references the other with a relation, you can quickly query “allies.” 3. **Filtering with Relationship Data**: If you store fields like `{ amount: 10 }`, you can filter the query to find relationships with specific amounts or states. --- ## Summary 1. **Store-Level Access**: `useStore()` gives you maximum control over how you read/write data, ideal for heavy numeric updates. 2. **Selective Trait Access**: `.select()` cuts down on unneeded data access in queries. 3. **Lifecycle Events**: Listen for additions, removals, or changes to traits at a global level. 4. **Whole-World Queries**: `world.query()` with no arguments returns all queryable entities. 5. **Conditional Scheduling**: Run only the systems you need at a given time. 6. **Advanced Relationship Usage**: Use relationships for complex data structures or parent-child hierarchies. By combining these advanced features with the **core ECS** docs, you can build extremely optimized, maintainable game logic in Viber3D. ================ File: 5.addons/1.physics.md ================ --- title: Physics description: Integrate real-time physics in Viber3D using Jolt (or another engine) with ECS traits and systems. --- # Physics in Viber3D Viber3D uses an **ECS** approach for organizing game data. You can integrate a physics engine—like [Jolt](https://github.com/jrouwe/JoltPhysics.js/)—by defining **traits** for physics objects and **systems** that step the simulation each frame. This guide covers: 1. [Overview & Key Concepts](#1-overview--key-concepts) 2. [Initializing a Physics World](#2-initializing-a-physics-world) 3. [Creating Physics Bodies](#3-creating-physics-bodies) 4. [Stepping & Syncing Transforms](#4-stepping--syncing-transforms) 5. [Collision Handling](#5-collision-handling) 6. [Motion Types & Continuous Collision](#6-motion-types--continuous-collision) 7. [Common Patterns & Tips](#7-common-patterns--tips) 8. [Summary & Next Steps](#8-summary--next-steps) --- ## 1. Overview & Key Concepts 1. **PhysicsWorld Trait** A specialized trait storing the engine’s “world” or “physics interface,” so it’s accessible anywhere in ECS. 2. **RigidBody / PhysicsBody Traits** Each entity that should have physics (e.g. a crate, sphere, or player) can hold a trait referencing its underlying physics **body** in Jolt. 3. **Systems** - A “build bodies” system to create bodies for newly spawned ECS entities. - A “step physics” system to advance the physics simulation each frame. - A “sync transforms” system to copy the updated positions/rotations back to Three.js objects (or to ECS `Position` / `Rotation` traits). ### Why Jolt? - **Speed**: Written in C++ and compiled to WebAssembly. - **Feature-rich**: Rigid bodies, collisions, advanced shape definitions, etc. - **Optional**: You can swap in a different library (e.g., Rapier) by following similar patterns. --- ## 2. Initializing a Physics World **Goal**: Load the Jolt library once, create a Jolt “world,” and store it in a custom ECS trait. ```ts // traits/jolt-world.ts import { trait } from 'koota' import type Jolt from 'jolt-physics' export const JoltWorld = trait(() => new JoltWorldImpl()) export class JoltWorldImpl { static JOLT_NATIVE: typeof Jolt joltInterface: Jolt.JoltInterface = null! bodyInterface: Jolt.BodyInterface = null! physicsSystem: Jolt.PhysicsSystem = null! initialized = false constructor() { if (!JoltWorldImpl.JOLT_NATIVE) { throw new Error("Jolt not loaded yet.") } // Create the JoltInterface with default settings const J = JoltWorldImpl.JOLT_NATIVE const settings = new J.JoltSettings() this._setupCollisionFiltering(settings) const jolt = new J.JoltInterface(settings) J.destroy(settings) this.physicsSystem = jolt.GetPhysicsSystem() this.bodyInterface = this.physicsSystem.GetBodyInterface() this.joltInterface = jolt this.initialized = true } stepPhysics(dt: number) { // E.g., do multiple substeps if dt is large const steps = dt > 1/55 ? 2 : 1 this.joltInterface.Step(dt, steps) } _setupCollisionFiltering(settings: Jolt.JoltSettings) { // your collision layers, etc. } } ``` **Initialize** the library at startup: ```ts // actions/use-jolt-actions.ts import { createActions } from 'koota/react' import { JoltWorld, JoltWorldImpl } from '../traits/jolt-world' export const useJoltActions = createActions((world) => ({ async initWorld() { if (world.has(JoltWorld)) { // already initialized return } // Load the Jolt WASM const joltInit = await import('jolt-physics') // Store the reference globally JoltWorldImpl.JOLT_NATIVE = await joltInit.default() // Add the JoltWorld trait to ECS world.add(JoltWorld()) console.log("Jolt world created.") } })) ``` You can call `initWorld()` from a React component, or anywhere else in your startup code. --- ## 3. Creating Physics Bodies ### a) “NeedsJoltBody” Trait Define a trait that marks an entity as **needing** a physics body. This can include settings: whether the body is dynamic, static, or kinematic, etc. ```ts // traits/needs-jolt-body.ts import { trait } from 'koota' export const NeedsJoltBody = trait({ layer: "moving" as "moving" | "non_moving", motionType: "dynamic" as "dynamic" | "static" | "kinematic", buildConvexShape: false, continuousCollisionMode: false, }) ``` You might also define a trait `JoltBody` referencing the final Jolt body object: ```ts // traits/jolt-body.ts import { trait } from 'koota' import type Jolt from 'jolt-physics' export const JoltBody = trait(null) ``` ### b) System to Build Bodies When new entities have `NeedsJoltBody` + a mesh or geometry trait (like `MeshRef`), we create an actual Jolt `Body`. ```ts // systems/build-jolt-bodies.ts import { World } from 'koota' import { NeedsJoltBody } from '../traits/needs-jolt-body' import { JoltBody } from '../traits/jolt-body' import { JoltWorld, JoltWorldImpl } from '../traits/jolt-world' import { MeshRef } from '../traits/mesh-ref' import { inferInitTransformsFromMesh, createShapeFromGeometry } from '../misc/jolt-helper' export function BuildJoltBodies({ world }: { world: World }) { if (!world.has(JoltWorld)) return const joltWorld = world.get(JoltWorld) const J = JoltWorldImpl.JOLT_NATIVE world.query(NeedsJoltBody, MeshRef).updateEach(([needs, meshRef], entity) => { const { pos, rot } = inferInitTransformsFromMesh(meshRef.ref) const shape = needs.buildConvexShape ? createShapeFromGeometry(meshRef.ref.geometry) : /* other shape logic, e.g. BoxShape */ new J.BoxShape(1,1,1) // create BodyCreationSettings const layer = needs.layer === 'moving' ? 1 : 0 const motionType = { dynamic: J.EMotionType_Dynamic, static: J.EMotionType_Static, kinematic: J.EMotionType_Kinematic }[needs.motionType] const creation = new J.BodyCreationSettings(shape, pos, rot, motionType, layer) const body = joltWorld.bodyInterface.CreateBody(creation) J.destroy(creation) // add the body to jolt joltWorld.bodyInterface.AddBody(body.GetID(), J.EActivation_Activate) // handle continuous collision if (needs.continuousCollisionMode) { joltWorld.bodyInterface.SetMotionQuality(body.GetID(), J.EMotionQuality_LinearCast) } // remove the NeedsJoltBody trait (we built the body) entity.remove(NeedsJoltBody) // add the JoltBody trait referencing the new body entity.add(JoltBody(body)) }) } ``` This approach ensures that each newly spawned entity gets a Jolt body exactly once. --- ## 4. Stepping & Syncing Transforms ### a) Update the Physics World A system that calls `stepPhysics` each frame: ```ts // systems/update-jolt.ts import { World } from 'koota' import { JoltWorld } from '../traits/jolt-world' export function UpdateJolt({ world, delta }: { world: World; delta: number }) { if (!world.has(JoltWorld)) return const joltWorld = world.get(JoltWorld) joltWorld.stepPhysics(delta) } ``` ### b) Sync to ECS or Three.js You can store transforms in ECS `Position` / `Rotation` traits, or directly sync to a `MeshRef`. For example: ```ts // systems/sync-three-to-jolt.ts import { World } from 'koota' import { JoltBody } from '../traits/jolt-body' import { MeshRef } from '../traits/mesh-ref' import { JoltWorld } from '../traits/jolt-world' export function SyncThreeToJolt({ world }: { world: World }) { if (!world.has(JoltWorld)) return world.query(JoltBody, MeshRef).useStores(([rb, meshRef], entities) => { for (const e of entities) { const body = rb[e.id()] const mesh = meshRef[e.id()] if (!body || !mesh) continue // read Jolt transform const pos = body.GetPosition() const rot = body.GetRotation() // apply to Three.js mesh mesh.position.set(pos.GetX(), pos.GetY(), pos.GetZ()) mesh.quaternion.set(rot.GetX(), rot.GetY(), rot.GetZ(), rot.GetW()) } }) } ``` > **Note**: `useStores` bypasses some overhead by accessing data in a more raw manner—useful for performance-critical loops. --- ## 5. Collision Handling ### a) Basic Approach Jolt can track collisions or contact points. Depending on your version, you might: - Read a contact list or events each step (`GetCollisionsThisFrame()`, or similar). - Subscribe to collision callbacks via Jolt’s event system. Once you have collisions, you can do ECS logic: ```ts function handleCollisions(world: World) { const joltWorld = world.get(JoltWorld) // some hypothetical API const collisions = joltWorld.physicsSystem.GetCollisionEvents() for (const c of collisions) { const bodyA = c.bodyA const bodyB = c.bodyB // find which entities correspond to bodyA, bodyB, then apply logic } } ``` Because Koota queries are trait-based, you might store a map of `BodyID -> Entity` to help quickly find which entity is colliding. You can store that map in your `JoltWorldImpl` or a separate trait. --- ## 6. Motion Types & Continuous Collision **Jolt** supports different motion types: - **Static**: For immovable objects (floors, walls). - **Dynamic**: Fully simulated by physics (gravity, collisions). - **Kinematic**: Driven by user code, but can push dynamic bodies out of the way. **Continuous Collision** (CCD) helps fast-moving objects (e.g., bullets) avoid tunneling. In the code, we check `continuousCollisionMode` and set `SetMotionQuality(body.GetID(), J.EMotionQuality_LinearCast)`. --- ## 7. Common Patterns & Tips 1. **Separation of Concerns** - Keep “build bodies” in one system, “update the world” in another, and “sync transforms” in a third. 2. **Deferred Body Creation** - If your engine loads slowly or geometry is not ready, a system can wait until necessary data is present. 3. **Destroying Entities** - Remember to remove or destroy the underlying Jolt body. For example, if an entity has a `DestroyMe` trait, a system can remove its Jolt body from the world. 4. **Performance** - Potentially run heavy collision checks at a lower frequency if your game can tolerate it. 5. **Accessing ECS or React** - In React code, prefer using actions or hooking into ECS queries to do spawns, impulses, etc. --- ## 8. Summary & Next Steps - **Initialize** Jolt in a single ECS trait (`JoltWorld`), which you can reference anywhere. - **Build** bodies for new ECS entities using a system that checks a `NeedsJoltBody` trait or similar. - **Step** the physics in an `UpdateJolt` system each frame, and **sync** transforms with your ECS or Three.js objects. - For collisions or advanced features (e.g. joints, triggers), adapt the same pattern: define traits, attach them, and process with systems. **Next Steps**: 1. **Batched Mesh**: If you have many objects, see [Batch Meshes](/addons/batch-mesh) for rendering optimizations. 2. **Actions**: Create ECS actions to spawn, remove, or apply forces to bodies in a centralized, testable manner. 3. **In-Depth**: Explore official [Jolt Physics docs](https://github.com/jrouwe/JoltPhysics.js/) for advanced usage: constraints, sleeping, broad-phase tuning, etc. That’s it! You now have a fully integrated ECS + Jolt physics pipeline in Viber3D. ================ File: 5.addons/2.batch-mesh.md ================ --- title: Batch Mesh description: Optimize rendering of large numbers of similar objects by batching them into a single Three.js mesh. --- # Batch Mesh When your scene has **many** identical or similar objects—like crates, bullets, or environment props—rendering each as a separate Three.js mesh can become a performance bottleneck. By **batching** them into a single mesh under the hood, you reduce draw calls and improve framerates. This guide covers: 1. [Why Use a Batched Mesh](#1-why-use-a-batched-mesh) 2. [ECS Setup & Traits](#2-ecs-setup--traits) 3. [Systems for Managing Instances](#3-systems-for-managing-instances) 4. [Handling Multiple Geometries](#4-handling-multiple-geometries) 5. [Adding & Removing Objects on the Fly](#5-adding--removing-objects-on-the-fly) 6. [Bounding Volumes & Culling](#6-bounding-volumes--culling) 7. [Common Patterns & Tips](#7-common-patterns--tips) 8. [Summary & Next Steps](#8-summary--next-steps) --- ## 1. Why Use a Batched Mesh **Draw Calls**: Each `Mesh` in Three.js can cost you a draw call. If you have hundreds or thousands of small objects, you can quickly hit performance limits. **Batch Mesh**: Combines many objects into a **single** Three.js mesh. This drastically cuts down the number of draw calls (often to just one). You can still manipulate individual “instances” via per-instance transform data. --- ## 2. ECS Setup & Traits ### a) The BatchedMesh Trait You’ll need a specialized ECS trait (e.g. `TBatchedMesh`) to store an instance of your custom “BatchedMesh” class: ```ts // traits.ts import { trait } from 'koota' import { BatchedMesh } from 'three-batched-mesh' // or your custom class export const TBatchedMesh = trait(() => new BatchedMesh()) ``` **Note**: How exactly you implement or import `BatchedMesh` is up to you. Some devs roll their own classes; others use a library that extends `InstancedMesh`. ### b) Storing Instance Info Each entity that’s **represented** inside the batch needs a small record of which geometry it uses and which instance ID is assigned in the batch. For example: ```ts export const BatchCoordinates = trait({ geometryId: -1, instanceId: -1, batchedMesh: null, // reference to the actual BatchedMesh instance batchEntity: -1, // ID or reference to the entity that owns the BatchedMesh }) ``` This trait will be added to each entity once it is successfully batched. ### c) Additional Traits 1. **`BatchCount`**: Track how many instances a single BatchedMesh can hold. 2. **`GeometryCache`**: Optionally map from a `BufferGeometry` to a geometry ID within the batch. 3. **`IsBatchedOriginOf`**: A relationship trait if you want the batch entity to “know” which child entities it spawned. --- ## 3. Systems for Managing Instances The workflow typically has **three** main systems: 1. **Spawn/Attach Instances**: If an entity has `(Position, some geometry trait)` but not yet in the batch, add it. 2. **Remove Instances**: If an entity is destroyed or flagged for removal, remove its instance from the batch. 3. **Update Transforms**: Each frame, sync ECS positions/rotations to the instance transforms. ### a) Spawn Instances ```ts // systems/spawn-batch-instances.ts import { World, Not } from 'koota' import { TBatchedMesh, BatchCoordinates, Position, TColor } from '../traits' import { Matrix4 } from 'three' const tempMatrix = new Matrix4() export function SpawnBatchInstances({ world }: { world: World }) { // Find a batch entity that isn't full const batchEntity = world.queryFirst(TBatchedMesh, /* possibly BatchCount, etc. */) if (!batchEntity) return const batchedMesh = batchEntity.get(TBatchedMesh) // For each entity that has a position/color but no BatchCoordinates world.query(Position, TColor, Not(BatchCoordinates)).updateEach(([pos, color], entity) => { // Possibly check if batch is full // Add geometry or shape as needed // add instance to the batched mesh const geometryId = 0 // or your chosen geometry slot const instanceId = batchedMesh.addInstance(geometryId) // record the new instance entity.add(BatchCoordinates({ geometryId, instanceId, batchedMesh, batchEntity: batchEntity.id() })) // Initial transform tempMatrix.setPosition(pos) batchedMesh.setMatrixAt(instanceId, tempMatrix) // If your batched mesh supports per-instance color: batchedMesh.setColorAt(instanceId, color) // Recompute bounding volume if needed batchedMesh.computeBoundingBox() batchedMesh.computeBoundingSphere() }) } ``` ### b) Removing Instances If an entity is destroyed or flagged for removal, remove its instance from the batch: ```ts // systems/remove-batch-instances.ts import { World } from 'koota' import { BatchCoordinates, DestroyMe } from '../traits' export function RemoveBatchInstances({ world }: { world: World }) { // For each entity that has batch coords and is flagged for removal world.query(BatchCoordinates, DestroyMe).updateEach(([coords], entity) => { const { batchedMesh, instanceId } = coords batchedMesh.deleteInstance(instanceId) // update bounding volumes if needed batchedMesh.computeBoundingBox() batchedMesh.computeBoundingSphere() // remove the trait or destroy the entity entity.destroy() }) } ``` ### c) Updating Transforms Each Frame ```ts // systems/sync-batch-transforms.ts import { World } from 'koota' import { BatchCoordinates, Position, Rotation, Scale } from '../traits' import { Matrix4, Vector3, Quaternion } from 'three' const tempMatrix = new Matrix4() const defaultScale = new Vector3(1,1,1) export function SyncBatchTransforms({ world }: { world: World }) { // For each entity with batch coords and transform data world.query(Position, Rotation, BatchCoordinates).updateEach(([pos, rot, coords], entity) => { const scale = entity.has(Scale) ? entity.get(Scale) : defaultScale tempMatrix.compose(pos, rot, scale) coords.batchedMesh.setMatrixAt(coords.instanceId, tempMatrix) }) } ``` **Remember** to call `batchedMesh.update()` or equivalent if your implementation requires it. --- ## 4. Handling Multiple Geometries Some batched mesh classes allow multiple geometries within one “batch.” You can store a `GeometryCache` trait to map from specific `BufferGeometry` objects to geometry IDs: ```ts // traits.ts export const GeometryCache = trait(() => new Map()) // systems/spawn-batch-instances.ts if (!geomCache.has(geometryRef)) { const id = batchedMesh.addGeometry(geometryRef) geomCache.set(geometryRef, id) } const geometryId = geomCache.get(geometryRef)! const instanceId = batchedMesh.addInstance(geometryId) ``` --- ## 5. Adding & Removing Objects on the Fly Because you’re using an ECS, you can dynamically add or remove entities: 1. **Spawn** new objects -> The spawn system creates new ECS entities with `(Position, TColor, maybe some geometry trait)`. 2. A system tries to attach them to a batch mesh. If no batch mesh is available or if it’s full, you might create a new batch entity or queue them for the next frame. 3. **Destroy** or “flag for removal” -> The remove system sees `DestroyMe` and updates the batch accordingly. This approach is flexible and handles large scenes or dynamic states seamlessly. --- ## 6. Bounding Volumes & Culling A typical `BatchedMesh` (or `InstancedMesh`) uses an overall bounding box/sphere for **all** instances. Update it whenever you add or remove instances, or if they move far from the original bounding region. Three.js uses this bounding volume for frustum culling. If you have advanced requirements for partial culling (culled per instance, not entire batch), you might need more sophisticated logic—like subdividing your batch or doing manual culling checks. --- ## 7. Common Patterns & Tips 1. **Batch Capacity** - Decide how many instances a single batch can hold (e.g. 100, 1000). Once full, create another batch entity. 2. **Single Material** - All instances in a single batch typically share **one** material. If you need varied materials, you might need multiple batches or advanced multi-material instancing. 3. **Sorting Entities** - Some devs group similar objects (same geometry, same material) in the same batch. 4. **Destroy or Reuse?** - Instead of destroying an entity’s instance, consider reusing the slot for a newly spawned object if you want to minimize overhead. 5. **Actions** - Provide a nice user-facing action (e.g., `spawnCube()`) that spawns the entity and sets up everything. This centralizes your logic. 6. **Performance** - For extremely large numbers of objects, ensure you only do matrix updates when necessary (e.g. if something’s changed). --- ## 8. Summary & Next Steps **Batch Mesh** is a powerful approach to rendering many objects efficiently: - Create a specialized ECS trait (e.g. `TBatchedMesh`) that wraps a class handling instanced geometry. - Write systems for **spawning** new instances, **removing** dead ones, and **syncing** transforms each frame. - Optionally handle multiple geometry types via a `GeometryCache`. - Keep bounding volumes updated so culling remains effective. ### Next Steps - **Physics**: If these objects need collisions, see [Physics](/addons/physics). You may handle physics each object individually, or treat them as a large static or dynamic chunk. - **Actions**: Provide convenient ECS actions so your React components can spawn or remove batched objects with a single call. - **Advanced**: Explore partial culling, LOD, or more sophisticated indexing methods for extremely large scenes. That’s it! You now have a blueprint for high-performance rendering of large numbers of similar objects in Viber3D. ================================================================ End of Codebase ================================================================