├── .gitignore
├── .nvmrc
├── LICENSE
├── README.md
├── docs
└── api
│ └── API.md
├── package.json
├── src
├── custom.d.ts
├── index.ts
├── logic
│ ├── ApiWrapper.tsx
│ ├── logicWorkerApp
│ │ ├── MessageHandler.tsx
│ │ ├── PhysicsHandler.tsx
│ │ ├── hooks
│ │ │ ├── messaging.ts
│ │ │ └── sync.ts
│ │ └── index.tsx
│ └── workerHelper.ts
├── main
│ ├── Body.tsx
│ ├── Engine.tsx
│ ├── InstancesProvider.tsx
│ ├── LogicHandler.tsx
│ ├── LogicWorker.tsx
│ ├── MeshRefs.tsx
│ ├── PhysicsWorker.tsx
│ ├── R3FPhysicsObjectUpdater.tsx
│ ├── hooks
│ │ ├── useBody.ts
│ │ ├── useCollisionEvents.ts
│ │ └── useWorkerMessages.ts
│ └── worker
│ │ ├── app
│ │ ├── Bodies.tsx
│ │ ├── Collisions.tsx
│ │ ├── World.tsx
│ │ ├── WorldState.tsx
│ │ ├── appContext.ts
│ │ ├── buffers.ts
│ │ ├── index.tsx
│ │ ├── logicWorker.ts
│ │ └── workerMessages.ts
│ │ ├── data.ts
│ │ ├── functions.ts
│ │ ├── logicWorker.ts
│ │ ├── methods.ts
│ │ ├── physicsWorkerHelper.ts
│ │ ├── planckjs
│ │ ├── bodies.ts
│ │ ├── cache.ts
│ │ ├── collisions
│ │ │ ├── collisions.ts
│ │ │ ├── data.ts
│ │ │ ├── filters.ts
│ │ │ └── types.ts
│ │ ├── config.ts
│ │ ├── data.ts
│ │ ├── shared.ts
│ │ └── world.ts
│ │ ├── shared.ts
│ │ ├── shared
│ │ └── types.ts
│ │ ├── utils.ts
│ │ └── worker.ts
├── shared
│ ├── CollisionsProvider.tsx
│ ├── MeshSubscriptions.tsx
│ ├── Messages.tsx
│ ├── PhysicsProvider.tsx
│ ├── PhysicsSync.tsx
│ ├── SendMessages.tsx
│ ├── StoredPhysicsData.tsx
│ ├── WorkerOnMessageProvider.tsx
│ ├── types.ts
│ └── utils.ts
└── utils
│ ├── hooks.ts
│ ├── numbers.ts
│ └── time.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | dist
6 | .idea
7 | temp
8 | temp2
9 | temp3
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 14.15.0
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## ⚠️ This will be replaced by an updated engine that I'm working on within a new repo. https://github.com/simonghales/rgg-engine The external API will be reasonably similar, but the internals are undergoing quite a few changes. I'm also working on an editor, you can check out a preview at https://rgg-demo.netlify.app/
2 |
3 |
4 | ## ⚠️ Currently under development, use at your own risk
5 |
6 | # react-three-game-engine
7 |
8 | [](https://www.npmjs.com/package/react-three-game-engine)
9 |
10 | A very early experimental work-in-progress package to help provide game engine functionality for [react-three-fiber](https://github.com/pmndrs/react-three-fiber).
11 |
12 | ### Key Features
13 | - [planck.js](https://github.com/shakiba/planck.js/) 2d physics integration
14 | - Physics update rate independent of frame rate
15 | - `OnFixedUpdate` functionality
16 | - Additional React app running in web worker, sync'd with physics, for handling game logic etc
17 |
18 | I will be working on an example starter-kit: [react-three-game-starter](https://github.com/simonghales/react-three-game-starter) which will be fleshed out over time.
19 |
20 | ### Note
21 | The planck.js integration currently isn't fully fleshed out. I've only been adding in support
22 | for functionality on an as-needed basis for my own games.
23 |
24 | Note: if you delve into the source code for this package, it's a bit messy!
25 |
26 | Note: right now I'm having issues getting this to run via codesandbox via create-react-app, hopefully I can resolve this eventually.
27 |
28 | ## Get Started
29 |
30 | ### General / Physics
31 |
32 | 1. Install required packages
33 |
34 | `npm install react-three-game-engine`
35 |
36 | plus
37 |
38 | `npm install three react-three-fiber planck-js`
39 |
40 | 2. Create Physics Web Worker
41 |
42 | You'll need to create your own web worker to handle the physics. You can
43 | check out my repo [react-three-game-starter](https://github.com/simonghales/react-three-game-starter)
44 | for an example of how you can do so with `create-react-app` without having to eject.
45 |
46 | Within this worker you'll need to import `physicsWorkerHandler` and pass it `self` as the param.
47 |
48 | Example:
49 |
50 | ```js
51 | // worker.js
52 | import {physicsWorkerHandler} from "react-three-game-engine";
53 |
54 | // because of some weird react/dev/webpack/something quirk
55 | self.$RefreshReg$ = () => {};
56 | self.$RefreshSig$ = () => () => {};
57 |
58 | physicsWorkerHandler(self);
59 | ```
60 |
61 | 3. Import and add [``](docs/api/API.md#Engine) component within your r3f `` component.
62 |
63 | ```jsx
64 | import { Engine } from 'react-three-game-engine'
65 | import { Canvas } from 'react-three-fiber'
66 | ```
67 |
68 | And pass it the physics worker you created in the previous step.
69 |
70 | ```jsx
71 |
72 | const physicsWorker = new Worker('path/to/worker.js', { type: 'module' });
73 |
74 | //...
75 |
76 |
81 | ```
82 |
83 | 4. Create a planck.js driven physics [body](docs/api/API.md#Bodies)
84 |
85 | ```jsx
86 | import { useBody, BodyType, BodyShape } from 'react-three-game-engine'
87 | import { Vec2 } from 'planck-js'
88 | ```
89 |
90 | ```jsx
91 | const [ref, api] = useBody(() => ({
92 | type: BodyType.dynamic,
93 | position: Vec2(0, 0),
94 | linearDamping: 4,
95 | fixtures: [{
96 | shape: BodyShape.circle,
97 | radius: 0.55,
98 | fixtureOptions: {
99 | density: 20,
100 | }
101 | }],
102 | }))
103 | ```
104 |
105 | 5. Control the body via the returned [api](docs/api/API.md#BodyApi)
106 |
107 | ```jsx
108 | api.setLinearVelocity(Vec2(1, 1))
109 | ```
110 |
111 | 6. Utilise [`useFixedUpdate`](docs/api/API.md#usefixedupdate) for controlling the body
112 |
113 | ```jsx
114 | import { useFixedUpdate } from 'react-three-game-engine'
115 | ```
116 |
117 | ```jsx
118 |
119 | const velocity = Vec2(0, 0)
120 |
121 | // ...
122 |
123 | const onFixedUpdate = useCallback(() => {
124 |
125 | // ...
126 |
127 | velocity.set(xVel * 5, yVel * 5)
128 | api.setLinearVelocity(velocity)
129 |
130 | }, [api])
131 |
132 | useFixedUpdate(onFixedUpdate)
133 |
134 | ```
135 |
136 | ### React Logic App Worker
137 |
138 | A key feature provided by react-three-game-engine is the separate React app running
139 | within a web worker. This helps keep the main thread free to handle rendering etc,
140 | helps keep performance smooth.
141 |
142 | To utilise this functionality you'll need to create your own web worker. You can
143 | check out my repo [react-three-game-starter](https://github.com/simonghales/react-three-game-starter)
144 | for an example of how you can do so with `create-react-app` without having to eject.
145 |
146 | 1. Create a React component to host your logic React app, export a new component wrapped with
147 | [`withLogicWrapper`](docs/api/API.md#withLogicWrapper)
148 |
149 | ```jsx
150 | import {withLogicWrapper} from "react-three-game-engine";
151 |
152 | const App = () => {
153 | // ... your new logic app goes here
154 | }
155 |
156 | export const LgApp = withLogicWrapper(App)
157 | ```
158 |
159 | 2. Set up your web worker like such
160 | (note requiring the file was due to jsx issues with my web worker compiler)
161 |
162 | ```jsx
163 | /* eslint-disable no-restricted-globals */
164 | import {logicWorkerHandler} from "react-three-game-engine";
165 |
166 | // because of some weird react/dev/webpack/something quirk
167 | (self).$RefreshReg$ = () => {};
168 | (self).$RefreshSig$ = () => () => {};
169 |
170 | logicWorkerHandler(self, require("../path/to/logic/app/component").LgApp)
171 | ```
172 |
173 | 3. Import your web worker (this is just example code)
174 |
175 | ```jsx
176 | const [logicWorker] = useState(() => new Worker('../path/to/worker', { type: 'module' }))
177 | ```
178 |
179 | 4. Pass worker to [``](docs/api/API.md#Engine)
180 |
181 | ```jsx
182 |
183 | {/* ... */}
184 |
185 | ```
186 |
187 | You should now have your Logic App running within React within your web worker,
188 | synchronised with the physics worker as well.
189 |
190 | ### Controlling a body through both the main, and logic apps.
191 |
192 | To control a body via either the main or logic apps, you would create the body
193 | within one app via [`useBody`](docs/api/API.md#useBody) and then within the other app you can get api
194 | access via [`useBodyApi`](docs/api/API.md#useBodyApi).
195 |
196 | However you need to know the `uuid` of the body you wish to control. By default
197 | the uuid is one generated via threejs, but you can specify one yourself.
198 |
199 | 1. Create body
200 |
201 | ```jsx
202 | useBody(() => ({
203 | type: BodyType.dynamic,
204 | position: Vec2(0, 0),
205 | linearDamping: 4,
206 | fixtures: [{
207 | shape: BodyShape.circle,
208 | radius: 0.55,
209 | fixtureOptions: {
210 | density: 20,
211 | }
212 | }],
213 | }), {
214 | uuid: 'player'
215 | })
216 | ```
217 |
218 | 2. Get body api
219 |
220 | ```jsx
221 | const api = useBodyApi('player')
222 |
223 | // ...
224 |
225 | api.setLinearVelocity(Vec2(1, 1))
226 |
227 | ```
228 |
229 | So with this approach, you can for example initiate your player body via the logic app,
230 | and then get api access via the main app, and use that to move the body around.
231 |
232 | 3. Additionally, if you are creating your body in main / logic, you'll likely want to have
233 | access to the position / rotation of the body as well.
234 |
235 | You can use [`useSubscribeMesh`](docs/api/API.md#useSubscribeMesh) and pass in a ref you've created, which will synchronize
236 | with the physics body.
237 |
238 | ```jsx
239 | const ref = useRef(new Object3D())
240 | useSubscribeMesh('player', ref.current, false)
241 |
242 | // ...
243 |
244 | return (
245 |
246 | {/*...*/}
247 |
248 | )
249 |
250 | ```
251 |
252 | ### Synchronising Logic App with Main App
253 |
254 | I've added in [`useSyncWithMainComponent`](docs/api/API.md#useSyncWithMainComponent) to sync from the logic app to the main app
255 |
256 | 1. Within a component running on the logic app
257 |
258 | ```jsx
259 | const updateProps = useSyncWithMainComponent("player", "uniqueKey", {
260 | foo: "bar"
261 | })
262 |
263 | // ...
264 |
265 | updateProps({
266 | foo: "updated"
267 | })
268 |
269 |
270 | ```
271 |
272 | 2. Then within the main app
273 |
274 | ```jsx
275 | const Player = ({foo}) => {
276 | // foo -> "bar"
277 | // foo -> "updated"
278 | }
279 |
280 | const mappedComponents = {
281 | "player": Player
282 | }
283 |
284 |
285 | {/* ... */}
286 |
287 |
288 | ```
289 |
290 | When [`useSyncWithMainComponent`](docs/api/API.md#useSyncWithMainComponent) is mounted / unmounted, the ``
291 | component will mount / unmount.
292 |
293 | Note: currently this only supports sync'ing from logic -> main, but I will add in the
294 | reverse soon.
295 |
296 | ### Communication
297 |
298 | To communicate between the main and logic workers you can use [`useSendMessage`](docs/api/API.md#useSendMessage)
299 | to send and [`useOnMessage`](docs/api/API.md#useOnMessage) to subscribe
300 |
301 | ```jsx
302 | import {useSendMessage} from "react-three-game-engine"
303 |
304 | // ...
305 |
306 | const sendMessage = useSendMessage()
307 |
308 | // ...
309 |
310 | sendMessage('messageKey', "any-data")
311 |
312 | ```
313 |
314 | ```jsx
315 | import {useOnMessage} from "react-three-game-engine"
316 |
317 | // ...
318 |
319 | const onMessage = useOnMessage()
320 |
321 | // ...
322 |
323 | const unsubscribe = onMessage('messageKey', (data) => {
324 | // data -> "any-data"
325 | })
326 |
327 | // ...
328 |
329 | unsubscribe()
330 |
331 | ```
332 |
333 | ## API
334 |
335 | [Read the API documentation](docs/api/API.md)
336 |
337 |
338 |
--------------------------------------------------------------------------------
/docs/api/API.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | This is a work in progress.
4 |
5 | # Engine
6 |
7 | Important: must be placed within the `Canvas` element provided by react-three-fiber.
8 |
9 | ```tsx
10 |
16 | ```
17 |
18 | #### config
19 | _optional_
20 |
21 | ```tsx
22 | type config = {
23 | maxNumberOfDynamicObjects: number // default 100
24 | updateRate: number // default 1000 / 30 (i.e. physics updates 30 times a second)
25 | }
26 | ```
27 |
28 | #### worldParams
29 | _optional_
30 |
31 | See the [planck.js documentation](https://github.com/shakiba/planck.js/blob/master/docs/interfaces/worlddef.md) for
32 | details regarding `worldDef`
33 |
34 | ```tsx
35 | type worldParams = {
36 | allowSleep: boolean // default to true
37 | gravity: Vec2 // default to Vec2(0, 0) (i.e. no gravity)
38 | ...worldDef
39 | }
40 | ```
41 |
42 | #### logicWorker
43 | _optional_
44 |
45 | A Web Worker that will be run as the logic worker, synchronized with the
46 | physics app. [Read more here.](../../README.md#react-logic-app-worker)
47 |
48 |
49 | #### logicMappedComponents
50 | _optional_
51 |
52 | Facilitates synchronising components from the logic app with the main app. [Read more here.](../../README.md#synchronising-logic-app-with-main-app)
53 |
54 | ```tsx
55 | type logicMappedComponents = {
56 | [key: string]: ReactComponent
57 | }
58 | ```
59 |
60 | See [useSyncWithMainComponent](#useSyncWithMainComponent) for further details.
61 |
62 | ## useFixedUpdate
63 |
64 | Inspired by Unity's `OnFixedUpDate`, `useFixedUpdate` is a hook that enables
65 | you to call a function after each physics update. For Unity this is the
66 | recommended way to apply physics effects.
67 |
68 | ```tsx
69 | import { useFixedUpdate } from "react-three-game-engine"
70 |
71 | useFixedUpdate((delta: number) => {
72 | // do something
73 | })
74 | ```
75 |
76 | # Physics
77 |
78 | ## Bodies
79 |
80 | #### BodyApi
81 |
82 | ```tsx
83 | type BodyApi = {
84 | // planck.js methods
85 | applyForceToCenter: (vec: Vec2, uuid?: string) => void;
86 | applyLinearImpulse: (vec: Vec2, pos: Vec2, uuid?: string) => void;
87 | setPosition: (vec: Vec2, uuid?: string) => void;
88 | setLinearVelocity: (vec: Vec2, uuid?: string) => void;
89 | setAngle: (angle: number, uuid?: string) => void;
90 | // custom
91 | updateBody: (data: UpdateBodyData, uuid?: string) => void;
92 | }
93 |
94 | type UpdateBodyData = {
95 | fixtureUpdate?: {
96 | groupIndex?: number,
97 | categoryBits?: number,
98 | maskBits?: number,
99 | }
100 | }
101 |
102 | ```
103 |
104 | For detail on what the planck.js methods do, see the [documentation for planck.js](https://github.com/shakiba/planck.js/blob/master/docs/api/classes/body.md#methods).
105 |
106 | #### useBody
107 |
108 | ```tsx
109 | useBody(PropsFn, Config): [ref: MutableRefObject, api: BodyApi, uuid: string]
110 | ```
111 |
112 | ```tsx
113 | type PropsFn = () => AddBodyDef
114 |
115 | type AddBodyDef = {
116 | bodyType: BodyType,
117 | fixtures: Fixture[],
118 | ...BodyDef,
119 | }
120 |
121 | type Fixture = BoxFixture | CircleFixture
122 |
123 | type BoxFixture = FixtureBase & {
124 | hx: number,
125 | hy: number,
126 | center?: [number, number],
127 | }
128 |
129 | type CircleFixture = FixtureBase & {
130 | radius: number,
131 | }
132 |
133 | type FixtureBase = {
134 | shape: BodyShape,
135 | fixtureOptions?: Partial,
136 | }
137 |
138 | enum BodyShape {
139 | box = 'box',
140 | circle = 'circle',
141 | }
142 |
143 | enum BodyType {
144 | static = 'static',
145 | kinematic = 'kinematic',
146 | dynamic = 'dynamic'
147 | }
148 |
149 | ```
150 |
151 | ```tsx
152 | type Config = {
153 | listenForCollisions?: boolean;
154 | applyAngle?: boolean;
155 | cacheKey?: string;
156 | uuid?: string;
157 | fwdRef?: MutableRefObject;
158 | }
159 | ```
160 |
161 | Note: for `BodyDef` [see the planck.js documentation](https://github.com/shakiba/planck.js/blob/master/docs/api/interfaces/bodydef.md).
162 | Note: for `FixtureOpt` [see the planck.js documentation](https://github.com/shakiba/planck.js/blob/master/docs/api/interfaces/fixtureopt.md).
163 |
164 | ```tsx
165 | import { useBody, BodyType, BodyShape } from "react-three-game-engine"
166 | import { Vec2 } from "planck-js"
167 |
168 | const [ref, api, uuid] = useBody(() => ({
169 | type: BodyType.dynamic,
170 | position: Vec2(0, 0),
171 | linearDamping: 4,
172 | fixtures: [{
173 | shape: BodyShape.circle,
174 | radius: 0.55,
175 | fixtureOptions: {
176 | density: 20,
177 | }
178 | }],
179 | }))
180 |
181 | ```
182 |
183 | Note: if you set `bodyType` to `static` it will not be synchronised with either the main app or
184 | logic app.
185 |
186 | #### createBoxFixture
187 |
188 | ```tsx
189 | createBoxFixture = ({
190 | width = 1,
191 | height = 1,
192 | center,
193 | fixtureOptions = {}
194 | }: {
195 | width?: number,
196 | height?: number,
197 | center?: [number, number],
198 | fixtureOptions?: Partial
199 | }) => BoxFixture
200 | ```
201 |
202 | ```tsx
203 |
204 | useBody(() => ({
205 | type: BodyType.dynamic,
206 | position: Vec2(0, 0),
207 | linearDamping: 4,
208 | fixtures: [
209 | createBoxFixture({
210 | width: 2,
211 | height: 2,
212 | })
213 | ],
214 | }))
215 | ```
216 |
217 | #### createCircleFixture
218 |
219 | ```tsx
220 | createCircleFixture = ({
221 | radius = 1,
222 | fixtureOptions = {}
223 | }: {
224 | radius?: number,
225 | fixtureOptions?: Partial
226 | }) => CircleFixture
227 | ```
228 |
229 | ```tsx
230 |
231 | useBody(() => ({
232 | type: BodyType.dynamic,
233 | position: Vec2(0, 0),
234 | linearDamping: 4,
235 | fixtures: [
236 | createCircleFixture({
237 | radius: 2,
238 | })
239 | ],
240 | }))
241 | ```
242 |
243 | #### useBodyApi
244 |
245 | ```tsx
246 | useBodyApi: (uuid: string) => BodyApi
247 | ```
248 |
249 | ```tsx
250 | const api = useBodyApi('player')
251 |
252 | // ...
253 |
254 | api.setLinearVelocity(Vec2(1, 1))
255 |
256 | ```
257 |
258 | #### useSubscribeMesh
259 |
260 | ```tsx
261 | useSubscribeMesh: (
262 | uuid: string,
263 | object: Object3D,
264 | applyAngle: boolean = true,
265 | isDynamic: boolean = true
266 | )
267 | ```
268 |
269 | ```tsx
270 | const ref = useRef(new Object3D())
271 | useSubscribeMesh('player', ref.current, false)
272 | ```
273 |
274 | # Logic
275 |
276 | ## Logic Worker
277 |
278 | #### logicWorkerHandler
279 |
280 | ```tsx
281 | logicWorkerHandler: (worker: Worker, appComponent: ReactComponent)
282 | ```
283 |
284 | ```jsx
285 | /* eslint-disable no-restricted-globals */
286 | import { logicWorkerHandler } from "react-three-game-engine";
287 |
288 | // because of some weird react/dev/webpack/something quirk
289 | (self).$RefreshReg$ = () => {};
290 | (self).$RefreshSig$ = () => () => {};
291 |
292 | logicWorkerHandler(self, require("../path/to/logic/app/component").LgApp)
293 | ```
294 |
295 | ## Logic App
296 |
297 | #### withLogicWrapper
298 |
299 | ```tsx
300 | withLogicWrapper: (appComponent: ReactComponent) => ReactComponent
301 | ```
302 |
303 | ```jsx
304 | import { withLogicWrapper } from "react-three-game-engine";
305 |
306 | const App = () => {
307 | // ... your new logic app goes here
308 | }
309 |
310 | export const LgApp = withLogicWrapper(App)
311 | ```
312 |
313 | #### useSyncWithMainComponent
314 |
315 | ```
316 | useSyncWithMainComponent: (componentKey: string, uniqueKey: string, props: {}) => UpdateProps
317 |
318 | type UpdateProps = (props: {}) => void
319 |
320 | ```
321 |
322 | ```tsx
323 | const updateProps = useSyncWithMainComponent("player", "uniqueKey", {
324 | foo: "bar",
325 | blah: "blah",
326 | })
327 |
328 | // ...
329 |
330 | updateProps({
331 | foo: "updated"
332 | })
333 | ```
334 |
335 | See [logicMappedComponents](#logicMappedComponents) for implementation details.
336 |
337 | ## Communication
338 |
339 | #### useSendMessage
340 |
341 | ```tsx
342 | useSendMessage: () => (messageKey: string, data: any) => void
343 | ```
344 |
345 | ```jsx
346 | import { useSendMessage } from "react-three-game-engine"
347 |
348 | // ...
349 |
350 | const sendMessage = useSendMessage()
351 |
352 | // ...
353 |
354 | sendMessage('messageKey', "any-data")
355 |
356 | ```
357 |
358 | #### useOnMessage
359 |
360 | ```tsx
361 | useOnMessage: () => (messageKey: string, callback: (data: any) => void) => UnsubscribeFn
362 | ```
363 |
364 | ```jsx
365 | import { useOnMessage } from "react-three-game-engine"
366 |
367 | // ...
368 |
369 | const onMessage = useOnMessage()
370 |
371 | // ...
372 |
373 | const unsubscribe = onMessage('messageKey', (data) => {
374 | // data -> "any-data"
375 | })
376 |
377 | // ...
378 |
379 | unsubscribe()
380 |
381 | ```
382 |
383 | ## Misc
384 |
385 | ### Mesh Storage
386 |
387 | #### useStoreMesh
388 |
389 | ```tsx
390 | useStoreMesh: (uuid: string, mesh: Object3D)
391 | ```
392 |
393 | #### useStoredMesh
394 |
395 | ```tsx
396 | useStoredMesh: (uuid: string) => Object3D | null
397 | ```
398 |
399 | ### Mesh Instancing
400 |
401 | You'll need to install `@react-three/drei`
402 |
403 | #### InstancesProvider
404 |
405 | Place inside of ``
406 |
407 | ```tsx
408 |
409 |
410 | {/*...*/}
411 |
412 |
413 | ```
414 |
415 | #### InstancedMesh
416 |
417 | ```tsx
418 | for r3f, e.g. castShadow
423 | />
424 |
425 | type Props = {
426 | meshKey: string,
427 | maxInstances: number,
428 | gltfPath: string,
429 | meshProps?: JSX.IntrinsicElements['instancedMesh']
430 | }
431 |
432 | ```
433 |
434 | gltfPath is passed to `useGltf` from `@react-three/drei`
435 |
436 | Place inside of ``
437 |
438 | You need to wrap it with React's ``
439 |
440 | ```tsx
441 |
442 |
443 |
450 |
451 |
452 | ```
453 |
454 | #### Instance
455 |
456 | ```tsx
457 |
463 |
464 | type Props = {
465 | meshKey: string,
466 | position?: [number, number, number],
467 | rotation?: [number, number, number],
468 | scale?: [number, number, number]
469 | }
470 |
471 |
472 |
473 | ```
474 |
475 | #### useInstancedMesh
476 |
477 | todo...
478 |
479 | #### useAddInstance
480 |
481 | todo...
482 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.14.6",
3 | "license": "MIT",
4 | "main": "dist/index.js",
5 | "typings": "dist/index.d.ts",
6 | "description": "A very early experimental work-in-progress package to help provide game engine functionality for react-three-fiber",
7 | "keywords": [
8 | "react",
9 | "react-three-fiber",
10 | "threejs",
11 | "three",
12 | "game",
13 | "engine"
14 | ],
15 | "files": [
16 | "dist",
17 | "src"
18 | ],
19 | "homepage": "https://github.com/simonghales/react-three-game-engine#readme",
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/simonghales/react-three-game-engine.git"
23 | },
24 | "engines": {
25 | "node": ">=10"
26 | },
27 | "scripts": {
28 | "start": "tsdx watch",
29 | "build-worker": "tsdx build --entry src/main/worker/worker.ts --name worker && rm -rf temp && mkdir temp && mv dist/* temp/",
30 | "build-worker-final": "mkdir dist/worker && mv temp/* dist/worker && rm -rf temp",
31 | "build-worker-app": "tsdx build --entry src/main/worker/app/index.tsx --name physicsApp && rm -rf temp3 && mkdir temp3 && mv dist/* temp3/",
32 | "build-worker-app-final": "mkdir dist/worker/app && mv temp3/* dist/worker/app && rm -rf temp3",
33 | "build-logic-worker": "tsdx build --entry src/logic/logicWorkerApp/index.tsx --name workerApp && rm -rf temp2 && mkdir temp2 && mv dist/* temp2/",
34 | "build-logic-worker-final": "mkdir dist/logicWorkerApp && mv temp2/* dist/logicWorkerApp && rm -rf temp2",
35 | "build": "npm run build-logic-worker && npm run build-worker && npm run build-worker-app && tsdx build && npm run build-worker-final && npm run build-worker-app-final && npm run build-logic-worker-final",
36 | "local-build": "yalc publish --push",
37 | "test": "tsdx test --passWithNoTests",
38 | "lint": "tsdx lint",
39 | "prepare": "npm run build",
40 | "size": "size-limit",
41 | "analyze": "size-limit --why"
42 | },
43 | "peerDependencies": {
44 | "@react-three/drei": ">=3.3",
45 | "planck-js": ">=0.3",
46 | "react": ">=16.13",
47 | "react-dom": ">=16.13",
48 | "react-three-fiber": ">=5.0",
49 | "typescript": ">=3.9"
50 | },
51 | "husky": {
52 | "hooks": {}
53 | },
54 | "prettier": {
55 | "printWidth": 80,
56 | "semi": true,
57 | "singleQuote": true,
58 | "trailingComma": "es5"
59 | },
60 | "name": "react-three-game-engine",
61 | "module": "dist/react-three-game-engine.esm.js",
62 | "size-limit": [],
63 | "devDependencies": {
64 | "@react-three/drei": "^3.3.0",
65 | "@size-limit/preset-small-lib": "^4.9.1",
66 | "@types/react": "^17.0.0",
67 | "@types/react-dom": "^17.0.0",
68 | "husky": "^4.3.6",
69 | "planck-js": "^0.3.23",
70 | "react": "^17.0.1",
71 | "react-dom": "^17.0.1",
72 | "react-three-fiber": "^5.3.11",
73 | "size-limit": "^4.9.1",
74 | "three": "^0.124.0",
75 | "tsdx": "^0.14.1",
76 | "tslib": "^2.0.3",
77 | "typescript": "^4.1.3"
78 | },
79 | "dependencies": {
80 | "react-nil": "^0.0.3",
81 | "valtio": "^0.5.2"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/custom.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-nil';
2 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { BodyApi, useBody, useBodyApi } from './main/hooks/useBody';
2 | import { Engine } from './main/Engine';
3 | import { logicWorkerHandler } from './logic/workerHelper';
4 | import { useSendSyncComponentMessage } from './logic/logicWorkerApp/hooks/messaging';
5 | import { withLogicWrapper } from './logic/ApiWrapper';
6 | import { useSyncWithMainComponent } from './logic/logicWorkerApp/hooks/sync';
7 | import { useFixedUpdate } from './shared/PhysicsSync';
8 | import {
9 | useSubscribeMesh,
10 | } from './shared/MeshSubscriptions';
11 | import { BodyShape, BodyType, createBoxFixture, createCircleFixture } from './main/worker/planckjs/bodies';
12 | import {useStoredMesh, useStoreMesh } from './main/MeshRefs';
13 | import { useOnMessage } from './shared/Messages';
14 | import { useSendMessage } from './shared/SendMessages';
15 | import { Body, BodySync } from "./main/Body";
16 | import {
17 | useAddInstance,
18 | useInstancedMesh,
19 | Instance,
20 | InstancedMesh,
21 | InstancesProvider,
22 | } from "./main/InstancesProvider"
23 | import { physicsWorkerHandler } from './main/worker/physicsWorkerHelper';
24 | import { useCollisionEvents } from './main/hooks/useCollisionEvents';
25 |
26 | export {
27 | Engine,
28 | useCollisionEvents,
29 | useBodyApi,
30 | useBody,
31 | BodyApi,
32 | logicWorkerHandler,
33 | useSendSyncComponentMessage,
34 | useSyncWithMainComponent,
35 | useFixedUpdate,
36 | useSubscribeMesh,
37 | withLogicWrapper,
38 | BodyShape,
39 | BodyType,
40 | useStoreMesh,
41 | useStoredMesh,
42 | useOnMessage,
43 | useSendMessage,
44 | createBoxFixture,
45 | createCircleFixture,
46 | Body,
47 | BodySync,
48 | useAddInstance,
49 | useInstancedMesh,
50 | Instance,
51 | InstancedMesh,
52 | InstancesProvider,
53 | physicsWorkerHandler,
54 | };
55 |
--------------------------------------------------------------------------------
/src/logic/ApiWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, {createContext, useContext} from 'react';
2 | import {MessageData} from "../shared/types";
3 | import PhysicsHandler from "./logicWorkerApp/PhysicsHandler";
4 | import CollisionsProvider from "../shared/CollisionsProvider";
5 | import MeshRefs from "../main/MeshRefs";
6 | import Messages from "../shared/Messages";
7 | import SendMessages from "../shared/SendMessages";
8 | import MessageHandler from "./logicWorkerApp/MessageHandler";
9 |
10 | export type ContextState = {
11 | physicsWorker: Worker | MessagePort;
12 | sendMessageToMain: (message: MessageData) => void;
13 | };
14 |
15 | export const Context = createContext((null as unknown) as ContextState);
16 |
17 | export const useWorkerAppContext = (): ContextState => {
18 | return useContext(Context);
19 | };
20 |
21 | export const useSendMessageToMain = () => {
22 | return useWorkerAppContext().sendMessageToMain;
23 | };
24 |
25 | const ApiWrapper: React.FC<{
26 | worker: Worker,
27 | physicsWorker: Worker | MessagePort,
28 | sendMessageToMain: (message: MessageData) => void,
29 | }> = ({
30 | children,
31 | worker,
32 | physicsWorker,
33 | sendMessageToMain
34 | }) => {
35 | return (
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {children}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default ApiWrapper;
55 |
56 | export const withLogicWrapper = (WrappedComponent: any) => {
57 | return (props: any) => {
58 | return (
59 |
60 |
61 |
62 | );
63 | };
64 | };
65 |
--------------------------------------------------------------------------------
/src/logic/logicWorkerApp/MessageHandler.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect} from "react"
2 | import {useMessagesContext} from "../../shared/Messages";
3 | import {WorkerOwnerMessageType} from "../../main/worker/shared/types";
4 | import {MessageData} from "../../shared/types";
5 |
6 | const MessageHandler: React.FC<{
7 | worker: Worker,
8 | }> = ({children, worker}) => {
9 |
10 | const { handleMessage } = useMessagesContext();
11 |
12 | useEffect(() => {
13 |
14 | worker.onmessage = (event: MessageEvent) => {
15 | const type = event.data.type;
16 |
17 | switch (type) {
18 | case WorkerOwnerMessageType.MESSAGE:
19 | handleMessage(event.data.message as MessageData);
20 | break;
21 | }
22 | };
23 | }, [worker]);
24 |
25 | return (
26 | <>
27 | {children}
28 | >
29 | )
30 | }
31 |
32 | export default MessageHandler
--------------------------------------------------------------------------------
/src/logic/logicWorkerApp/PhysicsHandler.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import StoredPhysicsData from '../../shared/StoredPhysicsData';
3 | import MeshSubscriptions from '../../shared/MeshSubscriptions';
4 | import WorkerOnMessageProvider from '../../shared/WorkerOnMessageProvider';
5 | import PhysicsSync from '../../shared/PhysicsSync';
6 | import PhysicsProvider from '../../shared/PhysicsProvider';
7 | import {useWorkerMessages} from "../../main/hooks/useWorkerMessages";
8 |
9 | const PhysicsHandler: React.FC<{
10 | worker: null | Worker | MessagePort;
11 | }> = ({ children, worker }) => {
12 | if (!worker) return null;
13 |
14 | const subscribe = useWorkerMessages(worker)
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 | {children}
23 |
24 |
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default PhysicsHandler;
32 |
--------------------------------------------------------------------------------
/src/logic/logicWorkerApp/hooks/messaging.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import {
3 | MessageKeys,
4 | SyncComponentMessageInfo,
5 | SyncComponentMessageType,
6 | } from '../../../shared/types';
7 | import {useSendMessageToMain} from "../../ApiWrapper";
8 |
9 | export const useSendSyncComponentMessage = () => {
10 | const sendMessageRaw = useSendMessageToMain();
11 |
12 | const sendMessage = useCallback(
13 | (
14 | messageType: SyncComponentMessageType,
15 | info: SyncComponentMessageInfo,
16 | data?: any
17 | ) => {
18 | sendMessageRaw({
19 | key: MessageKeys.SYNC_COMPONENT,
20 | data: {
21 | messageType,
22 | info,
23 | data,
24 | },
25 | });
26 | },
27 | [sendMessageRaw]
28 | );
29 |
30 | return sendMessage;
31 | };
32 |
--------------------------------------------------------------------------------
/src/logic/logicWorkerApp/hooks/sync.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useMemo } from 'react';
2 | import {
3 | SyncComponentMessageType,
4 | SyncComponentType,
5 | } from '../../../shared/types';
6 | import { useSendSyncComponentMessage } from './messaging';
7 |
8 | export const useSyncWithMainComponent = (
9 | componentType: SyncComponentType,
10 | componentKey: string,
11 | initialProps?: any
12 | ) => {
13 | const sendMessage = useSendSyncComponentMessage();
14 |
15 | const info = useMemo(
16 | () => ({
17 | componentType,
18 | componentKey,
19 | }),
20 | [componentType, componentKey]
21 | );
22 |
23 | const updateProps = useCallback(
24 | (props: any) => {
25 | sendMessage(SyncComponentMessageType.UPDATE, info, props);
26 | },
27 | [info]
28 | );
29 |
30 | useEffect(() => {
31 | sendMessage(SyncComponentMessageType.MOUNT, info, initialProps);
32 |
33 | return () => {
34 | sendMessage(SyncComponentMessageType.UNMOUNT, info);
35 | };
36 | }, [info]);
37 |
38 | return updateProps;
39 | };
40 |
--------------------------------------------------------------------------------
/src/logic/logicWorkerApp/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import { FC, useEffect } from 'react';
3 | import { useProxy } from 'valtio';
4 | import { MessageData } from '../../shared/types';
5 | import { WorkerOwnerMessageType } from '../../main/worker/shared/types';
6 |
7 | const WorkerApp: FC<{
8 | worker: Worker;
9 | workerRef: {
10 | physicsWorker: null | Worker | MessagePort;
11 | };
12 | state: {
13 | physicsWorkerLoaded: boolean;
14 | initiated: boolean;
15 | };
16 | app: any;
17 | }> = ({ app, worker, state, workerRef }) => {
18 | const proxyState = useProxy(state);
19 | const initiated = proxyState.initiated;
20 | const physicsWorkerLoaded = proxyState.physicsWorkerLoaded;
21 | const [physicsWorker, setPhysicsWorker] = useState<
22 | null | Worker | MessagePort
23 | >(null);
24 |
25 | useEffect(() => {
26 | if (physicsWorkerLoaded) {
27 | if (!workerRef.physicsWorker) {
28 | throw new Error(`Worker missing.`);
29 | }
30 | setPhysicsWorker(workerRef.physicsWorker);
31 | }
32 | }, [physicsWorkerLoaded]);
33 |
34 | const sendMessageToMain = useCallback(
35 | (message: MessageData) => {
36 | const update = {
37 | type: WorkerOwnerMessageType.MESSAGE,
38 | message,
39 | };
40 |
41 | worker.postMessage(update);
42 | },
43 | [worker]
44 | );
45 |
46 | if (!initiated || !physicsWorker) return null;
47 |
48 | const App = app
49 |
50 | return (
51 |
52 | )
53 |
54 | };
55 |
56 | export { WorkerApp };
57 |
--------------------------------------------------------------------------------
/src/logic/workerHelper.ts:
--------------------------------------------------------------------------------
1 | import { render } from 'react-nil';
2 | import { createElement, FC } from 'react';
3 | import { proxy } from 'valtio';
4 | import { WorkerMessageType } from '../main/worker/shared/types';
5 |
6 | export const logicWorkerHandler = (selfWorker: Worker, app: FC) => {
7 | let physicsWorkerPort: MessagePort;
8 |
9 | const state = proxy<{
10 | physicsWorkerLoaded: boolean;
11 | initiated: boolean;
12 | }>({
13 | physicsWorkerLoaded: false,
14 | initiated: false,
15 | });
16 |
17 | const workerRef: {
18 | physicsWorker: null | Worker | MessagePort;
19 | } = {
20 | physicsWorker: null,
21 | };
22 |
23 | selfWorker.onmessage = (event: MessageEvent) => {
24 | switch (event.data.command) {
25 | case 'connect':
26 | physicsWorkerPort = event.ports[0];
27 | workerRef.physicsWorker = physicsWorkerPort;
28 | state.physicsWorkerLoaded = true;
29 | return;
30 |
31 | case 'forward':
32 | physicsWorkerPort.postMessage(event.data.message);
33 | return;
34 | }
35 |
36 | const { type, props = {} } = event.data as {
37 | type: WorkerMessageType;
38 | props: any;
39 | };
40 |
41 | switch (type) {
42 | case WorkerMessageType.INIT:
43 | state.initiated = true;
44 | break;
45 | }
46 | };
47 | render(
48 | createElement(
49 | require('./logicWorkerApp/index').WorkerApp,
50 | {
51 | worker: selfWorker,
52 | state,
53 | workerRef,
54 | app,
55 | },
56 | null
57 | )
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/src/main/Body.tsx:
--------------------------------------------------------------------------------
1 | import React, {MutableRefObject, ReactElement, useRef} from "react"
2 | import {ValidUUID} from "./worker/shared/types";
3 | import {Object3D} from "three";
4 | import {BodyApi, BodyParams, useBody, useBodyApi} from "./hooks/useBody";
5 | import {AddBodyDef} from "./worker/planckjs/bodies";
6 | import { useSubscribeMesh } from "../shared/MeshSubscriptions";
7 |
8 | export const BodySync: React.FC<{
9 | children: ({uuid, ref, api}: {uuid: ValidUUID, ref: MutableRefObject, api?: BodyApi}) => ReactElement,
10 | uuid: ValidUUID,
11 | applyAngle?: boolean,
12 | isDynamic?: boolean,
13 | bodyRef?: MutableRefObject,
14 | wrapWithGroup?: boolean,
15 | }> = ({
16 | children,
17 | uuid,
18 | bodyRef,
19 | applyAngle = true,
20 | isDynamic = true,
21 | wrapWithGroup = false,
22 | }) => {
23 |
24 | const localRef = useRef(new Object3D())
25 |
26 | const ref = bodyRef ?? localRef
27 |
28 | useSubscribeMesh(uuid, ref.current, applyAngle, isDynamic)
29 |
30 | const api = useBodyApi(uuid)
31 |
32 | const inner = children({uuid, ref, api: api ?? undefined})
33 |
34 | if (wrapWithGroup) {
35 | return (
36 |
37 | {inner}
38 |
39 | )
40 | }
41 |
42 | return inner
43 |
44 | }
45 |
46 | export const Body: React.FC<{
47 | children: ({uuid, ref, api}: {uuid: ValidUUID, ref: MutableRefObject, api: BodyApi}) => ReactElement,
48 | bodyDef: AddBodyDef,
49 | params?: BodyParams,
50 | wrapWithGroup?: boolean,
51 | }> = ({children, params, bodyDef, wrapWithGroup}) => {
52 |
53 | const [ref, api, uuid] = useBody(() => bodyDef, params)
54 |
55 | const inner = children({ref, uuid, api})
56 |
57 | if (wrapWithGroup) {
58 | return (
59 |
60 | {inner}
61 |
62 | )
63 | }
64 |
65 | return inner
66 | }
--------------------------------------------------------------------------------
/src/main/Engine.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import PhysicsWorker from './PhysicsWorker';
3 | import R3FPhysicsObjectUpdater from './R3FPhysicsObjectUpdater';
4 | import CollisionsProvider from '../shared/CollisionsProvider';
5 | import { MappedComponents } from '../shared/types';
6 | import MeshRefs from "./MeshRefs";
7 | import {PhysicsProps} from "./worker/shared/types";
8 | import {LogicWorker} from "./LogicWorker";
9 |
10 | export const Engine: FC = ({
15 | children,
16 | physicsWorker,
17 | config,
18 | worldParams,
19 | logicWorker,
20 | logicMappedComponents = {},
21 | }) => {
22 | if (logicWorker) {
23 | return (
24 |
25 |
26 |
27 |
28 |
32 | {children}
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
41 | return (
42 |
43 |
44 |
45 |
46 | {children}
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/main/InstancesProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, {createContext, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState} from "react";
2 | import {InstancedMesh as TInstancedMesh, Matrix4, Mesh, Object3D} from "three";
3 | import {useGLTF} from "@react-three/drei/core/useGLTF";
4 | import {useDidMount} from "../utils/hooks";
5 |
6 | type InstanceData = {
7 | position?: [number, number, number],
8 | rotation?: [number, number, number],
9 | scale?: [number, number, number]
10 | }
11 |
12 | type UpdateInstanceFn = (data: InstanceData) => void
13 |
14 | type AddInstanceFn = (data: InstanceData) => [UpdateInstanceFn, () => void]
15 |
16 | type InstancedMeshes = {
17 | [key: string]: AddInstanceFn,
18 | }
19 |
20 |
21 | type ContextState = {
22 | instancedMeshes: InstancedMeshes,
23 | createInstancedMesh: (meshKey: string, addInstance: AddInstanceFn) => () => void,
24 | }
25 |
26 | const Context = createContext(null as unknown as ContextState)
27 |
28 | export const useCreateInstancedMesh = () => {
29 | return useContext(Context).createInstancedMesh
30 | }
31 |
32 | export const useAddInstance = (meshKey: string) => {
33 | const instancedMeshes = useContext(Context).instancedMeshes
34 | const addInstance = instancedMeshes[meshKey] ?? null
35 | return addInstance
36 | }
37 |
38 | export const useInstancedMesh = (meshKey: string, data: InstanceData) => {
39 | const updateRef = useRef()
40 | const addInstance = useAddInstance(meshKey)
41 |
42 | useEffect(() => {
43 |
44 | if (addInstance) {
45 |
46 | const [update, remove] = addInstance(data)
47 | updateRef.current = update
48 | return remove
49 |
50 | }
51 |
52 | return
53 |
54 | }, [addInstance, updateRef])
55 |
56 | return useCallback((data: InstanceData) => {
57 | if (updateRef.current) {
58 | updateRef.current(data)
59 | }
60 | }, [updateRef])
61 |
62 | }
63 |
64 | export const Instance: React.FC<{
65 | meshKey: string,
66 | } & InstanceData> = ({meshKey, ...data}) => {
67 |
68 | const isMountRef = useRef(true)
69 |
70 | const update = useInstancedMesh(meshKey, data)
71 |
72 | const {position, rotation, scale} = data
73 |
74 | useEffect(() => {
75 | if (isMountRef.current) {
76 | isMountRef.current = false
77 | return
78 | }
79 | update({position, rotation, scale})
80 | }, [position, rotation, scale, update])
81 |
82 | return null
83 |
84 | }
85 |
86 | const defaultObject = new Object3D()
87 |
88 | export const InstancedMesh: React.FC<{
89 | meshKey: string,
90 | maxInstances: number,
91 | gltfPath: string,
92 | meshProps?: JSX.IntrinsicElements['instancedMesh']
93 | }> = ({meshKey, maxInstances, gltfPath, meshProps = {}}) => {
94 |
95 | const createInstancedMesh = useCreateInstancedMesh()
96 |
97 | const {nodes} = useGLTF(gltfPath) as unknown as {nodes: {
98 | [key: string]: Object3D,
99 | }}
100 |
101 | const meshes = Object.values(nodes).filter(node => node.type === "Mesh") as Mesh[]
102 |
103 | const instancedMeshes = useRef([])
104 |
105 | const handleRef = useCallback((instancedMesh: TInstancedMesh) => {
106 | instancedMeshes.current.push(instancedMesh)
107 | }, [instancedMeshes])
108 |
109 | useLayoutEffect(() => {
110 | instancedMeshes.current.forEach(instancedMesh => {
111 | instancedMesh.count = 0
112 | })
113 | }, [instancedMeshes])
114 |
115 | const idRef = useRef(0)
116 | const instancesRef = useRef([])
117 |
118 | const removeInstance = useCallback((id: string) => {
119 |
120 | const instanceIndex = instancesRef.current.findIndex(instanceId => instanceId === id)
121 |
122 | instancedMeshes.current.forEach(instancedMesh => {
123 | const count = instancedMesh.count
124 |
125 | for (let i = instanceIndex + 1; i < count; i++) {
126 | const previousIndex = i - 1
127 | if (previousIndex < 0) break
128 | const matrix = new Matrix4()
129 | instancedMeshes.current.forEach(instancedMesh => {
130 | instancedMesh.getMatrixAt(i, matrix)
131 | instancedMesh.setMatrixAt(previousIndex, matrix)
132 | })
133 | }
134 |
135 | })
136 |
137 | instancesRef.current.splice(instanceIndex, 1)
138 |
139 | instancedMeshes.current.forEach(instancedMesh => {
140 | instancedMesh.count = instancesRef.current.length
141 | })
142 |
143 | }, [instancedMeshes, instancesRef])
144 |
145 | const updateInstance = useCallback((id: string, {
146 | position = [0, 0, 0],
147 | rotation = [0, 0, 0],
148 | scale = [1, 1, 1]
149 | }: InstanceData) => {
150 |
151 | const instanceIndex = instancesRef.current.findIndex(instanceId => instanceId === id)
152 |
153 | defaultObject.position.set(...position)
154 | defaultObject.rotation.set(...rotation)
155 | defaultObject.scale.set(...scale)
156 | defaultObject.updateMatrix()
157 |
158 | instancedMeshes.current.forEach(instancedMesh => {
159 | instancedMesh.setMatrixAt(instanceIndex, defaultObject.matrix)
160 | instancedMesh.instanceMatrix.needsUpdate = true
161 | })
162 |
163 | }, [instancedMeshes, instancesRef])
164 |
165 | const addInstance = useCallback((data: InstanceData): [UpdateInstanceFn, () => void] => {
166 |
167 | idRef.current += 1
168 | const id = `${idRef.current}`
169 |
170 | instancesRef.current.push(id)
171 | updateInstance(id, data)
172 |
173 | instancedMeshes.current.forEach(instancedMesh => {
174 | instancedMesh.count = instancesRef.current.length
175 | })
176 |
177 | const update = (updateData: InstanceData) => {
178 | updateInstance(id, updateData)
179 | }
180 |
181 | const unsubscribe = () => {
182 | removeInstance(id)
183 | }
184 |
185 | return [update, unsubscribe]
186 |
187 | }, [instancedMeshes, idRef, updateInstance, removeInstance])
188 |
189 | useEffect(() => {
190 | const unsubscribe = createInstancedMesh(meshKey, addInstance)
191 |
192 | return unsubscribe
193 | }, [createInstancedMesh, addInstance])
194 |
195 | return (
196 | <>
197 | {meshes.map((mesh) => (
198 |
200 | ))}
201 | >
202 | )
203 | }
204 |
205 | export const InstancesProvider: React.FC = ({children}) => {
206 |
207 | const [instancedMeshes, setInstancedMeshes] = useState({})
208 |
209 | const createInstancedMesh = useCallback((meshKey: string, addInstance: AddInstanceFn) => {
210 |
211 | setInstancedMeshes(state => {
212 | return {
213 | ...state,
214 | [meshKey]: addInstance,
215 | }
216 | })
217 |
218 | const unsubscribe = () => {
219 | setInstancedMeshes(state => {
220 | const updatedState = {
221 | ...state,
222 | }
223 | delete updatedState[meshKey]
224 | return updatedState
225 | })
226 | }
227 |
228 | return unsubscribe
229 |
230 | }, [setInstancedMeshes])
231 |
232 | return (
233 |
237 | {children}
238 |
239 | );
240 | };
--------------------------------------------------------------------------------
/src/main/LogicHandler.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useOnMessage } from '../shared/Messages';
3 | import {
4 | MappedComponents,
5 | MessageKeys,
6 | SyncComponentMessage,
7 | SyncComponentMessageType,
8 | SyncComponentType,
9 | ValidProps,
10 | } from '../shared/types';
11 |
12 | const LogicHandler: React.FC<{
13 | mappedComponentTypes: MappedComponents;
14 | }> = ({ children, mappedComponentTypes }) => {
15 | const subscribeToMessage = useOnMessage();
16 |
17 | const [components, setComponents] = useState<{
18 | [key: string]: {
19 | componentType: SyncComponentType;
20 | props: ValidProps;
21 | };
22 | }>({});
23 |
24 | useEffect(() => {
25 | const unsubscribe = subscribeToMessage(
26 | MessageKeys.SYNC_COMPONENT,
27 | ({ info, messageType, data }: SyncComponentMessage) => {
28 | const props = data || {};
29 |
30 | switch (messageType) {
31 | case SyncComponentMessageType.MOUNT:
32 | setComponents(state => {
33 | return {
34 | ...state,
35 | [info.componentKey]: {
36 | componentType: info.componentType,
37 | props,
38 | },
39 | };
40 | });
41 | break;
42 | case SyncComponentMessageType.UPDATE:
43 | setComponents(state => {
44 | const previousData = state[info.componentKey]
45 | const previousProps = previousData && previousData.props ? previousData.props : {}
46 | return {
47 | ...state,
48 | [info.componentKey]: {
49 | componentType: info.componentType,
50 | props: {
51 | ...previousProps,
52 | ...props,
53 | },
54 | },
55 | };
56 | });
57 | break;
58 | case SyncComponentMessageType.UNMOUNT:
59 | setComponents(state => {
60 | let update = {
61 | ...state,
62 | };
63 | delete update[info.componentKey];
64 | return update;
65 | });
66 | break;
67 | }
68 | }
69 | );
70 |
71 | return () => {
72 | unsubscribe();
73 | };
74 | }, []);
75 |
76 | return (
77 | <>
78 | {children}
79 | {Object.entries(components).map(([key, { componentType, props }]) => {
80 | const Component = mappedComponentTypes[componentType];
81 | return Component ? : null;
82 | })}
83 | >
84 | );
85 | };
86 |
87 | export default LogicHandler;
88 |
--------------------------------------------------------------------------------
/src/main/LogicWorker.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { usePhysicsWorker } from './PhysicsWorker';
3 | import {
4 | WorkerMessageType,
5 | WorkerOwnerMessageType,
6 | } from './worker/shared/types';
7 | import { MappedComponents, MessageData } from '../shared/types';
8 | import Messages, { useMessagesContext } from '../shared/Messages';
9 | import LogicHandler from './LogicHandler';
10 | import SendMessages from "../shared/SendMessages";
11 |
12 | const LogicWorkerInner: React.FC<{
13 | worker: Worker;
14 | }> = ({ children, worker }) => {
15 | const physicsWorker = usePhysicsWorker();
16 |
17 | const { handleMessage } = useMessagesContext();
18 |
19 | useEffect(() => {
20 | const channel = new MessageChannel();
21 | physicsWorker.postMessage({ command: 'connect' }, [channel.port1]);
22 | worker.postMessage({ command: 'connect' }, [channel.port2]);
23 |
24 | worker.onmessage = (event: MessageEvent) => {
25 | const type = event.data.type;
26 |
27 | switch (type) {
28 | case WorkerOwnerMessageType.MESSAGE:
29 | handleMessage(event.data.message as MessageData);
30 | break;
31 | }
32 | };
33 |
34 | worker.postMessage({
35 | type: WorkerMessageType.INIT,
36 | });
37 | }, [worker, physicsWorker]);
38 |
39 | return <>{children}>;
40 | };
41 |
42 | export const LogicWorker: React.FC<{
43 | worker: Worker;
44 | logicMappedComponents: MappedComponents;
45 | }> = ({ worker, children, logicMappedComponents }) => {
46 | return (
47 |
48 |
49 |
50 |
51 | {children}
52 |
53 |
54 |
55 |
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/src/main/MeshRefs.tsx:
--------------------------------------------------------------------------------
1 | import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from "react"
2 | import {Object3D} from "three";
3 |
4 | type MeshRefsContextState = {
5 | meshes: {
6 | [key: string]: Object3D,
7 | },
8 | addMesh: (uuid: string, mesh: Object3D) => () => void,
9 | }
10 |
11 | const MeshRefsContext = createContext(null as unknown as MeshRefsContextState)
12 |
13 | export const useStoreMesh = (uuid: string, mesh: Object3D) => {
14 | const addMesh = useContext(MeshRefsContext).addMesh
15 |
16 | useEffect(() => {
17 |
18 | const remove = addMesh(uuid, mesh)
19 |
20 | return () => {
21 | remove()
22 | }
23 |
24 | }, [addMesh, uuid, mesh])
25 |
26 | }
27 |
28 | export const useStoredMesh = (uuid: string): Object3D | null => {
29 | const meshes = useContext(MeshRefsContext).meshes
30 |
31 | const mesh = useMemo(() => {
32 | return meshes[uuid] ?? null
33 | }, [uuid, meshes])
34 |
35 | return mesh
36 | }
37 |
38 | const MeshRefs: React.FC = ({children}) => {
39 |
40 | const [meshes, setMeshes] = useState<{
41 | [key: string]: Object3D,
42 | }>({})
43 |
44 | const addMesh = useCallback((uuid: string, mesh: Object3D) => {
45 |
46 | setMeshes(state => {
47 | return {
48 | ...state,
49 | [uuid]: mesh,
50 | }
51 | })
52 |
53 | const removeMesh = () => {
54 | setMeshes(state => {
55 | const updated = {
56 | ...state,
57 | }
58 | delete updated[uuid]
59 | return updated
60 | })
61 | }
62 |
63 | return removeMesh
64 |
65 | }, [setMeshes])
66 |
67 | return (
68 |
72 | {children}
73 |
74 | )
75 | }
76 |
77 | export default MeshRefs
--------------------------------------------------------------------------------
/src/main/PhysicsWorker.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | FC,
4 | useContext,
5 | useEffect,
6 | useState,
7 | } from 'react';
8 | import {PhysicsProps, WorkerMessageType, WorkerOwnerMessageType} from './worker/shared/types';
9 | import WorkerOnMessageProvider from '../shared/WorkerOnMessageProvider';
10 | import PhysicsSync from '../shared/PhysicsSync';
11 | import StoredPhysicsData from '../shared/StoredPhysicsData';
12 | import MeshSubscriptions from '../shared/MeshSubscriptions';
13 | import PhysicsProvider from '../shared/PhysicsProvider';
14 | import {useWorkerMessages} from "./hooks/useWorkerMessages";
15 |
16 | type ContextState = {
17 | worker: Worker;
18 | };
19 |
20 | const Context = createContext((null as unknown) as ContextState);
21 |
22 | export const usePhysicsWorker = () => {
23 | return useContext(Context).worker;
24 | };
25 |
26 | const PhysicsWorker: FC = ({ children, physicsWorker, config, worldParams }) => {
29 |
30 | const worker = physicsWorker
31 |
32 | const [initiated, setInitiated] = useState(false)
33 |
34 | useEffect(() => {
35 | worker.postMessage({
36 | type: WorkerMessageType.INIT,
37 | props: {
38 | config,
39 | worldParams,
40 | },
41 | });
42 | }, [worker]);
43 |
44 | const subscribe = useWorkerMessages(worker)
45 |
46 | useEffect(() => {
47 |
48 | const unsubscribe = subscribe((event) => {
49 |
50 | const type = event.data.type;
51 |
52 | if (type === WorkerOwnerMessageType.INITIATED) {
53 | setInitiated(true)
54 | }
55 |
56 | return () => {
57 | unsubscribe()
58 | }
59 | })
60 |
61 | }, [subscribe, setInitiated])
62 |
63 | if (!initiated) return null
64 |
65 | return (
66 |
71 |
72 |
73 |
74 |
75 | {children}
76 |
77 |
78 |
79 |
80 |
81 | );
82 | };
83 |
84 | export default PhysicsWorker;
85 |
--------------------------------------------------------------------------------
/src/main/R3FPhysicsObjectUpdater.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { useFrame } from 'react-three-fiber';
3 | import { useGetPhysicsStepTimeRemainingRatio } from '../shared/PhysicsSync';
4 | import { useLerpMeshes } from '../shared/MeshSubscriptions';
5 |
6 | const R3FPhysicsObjectUpdater: React.FC = ({ children }) => {
7 | const getPhysicsStepTimeRemainingRatio = useGetPhysicsStepTimeRemainingRatio();
8 | const lerpMeshes = useLerpMeshes();
9 |
10 | const onFrame = useCallback((state: any, delta: number) => {
11 | lerpMeshes(getPhysicsStepTimeRemainingRatio);
12 | }, [getPhysicsStepTimeRemainingRatio, lerpMeshes]);
13 |
14 | useFrame(onFrame);
15 |
16 | return <>{children}>;
17 | };
18 |
19 | export default R3FPhysicsObjectUpdater;
20 |
--------------------------------------------------------------------------------
/src/main/hooks/useBody.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AddBodyDef,
3 | BodyType,
4 | UpdateBodyData,
5 | } from '../worker/planckjs/bodies';
6 | import {
7 | MutableRefObject,
8 | useLayoutEffect,
9 | useMemo,
10 | useRef,
11 | useState,
12 | } from 'react';
13 | import { Object3D } from 'three';
14 | import { usePhysicsProvider } from '../../shared/PhysicsProvider';
15 | import { ValidUUID } from '../worker/shared/types';
16 | import { Vec2 } from 'planck-js';
17 | import {
18 | useAddMeshSubscription,
19 | useSubscribeMesh,
20 | } from '../../shared/MeshSubscriptions';
21 |
22 | export type BodyApi = {
23 | applyForceToCenter: (vec: Vec2, uuid?: ValidUUID) => void;
24 | applyLinearImpulse: (vec: Vec2, pos: Vec2, uuid?: ValidUUID) => void;
25 | setPosition: (vec: Vec2, uuid?: ValidUUID) => void;
26 | setLinearVelocity: (vec: Vec2, uuid?: ValidUUID) => void;
27 | setAngle: (angle: number, uuid?: ValidUUID) => void;
28 | updateBody: (data: UpdateBodyData, uuid?: ValidUUID) => void;
29 | };
30 |
31 | export const useBodyApi = (passedUuid: ValidUUID): BodyApi => {
32 | const { workerSetBody, workerUpdateBody } = usePhysicsProvider();
33 |
34 | const api = useMemo(() => {
35 | return {
36 | applyForceToCenter: (vec, uuid) => {
37 | workerSetBody({
38 | uuid: uuid ?? passedUuid,
39 | method: 'applyForceToCenter',
40 | methodParams: [vec, true],
41 | });
42 | },
43 | applyLinearImpulse: (vec, pos, uuid) => {
44 | workerSetBody({
45 | uuid: uuid ?? passedUuid,
46 | method: 'applyLinearImpulse',
47 | methodParams: [vec, pos, true],
48 | });
49 | },
50 | setPosition: (vec, uuid) => {
51 | workerSetBody({
52 | uuid: uuid ?? passedUuid,
53 | method: 'setPosition',
54 | methodParams: [vec],
55 | });
56 | },
57 | setLinearVelocity: (vec, uuid) => {
58 | workerSetBody({
59 | uuid: uuid ?? passedUuid,
60 | method: 'setLinearVelocity',
61 | methodParams: [vec],
62 | });
63 | },
64 | updateBody: (data: UpdateBodyData, uuid) => {
65 | workerUpdateBody({ uuid: uuid ?? passedUuid, data });
66 | },
67 | setAngle: (angle: number, uuid) => {
68 | workerSetBody({
69 | uuid: uuid ?? passedUuid,
70 | method: 'setAngle',
71 | methodParams: [angle],
72 | });
73 | },
74 | };
75 | }, [passedUuid]);
76 |
77 | return api;
78 | };
79 |
80 | export type BodyParams = {
81 | syncBody?: boolean,
82 | listenForCollisions?: boolean;
83 | applyAngle?: boolean;
84 | cacheKey?: string;
85 | uuid?: ValidUUID;
86 | fwdRef?: MutableRefObject;
87 | };
88 |
89 | export const useBody = (
90 | propsFn: () => AddBodyDef,
91 | bodyParams: BodyParams = {}
92 | ): [MutableRefObject, BodyApi, ValidUUID] => {
93 | const {
94 | applyAngle = false,
95 | cacheKey,
96 | uuid: passedUUID,
97 | fwdRef,
98 | listenForCollisions = false,
99 | syncBody = true,
100 | } = bodyParams;
101 | const localRef = useRef((null as unknown) as Object3D);
102 | const ref = fwdRef ? fwdRef : localRef;
103 | const [uuid] = useState(() => {
104 | if (passedUUID) return passedUUID;
105 | if (!ref.current) {
106 | ref.current = new Object3D();
107 | }
108 | return ref.current.uuid;
109 | });
110 | const [isDynamic] = useState(() => {
111 | const props = propsFn();
112 | return props.type !== BodyType.static;
113 | });
114 | const { workerAddBody, workerRemoveBody } = usePhysicsProvider();
115 |
116 | useLayoutEffect(() => {
117 | const props = propsFn();
118 |
119 | const object = ref.current;
120 |
121 | if (object) {
122 | object.position.x = props.position?.x || 0;
123 | object.position.y = props.position?.y || 0;
124 | }
125 |
126 | workerAddBody({
127 | uuid,
128 | listenForCollisions,
129 | cacheKey,
130 | ...props,
131 | });
132 |
133 | return () => {
134 | workerRemoveBody({ uuid, cacheKey });
135 | };
136 | }, []);
137 |
138 | useSubscribeMesh(uuid, ref.current, applyAngle, isDynamic && syncBody);
139 |
140 | const api = useBodyApi(uuid);
141 |
142 | return [ref, api, uuid];
143 | };
144 |
--------------------------------------------------------------------------------
/src/main/hooks/useCollisionEvents.ts:
--------------------------------------------------------------------------------
1 | import {ValidUUID} from "../worker/shared/types";
2 | import {useCollisionsProviderContext} from "../../shared/CollisionsProvider";
3 | import {useEffect} from "react";
4 |
5 | export const useCollisionEvents = (
6 | uuid: ValidUUID,
7 | onCollideStart?: (data: any, fixtureIndex: number, isSensor: boolean) => void,
8 | onCollideEnd?: (data: any, fixtureIndex: number, isSensor: boolean) => void,
9 | ) => {
10 |
11 | const {
12 | addCollisionHandler,
13 | removeCollisionHandler
14 | } = useCollisionsProviderContext()
15 |
16 | // @ts-ignore
17 | useEffect(() => {
18 | if (onCollideStart) {
19 | addCollisionHandler(true, uuid, onCollideStart)
20 | return () => {
21 | removeCollisionHandler(true, uuid)
22 | }
23 | }
24 | }, [uuid, onCollideStart])
25 |
26 | // @ts-ignore
27 | useEffect(() => {
28 | if (onCollideEnd) {
29 | addCollisionHandler(false, uuid, onCollideEnd)
30 | return () => {
31 | removeCollisionHandler(false, uuid)
32 | }
33 | }
34 | }, [uuid, onCollideEnd])
35 |
36 | }
--------------------------------------------------------------------------------
/src/main/hooks/useWorkerMessages.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef } from 'react';
2 |
3 | export type Subscribe = (callback: (event: MessageEvent) => void) => () => void;
4 |
5 | export const useWorkerMessages = (worker: undefined | Worker | MessagePort) => {
6 | const idCount = useRef(0);
7 | const subscriptionsRef = useRef<{
8 | [key: string]: (event: MessageEvent) => void;
9 | }>({});
10 |
11 | const subscribe = useCallback(
12 | (callback: (event: MessageEvent) => void) => {
13 | const id = idCount.current;
14 | idCount.current += 1;
15 |
16 | subscriptionsRef.current[id] = callback;
17 |
18 | return () => {
19 | delete subscriptionsRef.current[id];
20 | };
21 | },
22 | [subscriptionsRef]
23 | );
24 |
25 | useEffect(() => {
26 | if (!worker) return;
27 | const previousOnMessage = worker.onmessage;
28 | worker.onmessage = (event: MessageEvent) => {
29 | Object.values(subscriptionsRef.current).forEach(callback => {
30 | callback(event);
31 | });
32 | if (previousOnMessage) {
33 | (previousOnMessage as any)(event);
34 | }
35 | };
36 | }, [worker, subscriptionsRef]);
37 |
38 | return subscribe;
39 | };
40 |
--------------------------------------------------------------------------------
/src/main/worker/app/Bodies.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useState } from 'react';
2 | import { useAppContext, useWorld } from './appContext';
3 | import { ValidUUID, WorkerMessageType } from '../shared/types';
4 | import {
5 | Body,
6 | BodyDef,
7 | Box,
8 | Circle,
9 | DistanceJoint,
10 | FixtureOpt,
11 | Joint,
12 | RopeJoint,
13 | Vec2,
14 | } from 'planck-js';
15 | import {
16 | AddBodyProps,
17 | BodyShape,
18 | BodyType,
19 | BoxFixture,
20 | CircleFixture,
21 | RemoveBodyProps,
22 | SetBodyProps,
23 | UpdateBodyProps,
24 | } from '../planckjs/bodies';
25 | import { Shape } from 'planck-js/lib/shape';
26 | import { useWorldState } from './WorldState';
27 |
28 | const tempVec = Vec2(0, 0);
29 |
30 | export const useSubscribeToWorkerMessages = (
31 | messageHandler: (event: MessageEvent) => void
32 | ) => {
33 | const { subscribe, logicSubscribe } = useAppContext();
34 |
35 | useEffect(() => {
36 | const unsubscribe = subscribe(messageHandler);
37 |
38 | const unsubscribeLogic = logicSubscribe(messageHandler);
39 |
40 | return () => {
41 | unsubscribe();
42 | unsubscribeLogic();
43 | };
44 | }, [subscribe, logicSubscribe, messageHandler]);
45 | };
46 |
47 | type BodiesMap = Map;
48 | type CachedBodiesMap = Map;
49 |
50 | const applyBodyConfigToExistingBody = (
51 | body: Body,
52 | data: AddBodyProps
53 | ): Body => {
54 | const {
55 | uuid,
56 | cacheKey,
57 | listenForCollisions,
58 | fixtures = [],
59 | attachToRope = false,
60 | ...props
61 | } = data;
62 |
63 | if (fixtures && fixtures.length > 0) {
64 | let bodyFixture = body.getFixtureList();
65 |
66 | fixtures.forEach((fixture, fixtureIndex) => {
67 | let fixtureOptions = fixture.fixtureOptions;
68 |
69 | fixtureOptions = {
70 | userData: {
71 | uuid,
72 | fixtureIndex,
73 | ...fixtureOptions?.userData,
74 | },
75 | ...fixtureOptions,
76 | };
77 |
78 | if (bodyFixture) {
79 | if (fixtureOptions) {
80 | bodyFixture.setUserData(fixtureOptions.userData);
81 | }
82 |
83 | bodyFixture = bodyFixture.getNext();
84 | }
85 | });
86 | }
87 |
88 | const { position, angle } = props;
89 |
90 | if (position) {
91 | body.setPosition(position);
92 | }
93 |
94 | if (angle) {
95 | body.setAngle(angle);
96 | }
97 |
98 | body.setActive(true);
99 |
100 | return body;
101 | };
102 |
103 | const useAddBody = (bodies: BodiesMap, cachedBodies: CachedBodiesMap) => {
104 | const {
105 | dynamicBodies,
106 | collisionListeners,
107 | bodiesNeedSyncRef,
108 | logicBodiesNeedSyncRef,
109 | } = useWorldState();
110 |
111 | const addDynamicBody = useCallback(
112 | (uuid: ValidUUID) => {
113 | dynamicBodies.add(uuid);
114 | bodiesNeedSyncRef.current = true;
115 | logicBodiesNeedSyncRef.current = true;
116 | },
117 | [dynamicBodies, bodiesNeedSyncRef, logicBodiesNeedSyncRef]
118 | );
119 |
120 | const addCollisionListeners = useCallback(
121 | (uuid: ValidUUID) => {
122 | collisionListeners.add(uuid);
123 | },
124 | [collisionListeners]
125 | );
126 |
127 | const world = useWorld();
128 |
129 | const getCachedBody = useCallback(
130 | (cacheKey: string) => {
131 | const cached = cachedBodies.get(cacheKey);
132 |
133 | if (cached && cached.length > 0) {
134 | const body = cached.pop();
135 | if (body) {
136 | return body;
137 | }
138 | }
139 |
140 | return null;
141 | },
142 | [cachedBodies]
143 | );
144 |
145 | return useCallback(
146 | (data: AddBodyProps) => {
147 | const {
148 | uuid,
149 | cacheKey,
150 | listenForCollisions,
151 | fixtures = [],
152 | attachToRope = false,
153 | ...props
154 | } = data;
155 |
156 | const existingBody = bodies.get(uuid);
157 |
158 | if (existingBody) {
159 | return existingBody;
160 | }
161 |
162 | if (listenForCollisions) {
163 | addCollisionListeners(uuid);
164 | }
165 |
166 | const bodyDef: BodyDef = {
167 | type: BodyType.static,
168 | fixedRotation: true,
169 | ...props,
170 | };
171 |
172 | const { type } = bodyDef;
173 |
174 | let body: Body | null = null;
175 |
176 | if (cacheKey) {
177 | let cachedBody = getCachedBody(cacheKey);
178 |
179 | if (cachedBody) {
180 | body = applyBodyConfigToExistingBody(cachedBody, data);
181 | }
182 | }
183 |
184 | if (!body) {
185 | body = world.createBody(bodyDef);
186 |
187 | if (fixtures && fixtures.length > 0) {
188 | fixtures.forEach((fixture, fixtureIndex) => {
189 | const { shape } = fixture;
190 |
191 | let fixtureOptions = fixture.fixtureOptions ?? {};
192 |
193 | fixtureOptions = {
194 | ...fixtureOptions,
195 | userData: {
196 | uuid,
197 | fixtureIndex,
198 | ...fixtureOptions?.userData,
199 | },
200 | };
201 |
202 | let bodyShape: Shape;
203 |
204 | switch (shape) {
205 | case BodyShape.box:
206 | const { hx, hy, center } = fixture as BoxFixture;
207 | bodyShape = (Box(
208 | (hx as number) / 2,
209 | (hy as number) / 2,
210 | center ? Vec2(center[0], center[1]) : undefined
211 | ) as unknown) as Shape;
212 | break;
213 | case BodyShape.circle:
214 | const { radius } = fixture as CircleFixture;
215 | bodyShape = (Circle(radius as number) as unknown) as Shape;
216 | break;
217 | default:
218 | throw new Error(`Unhandled body shape ${shape}`);
219 | }
220 |
221 | if (fixtureOptions) {
222 | if (body) {
223 | body.createFixture(bodyShape, fixtureOptions as FixtureOpt);
224 | }
225 | } else {
226 | if (body) {
227 | body.createFixture(bodyShape);
228 | }
229 | }
230 |
231 | // todo - handle rope properly...
232 | if (attachToRope) {
233 | const { position, angle } = props;
234 |
235 | const ropeJointDef = {
236 | maxLength: 0.5,
237 | localAnchorA: position,
238 | localAnchorB: position,
239 | };
240 |
241 | const startingBodyDef: BodyDef = {
242 | type: BodyType.static,
243 | fixedRotation: true,
244 | position,
245 | angle,
246 | };
247 |
248 | const startingBody = world.createBody(startingBodyDef);
249 |
250 | if (body) {
251 | const distanceJoint = DistanceJoint(
252 | {
253 | collideConnected: false,
254 | frequencyHz: 5,
255 | dampingRatio: 0.5,
256 | length: 0.15,
257 | },
258 | startingBody,
259 | body,
260 | position ?? Vec2(0, 0),
261 | position ?? Vec2(0, 0)
262 | );
263 |
264 | const rope2 = world.createJoint(
265 | (RopeJoint(
266 | ropeJointDef,
267 | startingBody,
268 | body,
269 | position ?? Vec2(0, 0)
270 | ) as unknown) as Joint
271 | );
272 | const rope = world.createJoint(
273 | (distanceJoint as unknown) as Joint
274 | );
275 | }
276 | }
277 | });
278 | }
279 | }
280 |
281 | if (type !== BodyType.static) {
282 | addDynamicBody(uuid);
283 | }
284 |
285 | if (!body) {
286 | throw new Error(`No body`);
287 | }
288 |
289 | bodies.set(uuid, body);
290 |
291 | return body;
292 | },
293 | [world, bodies, getCachedBody, addDynamicBody, addCollisionListeners]
294 | );
295 | };
296 |
297 | const useRemoveBody = (bodies: BodiesMap, cachedBodies: CachedBodiesMap) => {
298 | const world = useWorld();
299 | const {
300 | dynamicBodies,
301 | collisionListeners,
302 | bodiesNeedSyncRef,
303 | logicBodiesNeedSyncRef,
304 | } = useWorldState();
305 |
306 | return useCallback(
307 | ({ uuid, cacheKey }: RemoveBodyProps) => {
308 | if (dynamicBodies.has(uuid)) {
309 | dynamicBodies.delete(uuid);
310 | bodiesNeedSyncRef.current = true;
311 | logicBodiesNeedSyncRef.current = true;
312 | }
313 |
314 | collisionListeners.delete(uuid);
315 |
316 | const body = bodies.get(uuid);
317 |
318 | if (!body) {
319 | console.warn(`Body not found for ${uuid}`);
320 | return;
321 | }
322 |
323 | bodies.delete(uuid);
324 |
325 | if (cacheKey) {
326 | tempVec.set(-1000, -1000);
327 | body.setPosition(tempVec);
328 | tempVec.set(0, 0);
329 | body.setLinearVelocity(tempVec);
330 | body.setActive(false);
331 | const cached = cachedBodies.get(cacheKey);
332 | if (cached) {
333 | cached.push(body);
334 | } else {
335 | cachedBodies.set(cacheKey, [body]);
336 | }
337 | } else {
338 | world.destroyBody(body);
339 | }
340 | },
341 | [
342 | world,
343 | bodies,
344 | dynamicBodies,
345 | collisionListeners,
346 | bodiesNeedSyncRef,
347 | logicBodiesNeedSyncRef,
348 | cachedBodies,
349 | ]
350 | );
351 | };
352 |
353 | const useSetBody = (bodies: BodiesMap) => {
354 | return useCallback(
355 | ({ uuid, method, methodParams }: SetBodyProps) => {
356 | const body = bodies.get(uuid);
357 | if (!body) {
358 | console.warn(`Body not found for ${uuid}`, bodies);
359 | return;
360 | }
361 | switch (method) {
362 | default:
363 | (body as any)[method](...methodParams);
364 | }
365 | },
366 | [bodies]
367 | );
368 | };
369 |
370 | const useUpdateBody = (bodies: BodiesMap) => {
371 | return useCallback(
372 | ({ uuid, data }: UpdateBodyProps) => {
373 | const body = bodies.get(uuid);
374 | if (!body) {
375 | console.warn(`Body not found for ${uuid}`);
376 | return;
377 | }
378 | const { fixtureUpdate } = data;
379 | if (fixtureUpdate) {
380 | const fixture = body.getFixtureList();
381 | if (fixture) {
382 | const { groupIndex, categoryBits, maskBits } = fixtureUpdate;
383 | if (
384 | groupIndex !== undefined ||
385 | categoryBits !== undefined ||
386 | maskBits !== undefined
387 | ) {
388 | const originalGroupIndex = fixture.getFilterGroupIndex();
389 | const originalCategoryBits = fixture.getFilterCategoryBits();
390 | const originalMaskBits = fixture.getFilterMaskBits();
391 | fixture.setFilterData({
392 | groupIndex:
393 | groupIndex !== undefined ? groupIndex : originalGroupIndex,
394 | categoryBits:
395 | categoryBits !== undefined
396 | ? categoryBits
397 | : originalCategoryBits,
398 | maskBits: maskBits !== undefined ? maskBits : originalMaskBits,
399 | });
400 | }
401 | }
402 | }
403 | },
404 | [bodies]
405 | );
406 | };
407 |
408 | export const Bodies: React.FC = () => {
409 | const { bodies } = useWorldState();
410 | const [cachedBodies] = useState(() => new Map());
411 |
412 | const addBody = useAddBody(bodies, cachedBodies);
413 | const removeBody = useRemoveBody(bodies, cachedBodies);
414 | const setBody = useSetBody(bodies);
415 | const updateBody = useUpdateBody(bodies);
416 |
417 | const onMessage = useCallback(
418 | (event: MessageEvent) => {
419 | const { type, props = {} } = event.data as {
420 | type: WorkerMessageType;
421 | props: any;
422 | };
423 | switch (type) {
424 | case WorkerMessageType.ADD_BODY:
425 | addBody(props);
426 | break;
427 | case WorkerMessageType.REMOVE_BODY:
428 | removeBody(props);
429 | break;
430 | case WorkerMessageType.SET_BODY:
431 | setBody(props);
432 | break;
433 | case WorkerMessageType.UPDATE_BODY:
434 | updateBody(props);
435 | break;
436 | }
437 | },
438 | [addBody, removeBody, setBody, updateBody]
439 | );
440 |
441 | useSubscribeToWorkerMessages(onMessage);
442 |
443 | return null;
444 | };
445 |
--------------------------------------------------------------------------------
/src/main/worker/app/Collisions.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from 'react';
2 | import { useAppContext, useWorld } from './appContext';
3 | import { Contact, Fixture } from 'planck-js';
4 | import {
5 | getFixtureData,
6 | getFixtureIndex,
7 | getFixtureUuid,
8 | } from '../planckjs/collisions/collisions';
9 | import { useWorldState } from './WorldState';
10 | import { WorkerOwnerMessageType } from '../shared/types';
11 |
12 | const useHandleBeginCollision = () => {
13 | const { worker, logicWorker } = useAppContext();
14 | const { collisionListeners } = useWorldState();
15 | const sendCollisionBeginEvent = useCallback(
16 | (uuid: string, data: any, fixtureIndex: number, isSensor: boolean) => {
17 | const update = {
18 | type: WorkerOwnerMessageType.BEGIN_COLLISION,
19 | props: {
20 | uuid,
21 | data,
22 | fixtureIndex,
23 | isSensor,
24 | },
25 | };
26 | worker.postMessage(update);
27 | if (logicWorker) {
28 | logicWorker.postMessage(update);
29 | }
30 | },
31 | [worker, logicWorker]
32 | );
33 |
34 | return useCallback(
35 | (fixtureA: Fixture, fixtureB: Fixture) => {
36 | const aData = getFixtureData(fixtureA);
37 | const bData = getFixtureData(fixtureB);
38 | const aUUID = getFixtureUuid(aData);
39 | const bUUID = getFixtureUuid(bData);
40 |
41 | if (aUUID && collisionListeners.has(aUUID)) {
42 | sendCollisionBeginEvent(
43 | aUUID,
44 | bData,
45 | getFixtureIndex(aData),
46 | fixtureB.isSensor()
47 | );
48 | }
49 |
50 | if (bUUID && collisionListeners.has(bUUID)) {
51 | sendCollisionBeginEvent(
52 | bUUID,
53 | aData,
54 | getFixtureIndex(bData),
55 | fixtureA.isSensor()
56 | );
57 | }
58 | },
59 | [collisionListeners, sendCollisionBeginEvent]
60 | );
61 | };
62 |
63 | const useHandleEndCollision = () => {
64 | const { worker, logicWorker } = useAppContext();
65 | const { collisionListeners } = useWorldState();
66 |
67 | const sendCollisionEndEvent = useCallback(
68 | (uuid: string, data: any, fixtureIndex: number, isSensor: boolean) => {
69 | const update = {
70 | type: WorkerOwnerMessageType.END_COLLISION,
71 | props: {
72 | uuid,
73 | data,
74 | fixtureIndex,
75 | isSensor,
76 | },
77 | };
78 | worker.postMessage(update);
79 | if (logicWorker) {
80 | logicWorker.postMessage(update);
81 | }
82 | },
83 | [worker, logicWorker]
84 | );
85 |
86 | return useCallback(
87 | (fixtureA: Fixture, fixtureB: Fixture) => {
88 | const aData = getFixtureData(fixtureA);
89 | const bData = getFixtureData(fixtureB);
90 | const aUUID = getFixtureUuid(aData);
91 | const bUUID = getFixtureUuid(bData);
92 |
93 | if (aUUID && collisionListeners.has(aUUID)) {
94 | sendCollisionEndEvent(
95 | aUUID,
96 | bData,
97 | getFixtureIndex(aData),
98 | fixtureB.isSensor()
99 | );
100 | }
101 |
102 | if (bUUID && collisionListeners.has(bUUID)) {
103 | sendCollisionEndEvent(
104 | bUUID,
105 | aData,
106 | getFixtureIndex(bData),
107 | fixtureA.isSensor()
108 | );
109 | }
110 | },
111 | [collisionListeners, sendCollisionEndEvent]
112 | );
113 | };
114 |
115 | export const Collisions: React.FC = () => {
116 | const world = useWorld();
117 |
118 | const handleBeginCollision = useHandleBeginCollision();
119 | const handleEndCollision = useHandleEndCollision();
120 |
121 | useEffect(() => {
122 | world.on('begin-contact', (contact: Contact) => {
123 | const fixtureA = contact.getFixtureA();
124 | const fixtureB = contact.getFixtureB();
125 | handleBeginCollision(fixtureA, fixtureB);
126 | });
127 |
128 | world.on('end-contact', (contact: Contact) => {
129 | const fixtureA = contact.getFixtureA();
130 | const fixtureB = contact.getFixtureB();
131 | handleEndCollision(fixtureA, fixtureB);
132 | });
133 | }, [world]);
134 |
135 | return null;
136 | };
137 |
--------------------------------------------------------------------------------
/src/main/worker/app/World.tsx:
--------------------------------------------------------------------------------
1 | import React, {MutableRefObject, useCallback, useEffect, useRef, useState} from 'react';
2 | import {useAppContext, useWorld} from './appContext';
3 | import {Buffers, WorkerMessageType, WorkerOwnerMessageType,} from '../shared/types';
4 | import {useWorldState} from './WorldState';
5 | import {generateBuffers} from "./buffers";
6 |
7 | const useSyncData = () => {
8 | const { dynamicBodies, bodies } = useWorldState();
9 | return useCallback((positions: Float32Array, angles: Float32Array) => {
10 | const dynamicBodiesArray = Array.from(dynamicBodies);
11 |
12 | dynamicBodiesArray.forEach((uuid, index) => {
13 | const body = bodies.get(uuid);
14 | if (!body) return;
15 | const position = body.getPosition();
16 | const angle = body.getAngle();
17 | positions[2 * index + 0] = position.x;
18 | positions[2 * index + 1] = position.y;
19 | angles[index] = angle;
20 | });
21 | }, []);
22 | };
23 |
24 | const debug = {
25 | mainSent: false,
26 | mainLogged: false,
27 | mainLogged2: false,
28 | logicSent: false,
29 | logicLogged: false,
30 | logicLogged2: false,
31 | };
32 |
33 | const useSendPhysicsUpdate = (tickRef: MutableRefObject) => {
34 |
35 | const localStateRef = useRef({
36 | failedMainCount: 0,
37 | failedLogicCount: 0,
38 | lastPhysicsStep: 0,
39 | })
40 |
41 | const {
42 | bodiesNeedSyncRef,
43 | logicBodiesNeedSyncRef,
44 | dynamicBodies,
45 | } = useWorldState();
46 |
47 | const {
48 | buffers: mainBuffers,
49 | logicBuffers,
50 | worker,
51 | logicWorker,
52 | maxNumberOfDynamicObjects,
53 | } = useAppContext();
54 |
55 | const syncData = useSyncData();
56 |
57 | return useCallback(
58 | (target: Worker | MessagePort, buffer: Buffers, isMain: boolean) => {
59 | const { positions, angles } = buffer;
60 | if (!(positions.byteLength !== 0 && angles.byteLength !== 0)) {
61 | console.warn('cant send physics update to', isMain ? 'main' : 'logic')
62 | if (isMain) {
63 | if (localStateRef.current.failedMainCount >= 2) {
64 | const { positions: newPositions, angles: newAngles } = generateBuffers(maxNumberOfDynamicObjects);
65 | mainBuffers.positions = newPositions
66 | mainBuffers.angles = newAngles
67 | }
68 | } else {
69 | if (localStateRef.current.failedLogicCount >= 2) {
70 | const {positions: newPositions, angles: newAngles} = generateBuffers(maxNumberOfDynamicObjects);
71 | logicBuffers.positions = newPositions
72 | logicBuffers.angles = newAngles
73 | }
74 | }
75 | if (isMain) {
76 | localStateRef.current.failedMainCount += 1
77 | } else {
78 | localStateRef.current.failedLogicCount += 1
79 | }
80 | return;
81 | }
82 | if (isMain) {
83 | localStateRef.current.failedMainCount = 0
84 | } else {
85 | localStateRef.current.failedLogicCount = 0
86 | }
87 | syncData(positions, angles);
88 | const rawMessage: any = {
89 | type: WorkerOwnerMessageType.PHYSICS_STEP,
90 | physicsTick: tickRef.current,
91 | };
92 | if (isMain) {
93 | rawMessage.bodies = Array.from(dynamicBodies);
94 | bodiesNeedSyncRef.current = false;
95 | } else {
96 | rawMessage.bodies = Array.from(dynamicBodies);
97 | logicBodiesNeedSyncRef.current = false;
98 | }
99 | const message = {
100 | ...rawMessage,
101 | positions,
102 | angles,
103 | };
104 | target.postMessage(message, [positions.buffer, angles.buffer]);
105 | },
106 | [bodiesNeedSyncRef, logicBodiesNeedSyncRef, tickRef, syncData]
107 | );
108 | };
109 |
110 | const useSendPhysicsUpdates = (tickRef: MutableRefObject) => {
111 | const {
112 | buffers: mainBuffers,
113 | logicBuffers,
114 | worker,
115 | logicWorker,
116 | } = useAppContext();
117 |
118 | const sendPhysicsUpdate = useSendPhysicsUpdate(tickRef);
119 |
120 | const update = useCallback((isMain: boolean) => {
121 | if (isMain) {
122 | sendPhysicsUpdate(worker, mainBuffers, true);
123 | } else if (logicWorker) {
124 | sendPhysicsUpdate(logicWorker, logicBuffers, false);
125 | }
126 | }, [worker, logicWorker, sendPhysicsUpdate, mainBuffers, logicBuffers]);
127 |
128 | const updateRef = useRef(update);
129 |
130 | useEffect(() => {
131 | updateRef.current = update;
132 | }, [update, updateRef]);
133 |
134 | return useCallback((isMain: boolean) => {
135 | // using ref, as i don't want to interrupt the interval
136 | updateRef.current(isMain);
137 | }, [updateRef]);
138 | };
139 |
140 | const useStepProcessed = (tickRef: MutableRefObject) => {
141 | const {
142 | buffers: mainBuffers,
143 | logicBuffers,
144 | worker,
145 | logicWorker,
146 | buffersRef,
147 | } = useAppContext();
148 |
149 | return useCallback(
150 | (
151 | isMain: boolean,
152 | lastProcessedPhysicsTick: number,
153 | positions: Float32Array,
154 | angles: Float32Array
155 | ) => {
156 | const buffers = isMain ? mainBuffers : logicBuffers;
157 |
158 | if (isMain) {
159 | buffers.positions = positions;
160 | buffers.angles = angles;
161 | } else {
162 | buffers.positions = positions;
163 | buffers.angles = angles;
164 | }
165 | },
166 | [mainBuffers, logicBuffers, tickRef, worker, logicWorker]
167 | );
168 | };
169 |
170 | const useWorldLoop = () => {
171 | const world = useWorld();
172 | const { updateRate, subscribe, logicSubscribe } = useAppContext();
173 | const tickRef = useRef(0);
174 | const [tickCount, setTickCount] = useState(0)
175 |
176 | const lastSentMainUpdateRef = useRef(-1)
177 | const lastSentLogicUpdateRef = useRef(-1)
178 | const [mainBufferReady, setMainBufferReady] = useState(false)
179 | const [logicBufferReady, setLogicBufferReady] = useState(false)
180 | const sendPhysicsUpdate = useSendPhysicsUpdates(tickRef);
181 |
182 | useEffect(() => {
183 |
184 | if (mainBufferReady && lastSentMainUpdateRef.current < tickCount) {
185 | sendPhysicsUpdate(true)
186 | lastSentMainUpdateRef.current = tickCount
187 | setMainBufferReady(false)
188 | }
189 |
190 | }, [tickCount, mainBufferReady])
191 |
192 | useEffect(() => {
193 |
194 | if (logicBufferReady && lastSentLogicUpdateRef.current < tickCount) {
195 | sendPhysicsUpdate(false)
196 | lastSentLogicUpdateRef.current = tickCount
197 | setLogicBufferReady(false)
198 | }
199 |
200 | }, [tickCount, logicBufferReady])
201 |
202 | useEffect(() => {
203 |
204 | const step = () => {
205 | world.step(updateRate);
206 | };
207 |
208 | const interval = setInterval(() => {
209 | tickRef.current += 1;
210 | setTickCount(state => state + 1)
211 | step();
212 | }, updateRate);
213 |
214 | return () => {
215 | clearInterval(interval);
216 | };
217 | }, []);
218 |
219 | const stepProcessed = useStepProcessed(tickRef);
220 |
221 | useEffect(() => {
222 |
223 | const onMessage = (event: MessageEvent, isMain: boolean = true) => {
224 | const { type, props = {} } = event.data as {
225 | type: WorkerMessageType;
226 | props: any;
227 | };
228 | if (type === WorkerMessageType.READY_FOR_PHYSICS) {
229 | if (isMain) {
230 | setMainBufferReady(true)
231 | } else {
232 | setLogicBufferReady(true)
233 | }
234 | } else if (type === WorkerMessageType.PHYSICS_STEP_PROCESSED) {
235 | stepProcessed(
236 | isMain,
237 | event.data.physicsTick,
238 | event.data.positions,
239 | event.data.angles
240 | );
241 | if (isMain) {
242 | setMainBufferReady(true)
243 | } else {
244 | setLogicBufferReady(true)
245 | }
246 | }
247 | };
248 |
249 | const unsubscribe = subscribe(onMessage);
250 |
251 | const unsubscribeLogic = logicSubscribe(event => onMessage(event, false));
252 |
253 | return () => {
254 | unsubscribe();
255 | unsubscribeLogic();
256 | };
257 | }, [subscribe, logicSubscribe, stepProcessed]);
258 | };
259 |
260 | export const World: React.FC = () => {
261 | useWorldLoop();
262 | return null;
263 | };
264 |
--------------------------------------------------------------------------------
/src/main/worker/app/WorldState.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | MutableRefObject,
4 | useContext,
5 | useRef,
6 | useState,
7 | } from 'react';
8 | import { ValidUUID } from '../shared/types';
9 | import { Body } from 'planck-js';
10 |
11 | type BodiesMap = Map;
12 | type DynamicBodies = Set;
13 | type CollisionListeners = Set;
14 |
15 | type ContextState = {
16 | bodies: BodiesMap;
17 | dynamicBodies: DynamicBodies;
18 | collisionListeners: CollisionListeners;
19 | bodiesNeedSync: boolean;
20 | setBodiesNeedSync: (bool: boolean) => void;
21 | bodiesNeedSyncRef: MutableRefObject;
22 | logicBodiesNeedSyncRef: MutableRefObject;
23 | };
24 |
25 | const Context = createContext((null as unknown) as ContextState);
26 |
27 | export const useWorldState = (): ContextState => {
28 | return useContext(Context);
29 | };
30 |
31 | export const WorldState: React.FC = ({ children }) => {
32 | const [bodies] = useState(() => new Map());
33 | const [dynamicBodies] = useState(() => new Set());
34 | const [collisionListeners] = useState(() => new Set());
35 | const [bodiesNeedSync, setBodiesNeedSync] = useState(false);
36 | const bodiesNeedSyncRef = useRef(false);
37 | const logicBodiesNeedSyncRef = useRef(false);
38 |
39 | return (
40 |
51 | {children}
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/src/main/worker/app/appContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext, MutableRefObject, useContext } from 'react';
2 | import { World } from 'planck-js';
3 | import { Subscribe } from '../../hooks/useWorkerMessages';
4 | import { Buffers } from '../shared/types';
5 |
6 | export type AppContextState = {
7 | updateRate: number;
8 | world: World;
9 | worker: Worker;
10 | logicWorker?: Worker | MessagePort;
11 | subscribe: Subscribe;
12 | logicSubscribe: Subscribe;
13 | buffers: Buffers;
14 | logicBuffers: Buffers;
15 | buffersRef: MutableRefObject<{
16 | mainCount: number;
17 | logicCount: number;
18 | }>;
19 | maxNumberOfDynamicObjects: number;
20 | };
21 |
22 | export const AppContext = createContext((null as unknown) as AppContextState);
23 |
24 | export const useWorld = (): World => {
25 | return useContext(AppContext).world;
26 | };
27 |
28 | export const useAppContext = (): AppContextState => {
29 | return useContext(AppContext);
30 | };
31 |
--------------------------------------------------------------------------------
/src/main/worker/app/buffers.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useRef, useState} from 'react';
2 | import { Buffers } from '../shared/types';
3 | import { useDidMount } from '../../../utils/hooks';
4 |
5 | export const generateBuffers = (maxNumberOfDynamicObjects: number): Buffers => {
6 | return {
7 | positions: new Float32Array(maxNumberOfDynamicObjects * 2),
8 | angles: new Float32Array(maxNumberOfDynamicObjects),
9 | };
10 | };
11 |
12 | export const useBuffers = (maxNumberOfDynamicObjects: number, debug: string): Buffers => {
13 | const isMountRef = useRef(true)
14 | const [buffers] = useState(() => generateBuffers(maxNumberOfDynamicObjects));
15 |
16 | useEffect(() => {
17 | if (isMountRef.current) {
18 | isMountRef.current = false
19 | return
20 | }
21 | const { positions, angles } = generateBuffers(maxNumberOfDynamicObjects);
22 | buffers.positions = positions;
23 | buffers.angles = angles;
24 | }, [maxNumberOfDynamicObjects]);
25 |
26 | return buffers;
27 | };
28 |
--------------------------------------------------------------------------------
/src/main/worker/app/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import { Vec2, World, WorldDef } from 'planck-js';
3 | import { AppContext } from './appContext';
4 | import { useWorkerMessages } from '../../hooks/useWorkerMessages';
5 | import { Bodies } from './Bodies';
6 | import { World as WorldComponent } from './World';
7 | import { useSubscribeLogicWorker, useLogicWorker } from './logicWorker';
8 | import { WorldState } from './WorldState';
9 | import { Collisions } from './Collisions';
10 | import { WorkerOwnerMessageType } from '../shared/types';
11 | import { useBuffers } from './buffers';
12 |
13 | export const App: React.FC<{
14 | config: {
15 | maxNumberOfDynamicObjects: number;
16 | updateRate: number;
17 | };
18 | worldParams: WorldDef;
19 | worker: Worker;
20 | }> = ({ worldParams, worker, config }) => {
21 | const { updateRate, maxNumberOfDynamicObjects } = config;
22 |
23 | const defaultParams = {
24 | allowSleep: true,
25 | gravity: Vec2(0, 0),
26 | ...worldParams,
27 | };
28 |
29 | const [world] = useState(() => World(defaultParams));
30 |
31 | const subscribe = useWorkerMessages(worker);
32 |
33 | const logicWorker = useLogicWorker(worker, subscribe);
34 |
35 | const logicSubscribe = useSubscribeLogicWorker(logicWorker);
36 |
37 | const buffers = useBuffers(maxNumberOfDynamicObjects, 'main');
38 | const logicBuffers = useBuffers(!logicWorker ? 0 : maxNumberOfDynamicObjects, 'logic');
39 |
40 | const buffersRef = useRef({
41 | mainCount: 0,
42 | logicCount: 0,
43 | });
44 |
45 | useEffect(() => {
46 | worker.postMessage({
47 | type: WorkerOwnerMessageType.INITIATED,
48 | });
49 | }, [worker]);
50 |
51 | return (
52 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/src/main/worker/app/logicWorker.ts:
--------------------------------------------------------------------------------
1 | import { Subscribe, useWorkerMessages } from '../../hooks/useWorkerMessages';
2 | import { useEffect, useState } from 'react';
3 |
4 | export const useSubscribeLogicWorker = (
5 | worker: Worker | MessagePort | undefined
6 | ) => {
7 | const subscribe = useWorkerMessages(worker);
8 | return subscribe;
9 | };
10 |
11 | export const useLogicWorker = (
12 | worker: Worker,
13 | subscribe: Subscribe
14 | ): undefined | Worker | MessagePort => {
15 | const [logicWorker, setLogicWorker] = useState();
16 |
17 | useEffect(() => {
18 | let logicWorkerPort: MessagePort;
19 |
20 | const handleMessage = (event: MessageEvent) => {
21 | if (event.data.command === 'connect') {
22 | logicWorkerPort = event.ports[0];
23 | setLogicWorker(logicWorkerPort);
24 | return;
25 | } else if (event.data.command === 'forward') {
26 | logicWorkerPort.postMessage(event.data.message);
27 | return;
28 | }
29 | };
30 |
31 | const unsubscribe = subscribe(event => {
32 | if (event.data.command) {
33 | handleMessage(event);
34 | }
35 | });
36 |
37 | return () => {
38 | unsubscribe();
39 | };
40 | }, [worker, subscribe, setLogicWorker]);
41 |
42 | return logicWorker;
43 | };
44 |
--------------------------------------------------------------------------------
/src/main/worker/app/workerMessages.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 |
3 | export const useHandleWorkerMessage = (isMainWorker: boolean = true) => {
4 | const handleMessage = useCallback(
5 | (event: MessageEvent) => {
6 | // todo - iterate through subscribers...
7 | },
8 | [isMainWorker]
9 | );
10 |
11 | return handleMessage;
12 | };
13 |
--------------------------------------------------------------------------------
/src/main/worker/data.ts:
--------------------------------------------------------------------------------
1 | import { Buffers } from './shared/types';
2 | import { generateBuffers } from './utils';
3 |
4 | export const storedData: {
5 | physicsTick: number;
6 | unsyncedBodies: boolean;
7 | unsyncedLogicBodies: boolean;
8 | maxNumberOfPhysicsObjects: number;
9 | buffers: Buffers;
10 | logicBuffers: Buffers;
11 | mainWorker: null | Worker;
12 | logicWorker: null | MessagePort;
13 | } = {
14 | physicsTick: 0,
15 | unsyncedBodies: false,
16 | unsyncedLogicBodies: false,
17 | maxNumberOfPhysicsObjects: 0,
18 | buffers: generateBuffers(0),
19 | logicBuffers: generateBuffers(0),
20 | mainWorker: null,
21 | logicWorker: null,
22 | };
23 |
--------------------------------------------------------------------------------
/src/main/worker/functions.ts:
--------------------------------------------------------------------------------
1 | import { storedData } from './data';
2 | import { WorkerOwnerMessageType } from './shared/types';
3 |
4 | export const sendCollisionBeginEvent = (
5 | uuid: string,
6 | data: any,
7 | fixtureIndex: number,
8 | isSensor: boolean
9 | ) => {
10 | const update = {
11 | type: WorkerOwnerMessageType.BEGIN_COLLISION,
12 | props: {
13 | uuid,
14 | data,
15 | fixtureIndex,
16 | isSensor,
17 | },
18 | };
19 | if (storedData.mainWorker) {
20 | storedData.mainWorker.postMessage(update);
21 | }
22 | if (storedData.logicWorker) {
23 | storedData.logicWorker.postMessage(update);
24 | }
25 | };
26 |
27 | export const sendCollisionEndEvent = (
28 | uuid: string,
29 | data: any,
30 | fixtureIndex: number,
31 | isSensor: boolean
32 | ) => {
33 | const update = {
34 | type: WorkerOwnerMessageType.END_COLLISION,
35 | props: {
36 | uuid,
37 | data,
38 | fixtureIndex,
39 | isSensor,
40 | },
41 | };
42 | if (storedData.mainWorker) {
43 | storedData.mainWorker.postMessage(update);
44 | }
45 | if (storedData.logicWorker) {
46 | storedData.logicWorker.postMessage(update);
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/src/main/worker/logicWorker.ts:
--------------------------------------------------------------------------------
1 | import { storedData } from './data';
2 | import { WorkerMessageType } from './shared/types';
3 | import { generateBuffers } from './utils';
4 | import { stepProcessed } from './shared';
5 | import { addBody, removeBody, setBody, updateBody } from './planckjs/bodies';
6 |
7 | let logicWorkerPort: MessagePort;
8 |
9 | const onMessageFromLogicWorker = (event: MessageEvent) => {
10 | const { type, props = {} } = event.data as {
11 | type: WorkerMessageType;
12 | props: any;
13 | };
14 | switch (type) {
15 | case WorkerMessageType.PHYSICS_STEP_PROCESSED:
16 | stepProcessed(
17 | false,
18 | event.data.physicsTick,
19 | event.data.positions,
20 | event.data.angles
21 | );
22 | break;
23 | case WorkerMessageType.ADD_BODY:
24 | addBody(props);
25 | break;
26 | case WorkerMessageType.REMOVE_BODY:
27 | removeBody(props);
28 | break;
29 | case WorkerMessageType.SET_BODY:
30 | setBody(props);
31 | break;
32 | case WorkerMessageType.UPDATE_BODY:
33 | updateBody(props);
34 | break;
35 | }
36 | };
37 |
38 | export const handleLogicWorkerMessage = (event: MessageEvent) => {
39 | if (event.data.command === 'connect') {
40 | logicWorkerPort = event.ports[0];
41 | storedData.logicBuffers = generateBuffers(
42 | storedData.maxNumberOfPhysicsObjects
43 | );
44 | storedData.logicWorker = logicWorkerPort;
45 | logicWorkerPort.onmessage = onMessageFromLogicWorker;
46 | return;
47 | } else if (event.data.command === 'forward') {
48 | logicWorkerPort.postMessage(event.data.message);
49 | return;
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/src/main/worker/methods.ts:
--------------------------------------------------------------------------------
1 | import { storedData } from './data';
2 | import {initPhysicsListeners, stepWorld, syncData} from './planckjs/world';
3 | import { PHYSICS_UPDATE_RATE } from './planckjs/config';
4 | import { generateBuffers } from './utils';
5 | import { Buffers, WorkerOwnerMessageType } from './shared/types';
6 | import { dynamicBodiesUuids } from './planckjs/shared';
7 | import { setBodiesSynced, setLogicBodiesSynced } from './shared';
8 |
9 | const sendPhysicsUpdate = (
10 | target: Worker | MessagePort,
11 | buffer: Buffers,
12 | handleBodies: (message: any) => any
13 | ) => {
14 | const { positions, angles } = buffer;
15 | if (!(positions.byteLength !== 0 && angles.byteLength !== 0)) {
16 | return;
17 | }
18 | syncData(positions, angles);
19 | const rawMessage: any = {
20 | type: WorkerOwnerMessageType.PHYSICS_STEP,
21 | physicsTick: storedData.physicsTick,
22 | };
23 | handleBodies(rawMessage);
24 | const message = {
25 | ...rawMessage,
26 | positions,
27 | angles,
28 | };
29 | target.postMessage(message, [positions.buffer, angles.buffer]);
30 | };
31 |
32 | const sendPhysicsUpdateToLogic = () => {
33 | if (!storedData.logicWorker) return;
34 | sendPhysicsUpdate(
35 | storedData.logicWorker,
36 | storedData.logicBuffers,
37 | (message: any) => {
38 | if (storedData.unsyncedLogicBodies) {
39 | message['bodies'] = dynamicBodiesUuids;
40 | setLogicBodiesSynced();
41 | }
42 | }
43 | );
44 | };
45 |
46 | const sendPhysicsUpdateToMain = () => {
47 | if (!storedData.mainWorker) return;
48 | sendPhysicsUpdate(
49 | storedData.mainWorker,
50 | storedData.buffers,
51 | (message: any) => {
52 | if (storedData.unsyncedBodies) {
53 | message['bodies'] = dynamicBodiesUuids;
54 | setBodiesSynced();
55 | }
56 | }
57 | );
58 | };
59 |
60 | const beginPhysicsLoop = () => {
61 | setInterval(() => {
62 | storedData.physicsTick += 1;
63 | stepWorld();
64 | sendPhysicsUpdateToMain();
65 | sendPhysicsUpdateToLogic();
66 | }, PHYSICS_UPDATE_RATE);
67 | };
68 |
69 | export const init = ({
70 | maxNumberOfPhysicsObjects = 100,
71 | }: {
72 | maxNumberOfPhysicsObjects?: number;
73 | }) => {
74 | storedData.maxNumberOfPhysicsObjects = maxNumberOfPhysicsObjects;
75 | storedData.buffers = generateBuffers(maxNumberOfPhysicsObjects);
76 | if (storedData.logicWorker) {
77 | storedData.logicBuffers = generateBuffers(maxNumberOfPhysicsObjects);
78 | }
79 | initPhysicsListeners()
80 | beginPhysicsLoop();
81 | };
82 |
--------------------------------------------------------------------------------
/src/main/worker/physicsWorkerHelper.ts:
--------------------------------------------------------------------------------
1 | import {createElement, FC} from "react";
2 | import {PhysicsProps, WorkerMessageType} from "./shared/types";
3 | import {render} from "react-nil";
4 | import {PHYSICS_UPDATE_RATE} from "./planckjs/config";
5 |
6 | export const physicsWorkerHandler = (selfWorker: Worker) => {
7 |
8 | selfWorker.onmessage = (event: MessageEvent) => {
9 | const { type, props = {} } = event.data as {
10 | type: WorkerMessageType;
11 | props: any;
12 | };
13 | switch (type) {
14 | case WorkerMessageType.INIT:
15 | const { worldParams = {}, config = {} } = props as PhysicsProps;
16 | const {
17 | maxNumberOfDynamicObjects = 100,
18 | updateRate = PHYSICS_UPDATE_RATE,
19 | } = config;
20 | render(
21 | createElement(
22 | require('./worker/app/index').App,
23 | {
24 | worker: selfWorker,
25 | config: {
26 | maxNumberOfDynamicObjects,
27 | updateRate,
28 | },
29 | worldParams,
30 | },
31 | null
32 | )
33 | );
34 | break;
35 | }
36 | };
37 | };
--------------------------------------------------------------------------------
/src/main/worker/planckjs/bodies.ts:
--------------------------------------------------------------------------------
1 | import {dynamicBodiesUuids, existingBodies, planckWorld} from "./shared";
2 | import {Shape} from "planck-js/lib/shape";
3 | import {activeCollisionListeners} from "./collisions/data";
4 | import {addCachedBody, getCachedBody} from "./cache";
5 | import type {BodyDef, FixtureOpt, Body, Joint} from "planck-js";
6 | import {Box, Circle, DistanceJoint, RopeJoint, Vec2} from "planck-js";
7 | import {ValidUUID} from "../shared/types";
8 | import {syncBodies} from "../shared";
9 |
10 | export enum BodyType {
11 | static = 'static',
12 | kinematic = 'kinematic',
13 | dynamic = 'dynamic'
14 | }
15 |
16 | export enum BodyShape {
17 | box = 'box',
18 | circle = 'circle',
19 | }
20 |
21 | export type FixtureBase = {
22 | shape: BodyShape,
23 | fixtureOptions?: Partial,
24 | }
25 |
26 | export type BoxFixture = FixtureBase & {
27 | hx: number,
28 | hy: number,
29 | center?: [number, number],
30 | }
31 |
32 | export const createBoxFixture = ({
33 | width = 1,
34 | height = 1,
35 | center,
36 | fixtureOptions = {}
37 | }: {
38 | width?: number,
39 | height?: number,
40 | center?: [number, number],
41 | fixtureOptions?: Partial
42 | }): BoxFixture => {
43 | const fixture: BoxFixture = {
44 | shape: BodyShape.box,
45 | hx: width,
46 | hy: height,
47 | fixtureOptions,
48 | }
49 | if (center) {
50 | fixture.center = center
51 | }
52 | return fixture
53 | }
54 |
55 | export type CircleFixture = FixtureBase & {
56 | radius: number,
57 | }
58 |
59 | export const createCircleFixture = ({ radius = 1, fixtureOptions = {} }: {
60 | radius?: number,
61 | fixtureOptions?: Partial
62 | }): CircleFixture => {
63 | return {
64 | shape: BodyShape.circle,
65 | radius,
66 | fixtureOptions,
67 | }
68 | }
69 |
70 | type Fixture = BoxFixture | CircleFixture
71 |
72 | type BasicBodyProps = Partial & {
73 | fixtures?: Fixture[],
74 | }
75 |
76 | type AddBoxBodyProps = BasicBodyProps & {}
77 |
78 | type AddCircleBodyProps = BasicBodyProps & {}
79 |
80 | export type AddBodyDef = BasicBodyProps | AddBoxBodyProps | AddCircleBodyProps
81 |
82 | export type AddBodyProps = AddBodyDef & {
83 | uuid: ValidUUID,
84 | listenForCollisions: boolean,
85 | cacheKey?: string,
86 | attachToRope?: boolean,
87 | }
88 |
89 | export const addBody = ({uuid, cacheKey, listenForCollisions, fixtures = [], attachToRope = false, ...props}: AddBodyProps) => {
90 |
91 | const existingBody = existingBodies.get(uuid)
92 |
93 | if (existingBody) {
94 | return existingBody
95 | }
96 |
97 | if (listenForCollisions) {
98 | activeCollisionListeners[uuid] = true
99 | }
100 |
101 | const bodyDef: BodyDef = {
102 | type: BodyType.static,
103 | fixedRotation: true,
104 | ...props,
105 | }
106 |
107 | const {type} = bodyDef
108 |
109 | let body: Body | null = null;
110 |
111 | if (cacheKey) {
112 | const cachedBody = getCachedBody(cacheKey)
113 | if (cachedBody) {
114 |
115 | if (fixtures && fixtures.length > 0) {
116 |
117 | let bodyFixture = cachedBody.getFixtureList()
118 |
119 | fixtures.forEach((fixture, fixtureIndex) => {
120 |
121 | let fixtureOptions = fixture.fixtureOptions
122 |
123 | fixtureOptions = {
124 | userData: {
125 | uuid,
126 | fixtureIndex,
127 | ...fixtureOptions?.userData
128 | },
129 | ...fixtureOptions,
130 | }
131 |
132 | if (bodyFixture) {
133 |
134 | if (fixtureOptions) {
135 | bodyFixture.setUserData(fixtureOptions.userData)
136 | }
137 |
138 | bodyFixture = bodyFixture.getNext()
139 | }
140 |
141 | })
142 |
143 | }
144 |
145 | const {position, angle} = props
146 |
147 | if (position) {
148 | cachedBody.setPosition(position)
149 | }
150 |
151 | if (angle) {
152 | cachedBody.setAngle(angle)
153 | }
154 |
155 | cachedBody.setActive(true)
156 |
157 | body = cachedBody
158 |
159 | }
160 | }
161 |
162 | if (!body) {
163 |
164 | body = planckWorld.createBody(bodyDef)
165 |
166 | if (fixtures && fixtures.length > 0) {
167 |
168 | fixtures.forEach((fixture, fixtureIndex) => {
169 |
170 | const {shape} = fixture
171 |
172 | let fixtureOptions = fixture.fixtureOptions ?? {}
173 |
174 | fixtureOptions = {
175 | ...fixtureOptions,
176 | userData: {
177 | uuid,
178 | fixtureIndex,
179 | ...fixtureOptions?.userData
180 | },
181 | }
182 |
183 | let bodyShape: Shape;
184 |
185 | switch (shape) {
186 | case BodyShape.box:
187 | const {hx, hy, center} = fixture as BoxFixture
188 | bodyShape = Box((hx as number) / 2, (hy as number) / 2, center ? Vec2(center[0], center[1]) : undefined) as unknown as Shape
189 | break;
190 | case BodyShape.circle:
191 | const {radius} = fixture as CircleFixture
192 | bodyShape = Circle((radius as number)) as unknown as Shape
193 | break;
194 | default:
195 | throw new Error(`Unhandled body shape ${shape}`)
196 | }
197 |
198 | if (fixtureOptions) {
199 | if (body) {
200 | body.createFixture(bodyShape, fixtureOptions as FixtureOpt)
201 | }
202 | } else {
203 | if (body) {
204 | body.createFixture(bodyShape)
205 | }
206 | }
207 |
208 | // todo - handle rope properly...
209 | if (attachToRope) {
210 |
211 | const {position, angle} = props
212 |
213 | const ropeJointDef = {
214 | maxLength: 0.5,
215 | localAnchorA: position,
216 | localAnchorB: position,
217 | };
218 |
219 | const startingBodyDef: BodyDef = {
220 | type: BodyType.static,
221 | fixedRotation: true,
222 | position,
223 | angle,
224 | }
225 |
226 | const startingBody = planckWorld.createBody(startingBodyDef)
227 |
228 | if (body) {
229 |
230 | const distanceJoint = DistanceJoint({
231 | collideConnected: false,
232 | frequencyHz: 5,
233 | dampingRatio: 0.5,
234 | length: 0.15,
235 | }, startingBody, body, position ?? Vec2(0, 0), position ?? Vec2(0, 0))
236 |
237 | const rope2 = planckWorld.createJoint(RopeJoint(ropeJointDef, startingBody, body, position ?? Vec2(0, 0)) as unknown as Joint);
238 | const rope = planckWorld.createJoint(distanceJoint as unknown as Joint);
239 | }
240 |
241 |
242 | }
243 |
244 | })
245 |
246 |
247 | }
248 |
249 | }
250 |
251 | if (type !== BodyType.static) {
252 | dynamicBodiesUuids.push(uuid)
253 | syncBodies()
254 | }
255 |
256 | if (!body) {
257 | throw new Error(`No body`)
258 | }
259 |
260 | existingBodies.set(uuid, body)
261 |
262 | return body
263 |
264 | }
265 |
266 | export type RemoveBodyProps = {
267 | uuid: ValidUUID,
268 | cacheKey?: string
269 | }
270 |
271 | const tempVec = Vec2(0, 0)
272 |
273 | export const removeBody = ({uuid, cacheKey}: RemoveBodyProps) => {
274 | const index = dynamicBodiesUuids.indexOf(uuid)
275 | if (index > -1) {
276 | dynamicBodiesUuids.splice(index, 1)
277 | syncBodies()
278 | }
279 | const body = existingBodies.get(uuid)
280 | if (!body) {
281 | console.warn(`Body not found for ${uuid}`)
282 | return
283 | }
284 | existingBodies.delete(uuid)
285 | if (cacheKey) {
286 | tempVec.set(-1000, -1000)
287 | body.setPosition(tempVec)
288 | tempVec.set(0, 0)
289 | body.setLinearVelocity(tempVec)
290 | body.setActive(false)
291 | addCachedBody(cacheKey, body)
292 | } else {
293 | planckWorld.destroyBody(body)
294 | }
295 | }
296 |
297 | export type SetBodyProps = {
298 | uuid: ValidUUID,
299 | method: string,
300 | methodParams: any[],
301 | }
302 |
303 | export const setBody = ({uuid, method, methodParams}: SetBodyProps) => {
304 | const body = existingBodies.get(uuid)
305 | if (!body) {
306 | console.warn(`Body not found for ${uuid}`)
307 | return
308 | }
309 | switch (method) {
310 | //case 'setAngle':
311 | // const [angle] = methodParams
312 | // body.setTransform(body.getPosition(), angle)
313 | // break;
314 | case 'setLinearVelocity':
315 | // console.log('methodParams', methodParams[0].x, methodParams[0].y);
316 | (body as any)[method](...methodParams)
317 | break;
318 | default:
319 | (body as any)[method](...methodParams)
320 | }
321 | }
322 |
323 | export type UpdateBodyData = {
324 | fixtureUpdate?: {
325 | groupIndex?: number,
326 | categoryBits?: number,
327 | maskBits?: number,
328 | }
329 | }
330 |
331 | export type UpdateBodyProps = {
332 | uuid: ValidUUID,
333 | data: UpdateBodyData,
334 | }
335 |
336 | export const updateBody = ({uuid, data}: UpdateBodyProps) => {
337 | const body = existingBodies.get(uuid)
338 | if (!body) {
339 | console.warn(`Body not found for ${uuid}`)
340 | return
341 | }
342 | const {fixtureUpdate} = data
343 | if (fixtureUpdate) {
344 | const fixture = body.getFixtureList()
345 | if (fixture) {
346 | const {
347 | groupIndex,
348 | categoryBits,
349 | maskBits
350 | } = fixtureUpdate
351 | if (
352 | groupIndex !== undefined || categoryBits !== undefined || maskBits !== undefined
353 | ) {
354 | const originalGroupIndex = fixture.getFilterGroupIndex()
355 | const originalCategoryBits = fixture.getFilterCategoryBits()
356 | const originalMaskBits = fixture.getFilterMaskBits()
357 | fixture.setFilterData({
358 | groupIndex: groupIndex !== undefined ? groupIndex : originalGroupIndex,
359 | categoryBits: categoryBits !== undefined ? categoryBits : originalCategoryBits,
360 | maskBits: maskBits !== undefined ? maskBits : originalMaskBits,
361 | })
362 | }
363 | }
364 | }
365 | }
--------------------------------------------------------------------------------
/src/main/worker/planckjs/cache.ts:
--------------------------------------------------------------------------------
1 | import { Body } from 'planck-js';
2 |
3 | export const cachedBodies: {
4 | [key: string]: Body[];
5 | } = {};
6 |
7 | export const getCachedBody = (key: string): Body | null => {
8 | const bodies = cachedBodies[key];
9 | if (bodies && bodies.length > 0) {
10 | const body = bodies.pop();
11 | if (body) {
12 | return body;
13 | }
14 | }
15 | return null;
16 | };
17 |
18 | export const addCachedBody = (key: string, body: Body) => {
19 | if (cachedBodies[key]) {
20 | cachedBodies[key].push(body);
21 | } else {
22 | cachedBodies[key] = [body];
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/src/main/worker/planckjs/collisions/collisions.ts:
--------------------------------------------------------------------------------
1 | import { Fixture } from 'planck-js';
2 | import { FixtureUserData } from './types';
3 | import { activeCollisionListeners } from './data';
4 | import {
5 | sendCollisionBeginEvent,
6 | sendCollisionEndEvent,
7 | } from '../../functions';
8 |
9 | export const getFixtureData = (fixture: Fixture): FixtureUserData | null => {
10 | const userData = fixture.getUserData() as null | FixtureUserData;
11 | return userData || null;
12 | };
13 |
14 | export const getFixtureUuid = (data: FixtureUserData | null): string => {
15 | if (data && data['uuid']) {
16 | return data.uuid;
17 | }
18 | return '';
19 | };
20 |
21 | export const getFixtureIndex = (data: FixtureUserData | null): number => {
22 | if (data) {
23 | return data.fixtureIndex;
24 | }
25 | return -1;
26 | };
27 |
28 | export const handleBeginCollision = (fixtureA: Fixture, fixtureB: Fixture) => {
29 | const aData = getFixtureData(fixtureA);
30 | const bData = getFixtureData(fixtureB);
31 | const aUUID = getFixtureUuid(aData);
32 | const bUUID = getFixtureUuid(bData);
33 |
34 | if (aUUID && activeCollisionListeners[aUUID]) {
35 | sendCollisionBeginEvent(
36 | aUUID,
37 | bData,
38 | getFixtureIndex(aData),
39 | fixtureB.isSensor()
40 | );
41 | }
42 |
43 | if (bUUID && activeCollisionListeners[bUUID]) {
44 | sendCollisionBeginEvent(
45 | bUUID,
46 | aData,
47 | getFixtureIndex(bData),
48 | fixtureA.isSensor()
49 | );
50 | }
51 | };
52 |
53 | export const handleEndCollision = (fixtureA: Fixture, fixtureB: Fixture) => {
54 | const aData = getFixtureData(fixtureA);
55 | const bData = getFixtureData(fixtureB);
56 | const aUUID = getFixtureUuid(aData);
57 | const bUUID = getFixtureUuid(bData);
58 |
59 | if (aUUID && activeCollisionListeners[aUUID]) {
60 | sendCollisionEndEvent(
61 | aUUID,
62 | bData,
63 | getFixtureIndex(aData),
64 | fixtureB.isSensor()
65 | );
66 | }
67 |
68 | if (bUUID && activeCollisionListeners[bUUID]) {
69 | sendCollisionEndEvent(
70 | bUUID,
71 | aData,
72 | getFixtureIndex(bData),
73 | fixtureA.isSensor()
74 | );
75 | }
76 | };
77 |
--------------------------------------------------------------------------------
/src/main/worker/planckjs/collisions/data.ts:
--------------------------------------------------------------------------------
1 | export const activeCollisionListeners: {
2 | [uuid: string]: boolean;
3 | } = {};
4 |
--------------------------------------------------------------------------------
/src/main/worker/planckjs/collisions/filters.ts:
--------------------------------------------------------------------------------
1 | export const COLLISION_FILTER_GROUPS = {
2 | player: 1 << 0,
3 | duckling: 1 << 1,
4 | };
5 |
--------------------------------------------------------------------------------
/src/main/worker/planckjs/collisions/types.ts:
--------------------------------------------------------------------------------
1 | export enum FixtureType {
2 | PLAYER_RANGE,
3 | MOB,
4 | }
5 |
6 | export type FixtureUserData = {
7 | uuid: string;
8 | fixtureIndex: number;
9 | type: FixtureType;
10 | [key: string]: any;
11 | };
12 |
--------------------------------------------------------------------------------
/src/main/worker/planckjs/config.ts:
--------------------------------------------------------------------------------
1 | export const PHYSICS_UPDATE_RATE = 1000 / 60;
2 |
--------------------------------------------------------------------------------
/src/main/worker/planckjs/data.ts:
--------------------------------------------------------------------------------
1 | import { FixtureUserData } from './collisions/types';
2 |
3 | export type CollisionEventProps = {
4 | uuid: string;
5 | fixtureIndex: number;
6 | isSensor: boolean;
7 | data: FixtureUserData | null;
8 | };
9 |
10 | // todo - store in context...
11 | export const storedPhysicsData: {
12 | bodies: {
13 | [uuid: string]: number;
14 | };
15 | } = {
16 | bodies: {},
17 | };
18 |
--------------------------------------------------------------------------------
/src/main/worker/planckjs/shared.ts:
--------------------------------------------------------------------------------
1 | import {Vec2, World} from "planck-js/lib";
2 | import type {Body} from "planck-js/lib"
3 | import {ValidUUID} from "../shared/types";
4 |
5 | export const planckWorld = World({
6 | allowSleep: true,
7 | gravity: Vec2(0, 0),
8 | })
9 |
10 | export const dynamicBodiesUuids: ValidUUID[] = []
11 |
12 | export const existingBodies = new Map()
--------------------------------------------------------------------------------
/src/main/worker/planckjs/world.ts:
--------------------------------------------------------------------------------
1 | import { dynamicBodiesUuids, existingBodies, planckWorld } from './shared';
2 | import {
3 | handleBeginCollision,
4 | handleEndCollision,
5 | } from './collisions/collisions';
6 | import { Contact } from 'planck-js';
7 | import {PHYSICS_UPDATE_RATE} from "./config";
8 |
9 | let lastUpdate = 0;
10 |
11 | export const syncData = (positions: Float32Array, angles: Float32Array) => {
12 | dynamicBodiesUuids.forEach((uuid, index) => {
13 | const body = existingBodies.get(uuid);
14 | if (!body) return;
15 | const position = body.getPosition();
16 | const angle = body.getAngle();
17 | positions[2 * index + 0] = position.x;
18 | positions[2 * index + 1] = position.y;
19 | angles[index] = angle;
20 | });
21 | };
22 |
23 | export const stepWorld = () => {
24 | var now = Date.now();
25 | var delta = !lastUpdate ? 0 : (now - lastUpdate) / 1000;
26 | planckWorld.step(PHYSICS_UPDATE_RATE);
27 | lastUpdate = now;
28 | };
29 |
30 | export const initPhysicsListeners = () => {
31 | planckWorld.on('begin-contact', (contact: Contact) => {
32 | const fixtureA = contact.getFixtureA();
33 | const fixtureB = contact.getFixtureB();
34 | handleBeginCollision(fixtureA, fixtureB);
35 | });
36 |
37 | planckWorld.on('end-contact', (contact: Contact) => {
38 | const fixtureA = contact.getFixtureA();
39 | const fixtureB = contact.getFixtureB();
40 | handleEndCollision(fixtureA, fixtureB);
41 | });
42 | };
43 |
--------------------------------------------------------------------------------
/src/main/worker/shared.ts:
--------------------------------------------------------------------------------
1 | import { syncData } from './planckjs/world';
2 | import { dynamicBodiesUuids } from './planckjs/shared';
3 | import { storedData } from './data';
4 | import { Buffers, WorkerOwnerMessageType } from './shared/types';
5 |
6 | export const setBodiesSynced = () => {
7 | storedData.unsyncedBodies = false;
8 | };
9 |
10 | export const setLogicBodiesSynced = () => {
11 | storedData.unsyncedLogicBodies = false;
12 | };
13 |
14 | export const syncBodies = () => {
15 | storedData.unsyncedBodies = true;
16 | storedData.unsyncedLogicBodies = true;
17 | };
18 |
19 | const sendPhysicsUpdate = (
20 | target: Worker | MessagePort,
21 | buffer: Buffers,
22 | handleBodies: (message: any) => any
23 | ) => {
24 | const { positions, angles } = buffer;
25 | if (!(positions.byteLength !== 0 && angles.byteLength !== 0)) {
26 | return;
27 | }
28 | syncData(positions, angles);
29 | const rawMessage: any = {
30 | type: WorkerOwnerMessageType.PHYSICS_STEP,
31 | physicsTick: storedData.physicsTick,
32 | };
33 | handleBodies(rawMessage);
34 | const message = {
35 | ...rawMessage,
36 | positions,
37 | angles,
38 | };
39 | target.postMessage(message, [positions.buffer, angles.buffer]);
40 | };
41 |
42 | const sendPhysicsUpdateToLogic = () => {
43 | if (!storedData.logicWorker) return;
44 | sendPhysicsUpdate(
45 | storedData.logicWorker,
46 | storedData.logicBuffers,
47 | (message: any) => {
48 | if (storedData.unsyncedLogicBodies) {
49 | message['bodies'] = dynamicBodiesUuids;
50 | setLogicBodiesSynced();
51 | }
52 | }
53 | );
54 | };
55 |
56 | const sendPhysicsUpdateToMain = () => {
57 | if (!storedData.mainWorker) return;
58 | sendPhysicsUpdate(
59 | storedData.mainWorker,
60 | storedData.buffers,
61 | (message: any) => {
62 | if (storedData.unsyncedBodies) {
63 | message['bodies'] = dynamicBodiesUuids;
64 | setBodiesSynced();
65 | }
66 | }
67 | );
68 | };
69 |
70 | export const stepProcessed = (
71 | isMain: boolean,
72 | lastProcessedPhysicsTick: number,
73 | positions: Float32Array,
74 | angles: Float32Array
75 | ) => {
76 | const buffer = isMain ? storedData.buffers : storedData.logicBuffers;
77 | buffer.positions = positions;
78 | buffer.angles = angles;
79 | if (lastProcessedPhysicsTick < storedData.physicsTick) {
80 | if (isMain) {
81 | sendPhysicsUpdateToMain();
82 | } else {
83 | sendPhysicsUpdateToLogic();
84 | }
85 | }
86 | };
87 |
--------------------------------------------------------------------------------
/src/main/worker/shared/types.ts:
--------------------------------------------------------------------------------
1 | import { WorldDef } from 'planck-js';
2 |
3 | export enum WorkerMessageType {
4 | INIT,
5 | STEP,
6 | LOGIC_FRAME,
7 | ADD_BODY,
8 | REMOVE_BODY,
9 | SET_BODY,
10 | UPDATE_BODY,
11 | PHYSICS_STEP_PROCESSED,
12 | READY_FOR_PHYSICS,
13 | }
14 |
15 | export enum WorkerOwnerMessageType {
16 | FRAME,
17 | PHYSICS_STEP,
18 | SYNC_BODIES,
19 | BEGIN_COLLISION,
20 | END_COLLISION,
21 | MESSAGE,
22 | INITIATED,
23 | }
24 |
25 | export type Buffers = {
26 | positions: Float32Array;
27 | angles: Float32Array;
28 | };
29 |
30 | export type ValidUUID = string | number;
31 |
32 | export type PhysicsProps = {
33 | config?: {
34 | maxNumberOfDynamicObjects?: number;
35 | updateRate?: number;
36 | };
37 | worldParams?: WorldDef;
38 | };
39 |
--------------------------------------------------------------------------------
/src/main/worker/utils.ts:
--------------------------------------------------------------------------------
1 | import { Buffers } from './shared/types';
2 |
3 | export const generateBuffers = (maxNumberOfPhysicsObjects: number): Buffers => {
4 | return {
5 | positions: new Float32Array(maxNumberOfPhysicsObjects * 2),
6 | angles: new Float32Array(maxNumberOfPhysicsObjects),
7 | };
8 | };
9 |
--------------------------------------------------------------------------------
/src/main/worker/worker.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import { render } from 'react-nil';
3 | import { PhysicsProps, WorkerMessageType } from './shared/types';
4 | import { createElement } from 'react';
5 | import {PHYSICS_UPDATE_RATE} from "./planckjs/config";
6 |
7 | // because of some weird react/dev/webpack/something quirk
8 | (self as any).$RefreshReg$ = () => {};
9 | (self as any).$RefreshSig$ = () => () => {};
10 |
11 | const selfWorker = (self as unknown) as Worker;
12 |
13 | selfWorker.onmessage = (event: MessageEvent) => {
14 | const { type, props = {} } = event.data as {
15 | type: WorkerMessageType;
16 | props: any;
17 | };
18 | switch (type) {
19 | case WorkerMessageType.INIT:
20 | const { worldParams = {}, config = {} } = props as PhysicsProps;
21 | const {
22 | maxNumberOfDynamicObjects = 100,
23 | updateRate = PHYSICS_UPDATE_RATE,
24 | } = config;
25 | render(
26 | createElement(
27 | require('./app/index').App,
28 | {
29 | worker: selfWorker,
30 | config: {
31 | maxNumberOfDynamicObjects,
32 | updateRate,
33 | },
34 | worldParams,
35 | },
36 | null
37 | )
38 | );
39 | break;
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/src/shared/CollisionsProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, {createContext, useCallback, useContext, useEffect, useState} from 'react';
2 | import {ValidUUID, WorkerOwnerMessageType} from '../main/worker/shared/types';
3 | import { CollisionEventProps } from '../main/worker/planckjs/data';
4 | import {useWorkerOnMessage} from "./WorkerOnMessageProvider";
5 |
6 | type CollisionsProviderContextState = {
7 | addCollisionHandler: (
8 | started: boolean,
9 | uuid: ValidUUID,
10 | callback: (data: any, fixtureIndex: number, isSensor: boolean) => void
11 | ) => void;
12 | removeCollisionHandler: (started: boolean, uuid: ValidUUID) => void;
13 | handleBeginCollision: (data: CollisionEventProps) => void;
14 | handleEndCollision: (data: CollisionEventProps) => void;
15 | };
16 |
17 | const CollisionsProviderContext = createContext(
18 | (null as unknown) as CollisionsProviderContextState
19 | );
20 |
21 | export const useCollisionsProviderContext = (): CollisionsProviderContextState => {
22 | return useContext(CollisionsProviderContext);
23 | };
24 |
25 | const CollisionsProvider: React.FC = ({ children }) => {
26 | const [collisionStartedEvents] = useState<{
27 | [key: string]: (data: any, fixtureIndex: number, isSensor: boolean) => void;
28 | }>({});
29 |
30 | const [collisionEndedEvents] = useState<{
31 | [key: string]: (data: any, fixtureIndex: number, isSensor: boolean) => void;
32 | }>({});
33 |
34 | const addCollisionHandler = useCallback(
35 | (
36 | started: boolean,
37 | uuid: ValidUUID,
38 | callback: (data: any, fixtureIndex: number, isSensor: boolean) => void
39 | ) => {
40 | if (started) {
41 | collisionStartedEvents[uuid] = callback;
42 | } else {
43 | collisionEndedEvents[uuid] = callback;
44 | }
45 | },
46 | []
47 | );
48 |
49 | const removeCollisionHandler = useCallback(
50 | (started: boolean, uuid: ValidUUID) => {
51 | if (started) {
52 | delete collisionStartedEvents[uuid];
53 | } else {
54 | delete collisionEndedEvents[uuid];
55 | }
56 | },
57 | []
58 | );
59 |
60 | const handleBeginCollision = useCallback(
61 | (data: CollisionEventProps) => {
62 | if (collisionStartedEvents[data.uuid]) {
63 | collisionStartedEvents[data.uuid](
64 | data.data,
65 | data.fixtureIndex,
66 | data.isSensor
67 | );
68 | }
69 | },
70 | [collisionStartedEvents]
71 | );
72 |
73 | const handleEndCollision = useCallback(
74 | (data: CollisionEventProps) => {
75 | if (collisionEndedEvents[data.uuid]) {
76 | collisionEndedEvents[data.uuid](
77 | data.data,
78 | data.fixtureIndex,
79 | data.isSensor
80 | );
81 | }
82 | },
83 | [collisionEndedEvents]
84 | );
85 |
86 | const onMessage = useWorkerOnMessage();
87 |
88 | useEffect(() => {
89 |
90 | const unsubscribe = onMessage((event: MessageEvent) => {
91 | const type = event.data.type;
92 |
93 | switch (type) {
94 | case WorkerOwnerMessageType.BEGIN_COLLISION:
95 | handleBeginCollision(event.data.props as any)
96 | break;
97 | case WorkerOwnerMessageType.END_COLLISION:
98 | handleEndCollision(event.data.props as any)
99 | break;
100 | default:
101 | }
102 |
103 | })
104 |
105 | return unsubscribe
106 |
107 | }, [])
108 |
109 | return (
110 |
118 | {children}
119 |
120 | );
121 | };
122 |
123 | export default CollisionsProvider;
124 |
--------------------------------------------------------------------------------
/src/shared/MeshSubscriptions.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | useCallback,
4 | useContext,
5 | useEffect,
6 | useRef,
7 | useState,
8 | } from 'react';
9 | import { Object3D } from 'three';
10 | import { ValidUUID } from '../main/worker/shared/types';
11 | import { getPositionAndAngle } from './utils';
12 | import { useStoredData } from './StoredPhysicsData';
13 | import { lerp } from '../utils/numbers';
14 | import {getNow} from "../utils/time";
15 | import {PHYSICS_UPDATE_RATE} from "../main/worker/planckjs/config";
16 |
17 | export type ContextState = {
18 | lerpMeshes: (
19 | getPhysicsStepTimeRemainingRatio: (time: number) => number
20 | ) => void;
21 | updateMeshes: (
22 | positions: Float32Array,
23 | angles: Float32Array,
24 | immediate: boolean
25 | ) => void;
26 | addSubscription: (
27 | uuid: ValidUUID,
28 | object: Object3D,
29 | applyAngle: boolean
30 | ) => () => void;
31 | };
32 |
33 | export const Context = createContext((null as unknown) as ContextState);
34 |
35 | export const useLerpMeshes = () => {
36 | return useContext(Context).lerpMeshes;
37 | };
38 |
39 | export const useAddMeshSubscription = () => {
40 | return useContext(Context).addSubscription;
41 | };
42 |
43 | export const useSubscribeMesh = (
44 | uuid: ValidUUID,
45 | object: Object3D,
46 | applyAngle: boolean = true,
47 | isDynamic: boolean = true
48 | ) => {
49 | const addSubscription = useContext(Context).addSubscription;
50 |
51 | useEffect(() => {
52 | if (!isDynamic) return;
53 |
54 | const unsubscribe = addSubscription(uuid, object, applyAngle);
55 |
56 | return () => {
57 | unsubscribe();
58 | };
59 | }, [uuid, object, applyAngle, isDynamic, addSubscription]);
60 | };
61 |
62 | export const useUpdateMeshes = () => {
63 | return useContext(Context).updateMeshes;
64 | };
65 |
66 | type Subscriptions = {
67 | [uuid: string]: {
68 | uuid: ValidUUID,
69 | object: Object3D,
70 | applyAngle: boolean,
71 | lastUpdate?: number,
72 | lastRender?: number,
73 | previous?: {
74 | position: [number, number],
75 | angle: number,
76 | },
77 | target?: {
78 | position: [number, number],
79 | angle: number,
80 | },
81 | }
82 | }
83 |
84 | const MeshSubscriptions: React.FC = ({ children }) => {
85 | const subscriptionsRef = useRef({});
86 |
87 | const lerpMeshes = useCallback(
88 | (getPhysicsStepTimeRemainingRatio: (time: number) => number) => {
89 | Object.values(subscriptionsRef.current).forEach(
90 | ({ uuid,
91 | object,
92 | target,
93 | previous,
94 | applyAngle,
95 | lastUpdate , lastRender}) => {
96 | if (!target || !previous) {
97 | return;
98 | }
99 | const { position, angle } = target;
100 | const {position: previousPosition, angle: previousAngle} = previous
101 | lastUpdate = lastUpdate || getNow()
102 |
103 | // lastUpdate = 10
104 | // nextUpdate = 20
105 | // currentUpdate = 15
106 | // min = 10
107 | // max = 20
108 |
109 | const nextExpectedUpdate = lastUpdate + PHYSICS_UPDATE_RATE + 2
110 |
111 | const min = lastUpdate
112 | const max = nextExpectedUpdate
113 | const now = getNow()
114 |
115 | const timeSinceLastRender = now - (lastRender || now)
116 |
117 | const timeUntilNextUpdate = nextExpectedUpdate - now
118 |
119 | // console.log('timeUntilNextUpdate', timeUntilNextUpdate)
120 |
121 | let normalised = ((now - min) / (max - min))
122 |
123 | normalised = normalised < 0 ? 0 : normalised > 1 ? 1 : normalised
124 |
125 | const physicsRemainingRatio = normalised
126 |
127 | // console.log('physicsRemainingRatio', physicsRemainingRatio, timeUntilNextUpdate)
128 |
129 | object.position.x = lerp(
130 | previousPosition[0],
131 | position[0],
132 | physicsRemainingRatio
133 | );
134 | object.position.y = lerp(
135 | previousPosition[1],
136 | position[1],
137 | physicsRemainingRatio
138 | );
139 | if (applyAngle) {
140 | object.rotation.z = angle; // todo - lerp
141 | }
142 |
143 | subscriptionsRef.current[uuid as string].lastRender = getNow()
144 |
145 | }
146 | );
147 | },
148 | [subscriptionsRef]
149 | );
150 |
151 | const storedData = useStoredData();
152 |
153 | const updateMeshes = useCallback(
154 | (positions: Float32Array, angles: Float32Array, immediate: boolean) => {
155 | Object.entries(subscriptionsRef.current).forEach(
156 | ([uuid, { object, target, previous, applyAngle }]) => {
157 | const index = storedData.bodies[uuid];
158 | const update = getPositionAndAngle({ positions, angles }, index);
159 | if (update) {
160 | if (immediate) {
161 | object.position.x = update.position[0];
162 | object.position.y = update.position[1];
163 | if (applyAngle) {
164 | object.rotation.x = update.angle;
165 | }
166 | }
167 | const previousTarget = subscriptionsRef.current[uuid].target
168 | if (!previousTarget) {
169 | subscriptionsRef.current[uuid].previous = {
170 | position: [object.position.x, object.position.y],
171 | angle: object.rotation.x,
172 | };
173 | } else {
174 | subscriptionsRef.current[uuid].previous = {
175 | position: previousTarget.position,
176 | angle: previousTarget.angle,
177 | };
178 | }
179 | subscriptionsRef.current[uuid].target = {
180 | position: update.position,
181 | angle: update.angle,
182 | };
183 | subscriptionsRef.current[uuid as string].lastUpdate = getNow()
184 | }
185 | }
186 | );
187 | },
188 | [subscriptionsRef, storedData]
189 | );
190 |
191 | const addSubscription = useCallback(
192 | (uuid: ValidUUID, object: Object3D, applyAngle: boolean) => {
193 | subscriptionsRef.current[uuid as string] = {
194 | uuid,
195 | object,
196 | applyAngle,
197 | };
198 |
199 | const unsubscribe = () => {
200 | delete subscriptionsRef.current[uuid as string];
201 | };
202 |
203 | return unsubscribe;
204 | },
205 | [subscriptionsRef]
206 | );
207 |
208 | return (
209 |
216 | {children}
217 |
218 | );
219 | };
220 |
221 | export default MeshSubscriptions;
222 |
--------------------------------------------------------------------------------
/src/shared/Messages.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | useCallback,
4 | useContext,
5 | useRef,
6 | useState,
7 | } from 'react';
8 | import { MessageData } from './types';
9 |
10 | type MessagesContextState = {
11 | handleMessage: (message: MessageData) => void;
12 | subscribeToMessage: (
13 | messageKey: string,
14 | callback: (data: any) => void
15 | ) => () => void;
16 | };
17 |
18 | const MessagesContext = createContext(
19 | (null as unknown) as MessagesContextState
20 | );
21 |
22 | export const useMessagesContext = () => {
23 | return useContext(MessagesContext);
24 | };
25 |
26 | export const useOnMessage = () => {
27 | return useMessagesContext().subscribeToMessage;
28 | };
29 |
30 | const Messages: React.FC = ({ children }) => {
31 | const messageCountRef = useRef(0);
32 | const [messageSubscriptions] = useState<{
33 | [key: string]: {
34 | [id: number]: (data: any) => void;
35 | };
36 | }>({});
37 |
38 | const subscribeToMessage = useCallback(
39 | (messageKey: string, callback: (data: any) => void) => {
40 | const id = messageCountRef.current;
41 | messageCountRef.current += 1;
42 |
43 | if (!messageSubscriptions[messageKey]) {
44 | messageSubscriptions[messageKey] = {
45 | [id]: callback,
46 | };
47 | } else {
48 | messageSubscriptions[messageKey][id] = callback;
49 | }
50 |
51 | const unsubscribe = () => {
52 | delete messageSubscriptions[messageKey][id];
53 | };
54 |
55 | return unsubscribe;
56 | },
57 | [messageSubscriptions]
58 | );
59 |
60 | const handleMessage = useCallback(
61 | ({ key, data }: MessageData) => {
62 |
63 | const subscriptions = messageSubscriptions[key];
64 |
65 | if (subscriptions) {
66 | Object.values(subscriptions).forEach(subscription => {
67 | subscription(data);
68 | });
69 | }
70 | },
71 | [messageSubscriptions]
72 | );
73 |
74 | return (
75 |
81 | {children}
82 |
83 | );
84 | };
85 |
86 | export default Messages;
87 |
--------------------------------------------------------------------------------
/src/shared/PhysicsProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | useCallback,
4 | useContext,
5 | useEffect,
6 | useState,
7 | } from 'react';
8 | import {
9 | AddBodyProps,
10 | RemoveBodyProps,
11 | SetBodyProps,
12 | UpdateBodyProps,
13 | } from '../main/worker/planckjs/bodies';
14 | import { WorkerMessageType } from '../main/worker/shared/types';
15 |
16 | export type ContextState = {
17 | workerAddBody: (props: AddBodyProps) => void;
18 | workerRemoveBody: (props: RemoveBodyProps) => void;
19 | workerSetBody: (props: SetBodyProps) => void;
20 | workerUpdateBody: (props: UpdateBodyProps) => void;
21 | };
22 |
23 | export const Context = createContext((null as unknown) as ContextState);
24 |
25 | export const usePhysicsProvider = (): ContextState => {
26 | return useContext(Context);
27 | };
28 |
29 | const PhysicsProvider: React.FC<{
30 | worker: Worker | MessagePort;
31 | }> = ({ children, worker }) => {
32 | const workerAddBody = useCallback((props: AddBodyProps) => {
33 | worker.postMessage({
34 | type: WorkerMessageType.ADD_BODY,
35 | props: props,
36 | });
37 | }, []);
38 |
39 | const workerRemoveBody = useCallback((props: RemoveBodyProps) => {
40 | worker.postMessage({
41 | type: WorkerMessageType.REMOVE_BODY,
42 | props,
43 | });
44 | }, []);
45 |
46 | const workerSetBody = useCallback((props: SetBodyProps) => {
47 | worker.postMessage({
48 | type: WorkerMessageType.SET_BODY,
49 | props,
50 | });
51 | }, []);
52 |
53 | const workerUpdateBody = useCallback((props: UpdateBodyProps) => {
54 | worker.postMessage({
55 | type: WorkerMessageType.UPDATE_BODY,
56 | props,
57 | });
58 | }, []);
59 |
60 | return (
61 |
69 | {children}
70 |
71 | );
72 | };
73 |
74 | export default PhysicsProvider;
75 |
--------------------------------------------------------------------------------
/src/shared/PhysicsSync.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | FC,
4 | useCallback,
5 | useContext,
6 | useEffect,
7 | useRef,
8 | } from 'react';
9 | import { PHYSICS_UPDATE_RATE } from '../main/worker/planckjs/config';
10 | import { useWorkerOnMessage } from './WorkerOnMessageProvider';
11 | import {
12 | WorkerMessageType,
13 | WorkerOwnerMessageType,
14 | } from '../main/worker/shared/types';
15 | import { useStoredData } from './StoredPhysicsData';
16 | import { useUpdateMeshes } from './MeshSubscriptions';
17 | import {getNow} from "../utils/time";
18 |
19 | type State = {
20 | onFixedUpdate: (callback: (delta: number) => void) => () => void;
21 | getPhysicsStepTimeRemainingRatio: (time: number) => number;
22 | };
23 |
24 | const Context = createContext((null as unknown) as State);
25 |
26 | export const useGetPhysicsStepTimeRemainingRatio = () => {
27 | return useContext(Context).getPhysicsStepTimeRemainingRatio;
28 | };
29 |
30 | export const useFixedUpdate = (callback: (delta: number) => void) => {
31 | const onFixedUpdate = useContext(Context).onFixedUpdate;
32 |
33 | useEffect(() => {
34 | const unsubscribe = onFixedUpdate(callback);
35 |
36 | return () => {
37 | unsubscribe();
38 | };
39 | }, [onFixedUpdate, callback]);
40 | };
41 |
42 | const PhysicsSync: FC<{
43 | worker: Worker | MessagePort;
44 | noLerping?: boolean;
45 | }> = ({ children, worker, noLerping = false }) => {
46 | const lastUpdateRef = useRef(getNow());
47 | const countRef = useRef(0);
48 | const callbacksRef = useRef<{
49 | [key: string]: (delta: number) => void;
50 | }>({});
51 | const updateMeshes = useUpdateMeshes();
52 |
53 | const getPhysicsStepTimeRemainingRatio = useCallback(
54 | (previousTime: number) => {
55 | const nextExpectedUpdate =
56 | lastUpdateRef.current + PHYSICS_UPDATE_RATE + 1;
57 | const time = getNow();
58 | let ratio = (time - previousTime) / (nextExpectedUpdate - previousTime);
59 | ratio = ratio > 1 ? 1 : ratio;
60 | ratio = ratio < 0 ? 0 : ratio;
61 | return ratio;
62 | },
63 | [lastUpdateRef]
64 | );
65 |
66 | const onFixedUpdate = useCallback(
67 | (callback: (delta: number) => void) => {
68 | const key = countRef.current;
69 | countRef.current += 1;
70 |
71 | callbacksRef.current[key] = callback;
72 |
73 | const unsubscribe = () => {
74 | delete callbacksRef.current[key];
75 | };
76 |
77 | return unsubscribe;
78 | },
79 | [callbacksRef]
80 | );
81 |
82 | const onMessage = useWorkerOnMessage();
83 | const storedData = useStoredData();
84 |
85 | const debugRefs = useRef<{
86 | timer: any;
87 | hasReceived: boolean;
88 | }>({
89 | timer: null,
90 | hasReceived: false,
91 | });
92 |
93 | useEffect(() => {
94 | debugRefs.current.timer = setTimeout(() => {
95 | console.warn('no initial physics data received...');
96 | }, 1000);
97 |
98 | const onPhysicsStep = () => {
99 | const lastUpdate = lastUpdateRef.current;
100 | const now = getNow();
101 | const delta = !lastUpdate ? 1 / 60 : (now - lastUpdate) / 1000;
102 | lastUpdateRef.current = now;
103 |
104 | const callbacks = callbacksRef.current;
105 |
106 | Object.values(callbacks).forEach(callback => {
107 | callback(delta);
108 | });
109 | };
110 |
111 | const unsubscribe = onMessage((event: MessageEvent) => {
112 | const type = event.data.type;
113 |
114 | if (type === WorkerOwnerMessageType.PHYSICS_STEP) {
115 | debugRefs.current.hasReceived = true;
116 | if (debugRefs.current.timer) {
117 | clearInterval(debugRefs.current.timer);
118 | }
119 | debugRefs.current.timer = setTimeout(() => {
120 | console.warn('over 1 second since last physics step...');
121 | }, 1000);
122 | const positions = event.data.positions as Float32Array;
123 | const angles = event.data.angles as Float32Array;
124 | // console.log('update')
125 | updateMeshes(positions, angles, noLerping);
126 | worker.postMessage(
127 | {
128 | type: WorkerMessageType.PHYSICS_STEP_PROCESSED,
129 | positions,
130 | angles,
131 | physicsTick: event.data.physicsTick as number,
132 | },
133 | [positions.buffer, angles.buffer]
134 | );
135 |
136 | if (event.data.bodies) {
137 | storedData.bodies = event.data.bodies.reduce(
138 | (acc: { [key: string]: number }, id: string) => ({
139 | ...acc,
140 | [id]: (event.data as any).bodies.indexOf(id),
141 | }),
142 | {}
143 | );
144 | }
145 | onPhysicsStep();
146 | }
147 | });
148 |
149 | worker.postMessage(
150 | {
151 | type: WorkerMessageType.READY_FOR_PHYSICS,
152 | }
153 | )
154 |
155 | return () => {
156 | unsubscribe();
157 | };
158 | }, [
159 | onMessage,
160 | callbacksRef,
161 | lastUpdateRef,
162 | worker,
163 | updateMeshes,
164 | noLerping,
165 | storedData,
166 | ]);
167 |
168 | return (
169 |
175 | {children}
176 |
177 | );
178 | };
179 |
180 | export default PhysicsSync;
181 |
--------------------------------------------------------------------------------
/src/shared/SendMessages.tsx:
--------------------------------------------------------------------------------
1 | import React, {createContext, useCallback, useContext} from "react"
2 | import {WorkerOwnerMessageType} from "../main/worker/shared/types";
3 | import {MessageData, MessageKeys} from "./types";
4 | import {useMessagesContext} from "./Messages";
5 |
6 | type ContextState = {
7 | sendMessage: (key: string, data: any) => void,
8 | }
9 |
10 | const Context = createContext(null as unknown as ContextState)
11 |
12 | export const useSendMessage = () => {
13 | return useContext(Context).sendMessage
14 | }
15 |
16 | const SendMessages: React.FC<{
17 | worker: Worker,
18 | }> = ({children, worker}) => {
19 |
20 | const { handleMessage } = useMessagesContext();
21 |
22 | const sendMessage = useCallback((key: string, data: any) => {
23 |
24 | if (key === MessageKeys.SYNC_COMPONENT) {
25 | throw new Error(`${key} is a reserved message key.`)
26 | }
27 |
28 | const message: MessageData = {
29 | key,
30 | data
31 | }
32 |
33 | worker.postMessage({
34 | type: WorkerOwnerMessageType.MESSAGE,
35 | message,
36 | })
37 |
38 | handleMessage(message)
39 |
40 | }, [worker, handleMessage])
41 |
42 | return (
43 |
44 | {children}
45 |
46 | )
47 | }
48 |
49 | export default SendMessages
--------------------------------------------------------------------------------
/src/shared/StoredPhysicsData.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, FC, useContext, useState } from 'react';
2 |
3 | type Bodies = {
4 | [uuid: string]: number;
5 | };
6 |
7 | type ContextState = {
8 | data: {
9 | bodies: Bodies;
10 | };
11 | };
12 |
13 | const Context = createContext((null as unknown) as ContextState);
14 |
15 | export const useStoredData = () => {
16 | return useContext(Context).data;
17 | };
18 |
19 | const StoredPhysicsData: FC = ({ children }) => {
20 | const [data] = useState<{
21 | bodies: Bodies;
22 | }>({
23 | bodies: {},
24 | });
25 |
26 | return (
27 |
32 | {children}
33 |
34 | );
35 | };
36 |
37 | export default StoredPhysicsData;
38 |
--------------------------------------------------------------------------------
/src/shared/WorkerOnMessageProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | useCallback,
4 | useContext,
5 | useEffect,
6 | useRef,
7 | useState,
8 | } from 'react';
9 |
10 | export type ContextState = {
11 | subscribe: (callback: (event: MessageEvent) => void) => () => void;
12 | };
13 |
14 | export const Context = createContext((null as unknown) as ContextState);
15 |
16 | export const useWorkerOnMessage = () => {
17 | return useContext(Context).subscribe;
18 | };
19 |
20 | const WorkerOnMessageProvider: React.FC<{
21 | subscribe: (callback: (event: MessageEvent) => void) => () => void;
22 | }> = ({ children, subscribe }) => {
23 | return (
24 |
29 | {children}
30 |
31 | );
32 | };
33 |
34 | export default WorkerOnMessageProvider;
35 |
--------------------------------------------------------------------------------
/src/shared/types.ts:
--------------------------------------------------------------------------------
1 | export enum MessageKeys {
2 | SYNC_COMPONENT = 'SYNC_COMPONENT',
3 | }
4 |
5 | export type MessageData = {
6 | key: string;
7 | data: any;
8 | };
9 |
10 | export enum SyncComponentMessageType {
11 | MOUNT,
12 | UNMOUNT,
13 | UPDATE,
14 | }
15 |
16 | export enum SyncComponentType {
17 | PLAYER,
18 | }
19 |
20 | export type SyncComponentMessageInfo = {
21 | componentType: SyncComponentType;
22 | componentKey: string;
23 | };
24 |
25 | export type ValidProps =
26 | | undefined
27 | | {
28 | [key: string]: any;
29 | };
30 |
31 | export type SyncComponentMessage = {
32 | data: ValidProps;
33 | info: SyncComponentMessageInfo;
34 | messageType: SyncComponentMessageType;
35 | };
36 |
37 | export type MappedComponents = {
38 | [key: string]: any;
39 | };
40 |
--------------------------------------------------------------------------------
/src/shared/utils.ts:
--------------------------------------------------------------------------------
1 | import { Buffers } from '../main/worker/shared/types';
2 | import { Object3D } from 'three';
3 |
4 | export const getPositionAndAngle = (
5 | buffers: Buffers,
6 | index: number
7 | ): {
8 | position: [number, number];
9 | angle: number;
10 | } | null => {
11 | if (index !== undefined && buffers.positions.length) {
12 | const start = index * 2;
13 | const position = (buffers.positions.slice(start, start + 2) as unknown) as [
14 | number,
15 | number
16 | ];
17 | return {
18 | position,
19 | angle: buffers.angles[index],
20 | };
21 | } else {
22 | return null;
23 | }
24 | };
25 | export const applyPositionAngle = (
26 | buffers: Buffers,
27 | object: Object3D | null,
28 | index: number,
29 | applyAngle: boolean = false
30 | ) => {
31 | if (index !== undefined && buffers.positions.length && !!object) {
32 | const start = index * 2;
33 | const position = buffers.positions.slice(start, start + 2);
34 | object.position.x = position[0];
35 | object.position.y = position[1];
36 | if (applyAngle) {
37 | object.rotation.z = buffers.angles[index];
38 | }
39 | } else {
40 | // console.warn('no match?')
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/src/utils/hooks.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useRef} from "react";
2 |
3 | export const useDidMount = () => {
4 | const mountRef = useRef(false);
5 |
6 | useEffect(() => { mountRef.current = true }, []);
7 |
8 | return () => mountRef.current;
9 | }
--------------------------------------------------------------------------------
/src/utils/numbers.ts:
--------------------------------------------------------------------------------
1 | import { MathUtils } from 'three';
2 |
3 | export const lerp = MathUtils.lerp;
4 |
--------------------------------------------------------------------------------
/src/utils/time.ts:
--------------------------------------------------------------------------------
1 | export const getNow = () => {
2 | return performance.timing.navigationStart + performance.now()
3 | }
--------------------------------------------------------------------------------
/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": false,
21 | "noUnusedParameters": false,
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 | "allowSyntheticDefaultImports": true,
29 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
30 | "skipLibCheck": true,
31 | // error out if import and file system have a casing mismatch. Recommended by TS
32 | "forceConsistentCasingInFileNames": true,
33 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
34 | "noEmit": true
35 | }
36 | }
37 |
--------------------------------------------------------------------------------