diff --git a/packages/3d/build.config.ts b/packages/3d/build.config.ts index 4ff93f3..7150ae2 100644 --- a/packages/3d/build.config.ts +++ b/packages/3d/build.config.ts @@ -3,6 +3,7 @@ import { defineBuildConfig } from 'unbuild' export default defineBuildConfig({ entries: [ 'src/index', + 'src/pipeline/PhysicWorkerRun', ], declaration: true, clean: true, diff --git a/packages/3d/src/core/FScene.ts b/packages/3d/src/core/FScene.ts index d910a79..3651ffc 100644 --- a/packages/3d/src/core/FScene.ts +++ b/packages/3d/src/core/FScene.ts @@ -5,6 +5,8 @@ import type RAPIER from '@dimforge/rapier3d' import type { FCamera } from '../cameras/FCamera' import { FFixedCamera } from '../cameras/FFixedCamera' import type { FLight } from '../lights/FLight' +import { RenderPipeline } from '../pipeline/RenderPipeline' +import { PhysicPipeline } from '../pipeline/PhysicPipeline' import type { FComponent } from './FComponent' import type { FRigidBody } from './FRigidBody' import type { FCollider } from './FCollider' @@ -115,17 +117,8 @@ export class FScene extends FSceneCore { // Add renderer to DOM this.__DOM_NODE__.appendChild(this.renderer.domElement) - // Each frame - this.onFrame((delta) => { - // Call frame for each component - this.components.forEach(component => component.frame(delta)) - - // Call frame for the camera - this.camera.frame(delta) - - // Render the scene - this.renderer.render(this.scene, this.camera.__CAMERA__) - }) + // Initialize the render pipeline + this.addStandardPipeline(new RenderPipeline({ scene: this })) // Call the onReady callbacks this.__CALLBACKS_ON_READY__.forEach((callback) => { @@ -143,21 +136,8 @@ export class FScene extends FSceneCore { // Initialize Rapier event queue this.eventQueue = new RAPIER.EventQueue(true) - // onFrame loop - this.onFrame((delta) => { - // Step the physics world - this.world.timestep = delta - this.world.step(this.eventQueue) - - // Call frame for each collider and rigidBody - this.colliders.forEach(collider => collider.frame(delta)) - this.rigidBodies.forEach(rigidBody => rigidBody.frame(delta)) - - // Drain collision events - this.eventQueue.drainCollisionEvents((handle1: RAPIER.ColliderHandle, handle2: RAPIER.ColliderHandle, started: boolean) => { - this.handleCollision(handle1, handle2, started) - }) - }) + // Initialize the physic pipeline + this.addStandardPipeline(new PhysicPipeline({ scene: this })) } /** diff --git a/packages/3d/src/pipeline/PhysicPipeline.ts b/packages/3d/src/pipeline/PhysicPipeline.ts new file mode 100644 index 0000000..b2989f4 --- /dev/null +++ b/packages/3d/src/pipeline/PhysicPipeline.ts @@ -0,0 +1,35 @@ +import { StandardPipeline } from '@fibbojs/core' +import type RAPIER from '@dimforge/rapier3d' +import type { FScene } from '../core/FScene' + +export interface PhysicPipelineOptions { + scene: FScene +} + +/** + * Render pipeline. + */ +export class PhysicPipeline extends StandardPipeline { + scene: FScene + + constructor(options: PhysicPipelineOptions) { + super() + this.scene = options.scene + this.frameRate = 60 + } + + frame(delta: number) { + // Step the physics world + this.scene.world.timestep = delta + this.scene.world.step(this.scene.eventQueue) + + // Call frame for each collider and rigidBody + this.scene.colliders.forEach(collider => collider.frame(delta)) + this.scene.rigidBodies.forEach(rigidBody => rigidBody.frame(delta)) + + // Drain collision events + this.scene.eventQueue.drainCollisionEvents((handle1: RAPIER.ColliderHandle, handle2: RAPIER.ColliderHandle, started: boolean) => { + this.scene.handleCollision(handle1, handle2, started) + }) + } +} diff --git a/packages/3d/src/pipeline/PhysicPipelineTest.ts b/packages/3d/src/pipeline/PhysicPipelineTest.ts new file mode 100644 index 0000000..a983458 --- /dev/null +++ b/packages/3d/src/pipeline/PhysicPipelineTest.ts @@ -0,0 +1,20 @@ +import { BackgroundPipeline } from '@fibbojs/core' + +export enum PhysicPipelineCommands { + START = 'start', + STOP = 'stop', +} + +/** + * Physic pipeline. + * @category Pipeline + */ +export class PhysicPipelineTest extends BackgroundPipeline { + constructor() { + super('../../3d/dist/pipeline/PhysicWorkerRun.mjs') + } + + frame(_delta: number) { + // console.log('PhysicPipeline frame') + } +} diff --git a/packages/3d/src/pipeline/PhysicWorker.ts b/packages/3d/src/pipeline/PhysicWorker.ts new file mode 100644 index 0000000..2b84f94 --- /dev/null +++ b/packages/3d/src/pipeline/PhysicWorker.ts @@ -0,0 +1,30 @@ +import { BackgroundWorker } from '@fibbojs/core' + +/** + * Physic web worker. + * @category Pipeline + */ +export class PhysicWorker extends BackgroundWorker { + constructor(sw: DedicatedWorkerGlobalScope) { + super(sw) + } + + frame(delta: number) { + console.log(`Worker : ${delta}`) + + /* + // Step the physics world + this.world.timestep = delta + this.world.step(this.eventQueue) + + // Call frame for each collider and rigidBody + this.colliders.forEach(collider => collider.frame(delta)) + this.rigidBodies.forEach(rigidBody => rigidBody.frame(delta)) + + // Drain collision events + this.eventQueue.drainCollisionEvents((handle1: RAPIER.ColliderHandle, handle2: RAPIER.ColliderHandle, started: boolean) => { + this.handleCollision(handle1, handle2, started) + }) + */ + } +} diff --git a/packages/core/src/pipeline/RenderPipelineWorker.ts b/packages/3d/src/pipeline/PhysicWorkerRun.ts similarity index 56% rename from packages/core/src/pipeline/RenderPipelineWorker.ts rename to packages/3d/src/pipeline/PhysicWorkerRun.ts index 71a5619..3117dc1 100644 --- a/packages/core/src/pipeline/RenderPipelineWorker.ts +++ b/packages/3d/src/pipeline/PhysicWorkerRun.ts @@ -1,8 +1,8 @@ /// -import { RenderPipeline } from './RenderPipeline' +import { PhysicWorker } from './PhysicWorker' export type {} declare let self: DedicatedWorkerGlobalScope -new RenderPipeline(self) +new PhysicWorker(self) diff --git a/packages/3d/src/pipeline/RenderPipeline.ts b/packages/3d/src/pipeline/RenderPipeline.ts new file mode 100644 index 0000000..a4f56d9 --- /dev/null +++ b/packages/3d/src/pipeline/RenderPipeline.ts @@ -0,0 +1,30 @@ +import { StandardPipeline } from '@fibbojs/core' +import type { FScene } from '../core/FScene' + +export interface RenderPipelineOptions { + scene: FScene +} + +/** + * Render pipeline. + */ +export class RenderPipeline extends StandardPipeline { + scene: FScene + + constructor(options: RenderPipelineOptions) { + super() + this.scene = options.scene + this.frameRate = 60 + } + + frame(delta: number) { + // Call frame for each component + this.scene.components.forEach(component => component.frame(delta)) + + // Call frame for the camera + this.scene.camera.frame(delta) + + // Render the scene + this.scene.renderer.render(this.scene.scene, this.scene.camera.__CAMERA__) + } +} diff --git a/packages/core/build.config.ts b/packages/core/build.config.ts index b39054c..7acbef8 100644 --- a/packages/core/build.config.ts +++ b/packages/core/build.config.ts @@ -3,8 +3,6 @@ import { defineBuildConfig } from 'unbuild' export default defineBuildConfig({ entries: [ 'src/index', - 'src/pipeline/RenderPipelineWorker', - 'src/pipeline/PhysicPipelineWorker', ], declaration: true, clean: true, diff --git a/packages/core/src/FScene.ts b/packages/core/src/FScene.ts index 184b66c..b05cf43 100644 --- a/packages/core/src/FScene.ts +++ b/packages/core/src/FScene.ts @@ -2,7 +2,10 @@ import type RAPIER2D from '@dimforge/rapier2d' import type RAPIER3D from '@dimforge/rapier3d' import type { FComponent } from './FComponent' import type { FLight } from './FLight' -import { CustomWorker } from './pipeline/CustomWorker' +import type { BackgroundPipeline } from './pipeline/BackgroundPipeline' +import type { StandardPipeline } from './pipeline/StandardPipeline' +import { MainPipeline } from './pipeline/MainPipeline' +import { PipelineState } from './pipeline/Pipeline' export interface FSceneOptions { gravity?: { x: number, y: number, z: number } | { x: number, y: number } @@ -16,17 +19,15 @@ export interface FSceneOptions { * @category Core */ export abstract class FScene { - /** - * Internal flags - */ + // Internal flags public __IS_3D__: boolean = false public __IS_2D__: boolean = false /** * Pipelines */ - private __RENDER_PIPELINE__: CustomWorker | null = null - private __PHYSIC_PIPELINE__: CustomWorker | null = null + __STANDARD_PIPELINES__: StandardPipeline[] + __BACKGROUND_PIPELINES__: BackgroundPipeline[] /** * DOM element that the renderer will be appended to @@ -97,11 +98,11 @@ export abstract class FScene { this.__DOM_NODE__ = options.domNode /** - * Time management + * Pipelines */ - let lastTime = (new Date()).getTime() - let currentTime = 0 - let delta = 0 + this.__STANDARD_PIPELINES__ = [] + this.__BACKGROUND_PIPELINES__ = [] + this.addStandardPipeline(new MainPipeline({ scene: this })) /** * Auto loop function that calls the frame method every frame. @@ -109,28 +110,32 @@ export abstract class FScene { const autoLoop = () => { requestAnimationFrame(autoLoop) - // Calculate delta time - currentTime = (new Date()).getTime() - delta = (currentTime - lastTime) / 1000 - lastTime = currentTime - - // Call onFrame callbacks - this.frame(delta) + this.__STANDARD_PIPELINES__.forEach((pipeline) => { + // If the pipeline should be running + if (pipeline.state === PipelineState.RUNNING) { + // Calculate elapsed time + const currentTime = (new Date()).getTime() + const elapsedTime = currentTime - pipeline.lastTime + // If enough time has passed to match the expected framerate + if (elapsedTime > 1000 / pipeline.frameRate) { + const delta = (currentTime - pipeline.lastTime) / 1000 + // Keep track of the last time the pipeline was called + pipeline.lastTime = currentTime + // Call the pipeline + pipeline.frame(delta) + } + } + }) } - if (options.autoLoop) - autoLoop() - // Initialize the components array this.components = [] // Initialize the lights array this.lights = [] - // Initialize workers - this.__RENDER_PIPELINE__ = new CustomWorker('./pipeline/RenderPipelineWorker.mjs') - this.__RENDER_PIPELINE__.start() - this.__PHYSIC_PIPELINE__ = new CustomWorker('./pipeline/PhysicPipelineWorker.mjs') - this.__PHYSIC_PIPELINE__.start() + // Launch the autoLoop if needed + if (options.autoLoop) + autoLoop() } /** @@ -171,6 +176,44 @@ export abstract class FScene { this.__CALLBACKS_ON_LIGHT_REMOVED__.forEach(callback => callback(light)) } + /** + * Add a standard pipeline. + */ + addStandardPipeline(pipeline: StandardPipeline) { + this.__STANDARD_PIPELINES__.push(pipeline) + pipeline.start() + } + + /** + * Remove a standard pipeline. + */ + removeStandardPipeline(pipeline: StandardPipeline) { + pipeline.stop() + const index = this.__STANDARD_PIPELINES__.indexOf(pipeline) + if (index !== -1) { + this.__STANDARD_PIPELINES__.splice(index, 1) + } + } + + /** + * Add a background pipeline. + */ + addBackgroundPipeline(pipeline: BackgroundPipeline) { + this.__BACKGROUND_PIPELINES__.push(pipeline) + pipeline.start() + } + + /** + * Remove a background pipeline. + */ + removeBackgroundPipeline(pipeline: BackgroundPipeline) { + pipeline.stop() + const index = this.__BACKGROUND_PIPELINES__.indexOf(pipeline) + if (index !== -1) { + this.__BACKGROUND_PIPELINES__.splice(index, 1) + } + } + /** * Compute a frame with the given delta time. * By default, it is called every frame, but this behavior can be changed by giving the `autoLoop` option as `false` when creating the scene. diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fc13227..ba93098 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -9,10 +9,10 @@ export * from './FLight' export * from './FScene' // Pipeline -export * from './pipeline/CustomWorker' -export * from './pipeline/PhysicPipeline' +export * from './pipeline/BackgroundPipeline' +export * from './pipeline/BackgroundWorker' export * from './pipeline/Pipeline' -export * from './pipeline/RenderPipeline' +export * from './pipeline/StandardPipeline' // Types export * from './types/FVector2' diff --git a/packages/core/src/pipeline/BackgroundPipeline.ts b/packages/core/src/pipeline/BackgroundPipeline.ts new file mode 100644 index 0000000..b2281a2 --- /dev/null +++ b/packages/core/src/pipeline/BackgroundPipeline.ts @@ -0,0 +1,33 @@ +import { Pipeline, PipelineCommands } from './Pipeline' + +/** + * A pipeline that abstract the usage of a web worker. + * It provides better type checking, and more control over the worker. + * @category Pipeline + */ +export abstract class BackgroundPipeline extends Pipeline { + worker: Worker + + constructor(path: string) { + super() + /* + console.log(import.meta.url) + console.log(new URL(path, import.meta.url).href) + */ + this.worker = new Worker(new URL(path, import.meta.url), { type: 'module' }) + } + + /** + * Start the corresponding pipeline. + */ + start(): void { + this.worker.postMessage(PipelineCommands.START) + } + + /** + * Stop the corresponding pipeline. + */ + stop(): void { + this.worker.postMessage(PipelineCommands.STOP) + } +} diff --git a/packages/core/src/pipeline/BackgroundWorker.ts b/packages/core/src/pipeline/BackgroundWorker.ts new file mode 100644 index 0000000..f447635 --- /dev/null +++ b/packages/core/src/pipeline/BackgroundWorker.ts @@ -0,0 +1,65 @@ +/// + +import { Pipeline, PipelineCommands, PipelineState } from './Pipeline' + +/** + * Hided worker behind the BackgroundPipeline class. + * @category Pipeline + */ +export abstract class BackgroundWorker extends Pipeline { + /** + * The web worker instance. + */ + sw: DedicatedWorkerGlobalScope + /** + * The interval id that is used to run the pipeline. + * Corresponds to the id returned by setInterval. + */ + intervalId: ReturnType | null = null + + constructor(sw: DedicatedWorkerGlobalScope) { + super() + // Save the web worker instance + this.sw = sw + this.sw.addEventListener('message', (event) => { + this.handleMessage(event) + }) + } + + /** + * Handle a message sent to the pipeline. + * @param event The message event. + */ + handleMessage(event: MessageEvent) { + const command = event.data + + if (command === PipelineCommands.START) { + this.start() + } + else if (command === PipelineCommands.STOP) { + this.stop() + } + } + + start(): void { + if (this.intervalId !== null) { + return + } + // Start the pipeline by setting an interval with the frame rate + this.intervalId = setInterval(() => { + this.frame(1000 / this.frameRate / 1000) + }, 1000 / this.frameRate) + // Update the pipeline state + this.state = PipelineState.RUNNING + } + + stop(): void { + // Stop the pipeline by clearing the interval + if (this.intervalId !== null) { + clearInterval(this.intervalId) + } + this.intervalId = null + // Update the pipeline state + this.state = PipelineState.STOPPED + } +} diff --git a/packages/core/src/pipeline/CustomWorker.ts b/packages/core/src/pipeline/CustomWorker.ts deleted file mode 100644 index c6d2ec9..0000000 --- a/packages/core/src/pipeline/CustomWorker.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { PipelineCommands } from './Pipeline' - -/** - * A custom worker that extends the Worker class. - * It provides better type checking, and more control over the worker. - * @category Pipeline - */ -export class CustomWorker extends Worker { - constructor(path: string) { - super(new URL(path, import.meta.url), { type: 'module' }) - } - - /** - * Start the corresponding pipeline. - */ - start() { - this.postMessage(PipelineCommands.START) - } - - /** - * Stop the corresponding pipeline. - */ - stop() { - this.postMessage(PipelineCommands.STOP) - } -} diff --git a/packages/core/src/pipeline/MainPipeline.ts b/packages/core/src/pipeline/MainPipeline.ts new file mode 100644 index 0000000..b92cf2a --- /dev/null +++ b/packages/core/src/pipeline/MainPipeline.ts @@ -0,0 +1,23 @@ +import type { FScene } from '../FScene' +import { StandardPipeline } from './StandardPipeline' + +export interface MainPipelineOptions { + scene: FScene +} + +/** + * Main game loop. + */ +export class MainPipeline extends StandardPipeline { + scene: FScene + + constructor(options: MainPipelineOptions) { + super() + this.scene = options.scene + this.frameRate = 60 + } + + frame(delta: number) { + this.scene.frame(delta) + } +} diff --git a/packages/core/src/pipeline/PhysicPipeline.ts b/packages/core/src/pipeline/PhysicPipeline.ts deleted file mode 100644 index 5e9f2d2..0000000 --- a/packages/core/src/pipeline/PhysicPipeline.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Pipeline } from './Pipeline' - -/** - * Pipeline to handle physic simulation tasks. - * @category Pipeline - */ -export class PhysicPipeline extends Pipeline { - constructor(sw: DedicatedWorkerGlobalScope) { - super(sw) - } - - frame() { - // console.log('PhysicPipeline frame') - } -} diff --git a/packages/core/src/pipeline/PhysicPipelineWorker.ts b/packages/core/src/pipeline/PhysicPipelineWorker.ts deleted file mode 100644 index 02d7108..0000000 --- a/packages/core/src/pipeline/PhysicPipelineWorker.ts +++ /dev/null @@ -1,8 +0,0 @@ -/// - -import { PhysicPipeline } from './PhysicPipeline' - -export type {} -declare let self: DedicatedWorkerGlobalScope - -new PhysicPipeline(self) diff --git a/packages/core/src/pipeline/Pipeline.ts b/packages/core/src/pipeline/Pipeline.ts index ff5e10f..c42335d 100644 --- a/packages/core/src/pipeline/Pipeline.ts +++ b/packages/core/src/pipeline/Pipeline.ts @@ -11,24 +11,14 @@ export enum PipelineState { } /** - * Pipeline class that abstract the usage of a web worker. - * This is used for running background tasks that are generally CPU intensive. + * Pipeline class that helps handling many processes at a time. * @category Pipeline */ export abstract class Pipeline { - /** - * The web worker instance. - */ - sw: DedicatedWorkerGlobalScope /** * The current state of the pipeline. */ state: PipelineState - /** - * The interval id that is used to run the pipeline. - * Corresponds to the id returned by setInterval. - */ - intervalId: ReturnType | null = null /** * The frame rate of the pipeline. * This is the number of frames per second that the pipeline will run at. @@ -37,12 +27,7 @@ export abstract class Pipeline { */ frameRate: number - constructor(sw: DedicatedWorkerGlobalScope) { - // Save the web worker instance - this.sw = sw - this.sw.addEventListener('message', (event) => { - this.handleMessage(event) - }) + constructor() { // Set the initial state of the pipeline this.state = PipelineState.STOPPED this.frameRate = 30 @@ -52,34 +37,15 @@ export abstract class Pipeline { * The frame method is the main method that is called by the pipeline. * It should implement the desired behavior of the pipeline. */ - abstract frame(): void + abstract frame(delta: number): void /** - * Handle a message sent to the pipeline. - * @param event The message event. + * Start the pipeline */ - handleMessage(event: MessageEvent) { - const command = event.data + abstract start(): void - if (command === PipelineCommands.START) { - if (this.intervalId !== null) { - return - } - // Start the pipeline by setting an interval with the frame rate - this.intervalId = setInterval(() => { - this.frame() - }, 1000 / this.frameRate) - // Update the pipeline state - this.state = PipelineState.RUNNING - } - else if (command === PipelineCommands.STOP) { - // Stop the pipeline by clearing the interval - if (this.intervalId !== null) { - clearInterval(this.intervalId) - } - this.intervalId = null - // Update the pipeline state - this.state = PipelineState.STOPPED - } - } + /** + * Stop the pipeline + */ + abstract stop(): void } diff --git a/packages/core/src/pipeline/RenderPipeline.ts b/packages/core/src/pipeline/RenderPipeline.ts deleted file mode 100644 index 0b4cba4..0000000 --- a/packages/core/src/pipeline/RenderPipeline.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Pipeline } from './Pipeline' - -/** - * Pipeline to handle rendering tasks. - * @category Pipeline - */ -export class RenderPipeline extends Pipeline { - constructor(sw: DedicatedWorkerGlobalScope) { - super(sw) - } - - frame() { - // console.log('RenderPipeline frame') - } -} diff --git a/packages/core/src/pipeline/StandardPipeline.ts b/packages/core/src/pipeline/StandardPipeline.ts new file mode 100644 index 0000000..2cbee98 --- /dev/null +++ b/packages/core/src/pipeline/StandardPipeline.ts @@ -0,0 +1,24 @@ +import { Pipeline, PipelineState } from './Pipeline' + +/** + * Pipeline class. + * @category Pipeline + */ +export abstract class StandardPipeline extends Pipeline { + lastTime: number + + constructor() { + super() + this.lastTime = (new Date()).getTime() + } + + start(): void { + // Update the pipeline state + this.state = PipelineState.RUNNING + } + + stop(): void { + // Update the pipeline state + this.state = PipelineState.STOPPED + } +}