├── src ├── custom.d.ts ├── physics │ ├── config.ts │ ├── PhysicsProvider.context.ts │ ├── helpers │ │ ├── cannon │ │ │ ├── types.ts │ │ │ ├── buffers.ts │ │ │ ├── hooks.ts │ │ │ ├── CannonPhysicsConsumer.tsx │ │ │ ├── bodies.ts │ │ │ ├── CannonApp.tsx │ │ │ ├── CannonPhysicsWorkerMessagesHandler.tsx │ │ │ ├── CannonPhysicsHandler.tsx │ │ │ └── updates.ts │ │ ├── planckjs │ │ │ ├── buffers.ts │ │ │ ├── PlanckPhysicsHandler.context.ts │ │ │ ├── PlanckApp.context.ts │ │ │ ├── types.ts │ │ │ ├── PhysicsConsumerHelpers.tsx │ │ │ ├── PlanckPhysicsConsumer.tsx │ │ │ ├── bodies.ts │ │ │ ├── PlanckApp.tsx │ │ │ ├── PlanckPhysicsWorkerMessagesHandler.tsx │ │ │ ├── hooks.ts │ │ │ ├── updates.ts │ │ │ ├── WorkerSubscription.tsx │ │ │ └── PlanckPhysicsHandler.tsx │ │ └── rapier3d │ │ │ ├── hooks.ts │ │ │ ├── types.ts │ │ │ ├── custom.ts │ │ │ ├── Rapier3DPhysicsConsumer.tsx │ │ │ ├── updates.ts │ │ │ ├── Rapier3DApp.tsx │ │ │ ├── bodies.ts │ │ │ ├── Rapier3DPhysicsHandler.tsx │ │ │ ├── Rapier3DPhysicsWorkerMessagesHandler.tsx │ │ │ └── WorkerSubscription.tsx │ ├── PhysicsConsumer.context.ts │ ├── Physics.tsx │ ├── PhysicsConsumerSyncMeshes.tsx │ ├── types.ts │ ├── physicsLoopWorker.ts │ ├── PhysicsProvider.tsx │ └── PhysicsConsumer.tsx ├── utils │ ├── numbers.ts │ ├── ids.ts │ └── time.ts ├── createWorkerApp.ts ├── index.tsx ├── keys.tsx └── generic.tsx ├── example ├── .npmignore ├── src │ ├── config.ts │ ├── webWorker.ts │ ├── index.css │ ├── index.tsx │ ├── WorkerApp.tsx │ ├── Game.tsx │ └── Rapier3DGame.tsx ├── index.html ├── tsconfig.json └── package.json ├── .gitignore ├── .github └── workflows │ ├── size.yml │ └── main.yml ├── test └── blah.test.tsx ├── LICENSE ├── tsconfig.json ├── package.json └── README.md /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-nil'; 2 | -------------------------------------------------------------------------------- /example/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist -------------------------------------------------------------------------------- /example/src/config.ts: -------------------------------------------------------------------------------- 1 | export const STEP_RATE = 1000 / 60 -------------------------------------------------------------------------------- /src/physics/config.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_STEP_RATE = 1000 / 60 -------------------------------------------------------------------------------- /src/utils/numbers.ts: -------------------------------------------------------------------------------- 1 | import { MathUtils } from 'three'; 2 | 3 | export const lerp = MathUtils.lerp; 4 | -------------------------------------------------------------------------------- /src/utils/ids.ts: -------------------------------------------------------------------------------- 1 | import { MathUtils } from 'three'; 2 | 3 | export const generateUUID = MathUtils.generateUUID; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | .idea 7 | example/dist 8 | example/node_modules -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | const start = Date.now() 2 | 3 | export const getNow = () => { 4 | return start + performance.now() 5 | } -------------------------------------------------------------------------------- /example/src/webWorker.ts: -------------------------------------------------------------------------------- 1 | import WorkerApp from "./WorkerApp" 2 | import {createWorkerApp} from "../../src"; 3 | 4 | createWorkerApp(WorkerApp) -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | #root { 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | right: 0; 10 | bottom: 0; 11 | } -------------------------------------------------------------------------------- /src/physics/PhysicsProvider.context.ts: -------------------------------------------------------------------------------- 1 | import {createContext, useContext} from "react"; 2 | 3 | type State = {} 4 | 5 | export const Context = createContext(null as unknown as State) 6 | 7 | export const usePhysicsContext = () => { 8 | return useContext(Context) 9 | } -------------------------------------------------------------------------------- /src/createWorkerApp.ts: -------------------------------------------------------------------------------- 1 | import {render} from "react-nil"; 2 | import {createElement, FC} from "react"; 3 | 4 | export const createWorkerApp = (app: FC<{ 5 | worker?: Worker, 6 | }>) => { 7 | render(createElement(app, { 8 | worker: self as unknown as Worker, 9 | }), null) 10 | } -------------------------------------------------------------------------------- /src/physics/helpers/cannon/types.ts: -------------------------------------------------------------------------------- 1 | import {BodyOptions} from "objects/Body"; 2 | 3 | export type AddBodyDef = { 4 | body: Partial, 5 | shapes: { 6 | type: 'Sphere' | 'Box', 7 | args?: any[], 8 | }[], 9 | userData?: { 10 | [key: string]: any, 11 | } 12 | } -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /src/physics/helpers/cannon/buffers.ts: -------------------------------------------------------------------------------- 1 | import {Buffers} from "../planckjs/types"; 2 | 3 | export const generateBuffers = (maxNumberOfPhysicsObjects: number): Buffers => { 4 | return { 5 | positions: new Float32Array(maxNumberOfPhysicsObjects * 3), 6 | angles: new Float32Array(maxNumberOfPhysicsObjects * 4), 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /test/blah.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Thing } from '../src'; 4 | 5 | describe('it', () => { 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render(, div); 9 | ReactDOM.unmountComponentAtNode(div); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/physics/helpers/planckjs/buffers.ts: -------------------------------------------------------------------------------- 1 | import {ExtBuffers} from "./types"; 2 | 3 | export const generateBuffers = (maxNumberOfPhysicsObjects: number): ExtBuffers => { 4 | return { 5 | positions: new Float32Array(maxNumberOfPhysicsObjects * 2), 6 | angles: new Float32Array(maxNumberOfPhysicsObjects), 7 | velocities: new Float32Array(maxNumberOfPhysicsObjects * 2), 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /src/physics/helpers/rapier3d/hooks.ts: -------------------------------------------------------------------------------- 1 | import {Options, useBody} from "../planckjs/hooks"; 2 | import {MutableRefObject} from "react"; 3 | import {Object3D} from "three"; 4 | import {addToMessage} from "../cannon/hooks"; 5 | import {AddBodyDef} from "./types"; 6 | 7 | export const useRapier3DBody = (propsFn: () => AddBodyDef, options: Partial = {}): [ 8 | MutableRefObject, 9 | string 10 | ] => { 11 | return useBody(propsFn, options, addToMessage) 12 | } -------------------------------------------------------------------------------- /src/physics/helpers/planckjs/PlanckPhysicsHandler.context.ts: -------------------------------------------------------------------------------- 1 | import {createContext, useContext} from "react"; 2 | 3 | type State = { 4 | getPendingSyncedBodiesIteration: () => number, 5 | syncedBodies: { 6 | [key: string]: any, 7 | } 8 | syncedBodiesOrder: string[], 9 | maxNumberOfSyncedBodies: number, 10 | } 11 | 12 | export const Context = createContext(null as unknown as State) 13 | 14 | export const usePlanckPhysicsHandlerContext = () => useContext(Context) -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground 8 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "removeComments": true, 12 | "strictNullChecks": true, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "lib": ["es2015", "es2016", "dom"], 16 | "types": ["node"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/physics/helpers/planckjs/PlanckApp.context.ts: -------------------------------------------------------------------------------- 1 | import {createContext, useContext} from "react"; 2 | 3 | export type State = { 4 | world: any, 5 | addSyncedBody: (uid: string, body: any) => void, 6 | removeSyncedBody: (uid: string) => void, 7 | addBody: (id: string, body: any, synced?: boolean) => () => void, 8 | bodies: { 9 | [key: string]: any, 10 | }, 11 | } 12 | 13 | export const Context = createContext(null as unknown as State) 14 | 15 | export const usePlanckAppContext = () => useContext(Context) -------------------------------------------------------------------------------- /src/physics/helpers/rapier3d/types.ts: -------------------------------------------------------------------------------- 1 | import {BodyStatus} from "@dimforge/rapier3d-compat/rapier"; 2 | 3 | export type ColliderDef = { 4 | type: 'Cubiod' | 'Ball', 5 | args: any[], 6 | density?: number, 7 | } 8 | 9 | export type AddBodyDef = { 10 | body: { 11 | type: BodyStatus, 12 | position?: [number, number, number] | number[], 13 | quaternion?: [number, number, number, number] | number[], 14 | mass?: number, 15 | }, 16 | colliders: ColliderDef[], 17 | customBody?: string, 18 | } -------------------------------------------------------------------------------- /src/physics/helpers/rapier3d/custom.ts: -------------------------------------------------------------------------------- 1 | import {AddBodyDef} from "./types"; 2 | 3 | export type CustomBodyModifiers = { 4 | [key: string]: (body: any) => void, 5 | } 6 | 7 | export const customData: { 8 | customBodyModifiers: CustomBodyModifiers, 9 | } = { 10 | customBodyModifiers: {}, 11 | } 12 | 13 | export const getCustomBodyModifier = (bodyDef: AddBodyDef) => { 14 | if (bodyDef.customBody && customData.customBodyModifiers[bodyDef.customBody]) { 15 | return customData.customBodyModifiers[bodyDef.customBody] 16 | } 17 | return undefined 18 | } -------------------------------------------------------------------------------- /src/physics/PhysicsConsumer.context.ts: -------------------------------------------------------------------------------- 1 | import {createContext, MutableRefObject, useContext} from "react"; 2 | import {Object3D} from "three"; 3 | import {BodyData} from './types'; 4 | 5 | type State = { 6 | syncBody: (id: string, ref: MutableRefObject, applyRotation?: boolean) => () => void, 7 | syncMeshes: (_: any, delta: number) => void, 8 | sendMessage: (message: any) => void, 9 | bodiesData: { [id: string]: BodyData }, 10 | } 11 | 12 | export const Context = createContext(null as unknown as State) 13 | 14 | export const usePhysicsConsumerContext = () => useContext(Context) -------------------------------------------------------------------------------- /src/physics/helpers/planckjs/types.ts: -------------------------------------------------------------------------------- 1 | import {BodyDef, FixtureOpt} from "planck-js"; 2 | 3 | export type Buffers = { 4 | positions: Float32Array; 5 | angles: Float32Array; 6 | }; 7 | 8 | export type ExtBuffers = { 9 | positions: Float32Array; 10 | angles: Float32Array; 11 | velocities: Float32Array; 12 | }; 13 | 14 | export enum FixtureShape { 15 | Circle = 'Circle', 16 | Box = 'Box', 17 | } 18 | 19 | export type Fixtures = { 20 | shape: FixtureShape, 21 | args: any[], 22 | fixtureOptions: Partial, 23 | }[] 24 | 25 | export type AddBodyDef = { 26 | body: BodyDef, 27 | fixtures: Fixtures 28 | } -------------------------------------------------------------------------------- /src/physics/Physics.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PhysicsProvider from "./PhysicsProvider"; 3 | import {OnWorldStepFn} from "./types"; 4 | import {DEFAULT_STEP_RATE} from "./config"; 5 | import {OnFixedUpdateProvider} from "./PhysicsConsumer"; 6 | 7 | const Physics: React.FC<{ 8 | onWorldStep: OnWorldStepFn, 9 | stepRate?: number, 10 | }> = ({children, onWorldStep, stepRate = DEFAULT_STEP_RATE}) => { 11 | return ( 12 | 13 | 14 | {children} 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default Physics; -------------------------------------------------------------------------------- /src/physics/helpers/planckjs/PhysicsConsumerHelpers.tsx: -------------------------------------------------------------------------------- 1 | import React, {createContext, useContext} from "react" 2 | import {Object3D} from "three"; 3 | 4 | type State = { 5 | prepareObject: (object: Object3D, props: any) => void, 6 | } 7 | 8 | const Context = createContext(null as unknown as State) 9 | 10 | export const usePhysicsConsumerHelpers = () => useContext(Context) 11 | 12 | const PhysicsConsumerHelpers: React.FC<{ 13 | prepareObject: (object: Object3D, props: any) => void, 14 | }> = ({children, prepareObject}) => { 15 | return ( 16 | 17 | {children} 18 | 19 | ) 20 | } 21 | 22 | export default PhysicsConsumerHelpers -------------------------------------------------------------------------------- /src/physics/helpers/cannon/hooks.ts: -------------------------------------------------------------------------------- 1 | import {AddBodyDef as CannonAddBodyDef} from "./types"; 2 | import {MutableRefObject} from "react"; 3 | import {Object3D} from "three"; 4 | import {Options, useBody} from "../planckjs/hooks"; 5 | 6 | // @ts-ignore 7 | export const addToMessage = (props: CannonAddBodyDef, options: Partial) => { 8 | const message: { 9 | [key: string]: any, 10 | } = {} 11 | if (options.listenForCollisions) { 12 | message.listenForCollisions = true 13 | } 14 | return message 15 | } 16 | 17 | export const useCannonBody = (propsFn: () => CannonAddBodyDef, options: Partial = {}): [ 18 | MutableRefObject, 19 | string 20 | ] => { 21 | return useBody(propsFn, options, addToMessage) 22 | } -------------------------------------------------------------------------------- /src/physics/helpers/planckjs/PlanckPhysicsConsumer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {PhysicsConsumer} from "../../../index"; 3 | import {lerpBody, prepareObject, updateBodyData} from "./updates"; 4 | import PhysicsConsumerHelpers from "./PhysicsConsumerHelpers"; 5 | 6 | const PlanckPhysicsConsumer: React.FC<{ 7 | worker: Worker, 8 | stepRate: number, 9 | }> = ({worker, stepRate, children}) => { 10 | return ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ) 17 | } 18 | 19 | export default PlanckPhysicsConsumer -------------------------------------------------------------------------------- /src/physics/helpers/cannon/CannonPhysicsConsumer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {PhysicsConsumer} from "../../../index"; 3 | import {lerpBody, prepareObject, updateBodyData} from "./updates"; 4 | import PhysicsConsumerHelpers from "../planckjs/PhysicsConsumerHelpers"; 5 | 6 | const CannonPhysicsConsumer: React.FC<{ 7 | worker: Worker, 8 | stepRate: number, 9 | }> = ({worker, stepRate, children}) => { 10 | return ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ) 17 | } 18 | 19 | export default CannonPhysicsConsumer -------------------------------------------------------------------------------- /src/physics/helpers/rapier3d/Rapier3DPhysicsConsumer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {lerpBody, updateBodyData} from "../cannon/updates"; 3 | import PhysicsConsumerHelpers from "../planckjs/PhysicsConsumerHelpers"; 4 | import {PhysicsConsumer} from "../../../index"; 5 | import {prepareObject} from "./updates"; 6 | import {DefaultPhysicsConsumerProps} from "../../PhysicsConsumer"; 7 | 8 | const Rapier3DPhysicsConsumer: React.FC = ({children, ...props}) => { 9 | return ( 10 | 11 | 12 | {children} 13 | 14 | 15 | ) 16 | } 17 | 18 | export default Rapier3DPhysicsConsumer -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /src/physics/helpers/planckjs/bodies.ts: -------------------------------------------------------------------------------- 1 | import {BodyDef, Box, Circle, Shape, World} from "planck-js"; 2 | import {Fixtures, FixtureShape} from "./types"; 3 | 4 | const createFixtureShape = (shape: FixtureShape, args: any[]): Shape | null => { 5 | switch (shape) { 6 | case FixtureShape.Circle: 7 | return Circle(...args) 8 | case FixtureShape.Box: 9 | // @ts-ignore 10 | return Box(...args) 11 | } 12 | return null 13 | } 14 | 15 | export const createBody = (world: World, bodyDef: BodyDef, fixtures: Fixtures) => { 16 | const body = world.createBody(bodyDef) 17 | fixtures.forEach(({shape, args, fixtureOptions}) => { 18 | const fixtureShape = createFixtureShape(shape, args) 19 | if (fixtureShape) { 20 | body.createFixture(fixtureShape, fixtureOptions) 21 | } 22 | }) 23 | return body 24 | } -------------------------------------------------------------------------------- /src/physics/helpers/cannon/bodies.ts: -------------------------------------------------------------------------------- 1 | import {Body, Box, World, Sphere, Vec3} from "cannon-es"; 2 | import {AddBodyDef} from "./types"; 3 | 4 | export const createBody = (world: World, bodyDef: AddBodyDef) => { 5 | 6 | const body = new Body(bodyDef.body); 7 | 8 | bodyDef.shapes.forEach(({type, args}) => { 9 | switch (type) { 10 | case 'Box': 11 | // @ts-ignore 12 | const box = new Box(new Vec3(...args.map((v) => v / 2))) 13 | body.addShape(box as unknown as any) 14 | break; 15 | case 'Sphere': 16 | // @ts-ignore 17 | const sphere = new Sphere(...args) 18 | body.addShape(sphere as unknown as any) 19 | break; 20 | default: 21 | break; 22 | } 23 | }) 24 | world.addBody(body); 25 | return body; 26 | 27 | } -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "@dimforge/rapier3d": "^0.3.1", 12 | "@dimforge/rapier3d-compat": "^0.3.1", 13 | "@react-three/drei": "^3.7.0", 14 | "cannon-es": "^0.16.0", 15 | "planck-js": "^0.3.26", 16 | "react-app-polyfill": "^1.0.0" 17 | }, 18 | "alias": { 19 | "react": "../node_modules/react", 20 | "react-dom": "../node_modules/react-dom/profiling", 21 | "@react-three/fiber": "../node_modules/@react-three/fiber", 22 | "three": "../node_modules/three", 23 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 24 | }, 25 | "devDependencies": { 26 | "@types/react": "^16.9.11", 27 | "@types/react-dom": "^16.8.4", 28 | "parcel": "^1.12.3", 29 | "typescript": "^3.4.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/physics/PhysicsConsumerSyncMeshes.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react" 2 | import {useFrame} from "@react-three/fiber"; 3 | import {usePhysicsConsumerContext} from "./PhysicsConsumer.context"; 4 | 5 | const RAFSync: React.FC = () => { 6 | 7 | const {syncMeshes} = usePhysicsConsumerContext() 8 | useFrame(syncMeshes) 9 | 10 | return null 11 | } 12 | 13 | const IntervalSync: React.FC = () => { 14 | 15 | const {syncMeshes} = usePhysicsConsumerContext() 16 | 17 | useEffect(() => { 18 | const interval = setInterval(() => { 19 | syncMeshes(null, 1000 / 30) 20 | }, 1000 / 30) 21 | return () => { 22 | clearInterval(interval) 23 | } 24 | }, []) 25 | 26 | return null 27 | } 28 | 29 | const PhysicsConsumerSyncMeshes: React.FC<{ 30 | useRAF?: boolean 31 | }> = ({ 32 | useRAF = false, 33 | }) => { 34 | if (useRAF) return 35 | return 36 | } 37 | 38 | export default PhysicsConsumerSyncMeshes -------------------------------------------------------------------------------- /src/physics/types.ts: -------------------------------------------------------------------------------- 1 | import {MutableRefObject} from "react"; 2 | import {Object3D} from "three"; 3 | 4 | export type OnWorldStepFn = (delta: number) => void 5 | 6 | export enum WorkerMessageType { 7 | PHYSICS_UPDATE, 8 | PHYSICS_PROCESSED, 9 | PHYSICS_READY, 10 | PHYSICS_SET_PAUSED, 11 | PHYSICS_ACKNOWLEDGED, 12 | ADD_BODY, 13 | REMOVE_BODY, 14 | MODIFY_BODY, 15 | CUSTOM 16 | } 17 | 18 | export type WorkerMessageData = { 19 | type: WorkerMessageType, 20 | data?: any, 21 | [key: string]: any, 22 | } 23 | 24 | export type BodyData = { 25 | ref: MutableRefObject, 26 | index: number, 27 | position?: [number, number] | [number, number, number], 28 | angle?: number | [number, number, number, number], 29 | previous: { 30 | position?: [number, number] | [number, number, number], 31 | angle?: number | [number, number, number, number], 32 | }, 33 | velocity?: [number, number] | [number, number, number], 34 | lastUpdate: number, 35 | lastRender: number, 36 | applyRotation?: boolean, 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Simon Hales 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import 'react-app-polyfill/ie11'; 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import {Canvas} from "@react-three/fiber"; 5 | import {PlanckPhysicsConsumer, Rapier3DPhysicsConsumer} from "../../src"; 6 | import "./index.css" 7 | import {STEP_RATE} from "./config"; 8 | import Rapier3DGame from "./Rapier3DGame"; 9 | import {useEffect, useState} from "react"; 10 | 11 | const worker = new Worker("../src/webWorker.ts") 12 | 13 | const App = () => { 14 | 15 | const [paused, setPaused] = useState(false) 16 | 17 | // useEffect(() => { 18 | // setInterval(() => { 19 | // setPaused(state => !state) 20 | // }, 500) 21 | // }, []) 22 | 23 | return ( 24 | 25 | 26 | {/**/} 27 | 28 | {/**/} 29 | 30 | 31 | ); 32 | }; 33 | 34 | ReactDOM.render(, document.getElementById('root')); 35 | -------------------------------------------------------------------------------- /src/physics/helpers/planckjs/PlanckApp.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState} from "react"; 2 | import {World} from "planck-js"; 3 | import {DEFAULT_STEP_RATE} from "../../config"; 4 | import PlanckPhysicsHandler from "./PlanckPhysicsHandler"; 5 | import {WorkerMessaging} from "../../../generic"; 6 | 7 | const usePlanckPhysics = () => { 8 | 9 | const [world, setWorld] = useState(null) 10 | 11 | useEffect(() => { 12 | const planckWorld = new World({allowSleep: false}) 13 | setWorld(planckWorld) 14 | }, []) 15 | 16 | return { 17 | world, 18 | } 19 | } 20 | 21 | const PlanckApp: React.FC<{ 22 | worker: Worker, 23 | stepRate?: number, 24 | maxNumberOfSyncedBodies?: number, 25 | }> = ({children, 26 | stepRate = DEFAULT_STEP_RATE, 27 | maxNumberOfSyncedBodies = 100, 28 | worker}) => { 29 | 30 | const {world} = usePlanckPhysics() 31 | 32 | if (!world) return null 33 | 34 | return ( 35 | 36 | 37 | {children} 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default PlanckApp; -------------------------------------------------------------------------------- /example/src/WorkerApp.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | import PlanckApp from "../../src/physics/helpers/planckjs/PlanckApp"; 3 | import {CannonApp, Rapier3DApp, usePlanckAppContext} from "../../src"; 4 | import {BodyDef, Circle, Shape, Vec2} from "planck-js"; 5 | import {STEP_RATE} from "./config"; 6 | 7 | const Game: React.FC = () => { 8 | 9 | const { 10 | world, 11 | addSyncedBody, 12 | addBody, 13 | } = usePlanckAppContext() 14 | 15 | useEffect(() => { 16 | 17 | return 18 | 19 | const bodyDef: BodyDef = { 20 | type: 'dynamic', 21 | allowSleep: false, 22 | fixedRotation: true, 23 | position: Vec2(2, 2), 24 | linearDamping: 40, 25 | } 26 | const body = world.createBody(bodyDef) 27 | const circle = Circle() 28 | body.createFixture(circle as unknown as Shape, { 29 | density: 10, 30 | } as any) 31 | 32 | setInterval(() => { 33 | body.setLinearVelocity(Vec2(-3, 0)) 34 | }, 10) 35 | 36 | return addBody('player', body, true) 37 | 38 | }, []) 39 | 40 | return null 41 | } 42 | 43 | const WorkerApp: React.FC<{ 44 | worker: Worker 45 | }> = ({worker}) => { 46 | return ( 47 | 48 | ) 49 | }; 50 | 51 | export default WorkerApp; -------------------------------------------------------------------------------- /src/physics/helpers/rapier3d/updates.ts: -------------------------------------------------------------------------------- 1 | import {Buffers} from "../planckjs/types"; 2 | import {RigidBody} from "@dimforge/rapier3d-compat/rapier.js"; 3 | import {Object3D} from "three"; 4 | import { AddBodyDef } from "./types"; 5 | 6 | export type ApplyBufferDataFn = ( 7 | buffers: Buffers, 8 | syncedBodies: { 9 | [key: string]: any, 10 | }, 11 | syncedBodiesOrder: string[] 12 | ) => void 13 | 14 | export const applyBufferData = ( 15 | buffers: Buffers, 16 | syncedBodies: { 17 | [key: string]: RigidBody, 18 | }, syncedBodiesOrder: string[]) => { 19 | 20 | const { 21 | positions, 22 | angles, 23 | } = buffers 24 | 25 | syncedBodiesOrder.forEach((id, index) => { 26 | const body = syncedBodies[id] 27 | if (!body) return; 28 | const position = body.translation(); 29 | const quaternion = body.rotation(); 30 | positions[3 * index + 0] = position.x 31 | positions[3 * index + 1] = position.y 32 | positions[3 * index + 2] = position.z 33 | angles[4 * index + 0] = quaternion.x 34 | angles[4 * index + 1] = quaternion.y 35 | angles[4 * index + 2] = quaternion.z 36 | angles[4 * index + 3] = quaternion.w 37 | }) 38 | 39 | } 40 | 41 | export const prepareObject = (object: Object3D, props: AddBodyDef) => { 42 | if (props.body.position) { 43 | object.position.set(...props.body.position as [number, number, number]) 44 | } 45 | if (props.body.quaternion) { 46 | object.quaternion.set(...props.body.quaternion as [number, number, number, number]) 47 | } 48 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/physics/helpers/cannon/CannonApp.tsx: -------------------------------------------------------------------------------- 1 | import { World } from "cannon-es" 2 | import React, {useEffect, useState} from "react" 3 | import CannonPhysicsHandler from "./CannonPhysicsHandler"; 4 | import {DEFAULT_STEP_RATE} from "../../config"; 5 | import {WorkerMessaging} from "../../../generic"; 6 | 7 | const useCannonPhysics = () => { 8 | 9 | const [world, setWorld] = useState(null) 10 | 11 | useEffect(() => { 12 | 13 | const cannonWorld = new World() 14 | cannonWorld.gravity.set(0, -9.81, 0) 15 | setWorld(cannonWorld) 16 | 17 | cannonWorld.addEventListener('beginContact', () => { 18 | // todo 19 | }) 20 | 21 | cannonWorld.addEventListener('endContact', () => { 22 | // todo 23 | }) 24 | 25 | }, []) 26 | 27 | return { 28 | world, 29 | } 30 | 31 | } 32 | 33 | const CannonApp: React.FC<{ 34 | worker: Worker, 35 | stepRate?: number, 36 | maxNumberOfSyncedBodies?: number, 37 | }> = ({ 38 | children, 39 | stepRate = DEFAULT_STEP_RATE, 40 | maxNumberOfSyncedBodies = 100, 41 | worker}) => { 42 | 43 | const {world} = useCannonPhysics() 44 | 45 | if (!world) return null 46 | 47 | return ( 48 | 49 | 50 | {children} 51 | 52 | 53 | ) 54 | } 55 | 56 | export default CannonApp -------------------------------------------------------------------------------- /src/physics/physicsLoopWorker.ts: -------------------------------------------------------------------------------- 1 | export const createNewPhysicsLoopWebWorker = (stepRate: number) => { 2 | return new Worker('data:application/javascript,' + 3 | encodeURIComponent(` 4 | 5 | var start = performance.now(); 6 | var updateRate = ${stepRate}; 7 | var maxAccumulator = updateRate; 8 | 9 | function getNow() { 10 | return start + performance.now(); 11 | } 12 | 13 | var accumulator = 0; 14 | var lastAccumulation = getNow(); 15 | var now = getNow(); 16 | var numberOfUpdates = 0; 17 | 18 | function accumulate() { 19 | now = getNow(); 20 | accumulator += now - lastAccumulation; 21 | lastAccumulation = now; 22 | while (accumulator <= maxAccumulator) { 23 | now = getNow(); 24 | accumulator += now - lastAccumulation; 25 | lastAccumulation = now; 26 | } 27 | numberOfUpdates = Math.floor(accumulator / maxAccumulator); 28 | for (var i = 0; i < numberOfUpdates; i++) { 29 | self.postMessage('step'); 30 | accumulator -= maxAccumulator; 31 | } 32 | } 33 | 34 | function step() { 35 | 36 | accumulate(); 37 | 38 | setTimeout(step, updateRate - 2 - accumulator); 39 | 40 | } 41 | 42 | step() 43 | 44 | `) ); 45 | } -------------------------------------------------------------------------------- /src/physics/helpers/rapier3d/Rapier3DApp.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useEffect, useState} from "react" 2 | import * as RAPIER from "@dimforge/rapier3d-compat/rapier.js" 3 | import {DEFAULT_STEP_RATE} from "../../config"; 4 | import Rapier3DPhysicsHandler from "./Rapier3DPhysicsHandler"; 5 | import {CustomBodyModifiers, customData} from "./custom"; 6 | import {WorkerMessaging} from "../../../generic"; 7 | 8 | const useRapier3dPhysics = (stepRate: number) => { 9 | 10 | const [world, setWorld] = useState(null) 11 | 12 | const init = useCallback(async () => { 13 | // @ts-ignore 14 | await RAPIER.init() 15 | 16 | const gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); 17 | const rapierWorld = new RAPIER.World(gravity); 18 | rapierWorld.timestep = stepRate / 1000 19 | setWorld(rapierWorld) 20 | 21 | }, []) 22 | 23 | useEffect(() => { 24 | init() 25 | }, []) 26 | 27 | return { 28 | world, 29 | } 30 | 31 | } 32 | 33 | const Rapier3DApp: React.FC<{ 34 | worker: Worker, 35 | stepRate?: number, 36 | maxNumberOfSyncedBodies?: number, 37 | customBodyModifiers?: CustomBodyModifiers 38 | }> = ({ 39 | children, 40 | stepRate = DEFAULT_STEP_RATE, 41 | maxNumberOfSyncedBodies = 100, 42 | customBodyModifiers = {}, 43 | worker}) => { 44 | 45 | const {world} = useRapier3dPhysics(stepRate) 46 | 47 | useEffect(() => { 48 | customData.customBodyModifiers = customBodyModifiers 49 | }, [customBodyModifiers]) 50 | 51 | if (!world) return null 52 | 53 | return ( 54 | 55 | 56 | {children} 57 | 58 | 59 | ) 60 | } 61 | 62 | export default Rapier3DApp -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createWorkerApp } from './createWorkerApp'; 2 | import Physics from './physics/Physics'; 3 | import PlanckApp from "./physics/helpers/planckjs/PlanckApp"; 4 | import PhysicsConsumer from './physics/PhysicsConsumer'; 5 | import PhysicsConsumerSyncMeshes from './physics/PhysicsConsumerSyncMeshes'; 6 | import { usePhysicsConsumerContext } from './physics/PhysicsConsumer.context'; 7 | import { usePlanckAppContext } from './physics/helpers/planckjs/PlanckApp.context'; 8 | import { usePlanckBody, useBodyApi, useOnFixedUpdate, useSyncBody, useBodyProxy } from './physics/helpers/planckjs/hooks'; 9 | import { FixtureShape } from './physics/helpers/planckjs/types'; 10 | import CannonApp from './physics/helpers/cannon/CannonApp'; 11 | import PlanckPhysicsConsumer from './physics/helpers/planckjs/PlanckPhysicsConsumer'; 12 | import CannonPhysicsConsumer from './physics/helpers/cannon/CannonPhysicsConsumer'; 13 | import { useCannonBody } from './physics/helpers/cannon/hooks'; 14 | import Rapier3DApp from './physics/helpers/rapier3d/Rapier3DApp'; 15 | import Rapier3DPhysicsConsumer from './physics/helpers/rapier3d/Rapier3DPhysicsConsumer'; 16 | import { useRapier3DBody } from './physics/helpers/rapier3d/hooks'; 17 | import { createBody } from './physics/helpers/rapier3d/bodies'; 18 | import { AddBodyDef } from './physics/helpers/rapier3d/types'; 19 | import {SyncComponents, SyncedComponent } from './generic'; 20 | import {rawActiveKeys, useActiveKeys } from './keys'; 21 | 22 | export { 23 | createWorkerApp, 24 | Physics, 25 | PlanckApp, 26 | CannonApp, 27 | PhysicsConsumer, 28 | PhysicsConsumerSyncMeshes, 29 | usePhysicsConsumerContext, 30 | usePlanckAppContext, 31 | usePlanckBody, 32 | useBodyApi, 33 | useOnFixedUpdate, 34 | FixtureShape, 35 | PlanckPhysicsConsumer, 36 | CannonPhysicsConsumer, 37 | useCannonBody, 38 | Rapier3DApp, 39 | Rapier3DPhysicsConsumer, 40 | useRapier3DBody, 41 | createBody, 42 | AddBodyDef, 43 | useSyncBody, 44 | SyncedComponent, 45 | SyncComponents, 46 | useActiveKeys, 47 | rawActiveKeys, 48 | useBodyProxy, 49 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.4.14", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist", 8 | "src" 9 | ], 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "scripts": { 14 | "start": "tsdx watch", 15 | "build": "tsdx build", 16 | "test": "tsdx test --passWithNoTests", 17 | "lint": "tsdx lint", 18 | "prepare": "tsdx build", 19 | "size": "size-limit", 20 | "analyze": "size-limit --why" 21 | }, 22 | "peerDependencies": { 23 | "@dimforge/rapier3d": ">=0.3", 24 | "@dimforge/rapier3d-compat": ">=0.3", 25 | "@react-three/fiber": ">=6.0.5", 26 | "cannon-es": ">=0.16", 27 | "planck-js": ">=0.3", 28 | "react": ">=16", 29 | "three": ">=0.127.0", 30 | "zustand": ">=^3.3.3" 31 | }, 32 | "husky": { 33 | "hooks": { 34 | "pre-commit": "tsdx lint" 35 | } 36 | }, 37 | "prettier": { 38 | "printWidth": 80, 39 | "semi": true, 40 | "singleQuote": true, 41 | "trailingComma": "es5" 42 | }, 43 | "name": "rgg-engine", 44 | "author": "Simon Hales", 45 | "module": "dist/rgg-engine.esm.js", 46 | "size-limit": [ 47 | { 48 | "path": "dist/rgg-engine.cjs.production.min.js", 49 | "limit": "10 KB" 50 | }, 51 | { 52 | "path": "dist/rgg-engine.esm.js", 53 | "limit": "10 KB" 54 | } 55 | ], 56 | "devDependencies": { 57 | "@dimforge/rapier3d": "^0.3.1", 58 | "@dimforge/rapier3d-compat": "^0.3.1", 59 | "@react-three/fiber": "^6.0.21", 60 | "@size-limit/preset-small-lib": "^4.9.2", 61 | "@types/react": "^17.0.2", 62 | "@types/react-dom": "^17.0.1", 63 | "@types/three": "^0.127.0", 64 | "cannon-es": "^0.16.0", 65 | "husky": "^5.0.9", 66 | "planck-js": "^0.3.26", 67 | "react": "^17.0.1", 68 | "react-dom": "^17.0.1", 69 | "size-limit": "^4.9.2", 70 | "three": "^0.127.0", 71 | "tsdx": "^0.14.1", 72 | "tslib": "^2.1.0", 73 | "typescript": "^4.1.5", 74 | "zustand": "^3.3.3" 75 | }, 76 | "dependencies": { 77 | "react-nil": "^0.0.3", 78 | "shapes": "^0.4.0", 79 | "valtio": "^1.0.6" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/physics/helpers/rapier3d/bodies.ts: -------------------------------------------------------------------------------- 1 | import {World, RigidBodyDesc, ColliderDesc, RigidBody} from "@dimforge/rapier3d-compat/rapier.js"; 2 | import {AddBodyDef, ColliderDef} from "./types"; 3 | 4 | const createColliderDesc = (colliderDef: ColliderDef): ColliderDesc | null => { 5 | switch (colliderDef.type) { 6 | case "Ball": 7 | // @ts-ignore 8 | return ColliderDesc.ball(...colliderDef.args); 9 | case "Cubiod": 10 | // @ts-ignore 11 | return ColliderDesc.cuboid(...colliderDef.args); 12 | } 13 | return null 14 | } 15 | 16 | // const getCollisionGroups = (myGroups: number[], interactGroups: number[]) => { 17 | // let result = 0; 18 | // for (let g of myGroups) 19 | // { 20 | // result += (1 << g); 21 | // } 22 | // result = result << 16; 23 | // 24 | // for (let f of interactGroups) 25 | // { 26 | // result += (1 << f); 27 | // } 28 | // return result; 29 | // } 30 | 31 | const createCollider = (world: World, body: RigidBody, colliderDef: ColliderDef) => { 32 | const collider = createColliderDesc(colliderDef) 33 | if (!collider) return 34 | world.createCollider(collider, body.handle) 35 | } 36 | 37 | export const removeBody = (world: World, body: RigidBody) => { 38 | world.removeRigidBody(body) 39 | } 40 | 41 | export const createBody = (world: World, bodyDef: AddBodyDef) => { 42 | 43 | const rigidBodyDesc = new RigidBodyDesc(bodyDef.body.type); 44 | 45 | if (bodyDef.body.mass != undefined) { 46 | rigidBodyDesc.setMass(bodyDef.body.mass) 47 | } 48 | if (bodyDef.body.position) { 49 | rigidBodyDesc.setTranslation(...(bodyDef.body.position as [number, number, number])) 50 | } 51 | if (bodyDef.body.quaternion) { 52 | rigidBodyDesc.setRotation({ 53 | x: bodyDef.body.quaternion[0], 54 | y: bodyDef.body.quaternion[1], 55 | z: bodyDef.body.quaternion[2], 56 | w: bodyDef.body.quaternion[3], 57 | }) 58 | } 59 | const body = world.createRigidBody(rigidBodyDesc); 60 | 61 | bodyDef.colliders.forEach((collider) => { 62 | createCollider(world, body, collider) 63 | }) 64 | 65 | return body 66 | 67 | } -------------------------------------------------------------------------------- /src/physics/PhysicsProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useMemo, useRef} from "react"; 2 | import {Context} from "./PhysicsProvider.context"; 3 | import {OnWorldStepFn} from "./types"; 4 | import {createNewPhysicsLoopWebWorker} from "./physicsLoopWorker"; 5 | import {getNow} from "../utils/time"; 6 | import {useFixedUpdateContext} from "./PhysicsConsumer"; 7 | 8 | let now = 0 9 | let delta = 0 10 | 11 | const usePhysicsWorldStepHandler = (onWorldStep: OnWorldStepFn, stepRate: number, paused: boolean) => { 12 | 13 | const localStateRef = useRef({ 14 | lastUpdate: getNow(), 15 | }) 16 | 17 | const { 18 | updateSubscriptions 19 | } = useFixedUpdateContext() 20 | 21 | const { 22 | stepWorld 23 | } = useMemo(() => ({ 24 | stepWorld: () => { 25 | now = getNow() 26 | delta = now - localStateRef.current.lastUpdate 27 | localStateRef.current.lastUpdate = now 28 | if (paused) return 29 | onWorldStep(delta) 30 | updateSubscriptions(delta / 1000) 31 | } 32 | }), [paused, onWorldStep, updateSubscriptions]) 33 | 34 | const stepWorldRef = useRef(stepWorld) 35 | 36 | useEffect(() => { 37 | stepWorldRef.current = stepWorld 38 | }, [stepWorld]) 39 | 40 | useEffect(() => { 41 | const worker = createNewPhysicsLoopWebWorker(stepRate) 42 | // let lastStep = getNow() 43 | worker.onmessage = (event) => { 44 | if (event.data === 'step') { 45 | // now = getNow() 46 | // delta = now - lastStep 47 | // lastStep = now 48 | // console.log('delta', delta) 49 | stepWorldRef.current() 50 | } 51 | } 52 | }, [stepWorldRef]) 53 | 54 | return null 55 | } 56 | 57 | const PhysicsProvider: React.FC<{ 58 | onWorldStep: OnWorldStepFn, 59 | stepRate: number, 60 | }> = ({children, onWorldStep, stepRate}) => { 61 | 62 | const paused = false 63 | usePhysicsWorldStepHandler(onWorldStep, stepRate, paused) 64 | 65 | return ( 66 | 67 | {children} 68 | 69 | ); 70 | }; 71 | 72 | export default PhysicsProvider; -------------------------------------------------------------------------------- /src/physics/helpers/planckjs/PlanckPhysicsWorkerMessagesHandler.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useMemo} from "react" 2 | import {WorkerMessageData, WorkerMessageType} from "../../types"; 3 | import {AddBodyDef} from "./types"; 4 | import {createBody} from "./bodies"; 5 | import {World} from "planck-js"; 6 | import {usePlanckAppContext} from "./PlanckApp.context"; 7 | 8 | const PlanckPhysicsWorkerMessagesHandler: React.FC<{ 9 | world: World, 10 | worker: Worker, 11 | }> = ({ 12 | world, 13 | worker, 14 | }) => { 15 | 16 | const { 17 | addBody, 18 | bodies, 19 | } = usePlanckAppContext() 20 | 21 | const { 22 | handleAddBody, 23 | handleModifyBody, 24 | } = useMemo(() => ({ 25 | handleModifyBody: ({id, method, args}: { 26 | id: string, 27 | method: string, 28 | args: any[], 29 | }) => { 30 | const body = bodies[id] 31 | if (!body) { 32 | console.warn(`No body found matching ${id}`) 33 | return 34 | } 35 | (body as any)[method](...args) 36 | }, 37 | handleAddBody: ({id, props, synced}: { 38 | id: string, 39 | props: AddBodyDef, 40 | synced: boolean, 41 | }) => { 42 | 43 | const body = createBody(world, props.body, props.fixtures) 44 | addBody(id, body, synced) 45 | 46 | } 47 | }), []) 48 | 49 | useEffect(() => { 50 | 51 | const previousOnMessage: any = worker.onmessage 52 | 53 | worker.onmessage = (event: any) => { 54 | 55 | const message = event.data as WorkerMessageData 56 | 57 | switch (message.type) { 58 | case WorkerMessageType.ADD_BODY: 59 | handleAddBody(message.data) 60 | break; 61 | case WorkerMessageType.MODIFY_BODY: 62 | handleModifyBody(message.data) 63 | break; 64 | } 65 | 66 | if (previousOnMessage) { 67 | previousOnMessage(event) 68 | } 69 | } 70 | 71 | }, []) 72 | 73 | return null 74 | } 75 | 76 | export default PlanckPhysicsWorkerMessagesHandler -------------------------------------------------------------------------------- /src/physics/helpers/cannon/CannonPhysicsWorkerMessagesHandler.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useMemo} from "react" 2 | import {WorkerMessageData, WorkerMessageType} from "../../types"; 3 | import {usePlanckAppContext} from "../planckjs/PlanckApp.context"; 4 | import {World} from "cannon-es"; 5 | import {AddBodyDef} from "./types"; 6 | import {createBody} from "./bodies"; 7 | 8 | const CannonPhysicsWorkerMessagesHandler: React.FC<{ 9 | world: World, 10 | worker: Worker, 11 | }> = ({ 12 | world, 13 | worker, 14 | }) => { 15 | 16 | const { 17 | addBody, 18 | bodies, 19 | } = usePlanckAppContext() 20 | 21 | const { 22 | handleAddBody, 23 | handleModifyBody, 24 | } = useMemo(() => ({ 25 | handleModifyBody: ({id, method, args}: { 26 | id: string, 27 | method: string, 28 | args: any[], 29 | }) => { 30 | const body = bodies[id] 31 | if (!body) { 32 | console.warn(`No body found matching ${id}`) 33 | return 34 | } 35 | (body as any)[method](...args) 36 | }, 37 | handleAddBody: ({id, props, synced, listenForCollisions}: { 38 | id: string, 39 | props: AddBodyDef, 40 | synced: boolean, 41 | listenForCollisions?: boolean, 42 | }) => { 43 | const body = createBody(world, props) 44 | // @ts-ignore 45 | body.userData = { 46 | id, 47 | ...(props.userData ?? {}), 48 | } 49 | addBody(id, body, synced) 50 | if (listenForCollisions) { 51 | console.log('listenForCollisions') 52 | } 53 | } 54 | }), []) 55 | 56 | useEffect(() => { 57 | 58 | const previousOnMessage: any = worker.onmessage 59 | 60 | worker.onmessage = (event: any) => { 61 | 62 | const message = event.data as WorkerMessageData 63 | 64 | switch (message.type) { 65 | case WorkerMessageType.ADD_BODY: 66 | handleAddBody(message.data) 67 | break; 68 | case WorkerMessageType.MODIFY_BODY: 69 | handleModifyBody(message.data) 70 | break; 71 | } 72 | 73 | if (previousOnMessage) { 74 | previousOnMessage(event) 75 | } 76 | } 77 | 78 | }, []) 79 | 80 | return null 81 | } 82 | 83 | export default CannonPhysicsWorkerMessagesHandler -------------------------------------------------------------------------------- /src/physics/helpers/rapier3d/Rapier3DPhysicsHandler.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useMemo, useState} from "react" 2 | import {World} from "@dimforge/rapier3d-compat/rapier.js"; 3 | import {usePhysics} from "../planckjs/PlanckPhysicsHandler"; 4 | import {Context} from "../planckjs/PlanckPhysicsHandler.context"; 5 | import WorkerSubscription from "./WorkerSubscription"; 6 | import {generateBuffers} from "../cannon/buffers"; 7 | import {Context as AppContext} from "../planckjs/PlanckApp.context"; 8 | import Physics from "../../Physics"; 9 | import {applyBufferData} from "./updates"; 10 | import Rapier3DPhysicsWorkerMessagesHandler from "./Rapier3DPhysicsWorkerMessagesHandler"; 11 | import {removeBody} from "./bodies"; 12 | 13 | const Rapier3DPhysicsHandler: React.FC<{ 14 | world: World, 15 | worker: Worker, 16 | stepRate: number, 17 | maxNumberOfSyncedBodies: number, 18 | }> = ({ 19 | children, 20 | world, 21 | stepRate, 22 | worker, 23 | maxNumberOfSyncedBodies 24 | }) => { 25 | 26 | const customRemoveBody = useCallback((body: any) => { 27 | removeBody(world, body) 28 | }, []) 29 | 30 | const { 31 | subscribeToPhysicsUpdates, 32 | getPendingSyncedBodiesIteration, 33 | syncedBodies, 34 | syncedBodiesOrder, 35 | addSyncedBody, 36 | removeSyncedBody, 37 | addBody, 38 | bodies, 39 | onUpdate, 40 | } = usePhysics(customRemoveBody) 41 | 42 | const [paused, setPaused] = useState(false) 43 | 44 | const { 45 | onWorldStep 46 | } = useMemo(() => ({ 47 | onWorldStep: () => { 48 | if (paused) return 49 | world.step() 50 | onUpdate() 51 | } 52 | }), [paused]) 53 | 54 | return ( 55 | 61 | 63 | 70 | 71 | 72 | {children} 73 | 74 | 75 | 76 | ) 77 | } 78 | 79 | export default Rapier3DPhysicsHandler -------------------------------------------------------------------------------- /src/physics/helpers/cannon/CannonPhysicsHandler.tsx: -------------------------------------------------------------------------------- 1 | import { World, Body } from "cannon-es" 2 | import React, {useCallback, useMemo, useRef} from "react" 3 | import {getNow} from "../../../utils/time"; 4 | import Physics from "../../Physics"; 5 | import WorkerSubscription from "../rapier3d/WorkerSubscription"; 6 | import {usePhysics} from "../planckjs/PlanckPhysicsHandler"; 7 | import {Context} from "../planckjs/PlanckPhysicsHandler.context"; 8 | import {Context as AppContext} from "../planckjs/PlanckApp.context"; 9 | import CannonPhysicsWorkerMessagesHandler from "./CannonPhysicsWorkerMessagesHandler"; 10 | import {applyBufferData} from "./updates"; 11 | import {generateBuffers} from "./buffers"; 12 | 13 | const CannonPhysicsHandler: React.FC<{ 14 | world: World, 15 | worker: Worker, 16 | stepRate: number, 17 | maxNumberOfSyncedBodies: number, 18 | }> = ({children, world, stepRate, worker, maxNumberOfSyncedBodies}) => { 19 | 20 | const removeBody = useCallback((body: Body) => { 21 | world.removeBody(body) 22 | }, []) 23 | 24 | const { 25 | subscribeToPhysicsUpdates, 26 | getPendingSyncedBodiesIteration, 27 | syncedBodies, 28 | syncedBodiesOrder, 29 | addSyncedBody, 30 | removeSyncedBody, 31 | addBody, 32 | bodies, 33 | onUpdate, 34 | } = usePhysics(removeBody) 35 | 36 | const localStateRef = useRef({ 37 | lastUpdate: getNow() 38 | }) 39 | 40 | const { 41 | onWorldStep 42 | } = useMemo(() => ({ 43 | onWorldStep: () => { 44 | const now = getNow() 45 | const delta = (now - localStateRef.current.lastUpdate) / 1000; 46 | localStateRef.current.lastUpdate = now; 47 | world.step(stepRate / 1000, delta) 48 | onUpdate() 49 | } 50 | }), []) 51 | 52 | return ( 53 | 59 | 61 | 68 | 69 | 70 | {children} 71 | 72 | 73 | 74 | ) 75 | } 76 | 77 | export default CannonPhysicsHandler -------------------------------------------------------------------------------- /src/keys.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect} from "react"; 2 | import create from "zustand"; 3 | 4 | export const useTransferKeyEvents = (worker: Worker) => { 5 | 6 | useEffect(() => { 7 | const onKeyDown = (event: KeyboardEvent) => { 8 | if (event.repeat) return 9 | const { 10 | code, 11 | key, 12 | keyCode 13 | } = event 14 | worker.postMessage({ 15 | type: 'KEYDOWN', 16 | data: { 17 | code, 18 | key, 19 | keyCode, 20 | } 21 | }) 22 | } 23 | const onKeyUp = (event: KeyboardEvent) => { 24 | const { 25 | code, 26 | key, 27 | keyCode 28 | } = event 29 | worker.postMessage({ 30 | type: 'KEYUP', 31 | data: { 32 | code, 33 | key, 34 | keyCode, 35 | } 36 | }) 37 | } 38 | window.addEventListener('keydown', onKeyDown) 39 | window.addEventListener('keyup', onKeyUp) 40 | 41 | return () => { 42 | window.removeEventListener('keydown', onKeyDown) 43 | window.removeEventListener('keyup', onKeyUp) 44 | } 45 | 46 | }, []) 47 | 48 | } 49 | 50 | export const useActiveKeys = create<{ 51 | activeKeys: { 52 | [key: number]: boolean, 53 | } 54 | }>(() => ({ 55 | activeKeys: {}, 56 | })) 57 | 58 | export const rawActiveKeys: { 59 | [key: number]: boolean, 60 | } = {} 61 | 62 | const setActiveKey = (code: number, active: boolean) => { 63 | rawActiveKeys[code] = active 64 | return useActiveKeys.setState(state => ({ 65 | activeKeys: { 66 | ...state.activeKeys, 67 | [code]: active, 68 | } 69 | })) 70 | } 71 | 72 | export const useHandleKeyEvents = (worker: Worker) => { 73 | 74 | useEffect(() => { 75 | 76 | const previousOnMessage = worker.onmessage as any 77 | 78 | worker.onmessage = (event: any) => { 79 | 80 | if (previousOnMessage) { 81 | previousOnMessage(event) 82 | } 83 | 84 | const data = event.data 85 | 86 | switch (data.type) { 87 | case 'KEYUP': 88 | setActiveKey(data.data.keyCode, false) 89 | break; 90 | case 'KEYDOWN': 91 | setActiveKey(data.data.keyCode, true) 92 | break; 93 | } 94 | 95 | } 96 | 97 | return () => { 98 | worker.onmessage = previousOnMessage 99 | } 100 | 101 | }, [worker]) 102 | 103 | } -------------------------------------------------------------------------------- /example/src/Game.tsx: -------------------------------------------------------------------------------- 1 | import {Box, Sphere } from "@react-three/drei" 2 | import React, {useEffect, useRef} from "react" 3 | import { 4 | FixtureShape, 5 | usePlanckBody, 6 | useBodyApi, 7 | useOnFixedUpdate, 8 | usePhysicsConsumerContext, 9 | useCannonBody 10 | } from "../../src"; 11 | import {Vec2} from "planck-js"; 12 | import { Vec3, Quaternion, Body } from "cannon-es"; 13 | 14 | // useBody(() => ({ 15 | // body: { 16 | // type: 'dynamic', 17 | // allowSleep: false, 18 | // fixedRotation: true, 19 | // position: Vec2(2, 2), 20 | // linearDamping: 40, 21 | // }, 22 | // fixtures: [{ 23 | // shape: FixtureShape.Circle, 24 | // args: [], 25 | // fixtureOptions: { 26 | // density: 10, 27 | // } 28 | // }], 29 | // }), { 30 | // id: 'player', 31 | // }) 32 | 33 | const Game: React.FC = () => { 34 | 35 | const [sphereRef] = useCannonBody(() => ({ 36 | body: { 37 | mass: 1, 38 | position: new Vec3(0, 20, -5) 39 | }, 40 | shapes: [{ 41 | type: 'Sphere', 42 | args: [1], 43 | }], 44 | })) 45 | 46 | const [sphere2Ref] = useCannonBody(() => ({ 47 | body: { 48 | mass: 1, 49 | position: new Vec3(0, 10, -5) 50 | }, 51 | shapes: [{ 52 | type: 'Sphere', 53 | args: [1], 54 | }], 55 | })) 56 | 57 | const [staticBoxRef] = useCannonBody(() => ({ 58 | body: { 59 | position: new Vec3(0, -5, -5), 60 | quaternion: new Quaternion().setFromEuler(0, 0, 0.05), 61 | type: Body.STATIC, 62 | }, 63 | shapes: [{ 64 | type: 'Box', 65 | args: [2, 2, 2], 66 | }], 67 | }), { 68 | listenForCollisions: true, 69 | }) 70 | 71 | const [boxRef] = useCannonBody(() => ({ 72 | body: { 73 | mass: 1, 74 | position: new Vec3(0, 5, -5) 75 | }, 76 | shapes: [{ 77 | type: 'Box', 78 | args: [1, 1, 1], 79 | }], 80 | })) 81 | 82 | const api = useBodyApi('player') 83 | 84 | useOnFixedUpdate((delta) => { 85 | // api('applyForce', [new Vec3(-0.25, -0.25, 0.25)]) 86 | }) 87 | 88 | // console.log('boxRef', boxRef, boxId) 89 | 90 | return ( 91 | <> 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | ) 106 | } 107 | 108 | export default Game -------------------------------------------------------------------------------- /src/physics/helpers/rapier3d/Rapier3DPhysicsWorkerMessagesHandler.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useMemo, useRef} from "react" 2 | import {World} from "@dimforge/rapier3d-compat/rapier.js"; 3 | import {usePlanckAppContext} from "../planckjs/PlanckApp.context"; 4 | import {WorkerMessageData, WorkerMessageType} from "../../types"; 5 | import {AddBodyDef} from "./types"; 6 | import {createBody} from "./bodies"; 7 | import {getCustomBodyModifier} from "./custom"; 8 | 9 | const Rapier3DPhysicsWorkerMessagesHandler: React.FC<{ 10 | world: World, 11 | worker: Worker, 12 | }> = ({ 13 | world, 14 | worker, 15 | }) => { 16 | 17 | const { 18 | addBody, 19 | bodies, 20 | } = usePlanckAppContext() 21 | 22 | const localStateRef = useRef<{ 23 | removeCallbacks: { 24 | [key: string]: () => void, 25 | } 26 | }>({ 27 | removeCallbacks: {}, 28 | }) 29 | 30 | const { 31 | handleAddBody, 32 | handleModifyBody, 33 | handleRemoveBody, 34 | } = useMemo(() => ({ 35 | handleModifyBody: ({id, method, args}: { 36 | id: string, 37 | method: string, 38 | args: any[], 39 | }) => { 40 | const body = bodies[id] 41 | if (!body) { 42 | console.warn(`No body found matching ${id}`) 43 | return 44 | } 45 | (body as any)[method](...args) 46 | }, 47 | handleAddBody: ({id, props, synced}: { 48 | id: string, 49 | props: AddBodyDef, 50 | synced: boolean, 51 | listenForCollisions?: boolean, 52 | }) => { 53 | const body = createBody(world, props) 54 | const customModifier = getCustomBodyModifier(props) 55 | if (customModifier) { 56 | customModifier(body) 57 | } 58 | localStateRef.current.removeCallbacks[id] = addBody(id, body, synced) 59 | }, 60 | handleRemoveBody: ({id}: { 61 | id: string 62 | }) => { 63 | const body = bodies[id] 64 | world.removeRigidBody(body) 65 | if (localStateRef.current.removeCallbacks[id]) { 66 | localStateRef.current.removeCallbacks[id]() 67 | } 68 | } 69 | }), []) 70 | 71 | useEffect(() => { 72 | 73 | const previousOnMessage: any = worker.onmessage 74 | 75 | worker.onmessage = (event: any) => { 76 | 77 | const message = event.data as WorkerMessageData 78 | 79 | switch (message.type) { 80 | case WorkerMessageType.ADD_BODY: 81 | handleAddBody(message.data) 82 | break; 83 | case WorkerMessageType.REMOVE_BODY: 84 | handleRemoveBody(message.data) 85 | break; 86 | case WorkerMessageType.MODIFY_BODY: 87 | handleModifyBody(message.data) 88 | break; 89 | } 90 | 91 | if (previousOnMessage) { 92 | previousOnMessage(event) 93 | } 94 | } 95 | 96 | }, []) 97 | 98 | return null 99 | } 100 | 101 | export default Rapier3DPhysicsWorkerMessagesHandler -------------------------------------------------------------------------------- /src/physics/helpers/planckjs/hooks.ts: -------------------------------------------------------------------------------- 1 | import {AddBodyDef} from "./types"; 2 | import {MutableRefObject, useCallback, useEffect, useLayoutEffect, useRef, useState} from "react"; 3 | import {generateUUID} from "../../../utils/ids"; 4 | import {usePhysicsConsumerContext} from "../../PhysicsConsumer.context"; 5 | import {WorkerMessageType} from "../../types"; 6 | import {Object3D} from "three"; 7 | import {usePhysicsConsumerHelpers} from "./PhysicsConsumerHelpers"; 8 | import {useFixedUpdateContext} from "../../PhysicsConsumer"; 9 | 10 | export const useOnFixedUpdate = (callback: (delta: number) => void) => { 11 | const { 12 | subscribeToOnPhysicsUpdate 13 | } = useFixedUpdateContext() 14 | 15 | const callbackRef = useRef(callback) 16 | 17 | useEffect(() => { 18 | callbackRef.current = callback 19 | }, [callback]) 20 | 21 | useEffect(() => { 22 | return subscribeToOnPhysicsUpdate(callbackRef) 23 | }, []) 24 | 25 | } 26 | 27 | export const useBodyApi = (id: string) => { 28 | const {sendMessage} = usePhysicsConsumerContext() 29 | 30 | return useCallback((method: string, 31 | args: any[],) => { 32 | sendMessage({ 33 | type: WorkerMessageType.MODIFY_BODY, 34 | data: { 35 | id, 36 | method, 37 | args, 38 | } 39 | }) 40 | }, []) 41 | 42 | } 43 | 44 | export const useBodyProxy = (id: string) => { 45 | const {bodiesData} = usePhysicsConsumerContext(); 46 | return bodiesData[id] || {}; 47 | } 48 | 49 | export type Options = { 50 | id?: string, 51 | synced?: boolean, 52 | listenForCollisions?: boolean, 53 | ref?: MutableRefObject, 54 | } 55 | 56 | export const useSyncBody = (id: string, ref: MutableRefObject | undefined, options?: { 57 | applyRotation?: boolean, 58 | }) => { 59 | const { 60 | applyRotation = true 61 | } = options ?? {} 62 | const { 63 | syncBody 64 | } = usePhysicsConsumerContext() 65 | // @ts-ignore 66 | useLayoutEffect(() => { 67 | if (!ref) return 68 | if (!ref.current) { 69 | ref.current = new Object3D() 70 | } 71 | return syncBody(id, ref as MutableRefObject, applyRotation) 72 | }, [ref]) 73 | } 74 | 75 | export const useBody = (propsFn: () => any, options: Partial = {}, addToMessage?: (props: any, options: Partial) => any): [ 76 | MutableRefObject, 77 | string 78 | ] => { 79 | 80 | const {sendMessage} = usePhysicsConsumerContext() 81 | const [id] = useState(() => options.id ?? generateUUID()) 82 | const localRef = useRef(null as unknown as Object3D) 83 | 84 | const { 85 | prepareObject 86 | } = usePhysicsConsumerHelpers() || {} 87 | 88 | const [ref] = useState(() => options.ref || localRef) 89 | 90 | useSyncBody(id, ref) 91 | 92 | useLayoutEffect(() => { 93 | 94 | const props = propsFn() 95 | 96 | const object = ref.current 97 | 98 | if (prepareObject) { 99 | prepareObject(object, props) 100 | } 101 | 102 | sendMessage({ 103 | type: WorkerMessageType.ADD_BODY, 104 | data: { 105 | id, 106 | props, 107 | synced: options.synced ?? true, 108 | ...(addToMessage ? addToMessage(props, options) : {}) 109 | }, 110 | }) 111 | 112 | return () => { 113 | sendMessage({ 114 | type: WorkerMessageType.REMOVE_BODY, 115 | data: { 116 | id, 117 | }, 118 | }) 119 | } 120 | 121 | }, []) 122 | 123 | return [ref, id] 124 | 125 | } 126 | 127 | export const usePlanckBody = (propsFn: () => AddBodyDef, options: Partial = {}): [ 128 | MutableRefObject, 129 | string 130 | ] => { 131 | return useBody(propsFn, options) 132 | } -------------------------------------------------------------------------------- /src/physics/helpers/cannon/updates.ts: -------------------------------------------------------------------------------- 1 | import {Body} from "cannon-es"; 2 | import {BodyData} from "../../types"; 3 | import {Object3D, Quaternion} from "three"; 4 | import {getNow} from "../../../utils/time"; 5 | import {lerp} from "../../../utils/numbers"; 6 | import { AddBodyDef } from "./types"; 7 | import { Buffers } from "../planckjs/types"; 8 | 9 | export const applyBufferData = ( 10 | buffers: Buffers, 11 | syncedBodies: { 12 | [key: string]: Body, 13 | }, syncedBodiesOrder: string[]) => { 14 | 15 | const { 16 | positions, 17 | angles, 18 | } = buffers 19 | 20 | syncedBodiesOrder.forEach((id, index) => { 21 | const body = syncedBodies[id] 22 | if (!body) return; 23 | const position = body.position; 24 | const quaternion = body.quaternion; 25 | positions[3 * index + 0] = position.x 26 | positions[3 * index + 1] = position.y 27 | positions[3 * index + 2] = position.z 28 | angles[4 * index + 0] = quaternion.x 29 | angles[4 * index + 1] = quaternion.y 30 | angles[4 * index + 2] = quaternion.z 31 | angles[4 * index + 3] = quaternion.w 32 | }) 33 | 34 | } 35 | 36 | const quat = new Quaternion() 37 | 38 | export const lerpBody = (body: BodyData, object: Object3D, stepRate: number) => { 39 | const { 40 | position, 41 | angle, 42 | lastUpdate, 43 | previous, 44 | } = body 45 | 46 | if (!position || !angle) return 47 | 48 | if (!previous.position || !previous.angle) { 49 | object.position.set(...position as [number, number, number]) 50 | object.quaternion.set(...angle as [number, number, number, number]) 51 | return 52 | } 53 | 54 | const now = getNow() 55 | 56 | const nextExpectedUpdate = lastUpdate + stepRate + 1 57 | 58 | const min = lastUpdate 59 | const max = nextExpectedUpdate 60 | 61 | let normalised = ((now - min) / (max - min)) 62 | 63 | normalised = normalised < 0 ? 0 : normalised > 1 ? 1 : normalised 64 | 65 | const physicsRemainingRatio = normalised 66 | 67 | object.position.x = lerp( 68 | previous.position[0], 69 | position[0], 70 | physicsRemainingRatio 71 | ); 72 | 73 | object.position.y = lerp( 74 | previous.position[1], 75 | position[1], 76 | physicsRemainingRatio 77 | ); 78 | 79 | object.position.z = lerp( 80 | previous.position[2] as number, 81 | position[2] as number, 82 | physicsRemainingRatio 83 | ); 84 | 85 | object.quaternion.fromArray(previous.angle as [number, number, number, number]) 86 | quat.fromArray(angle as [number, number, number, number]) 87 | object.quaternion.slerp(quat, physicsRemainingRatio) 88 | } 89 | 90 | const getPositionAndAngle = ( 91 | buffers: Buffers, 92 | index: number 93 | ): { 94 | position: [number, number, number]; 95 | angle: [number, number, number, number]; 96 | } | null => { 97 | if (index !== undefined && buffers.positions.length && buffers.angles.length) { 98 | const start = index * 3; 99 | const position = (buffers.positions.slice(start, start + 3) as unknown) as [ 100 | number, 101 | number, 102 | number, 103 | ]; 104 | const angleStart = index * 4; 105 | const angle = (buffers.angles.slice(angleStart, angleStart + 4) as unknown) as [ 106 | number, 107 | number, 108 | number, 109 | number, 110 | ]; 111 | return { 112 | position, 113 | angle, 114 | }; 115 | } else { 116 | return null; 117 | } 118 | }; 119 | 120 | export const updateBodyData = (bodyData: BodyData, positions: Float32Array, angles: Float32Array) => { 121 | bodyData.previous.position = bodyData.position 122 | bodyData.previous.angle = bodyData.angle 123 | const update = getPositionAndAngle({ 124 | positions, 125 | angles, 126 | }, bodyData.index) 127 | if (update) { 128 | bodyData.position = update.position 129 | bodyData.angle = update.angle 130 | } 131 | } 132 | 133 | export const prepareObject = (object: Object3D, props: AddBodyDef) => { 134 | if (props.body.position) { 135 | object.position.set(...props.body.position.toArray()) 136 | } 137 | if (props.body.quaternion) { 138 | object.quaternion.set(...props.body.quaternion.toArray()) 139 | } 140 | } -------------------------------------------------------------------------------- /src/physics/helpers/rapier3d/WorkerSubscription.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useEffect, useRef, useState} from "react"; 2 | import {usePlanckPhysicsHandlerContext} from "../planckjs/PlanckPhysicsHandler.context"; 3 | import {getNow} from "../../../utils/time"; 4 | import {WorkerMessageData, WorkerMessageType} from "../../types"; 5 | import {ApplyBufferDataFn} from "./updates"; 6 | import {Buffers} from "../planckjs/types"; 7 | 8 | const WorkerSubscription: React.FC<{ 9 | worker: Worker, 10 | subscribe: (callback: () => void) => () => void, 11 | applyBufferData: ApplyBufferDataFn, 12 | generateBuffers: (maxNumberOfSyncedBodies: number) => Buffers, 13 | setPaused?: (paused: boolean) => void, 14 | }> = ({worker, subscribe, applyBufferData, generateBuffers, setPaused}) => { 15 | 16 | const { 17 | getPendingSyncedBodiesIteration, 18 | syncedBodies, 19 | syncedBodiesOrder, 20 | maxNumberOfSyncedBodies, 21 | } = usePlanckPhysicsHandlerContext() 22 | 23 | const [buffers] = useState(() => generateBuffers(maxNumberOfSyncedBodies)) 24 | const localStateRef = useRef({ 25 | lastUpdate: -1, 26 | bodiesIteration: -1, 27 | }) 28 | const [buffersAvailable, setBuffersAvailable] = useState(false) 29 | const [updateCount, setUpdateCount] = useState(0) 30 | 31 | const updateWorker = useCallback((update: number) => { 32 | localStateRef.current.lastUpdate = update 33 | setBuffersAvailable(false) 34 | 35 | const bodiesIteration = getPendingSyncedBodiesIteration() 36 | const shouldSyncBodies = bodiesIteration !== localStateRef.current.bodiesIteration 37 | 38 | applyBufferData(buffers, syncedBodies, syncedBodiesOrder) 39 | 40 | const { 41 | positions, 42 | angles, 43 | } = buffers 44 | 45 | const message: any = { 46 | type: WorkerMessageType.PHYSICS_UPDATE, 47 | updateTime: getNow(), 48 | positions: positions, 49 | angles: angles, 50 | } 51 | 52 | if (shouldSyncBodies) { 53 | message.bodies = syncedBodiesOrder 54 | localStateRef.current.bodiesIteration = bodiesIteration 55 | } 56 | 57 | worker.postMessage(message, [positions.buffer, angles.buffer]) 58 | 59 | // process local fixed updates 60 | 61 | }, [getPendingSyncedBodiesIteration, syncedBodies, syncedBodiesOrder]) 62 | 63 | const updateWorkerRef = useRef(updateWorker) 64 | 65 | useEffect(() => { 66 | updateWorkerRef.current = updateWorker 67 | }, [updateWorker]) 68 | 69 | useEffect(() => { 70 | if (!buffersAvailable) return 71 | if (updateCount <= localStateRef.current.lastUpdate) return 72 | updateWorkerRef.current(updateCount) 73 | }, [updateCount, buffersAvailable]) 74 | 75 | const onUpdate = useCallback(() => { 76 | setUpdateCount(state => state + 1) 77 | }, []) 78 | 79 | const onUpdateRef = useRef(onUpdate) 80 | 81 | useEffect(() => { 82 | onUpdateRef.current = onUpdate 83 | }, [onUpdate]) 84 | 85 | useEffect(() => { 86 | 87 | return subscribe(() => onUpdateRef.current()) 88 | 89 | }, []) 90 | 91 | useEffect(() => { 92 | const previousOnMessage: any = worker.onmessage 93 | 94 | worker.onmessage = (event: any) => { 95 | 96 | const message = event.data as WorkerMessageData 97 | 98 | switch (message.type) { 99 | case WorkerMessageType.PHYSICS_PROCESSED: 100 | buffers.positions = message.positions 101 | buffers.angles = message.angles 102 | setBuffersAvailable(true) 103 | break; 104 | case WorkerMessageType.PHYSICS_SET_PAUSED: 105 | if (setPaused) { 106 | setPaused(message.paused ?? false) 107 | } 108 | break; 109 | case WorkerMessageType.PHYSICS_READY: 110 | if (setPaused) { 111 | setPaused(message.paused ?? false) 112 | } 113 | setBuffersAvailable(true) 114 | worker.postMessage({ 115 | type: WorkerMessageType.PHYSICS_ACKNOWLEDGED, 116 | }) 117 | break; 118 | } 119 | 120 | if (previousOnMessage) { 121 | previousOnMessage(event) 122 | } 123 | } 124 | 125 | }, []) 126 | 127 | return null 128 | } 129 | 130 | export default WorkerSubscription; -------------------------------------------------------------------------------- /src/physics/helpers/planckjs/updates.ts: -------------------------------------------------------------------------------- 1 | import {AddBodyDef, ExtBuffers} from "./types"; 2 | import {Body} from "planck-js"; 3 | // import {getNow} from "../../../utils/time"; 4 | import {lerp} from "../../../utils/numbers"; 5 | import {Object3D} from "three"; 6 | import {BodyData} from "../../types"; 7 | 8 | export type ApplyBufferDataFn = ( 9 | buffers: ExtBuffers, 10 | syncedBodies: { 11 | [key: string]: any, 12 | }, 13 | syncedBodiesOrder: string[] 14 | ) => void 15 | 16 | export const lerpBody = (body: BodyData, object: Object3D, delta: number /* stepRate: number */) => { 17 | 18 | // const maxLerp: number = 1; 19 | 20 | const { 21 | position, 22 | angle, 23 | // lastUpdate, 24 | previous, 25 | applyRotation = true, 26 | } = body 27 | 28 | if (!position || angle == undefined) { 29 | return 30 | } 31 | 32 | if (!previous.position || previous.angle == undefined) { 33 | object.position.x = position[0] 34 | object.position.z = position[1] 35 | if (applyRotation) { 36 | object.rotation.y = angle as number 37 | } 38 | return 39 | } 40 | 41 | // const now = getNow() 42 | 43 | // const step = stepRate + 2 44 | // const nextExpectedUpdate = lastUpdate + step 45 | 46 | // const timeRemaining = nextExpectedUpdate - now 47 | // let physicsRemainingRatio = timeRemaining / step 48 | 49 | // if (physicsRemainingRatio < 0) { 50 | // physicsRemainingRatio = 0 51 | // } 52 | 53 | // physicsRemainingRatio = 1 - physicsRemainingRatio 54 | // const scalar = 1 / maxLerp 55 | // physicsRemainingRatio *= scalar 56 | 57 | object.position.x = lerp( 58 | object.position.x, 59 | position[0], 60 | 1 - 0.0000000000000000005 ** delta, 61 | ); 62 | 63 | object.position.z = lerp( 64 | object.position.z, 65 | position[1], 66 | 1 - 0.0000000000000000005 ** delta, 67 | ); 68 | 69 | if (applyRotation) { 70 | object.rotation.y = angle as number; // todo - lerp 71 | } 72 | } 73 | 74 | const getPositionAndAngle = ( 75 | buffers: ExtBuffers, 76 | index: number 77 | ): { 78 | position: [number, number]; 79 | angle: number; 80 | velocity: [number, number]; 81 | } | null => { 82 | if (index !== undefined && buffers.positions.length && buffers.angles.length) { 83 | const start = index * 2; 84 | const position = (buffers.positions.slice(start, start + 2) as unknown) as [ 85 | number, 86 | number, 87 | ]; 88 | const velocityStart = index * 2; 89 | const velocity = (buffers.velocities.slice(velocityStart, velocityStart + 2) as unknown) as [ 90 | number, 91 | number, 92 | ]; 93 | return { 94 | position, 95 | angle: buffers.angles[index], 96 | velocity, 97 | }; 98 | } else { 99 | return null; 100 | } 101 | }; 102 | 103 | export const updateBodyData = (bodyData: BodyData, positions: Float32Array, angles: Float32Array, velocities: Float32Array ) => { 104 | bodyData.previous.position = bodyData.position 105 | bodyData.previous.angle = bodyData.angle 106 | const update = getPositionAndAngle({ 107 | positions, 108 | angles, 109 | velocities, 110 | }, bodyData.index) 111 | if (update) { 112 | bodyData.position = update.position 113 | bodyData.angle = update.angle 114 | bodyData.velocity = update.velocity 115 | } 116 | } 117 | 118 | export const applyBufferData = ( 119 | buffers: ExtBuffers, 120 | syncedBodies: { 121 | [key: string]: Body, 122 | }, syncedBodiesOrder: string[]) => { 123 | 124 | const { 125 | positions, 126 | angles, 127 | velocities, 128 | } = buffers 129 | 130 | syncedBodiesOrder.forEach((id, index) => { 131 | const body = syncedBodies[id] 132 | if (!body) return; 133 | const position = body.getPosition(); 134 | const angle = body.getAngle(); 135 | const velocity = body.getLinearVelocity(); 136 | positions[2 * index + 0] = position.x; 137 | positions[2 * index + 1] = position.y; 138 | angles[index] = angle; 139 | velocities[2 * index] = velocity.x; 140 | velocities[2 * index + 1] = velocity.y; 141 | }) 142 | 143 | } 144 | 145 | export const prepareObject = (object: Object3D, props: AddBodyDef) => { 146 | if (props.body.position) { 147 | object.position.x = props.body.position.x 148 | object.position.z = props.body.position.y 149 | } 150 | if (props.body.angle) { 151 | object.rotation.y = props.body.angle 152 | } 153 | } -------------------------------------------------------------------------------- /src/physics/helpers/planckjs/WorkerSubscription.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useEffect, useRef, useState} from "react"; 2 | import {usePlanckPhysicsHandlerContext} from "./PlanckPhysicsHandler.context"; 3 | import {getNow} from "../../../utils/time"; 4 | import {WorkerMessageData, WorkerMessageType} from "../../types"; 5 | import {ApplyBufferDataFn} from "./updates"; 6 | import {ExtBuffers} from "./types"; 7 | 8 | const WorkerSubscription: React.FC<{ 9 | worker: Worker, 10 | subscribe: (callback: () => void) => () => void, 11 | applyBufferData: ApplyBufferDataFn, 12 | generateBuffers: (maxNumberOfSyncedBodies: number) => ExtBuffers, 13 | setPaused?: (paused: boolean) => void, 14 | }> = ({worker, subscribe, applyBufferData, generateBuffers, setPaused}) => { 15 | 16 | const { 17 | getPendingSyncedBodiesIteration, 18 | syncedBodies, 19 | syncedBodiesOrder, 20 | maxNumberOfSyncedBodies, 21 | } = usePlanckPhysicsHandlerContext() 22 | 23 | const [buffers] = useState(() => generateBuffers(maxNumberOfSyncedBodies)) 24 | const localStateRef = useRef({ 25 | lastUpdate: -1, 26 | bodiesIteration: -1, 27 | }) 28 | const [buffersAvailable, setBuffersAvailable] = useState(false) 29 | const [updateCount, setUpdateCount] = useState(0) 30 | 31 | const updateWorker = useCallback((update: number) => { 32 | localStateRef.current.lastUpdate = update 33 | setBuffersAvailable(false) 34 | 35 | const bodiesIteration = getPendingSyncedBodiesIteration() 36 | const shouldSyncBodies = bodiesIteration !== localStateRef.current.bodiesIteration 37 | 38 | applyBufferData(buffers, syncedBodies, syncedBodiesOrder) 39 | 40 | const { 41 | positions, 42 | angles, 43 | velocities, 44 | } = buffers 45 | 46 | const message: any = { 47 | type: WorkerMessageType.PHYSICS_UPDATE, 48 | updateTime: getNow(), 49 | positions: positions, 50 | angles: angles, 51 | velocities: velocities, 52 | } 53 | 54 | if (shouldSyncBodies) { 55 | message.bodies = syncedBodiesOrder 56 | localStateRef.current.bodiesIteration = bodiesIteration 57 | } 58 | 59 | worker.postMessage(message, [positions.buffer, angles.buffer, velocities.buffer]) 60 | 61 | // process local fixed updates 62 | 63 | }, [getPendingSyncedBodiesIteration, syncedBodies, syncedBodiesOrder]) 64 | 65 | const updateWorkerRef = useRef(updateWorker) 66 | 67 | useEffect(() => { 68 | updateWorkerRef.current = updateWorker 69 | }, [updateWorker]) 70 | 71 | useEffect(() => { 72 | if (!buffersAvailable) return 73 | if (updateCount <= localStateRef.current.lastUpdate) return 74 | updateWorkerRef.current(updateCount) 75 | }, [updateCount, buffersAvailable]) 76 | 77 | const onUpdate = useCallback(() => { 78 | setUpdateCount(state => state + 1) 79 | }, []) 80 | 81 | const onUpdateRef = useRef(onUpdate) 82 | 83 | useEffect(() => { 84 | onUpdateRef.current = onUpdate 85 | }, [onUpdate]) 86 | 87 | useEffect(() => { 88 | 89 | return subscribe(() => onUpdateRef.current()) 90 | 91 | }, []) 92 | 93 | useEffect(() => { 94 | const previousOnMessage: any = worker.onmessage 95 | 96 | worker.onmessage = (event: any) => { 97 | 98 | const message = event.data as WorkerMessageData 99 | 100 | switch (message.type) { 101 | case WorkerMessageType.PHYSICS_PROCESSED: 102 | buffers.positions = message.positions 103 | buffers.angles = message.angles 104 | buffers.velocities = message.velocities 105 | setBuffersAvailable(true) 106 | break; 107 | case WorkerMessageType.PHYSICS_SET_PAUSED: 108 | if (setPaused) { 109 | setPaused(message.paused ?? false) 110 | } 111 | break; 112 | case WorkerMessageType.PHYSICS_READY: 113 | if (setPaused) { 114 | setPaused(message.paused ?? false) 115 | } 116 | setBuffersAvailable(true) 117 | worker.postMessage({ 118 | type: WorkerMessageType.PHYSICS_ACKNOWLEDGED, 119 | }) 120 | break; 121 | } 122 | 123 | if (previousOnMessage) { 124 | previousOnMessage(event) 125 | } 126 | } 127 | 128 | }, []) 129 | 130 | return null 131 | } 132 | 133 | export default WorkerSubscription; -------------------------------------------------------------------------------- /example/src/Rapier3DGame.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {usePlanckBody, useRapier3DBody} from "../../src"; 3 | import {BodyStatus} from "@dimforge/rapier3d-compat"; 4 | import {Box, Plane, Sphere, Text} from "@react-three/drei"; 5 | import {Euler, Quaternion} from "three"; 6 | 7 | const Rapier3DGame: React.FC = () => { 8 | 9 | usePlanckBody(() => ({ 10 | body: { 11 | type: "static", 12 | }, 13 | fixtures: [], 14 | })) 15 | 16 | return null 17 | 18 | // const [ref] = useRapier3DBody(() => ({ 19 | // body: { 20 | // type: BodyStatus.Dynamic, 21 | // position: [0, 100, -5], 22 | // mass: 1, 23 | // }, 24 | // colliders: [{ 25 | // type: 'Ball', 26 | // args: [1], 27 | // }] 28 | // })) 29 | // 30 | // const [ref2] = useRapier3DBody(() => ({ 31 | // body: { 32 | // type: BodyStatus.Dynamic, 33 | // position: [0, 110, -5], 34 | // mass: 1, 35 | // }, 36 | // colliders: [{ 37 | // type: 'Ball', 38 | // args: [1], 39 | // }] 40 | // })) 41 | // 42 | // const [ref3] = useRapier3DBody(() => ({ 43 | // body: { 44 | // type: BodyStatus.Dynamic, 45 | // position: [0, 120, -5], 46 | // mass: 1, 47 | // }, 48 | // colliders: [{ 49 | // type: 'Ball', 50 | // args: [1], 51 | // }] 52 | // })) 53 | // 54 | // const [staticBoxRef] = useRapier3DBody(() => ({ 55 | // body: { 56 | // type: BodyStatus.Static, 57 | // position: [0, -5, -5], 58 | // // quaternion: new Quaternion().setFromEuler(Math.PI / 4, Math.PI / 4, Math.PI / 4).toArray(), 59 | // }, 60 | // colliders: [{ 61 | // type: 'Cubiod', 62 | // args: [2, 2, 2], 63 | // }] 64 | // })) 65 | // 66 | // const [rRef] = useRapier3DBody(() => ({ 67 | // body: { 68 | // type: BodyStatus.Dynamic, 69 | // position: [0, 20, -5], 70 | // }, 71 | // colliders: [{ 72 | // type: 'Cubiod', 73 | // args: [1, 1, 1], 74 | // }] 75 | // })) 76 | // 77 | // const [gRef] = useRapier3DBody(() => ({ 78 | // body: { 79 | // type: BodyStatus.Dynamic, 80 | // position: [0, 15, -5], 81 | // }, 82 | // colliders: [{ 83 | // type: 'Cubiod', 84 | // args: [1, 1, 1], 85 | // }] 86 | // })) 87 | // 88 | // const [boxRef] = useRapier3DBody(() => ({ 89 | // body: { 90 | // type: BodyStatus.Dynamic, 91 | // position: [0, 10, -5], 92 | // }, 93 | // colliders: [{ 94 | // type: 'Cubiod', 95 | // args: [1, 1, 1], 96 | // }] 97 | // })) 98 | // 99 | // const [testRef] = useRapier3DBody(() => ({ 100 | // body: { 101 | // type: BodyStatus.Static, 102 | // position: [0, -5, -5], 103 | // quaternion: new Quaternion().setFromEuler(new Euler(Math.PI / 4, Math.PI / 4, Math.PI / 4)).toArray() as [number, number, number, number], 104 | // }, 105 | // colliders: [{ 106 | // type: 'Cubiod', 107 | // args: [2, 2, 2], 108 | // }] 109 | // })) 110 | // 111 | // return ( 112 | // <> 113 | // 114 | // 115 | // 116 | // 117 | // 118 | // 119 | // 120 | // {/**/} 121 | // {/* R*/} 122 | // {/**/} 123 | // 124 | // 125 | // 126 | // {/**/} 127 | // {/* G*/} 128 | // {/**/} 129 | // 130 | // 131 | // 132 | // {/**/} 133 | // {/* G*/} 134 | // {/**/} 135 | // 136 | // 137 | // 138 | // 139 | // 140 | // 141 | // 142 | // 143 | // 144 | // 145 | // 146 | // 147 | // ) 148 | } 149 | 150 | export default Rapier3DGame -------------------------------------------------------------------------------- /src/physics/helpers/planckjs/PlanckPhysicsHandler.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useMemo, useRef, useState} from "react"; 2 | import {World, Body} from "planck-js"; 3 | import {Context} from "./PlanckPhysicsHandler.context"; 4 | import {Context as AppContext} from "./PlanckApp.context"; 5 | import Physics from "../../Physics"; 6 | import WorkerSubscription from "./WorkerSubscription"; 7 | import PlanckPhysicsWorkerMessagesHandler from "./PlanckPhysicsWorkerMessagesHandler"; 8 | import {applyBufferData} from "./updates"; 9 | import {generateBuffers} from "./buffers"; 10 | 11 | 12 | export const usePhysicsBodies = (removeBody: any) => { 13 | 14 | const [bodies] = useState<{ 15 | [key: string]: any, 16 | }>({}) 17 | const [syncedBodies] = useState<{ 18 | [key: string]: any, 19 | }>({}) 20 | const [syncedBodiesOrder] = useState([]) 21 | const hasPendingSyncedBodiesRef = useRef(0) 22 | 23 | const getPendingSyncedBodiesIteration = useCallback(() => { 24 | return hasPendingSyncedBodiesRef.current 25 | }, []) 26 | 27 | const addSyncedBody = useCallback((uid: string, body: any) => { 28 | syncedBodiesOrder.push(uid) 29 | syncedBodies[uid] = body 30 | hasPendingSyncedBodiesRef.current += 1 31 | return () => { 32 | const index = syncedBodiesOrder.indexOf(uid) 33 | syncedBodiesOrder.splice(index, 1) 34 | delete syncedBodies[uid] 35 | hasPendingSyncedBodiesRef.current += 1 36 | } 37 | }, []) 38 | 39 | const removeSyncedBody = useCallback((uid: string) => { 40 | const index = syncedBodiesOrder.indexOf(uid) 41 | syncedBodiesOrder.splice(index, 1) 42 | delete syncedBodies[uid] 43 | hasPendingSyncedBodiesRef.current += 1 44 | }, []) 45 | 46 | const addBody = useCallback((uid: string, body: any, synced: boolean = false) => { 47 | bodies[uid] = body 48 | let syncedUnsub: any 49 | if (synced) { 50 | syncedUnsub = addSyncedBody(uid, body) 51 | } 52 | return () => { 53 | delete bodies[uid] 54 | if (syncedUnsub) { 55 | syncedUnsub() 56 | } 57 | if (removeBody) { 58 | removeBody(body) 59 | } 60 | } 61 | }, []) 62 | 63 | return { 64 | addSyncedBody, 65 | removeSyncedBody, 66 | getPendingSyncedBodiesIteration, 67 | syncedBodiesOrder, 68 | syncedBodies, 69 | addBody, 70 | bodies, 71 | } 72 | 73 | } 74 | 75 | export const usePhysicsUpdate = () => { 76 | 77 | const countRef = useRef(0) 78 | 79 | const workerSubscriptionsRef = useRef<{ 80 | [key: string]: () => void, 81 | }>({}) 82 | 83 | const subscribeToPhysicsUpdates = useCallback((callback: () => void) => { 84 | const id = countRef.current.toString() 85 | countRef.current += 1 86 | workerSubscriptionsRef.current[id] = callback 87 | 88 | return () => { 89 | delete workerSubscriptionsRef.current[id] 90 | } 91 | 92 | }, []) 93 | 94 | const onUpdate = useCallback(() => { 95 | 96 | Object.values(workerSubscriptionsRef.current).forEach(callback => callback()) 97 | 98 | }, []) 99 | 100 | return { 101 | onUpdate, 102 | subscribeToPhysicsUpdates, 103 | } 104 | 105 | } 106 | 107 | export const usePhysics = (removeBody: any = () => {}) => { 108 | 109 | const { 110 | addSyncedBody, 111 | removeSyncedBody, 112 | getPendingSyncedBodiesIteration, 113 | syncedBodies, 114 | syncedBodiesOrder, 115 | addBody, 116 | bodies, 117 | } = usePhysicsBodies(removeBody) 118 | 119 | const { 120 | onUpdate, 121 | subscribeToPhysicsUpdates, 122 | } = usePhysicsUpdate() 123 | 124 | return { 125 | subscribeToPhysicsUpdates, 126 | getPendingSyncedBodiesIteration, 127 | syncedBodies, 128 | syncedBodiesOrder, 129 | addSyncedBody, 130 | removeSyncedBody, 131 | addBody, 132 | bodies, 133 | onUpdate, 134 | } 135 | } 136 | 137 | const PlanckPhysicsHandler: React.FC<{ 138 | world: World, 139 | worker: Worker, 140 | stepRate: number, 141 | maxNumberOfSyncedBodies: number, 142 | }> = ({children, world, worker, stepRate, maxNumberOfSyncedBodies}) => { 143 | 144 | const removeBody = useCallback((body: Body) => { 145 | world.destroyBody(body) 146 | }, []) 147 | 148 | const { 149 | subscribeToPhysicsUpdates, 150 | getPendingSyncedBodiesIteration, 151 | syncedBodies, 152 | syncedBodiesOrder, 153 | addSyncedBody, 154 | removeSyncedBody, 155 | addBody, 156 | bodies, 157 | onUpdate, 158 | } = usePhysics(removeBody) 159 | 160 | const { 161 | onWorldStep 162 | } = useMemo(() => ({ 163 | onWorldStep: () => { 164 | world.step(stepRate / 1000) 165 | world.clearForces() 166 | onUpdate() 167 | } 168 | }), []) 169 | 170 | return ( 171 | 177 | 179 | 186 | 187 | 188 | {children} 189 | 190 | 191 | 192 | ); 193 | }; 194 | 195 | export default PlanckPhysicsHandler; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TSDX React User Guide 2 | 3 | Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Let’s get you oriented with what’s here and how to use it. 4 | 5 | > This TSDX setup is meant for developing React component libraries (not apps!) that can be published to NPM. If you’re looking to build a React-based app, you should use `create-react-app`, `razzle`, `nextjs`, `gatsby`, or `react-static`. 6 | 7 | > If you’re new to TypeScript and React, checkout [this handy cheatsheet](https://github.com/sw-yx/react-typescript-cheatsheet/) 8 | 9 | ## Commands 10 | 11 | TSDX scaffolds your new library inside `/src`, and also sets up a [Parcel-based](https://parceljs.org) playground for it inside `/example`. 12 | 13 | The recommended workflow is to run TSDX in one terminal: 14 | 15 | ```bash 16 | npm start # or yarn start 17 | ``` 18 | 19 | This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. 20 | 21 | Then run the example inside another: 22 | 23 | ```bash 24 | cd example 25 | npm i # or yarn to install dependencies 26 | npm start # or yarn start 27 | ``` 28 | 29 | The default example imports and live reloads whatever is in `/dist`, so if you are seeing an out of date component, make sure TSDX is running in watch mode like we recommend above. **No symlinking required**, we use [Parcel's aliasing](https://parceljs.org/module_resolution.html#aliases). 30 | 31 | To do a one-off build, use `npm run build` or `yarn build`. 32 | 33 | To run tests, use `npm test` or `yarn test`. 34 | 35 | ## Configuration 36 | 37 | Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly. 38 | 39 | ### Jest 40 | 41 | Jest tests are set up to run with `npm test` or `yarn test`. 42 | 43 | ### Bundle analysis 44 | 45 | Calculates the real cost of your library using [size-limit](https://github.com/ai/size-limit) with `npm run size` and visulize it with `npm run analyze`. 46 | 47 | #### Setup Files 48 | 49 | This is the folder structure we set up for you: 50 | 51 | ```txt 52 | /example 53 | index.html 54 | index.tsx # test your component here in a demo app 55 | package.json 56 | tsconfig.json 57 | /src 58 | index.tsx # EDIT THIS 59 | /test 60 | blah.test.tsx # EDIT THIS 61 | .gitignore 62 | package.json 63 | README.md # EDIT THIS 64 | tsconfig.json 65 | ``` 66 | 67 | #### React Testing Library 68 | 69 | We do not set up `react-testing-library` for you yet, we welcome contributions and documentation on this. 70 | 71 | ### Rollup 72 | 73 | TSDX uses [Rollup](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details. 74 | 75 | ### TypeScript 76 | 77 | `tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs. 78 | 79 | ## Continuous Integration 80 | 81 | ### GitHub Actions 82 | 83 | Two actions are added by default: 84 | 85 | - `main` which installs deps w/ cache, lints, tests, and builds on all pushes against a Node and OS matrix 86 | - `size` which comments cost comparison of your library on every pull request using [`size-limit`](https://github.com/ai/size-limit) 87 | 88 | ## Optimizations 89 | 90 | Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations: 91 | 92 | ```js 93 | // ./types/index.d.ts 94 | declare var __DEV__: boolean; 95 | 96 | // inside your code... 97 | if (__DEV__) { 98 | console.log('foo'); 99 | } 100 | ``` 101 | 102 | You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions. 103 | 104 | ## Module Formats 105 | 106 | CJS, ESModules, and UMD module formats are supported. 107 | 108 | The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found. 109 | 110 | ## Deploying the Example Playground 111 | 112 | The Playground is just a simple [Parcel](https://parceljs.org) app, you can deploy it anywhere you would normally deploy that. Here are some guidelines for **manually** deploying with the Netlify CLI (`npm i -g netlify-cli`): 113 | 114 | ```bash 115 | cd example # if not already in the example folder 116 | npm run build # builds to dist 117 | netlify deploy # deploy the dist folder 118 | ``` 119 | 120 | Alternatively, if you already have a git repo connected, you can set up continuous deployment with Netlify: 121 | 122 | ```bash 123 | netlify init 124 | # build command: yarn build && cd example && yarn && yarn build 125 | # directory to deploy: example/dist 126 | # pick yes for netlify.toml 127 | ``` 128 | 129 | ## Named Exports 130 | 131 | Per Palmer Group guidelines, [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library. 132 | 133 | ## Including Styles 134 | 135 | There are many ways to ship styles, including with CSS-in-JS. TSDX has no opinion on this, configure how you like. 136 | 137 | For vanilla CSS, you can include it at the root directory and add it to the `files` section in your `package.json`, so that it can be imported separately by your users and run through their bundler's loader. 138 | 139 | ## Publishing to NPM 140 | 141 | We recommend using [np](https://github.com/sindresorhus/np). 142 | 143 | ## Usage with Lerna 144 | 145 | When creating a new package with TSDX within a project set up with Lerna, you might encounter a `Cannot resolve dependency` error when trying to run the `example` project. To fix that you will need to make changes to the `package.json` file _inside the `example` directory_. 146 | 147 | The problem is that due to the nature of how dependencies are installed in Lerna projects, the aliases in the example project's `package.json` might not point to the right place, as those dependencies might have been installed in the root of your Lerna project. 148 | 149 | Change the `alias` to point to where those packages are actually installed. This depends on the directory structure of your Lerna project, so the actual path might be different from the diff below. 150 | 151 | ```diff 152 | "alias": { 153 | - "react": "../node_modules/react", 154 | - "react-dom": "../node_modules/react-dom" 155 | + "react": "../../../node_modules/react", 156 | + "react-dom": "../../../node_modules/react-dom" 157 | }, 158 | ``` 159 | 160 | An alternative to fixing this problem would be to remove aliases altogether and define the dependencies referenced as aliases as dev dependencies instead. [However, that might cause other problems.](https://github.com/palmerhq/tsdx/issues/64) 161 | -------------------------------------------------------------------------------- /src/generic.tsx: -------------------------------------------------------------------------------- 1 | import React, {createContext, useCallback, useContext, useEffect, useRef, useState} from "react" 2 | import {generateUUID} from "./utils/ids"; 3 | import {WorkerMessageData, WorkerMessageType} from "./physics/types"; 4 | import {useHandleKeyEvents} from "./keys"; 5 | 6 | enum MessageType { 7 | mounted = 'mounted', 8 | unmounted = 'unmounted', 9 | propUpdate = 'propUpdate', 10 | propRemoved = 'propRemoved', 11 | } 12 | 13 | type Message = { 14 | message: MessageType, 15 | id: string, 16 | [key: string]: any, 17 | } 18 | 19 | const useSyncComponent = (type: string, id: string, props: any, sendMessage: any) => { 20 | 21 | useEffect(() => { 22 | sendMessage({ 23 | message: MessageType.mounted, 24 | id, 25 | type, 26 | value: props, 27 | }) 28 | return () => { 29 | sendMessage({ 30 | message: MessageType.unmounted, 31 | id, 32 | }) 33 | } 34 | }, [type, id, sendMessage]) 35 | 36 | } 37 | 38 | const SyncedComponentProp: React.FC<{ 39 | id: string, 40 | propKey: string, 41 | value: any, 42 | sendMessage: any, 43 | }> = ({propKey, id, value, sendMessage}) => { 44 | const firstUpdateRef = useRef(true) 45 | useEffect(() => { 46 | if (firstUpdateRef.current) { 47 | firstUpdateRef.current = false 48 | return 49 | } 50 | sendMessage({ 51 | message: MessageType.propUpdate, 52 | id, 53 | value, 54 | propKey, 55 | }) 56 | }, [value, id, propKey]) 57 | useEffect(() => { 58 | return () => { 59 | sendMessage({ 60 | message: MessageType.propRemoved, 61 | id, 62 | propKey, 63 | }) 64 | } 65 | }, [id, propKey]) 66 | return null 67 | } 68 | 69 | export const SyncedComponent: React.FC<{ 70 | [key: string]: any, 71 | type: string, 72 | id?: string, 73 | }> = ({type, id: passedId, ...props}) => { 74 | 75 | const sendMessage = useWorkerSendMessage() 76 | 77 | const [id] = useState(() => passedId ?? generateUUID()) 78 | 79 | useSyncComponent(type, id, props, sendMessage) 80 | 81 | return ( 82 | <> 83 | { 84 | Object.entries(props).map(([key, value]) => ( 85 | 86 | )) 87 | } 88 | 89 | ) 90 | } 91 | 92 | const Context = createContext<{ 93 | sendMessage: (message: any) => void, 94 | worker: Worker, 95 | }>(null!) 96 | 97 | export const useWorkerSendMessage = () => { 98 | return useContext(Context).sendMessage 99 | } 100 | 101 | export const useWorker = () => { 102 | return useContext(Context).worker 103 | } 104 | 105 | export const WorkerMessaging: React.FC<{ 106 | worker: Worker 107 | }> = ({worker, children}) => { 108 | 109 | const sendMessage = useCallback((message: any) => { 110 | worker.postMessage({ 111 | type: WorkerMessageType.CUSTOM, 112 | message, 113 | }) 114 | }, [worker]) 115 | 116 | useHandleKeyEvents(worker) 117 | 118 | return ( 119 | 120 | {children} 121 | 122 | ) 123 | 124 | } 125 | 126 | export const SyncComponents: React.FC<{ 127 | components: { 128 | [key: string]: any, 129 | } 130 | }> = ({components}) => { 131 | 132 | 133 | const worker = useWorker() 134 | 135 | const [storedComponents, setComponents] = useState<{ 136 | [key: string]: { 137 | id: string, 138 | type: string, 139 | props: { 140 | [key: string]: any, 141 | } 142 | } 143 | }>({}) 144 | 145 | const handleCustomMessage = useCallback((message: Message) => { 146 | const {id, type, value, propKey} = message 147 | switch (message.message) { 148 | case MessageType.mounted: 149 | setComponents(state => ({ 150 | ...state, 151 | [id]: { 152 | id, 153 | type, 154 | props: value, 155 | } 156 | })) 157 | break; 158 | case MessageType.unmounted: 159 | setComponents(state => { 160 | const updated = { 161 | ...state, 162 | } 163 | delete updated[id] 164 | return updated 165 | }) 166 | break; 167 | case MessageType.propUpdate: 168 | setComponents(state => { 169 | const existing = state[id] ?? { 170 | props: {}, 171 | } 172 | return { 173 | ...state, 174 | [id]: { 175 | ...existing, 176 | props: { 177 | ...existing.props, 178 | [propKey]: value, 179 | } 180 | } 181 | } 182 | }) 183 | break; 184 | case MessageType.propRemoved: 185 | setComponents(state => { 186 | const existing = state[id] ?? { 187 | props: {}, 188 | } 189 | const updatedProps = { 190 | ...existing.props 191 | } 192 | delete updatedProps[propKey] 193 | return { 194 | ...state, 195 | [id]: { 196 | ...existing, 197 | props: updatedProps, 198 | } 199 | } 200 | }) 201 | break; 202 | } 203 | }, []) 204 | 205 | useEffect(() => { 206 | const previousOnMessage = worker.onmessage 207 | 208 | worker.onmessage = (event: any) => { 209 | 210 | if (previousOnMessage) { 211 | // @ts-ignore 212 | previousOnMessage(event) 213 | } 214 | 215 | const message = event.data as WorkerMessageData 216 | 217 | if (message.type === WorkerMessageType.CUSTOM) { 218 | handleCustomMessage(message.message as Message) 219 | } 220 | 221 | } 222 | 223 | return () => { 224 | worker.onmessage = previousOnMessage 225 | } 226 | 227 | }, []) 228 | 229 | return ( 230 | <> 231 | { 232 | Object.entries(storedComponents).map(([key, component]) => { 233 | const Component = components[component.type] 234 | if (!Component) return null 235 | return 236 | }) 237 | } 238 | 239 | ) 240 | } -------------------------------------------------------------------------------- /src/physics/PhysicsConsumer.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | MutableRefObject, 4 | useCallback, 5 | useContext, 6 | useEffect, 7 | useMemo, 8 | useRef, 9 | useState 10 | } from "react"; 11 | import {BodyData, WorkerMessageData, WorkerMessageType} from "./types"; 12 | import {getNow} from "../utils/time"; 13 | import {Object3D} from "three"; 14 | // import {DEFAULT_STEP_RATE} from "./config"; 15 | import {Context} from "./PhysicsConsumer.context"; 16 | import {PhysicsConsumerSyncMeshes} from "../index"; 17 | import {WorkerMessaging} from "../generic"; 18 | import {useTransferKeyEvents} from "../keys"; 19 | 20 | export type DefaultPhysicsConsumerProps = { 21 | worker: Worker, 22 | stepRate?: number, 23 | paused?: boolean, 24 | } 25 | 26 | const FixedUpdateContext = createContext<{ 27 | onFixedUpdateSubscriptions: MutableRefObject<{ 28 | [key: string]: MutableRefObject<(delta: number) => void>, 29 | }>, 30 | subscribeToOnPhysicsUpdate: (callback: MutableRefObject<(delta: number) => void>) => () => void, 31 | updateSubscriptions: (delta: number) => void, 32 | }>(null!) 33 | 34 | export const useFixedUpdateContext = () =>{ 35 | return useContext(FixedUpdateContext) 36 | } 37 | 38 | export const OnFixedUpdateProvider: React.FC = ({children}) => { 39 | 40 | const localStateRef = useRef<{ 41 | subscriptionsIterator: number, 42 | }>({ 43 | subscriptionsIterator: 0, 44 | }) 45 | 46 | const onFixedUpdateSubscriptions = useRef<{ 47 | [key: string]: MutableRefObject<(delta: number) => void>, 48 | }>({}) 49 | 50 | const { 51 | subscribeToOnPhysicsUpdate, 52 | updateSubscriptions, 53 | } = useMemo(() => ({ 54 | subscribeToOnPhysicsUpdate: (callback: MutableRefObject<(delta: number) => void>) => { 55 | const id = localStateRef.current.subscriptionsIterator.toString() 56 | localStateRef.current.subscriptionsIterator += 1 57 | onFixedUpdateSubscriptions.current[id] = callback 58 | return () => { 59 | delete onFixedUpdateSubscriptions.current[id] 60 | } 61 | }, 62 | updateSubscriptions: (delta: number) => { 63 | Object.values(onFixedUpdateSubscriptions.current).forEach(callback => callback.current(delta)) 64 | } 65 | }), []) 66 | 67 | return ( 68 | 73 | {children} 74 | 75 | ) 76 | } 77 | 78 | type Props = DefaultPhysicsConsumerProps & { 79 | lerpBody: (body: BodyData, object: Object3D, delta: number) => void, 80 | updateBodyData: (bodyData: BodyData, positions: Float32Array, angles: Float32Array, velocities: Float32Array) => void, 81 | } 82 | 83 | const PhysicsConsumer: React.FC = ({ 84 | paused = false, 85 | updateBodyData, 86 | worker, 87 | children, 88 | // stepRate = DEFAULT_STEP_RATE, 89 | lerpBody 90 | }) => { 91 | 92 | const [connected, setConnected] = useState(false) 93 | const [bodiesData] = useState<{ 94 | [id: string]: BodyData 95 | }>({}) 96 | const localStateRef = useRef<{ 97 | lastUpdate: number, 98 | subscriptionsIterator: number, 99 | bodies: string[], 100 | }>({ 101 | lastUpdate: getNow(), 102 | subscriptionsIterator: 0, 103 | bodies: [] 104 | }) 105 | 106 | const { 107 | updateSubscriptions, 108 | } = useFixedUpdateContext() 109 | 110 | const onFrameCallbacks = useRef<{ 111 | [id: string]: (delta: number) => void, 112 | }>({}) 113 | 114 | const lerpMesh = useCallback((body: BodyData, ref: MutableRefObject, delta: number) => { 115 | if (!ref.current) return 116 | const object = ref.current 117 | lerpBody(body, object, delta) 118 | }, []) 119 | 120 | const onUpdate = useCallback((_updateTime: number, positions: Float32Array, angles: Float32Array, bodies: undefined | string[], velocities: Float32Array) => { 121 | 122 | if (!updateSubscriptions) return; 123 | 124 | const now = getNow() 125 | 126 | if (bodies) { 127 | localStateRef.current.bodies = bodies 128 | } 129 | 130 | Object.entries(bodiesData).forEach(([id, bodyData]) => { 131 | if (bodies) { 132 | bodyData.index = bodies.indexOf(id) 133 | } 134 | if (bodyData.index >= 0) { 135 | updateBodyData(bodyData, positions, angles, velocities); 136 | bodyData.lastUpdate = now 137 | // console.log('lastUpdate', updateTime, getNow()) 138 | } 139 | }) 140 | 141 | // const now = updateTime 142 | const delta = (now - localStateRef.current.lastUpdate) / 1000 143 | localStateRef.current.lastUpdate = now 144 | updateSubscriptions(delta) 145 | 146 | }, [updateSubscriptions]) 147 | 148 | const onUpdateRef = useRef(onUpdate); 149 | 150 | useEffect(() => { 151 | onUpdateRef.current = onUpdate; 152 | }, [onUpdate]); 153 | 154 | useEffect(() => { 155 | if (connected) return 156 | worker.postMessage({ 157 | type: WorkerMessageType.PHYSICS_READY, 158 | paused, 159 | }) 160 | }, [connected, paused]) 161 | 162 | useEffect(() => { 163 | worker.postMessage({ 164 | type: WorkerMessageType.PHYSICS_SET_PAUSED, 165 | paused, 166 | }) 167 | }, [paused]) 168 | 169 | useEffect(() => { 170 | 171 | const previousOnMessage: any = worker.onmessage 172 | 173 | worker.onmessage = (event: any) => { 174 | 175 | const message = event.data as WorkerMessageData 176 | 177 | switch (message.type) { 178 | case WorkerMessageType.PHYSICS_ACKNOWLEDGED: 179 | setConnected(true) 180 | break; 181 | case WorkerMessageType.PHYSICS_UPDATE: 182 | onUpdateRef.current(message.updateTime, message.positions, message.angles, message.bodies, message.velocities) 183 | 184 | worker.postMessage({ 185 | type: WorkerMessageType.PHYSICS_PROCESSED, 186 | positions: message.positions, 187 | angles: message.angles, 188 | velocities: message.velocities, 189 | 190 | }, [message.positions.buffer, message.angles.buffer, message.velocities.buffer]) 191 | break; 192 | } 193 | 194 | if (previousOnMessage) { 195 | previousOnMessage(event) 196 | } 197 | 198 | } 199 | 200 | }, []) 201 | 202 | const { 203 | syncBody, 204 | } = useMemo(() => ({ 205 | syncBody: (id: string, ref: MutableRefObject, applyRotation: boolean = true) => { 206 | localStateRef.current.subscriptionsIterator += 1 207 | const body: BodyData = { 208 | ref, 209 | index: localStateRef.current.bodies.indexOf(id), 210 | lastUpdate: getNow(), 211 | lastRender: getNow(), 212 | previous: {}, 213 | applyRotation, 214 | } 215 | bodiesData[id] = body 216 | onFrameCallbacks.current[id] = (delta: number) => lerpMesh(body, ref, delta) 217 | return () => { 218 | delete onFrameCallbacks.current[id] 219 | delete bodiesData[id] 220 | } 221 | } 222 | }), []) 223 | 224 | const syncMeshes = useCallback((_, delta: number) => { 225 | Object.values(onFrameCallbacks.current).forEach(callback => callback(delta)) 226 | }, []) 227 | 228 | const sendMessage = useCallback((message: any) => { 229 | worker.postMessage(message) 230 | }, []) 231 | 232 | useTransferKeyEvents(worker) 233 | 234 | if (!connected) return null 235 | 236 | return ( 237 | 238 | 244 | 245 | {children} 246 | 247 | 248 | ); 249 | }; 250 | 251 | const Wrapper: React.FC = (props) => { 252 | return ( 253 | 254 | 255 | 256 | ) 257 | } 258 | 259 | export default Wrapper; --------------------------------------------------------------------------------