├── LICENSE
├── js-physics-benchmark
├── .gitignore
├── .vscode
│ └── extensions.json
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favicon.png
│ ├── global.css
│ └── index.html
├── rollup.config.js
├── src
│ ├── App.svelte
│ ├── global.d.ts
│ └── main.ts
└── tsconfig.json
├── matterjs-pixi-worker
├── .gitignore
├── README.md
├── favicon.svg
├── index.html
├── package-lock.json
├── package.json
├── public
│ └── square.png
├── src
│ ├── PhysicsMain.ts
│ ├── main.ts
│ ├── physicsWorker.ts
│ ├── renderer.ts
│ ├── style
│ │ └── global.css
│ ├── util.ts
│ └── vite-env.d.ts
├── tsconfig.json
└── vite.config.js
├── rapier-array-buffer-performance
├── .gitignore
├── README.md
├── favicon.svg
├── index.html
├── package-lock.json
├── package.json
├── public
│ └── square.png
├── server.js
├── src
│ ├── main.ts
│ ├── physicsWorker.ts
│ ├── rapier.ts
│ ├── renderer.ts
│ ├── style
│ │ └── global.css
│ ├── util.ts
│ └── vite-env.d.ts
├── tsconfig.json
└── vite.config.js
├── rapier-pixi-character-controller-dynamic
├── .gitignore
├── README.md
├── favicon.svg
├── index.html
├── package-lock.json
├── package.json
├── public
│ └── square.png
├── src
│ ├── draw
│ │ ├── _colorTheme.ts
│ │ ├── envBallGraphics.ts
│ │ └── wallGraphics.ts
│ ├── main.ts
│ ├── physics
│ │ ├── ballFactory.ts
│ │ ├── core.ts
│ │ └── wallFactory.ts
│ ├── player.ts
│ ├── rapier.ts
│ ├── renderer.ts
│ ├── style
│ │ └── global.css
│ └── vite-env.d.ts
├── tsconfig.json
└── vite.config.js
└── rapier-pixi-worker
├── .gitignore
├── README.md
├── favicon.svg
├── index.html
├── package-lock.json
├── package.json
├── public
└── square.png
├── src
├── main.ts
├── physicsWorker.ts
├── rapier.ts
├── renderer.ts
├── style
│ └── global.css
├── util.ts
└── vite-env.d.ts
├── tsconfig.json
└── vite.config.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Marcin Jerzak
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.
22 |
--------------------------------------------------------------------------------
/js-physics-benchmark/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /public/build/
3 |
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/js-physics-benchmark/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["svelte.svelte-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/js-physics-benchmark/README.md:
--------------------------------------------------------------------------------
1 | *Psst — looking for a more complete solution? Check out [SvelteKit](https://kit.svelte.dev), the official framework for building web applications of all sizes, with a beautiful development experience and flexible filesystem-based routing.*
2 |
3 | *Looking for a shareable component template instead? You can [use SvelteKit for that as well](https://kit.svelte.dev/docs#packaging) or the older [sveltejs/component-template](https://github.com/sveltejs/component-template)*
4 |
5 | ---
6 |
7 | # svelte app
8 |
9 | This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
10 |
11 | To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
12 |
13 | ```bash
14 | npx degit sveltejs/template svelte-app
15 | cd svelte-app
16 | ```
17 |
18 | *Note that you will need to have [Node.js](https://nodejs.org) installed.*
19 |
20 |
21 | ## Get started
22 |
23 | Install the dependencies...
24 |
25 | ```bash
26 | cd svelte-app
27 | npm install
28 | ```
29 |
30 | ...then start [Rollup](https://rollupjs.org):
31 |
32 | ```bash
33 | npm run dev
34 | ```
35 |
36 | Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
37 |
38 | By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
39 |
40 | If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense.
41 |
42 | ## Building and running in production mode
43 |
44 | To create an optimised version of the app:
45 |
46 | ```bash
47 | npm run build
48 | ```
49 |
50 | You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
51 |
52 |
53 | ## Single-page app mode
54 |
55 | By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
56 |
57 | If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
58 |
59 | ```js
60 | "start": "sirv public --single"
61 | ```
62 |
63 | ## Using TypeScript
64 |
65 | This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with:
66 |
67 | ```bash
68 | node scripts/setupTypeScript.js
69 | ```
70 |
71 | Or remove the script via:
72 |
73 | ```bash
74 | rm scripts/setupTypeScript.js
75 | ```
76 |
77 | If you want to use `baseUrl` or `path` aliases within your `tsconfig`, you need to set up `@rollup/plugin-alias` to tell Rollup to resolve the aliases. For more info, see [this StackOverflow question](https://stackoverflow.com/questions/63427935/setup-tsconfig-path-in-svelte).
78 |
79 | ## Deploying to the web
80 |
81 | ### With [Vercel](https://vercel.com)
82 |
83 | Install `vercel` if you haven't already:
84 |
85 | ```bash
86 | npm install -g vercel
87 | ```
88 |
89 | Then, from within your project folder:
90 |
91 | ```bash
92 | cd public
93 | vercel deploy --name my-project
94 | ```
95 |
96 | ### With [surge](https://surge.sh/)
97 |
98 | Install `surge` if you haven't already:
99 |
100 | ```bash
101 | npm install -g surge
102 | ```
103 |
104 | Then, from within your project folder:
105 |
106 | ```bash
107 | npm run build
108 | surge public my-project.surge.sh
109 | ```
110 |
--------------------------------------------------------------------------------
/js-physics-benchmark/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-app",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "rollup -c",
7 | "dev": "rollup -c -w",
8 | "start": "sirv public --no-clear",
9 | "check": "svelte-check --tsconfig ./tsconfig.json"
10 | },
11 | "devDependencies": {
12 | "@rollup/plugin-commonjs": "^17.0.0",
13 | "@rollup/plugin-node-resolve": "^11.0.0",
14 | "rollup": "^2.3.4",
15 | "rollup-plugin-css-only": "^3.1.0",
16 | "rollup-plugin-livereload": "^2.0.0",
17 | "rollup-plugin-svelte": "^7.0.0",
18 | "rollup-plugin-terser": "^7.0.0",
19 | "svelte": "^3.0.0",
20 | "svelte-check": "^2.0.0",
21 | "svelte-preprocess": "^4.0.0",
22 | "@rollup/plugin-typescript": "^8.0.0",
23 | "typescript": "^4.0.0",
24 | "tslib": "^2.0.0",
25 | "@tsconfig/svelte": "^2.0.0"
26 | },
27 | "dependencies": {
28 | "sirv-cli": "^1.0.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/js-physics-benchmark/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerzakm/gamedev-experiments/b482122293b6373d33d2f8ab91a9084cbc24d3e7/js-physics-benchmark/public/favicon.png
--------------------------------------------------------------------------------
/js-physics-benchmark/public/global.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | position: relative;
3 | width: 100%;
4 | height: 100%;
5 | }
6 |
7 | body {
8 | color: #333;
9 | margin: 0;
10 | padding: 8px;
11 | box-sizing: border-box;
12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
13 | }
14 |
15 | a {
16 | color: rgb(0,100,200);
17 | text-decoration: none;
18 | }
19 |
20 | a:hover {
21 | text-decoration: underline;
22 | }
23 |
24 | a:visited {
25 | color: rgb(0,80,160);
26 | }
27 |
28 | label {
29 | display: block;
30 | }
31 |
32 | input, button, select, textarea {
33 | font-family: inherit;
34 | font-size: inherit;
35 | -webkit-padding: 0.4em 0;
36 | padding: 0.4em;
37 | margin: 0 0 0.5em 0;
38 | box-sizing: border-box;
39 | border: 1px solid #ccc;
40 | border-radius: 2px;
41 | }
42 |
43 | input:disabled {
44 | color: #ccc;
45 | }
46 |
47 | button {
48 | color: #333;
49 | background-color: #f4f4f4;
50 | outline: none;
51 | }
52 |
53 | button:disabled {
54 | color: #999;
55 | }
56 |
57 | button:not(:disabled):active {
58 | background-color: #ddd;
59 | }
60 |
61 | button:focus {
62 | border-color: #666;
63 | }
64 |
--------------------------------------------------------------------------------
/js-physics-benchmark/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Svelte app
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/js-physics-benchmark/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import resolve from '@rollup/plugin-node-resolve';
4 | import livereload from 'rollup-plugin-livereload';
5 | import { terser } from 'rollup-plugin-terser';
6 | import sveltePreprocess from 'svelte-preprocess';
7 | import typescript from '@rollup/plugin-typescript';
8 | import css from 'rollup-plugin-css-only';
9 |
10 | const production = !process.env.ROLLUP_WATCH;
11 |
12 | function serve() {
13 | let server;
14 |
15 | function toExit() {
16 | if (server) server.kill(0);
17 | }
18 |
19 | return {
20 | writeBundle() {
21 | if (server) return;
22 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
23 | stdio: ['ignore', 'inherit', 'inherit'],
24 | shell: true
25 | });
26 |
27 | process.on('SIGTERM', toExit);
28 | process.on('exit', toExit);
29 | }
30 | };
31 | }
32 |
33 | export default {
34 | input: 'src/main.ts',
35 | output: {
36 | sourcemap: true,
37 | format: 'iife',
38 | name: 'app',
39 | file: 'public/build/bundle.js'
40 | },
41 | plugins: [
42 | svelte({
43 | preprocess: sveltePreprocess({ sourceMap: !production }),
44 | compilerOptions: {
45 | // enable run-time checks when not in production
46 | dev: !production
47 | }
48 | }),
49 | // we'll extract any component CSS out into
50 | // a separate file - better for performance
51 | css({ output: 'bundle.css' }),
52 |
53 | // If you have external dependencies installed from
54 | // npm, you'll most likely need these plugins. In
55 | // some cases you'll need additional configuration -
56 | // consult the documentation for details:
57 | // https://github.com/rollup/plugins/tree/master/packages/commonjs
58 | resolve({
59 | browser: true,
60 | dedupe: ['svelte']
61 | }),
62 | commonjs(),
63 | typescript({
64 | sourceMap: !production,
65 | inlineSources: !production
66 | }),
67 |
68 | // In dev mode, call `npm run start` once
69 | // the bundle has been generated
70 | !production && serve(),
71 |
72 | // Watch the `public` directory and refresh the
73 | // browser on changes when not in production
74 | !production && livereload('public'),
75 |
76 | // If we're building for production (npm run build
77 | // instead of npm run dev), minify
78 | production && terser()
79 | ],
80 | watch: {
81 | clearScreen: false
82 | }
83 | };
84 |
--------------------------------------------------------------------------------
/js-physics-benchmark/src/App.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 | Hello {name}!
26 |
27 | Visit the Svelte tutorial to learn
28 | how to build Svelte apps.
29 |
30 |
31 |
32 |
53 |
--------------------------------------------------------------------------------
/js-physics-benchmark/src/global.d.ts:
--------------------------------------------------------------------------------
1 | ///
--------------------------------------------------------------------------------
/js-physics-benchmark/src/main.ts:
--------------------------------------------------------------------------------
1 | import App from './App.svelte';
2 |
3 | const app = new App({
4 | target: document.body,
5 | props: {
6 | name: 'world'
7 | }
8 | });
9 |
10 | export default app;
--------------------------------------------------------------------------------
/js-physics-benchmark/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/svelte/tsconfig.json",
3 |
4 | "include": ["src/**/*"],
5 | "exclude": ["node_modules/*", "__sapper__/*", "public/*"]
6 | }
--------------------------------------------------------------------------------
/matterjs-pixi-worker/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
--------------------------------------------------------------------------------
/matterjs-pixi-worker/README.md:
--------------------------------------------------------------------------------
1 | ## Running JS physics in a webworker - my experience building a proof of concept
2 |
3 | Web workers are a great way of offloading compute intensive tasks from the main thread. I have been interested in using them for quite a while, but none of the projects I worked on really justified using them. Until now! In this short series I'm going to explore using webworkers, physics, pixi.js and others to create interactive web experiences and games.
4 |
5 | - [Live](https://workerized-matterjs-pixi.netlify.app/)
6 | - [Github](https://github.com/jerzakm/gamedev-experiments/tree/main/matterjs-pixi-worker)
7 |
8 | 
9 |
10 | ## Webworkers tldr;
11 |
12 | - scripts that run in background threads
13 | - they communicate with the main thread by sending and receiving messages
14 |
15 | In depth info, better than I could ever explain:
16 |
17 | - [Using web workers for safe, concurrent JavaScript - Bruce Wilson, Logrocket](https://blog.logrocket.com/using-webworkers-for-safe-concurrent-javascript-3f33da4eb0b2/)
18 | - [MDN entry](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
19 |
20 | ## Why?
21 |
22 | The benefits of using webworkers are undeniable. Most importantly it **keeps main thread responsive.** Frozen webpages and slow UI make for terrible user experience. In my case, even if the physics simulation slows down to 20-30 fps, mainthread renderer still runs at a constant 144 fps. It helps keep animations nice and juicy and the page responsive to user inputs.
23 |
24 | I am guilty of making very CPU intensive terrain generation in the past, it would freeze a user's browser for 2-3 seconds and it was terrible.
25 |
26 | ## Proof of concept implementation:
27 |
28 | This is not a step by step tutorial, I wanted to keep this article more conceptual and code-light. You should be able to follow my Spaghetti code in [the project repo](https://github.com/jerzakm/gamedev-experiments/tree/main/matterjs-pixi-worker).
29 |
30 | ### 1. Vite bundler
31 |
32 | I decided against using any framework to avoid unnecessary complexity. For my bundler I decided to use Vite since I was familiar with it [and the provided vanilla Typescript template](https://github.com/vitejs/vite/tree/main/packages/create-vite). It provides an [easy way to import webworkers](https://vitejs.dev/guide/features.html#web-workers) and their dependencies even from Typescript files.
33 |
34 | ### 2. Pixi.js renderer
35 |
36 | [Pixi.js](https://pixijs.com/) is a fairly easy to use WebGL renderer. It will show what we're doing on screen. Everything I'm doing can be replicated by copying one of [the examples](https://pixijs.io/examples/#/demos-basic/container.js). All you need is to:
37 |
38 | - setup the renderer
39 | - load texture and make sprites
40 | - update sprite position and angle in the ticker
41 |
42 | ### 3. Finally, making the worker!
43 |
44 | - make a file with a worker, like `physicsWorker.ts`. Code gets executed on worker load.
45 | - import and initialize the worker in the main thread - [vite docs](https://vitejs.dev/guide/features.html#web-workers)
46 | - from now on you can setup listeners and send messages between main thread and the worker
47 |
48 | ### 4. Physics engine in the worker.
49 |
50 | [Matter.js](https://brm.io/matter-js/) is a 2D physics engine I've decided to use. It's far from being the most performant, but it's user friendly and helps keep code complexity down.
51 |
52 | Engine, World and a 'gameloop' get created when web worker is loaded. Gameloop is a function that continuously runs and calls `Engine.update(physics.engine, delta);`
53 |
54 | ### 5. Communication & command pattern
55 |
56 | Like I mentioned before, worker and the thread communicate with messages. I found this to be a natural fit for a [command pattern](https://gameprogrammingpatterns.com/command.html).
57 |
58 | Actor (either main or worker thread) sends an object that has all information required to perform an action by the subject. I decided to structure my commands like below.
59 |
60 | ```ts
61 | const command = {
62 | type: "ADD_BODY",
63 | data: {
64 | x: 0,
65 | y: 0,
66 | width: 10,
67 | height: 10,
68 | options: {
69 | restitution: 0,
70 | },
71 | },
72 | };
73 | ```
74 |
75 | To send the above command, main thread calls `worker.postMessage(command);`. For a worker to receive it, we need to set up a listener.
76 |
77 | ```ts
78 | // Worker has to call 'self' to send and receive
79 | self.addEventListener("message", (e) => {
80 | const message = e.data || e;
81 |
82 | // Worker receives a command to ADD_BODY
83 | if (message.type == "ADD_BODY") {
84 | // it does stuff
85 | const { x, y, width, height, options } = message.data;
86 | const body = physics.addBody(x, y, width, height, options);
87 |
88 | // Worker sends a command to main thread (BODY_CREATED)
89 | // it will be used to spawn a sprite
90 | self.postMessage({
91 | type: "BODY_CREATED",
92 | data: {
93 | id: body.id,
94 | x,
95 | y,
96 | width,
97 | height,
98 | angle: 0,
99 | sprite: undefined,
100 | },
101 | });
102 | }
103 | });
104 | ```
105 |
106 | **Here's a general overview of how this example works**
107 | 
108 |
109 | ### 6. Features explained
110 |
111 | #### Create body
112 |
113 | - Main thread sends a command `ADD_BODY` with position, width, height and [physics options](https://brm.io/matter-js/docs/classes/Body.html#properties)
114 | - When worker thread receives an `ADD_BODY` it adds the body with given parameters to the world
115 | - After body is added, worker sends `BODY_CREATED` command back to main thread. **The most important part of this message is the id**. This is how technically unrelated javascript objects (body in worker and sprite in main) will sync. It also sends width, height, position, angle
116 | - When main thread receives `BODY_CREATED` position it creates an object containing the data received as well as a `PIXI.Sprite` it assigns to it.
117 |
118 | #### Synchronising object position between physics engine and renderer
119 |
120 | - each frame physics engine sends command `BODY_SYNC`, it contains position and angle of every body in the physics world. It's stored in the hashmap format, with body id being the key.
121 |
122 | ```ts
123 | const data: any = {};
124 |
125 | for (const body of world.bodies) {
126 | data[body] = {
127 | x: body.position.x,
128 | y: body.position.y,
129 | angle: body.angle,
130 | };
131 | }
132 | self.postMessage({
133 | type: "BODY_SYNC",
134 | data,
135 | });
136 | ```
137 |
138 | - mainthread receives the body `BODY_SYNC`. It loops over every body previously added and updates it.
139 |
140 | ```ts
141 | if (e.data.type == "BODY_SYNC") {
142 | const physData = e.data.data;
143 |
144 | bodySyncDelta = e.data.delta;
145 |
146 | for (const obj of physicsObjects) {
147 | const { x, y, angle } = physData[obj.id];
148 | if (!obj.sprite) return;
149 | obj.sprite.position.x = x;
150 | obj.sprite.position.y = y;
151 | obj.sprite.rotation = angle;
152 | }
153 | }
154 | ```
155 |
156 | ## It works!
157 |
158 | 
159 |
160 | ### What went wrong:
161 |
162 | - Physics performance is lacking, but there are a lot of good areas for improvement.
163 | - Sometimes objects got out of bounds and kept flying into x,y coords of 10000+, causing slowdown and eventual crash. I quickly dealt with it by freezing any object whose coordinate is more than 3000, it's not a perfect solution and something to look out for in the future.
164 | - Simple command pattern worked fine here but it could get very complex in some use cases
165 |
166 | ## Future improvement considerations
167 |
168 | ### 1. Matter.js is slow
169 |
170 | According to [this outdated benchmark](http://olegkikin.com/js-physics-engines-benchmark/) matter.js is one of the slowest available javascript physics engines. It's performance has improved since then, but there are other alternatives. I am especially interested in WASM libraries with js binding, like
171 |
172 | - [box2dwasm](https://github.com/Birch-san/box2d-wasm) - an old, still maintained C++ library compiled to WASM. The documentation is lacking and developer experience seems poor.
173 | - [rapier.rs](https://rapier.rs) - modern physics library written in Rust. It looks good and performant, at a first glance dev experience is a lot better than box2d. [Documentation](https://rapier.rs/docs/user_guides/javascript/getting_started_js) gives me hope!
174 |
175 | In general, chosing a WASM engine over JS one should yield large performance gain.
176 |
177 | ### 2. Webworker messages
178 |
179 | Sending large amounts of data at high frequency (gameloop) between worker and mainthread with messages can cause large performance drops.
180 |
181 | In depth dive into the issue: ["Is postmessage slow?" - surma.dev](https://surma.dev/things/is-postmessage-slow/)
182 |
183 | Approaches to consider:
184 |
185 | - JSON.stringify then JSON.parse of the data (this doesn't seem to boost performance for my usecase)
186 | - Using [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) and transfering ownership between worker and main
187 | - Using [SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) so the origin retains ownership and both threads can access the data with [Atomics](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics)
188 |
189 | I guess it's time for my own benchmark!
190 |
191 | ### 3. Using a webworker library instead of vanilla implementation
192 |
193 | I can imagine that communication with vanilla webworkers could get very complex. [Comlink](https://github.com/GoogleChromeLabs/comlink) is something that's been on my list for a while and I'd like to try it out.
194 |
195 | **From the [Comlink Github page](https://github.com/GoogleChromeLabs/comlink):**
196 |
197 | Comlink makes WebWorkers enjoyable. Comlink is a tiny library (1.1kB), that removes the mental barrier of thinking about postMessage and hides the fact that you are working with workers.
198 |
199 | At a more abstract level it is an RPC implementation for postMessage and ES6 Proxies.
200 |
201 | ### 4. Renderer interpolation
202 |
203 | If the use case doesn't call for more, I could keep the physics engine locked at 30 or 60 fps. The issue with this, is that the movement will look 'choppy'.
204 | I could use interpolation and use available position and velocity data to 'predict' object movement and generate the frames up to say 144fps for smooth animations.
205 |
206 | ## The end.
207 |
208 | This turned out much longer than I expected. More to come?
209 |
--------------------------------------------------------------------------------
/matterjs-pixi-worker/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/matterjs-pixi-worker/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pixijs + physics
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/matterjs-pixi-worker/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "matterjs-pixi-worker",
3 | "version": "0.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@pixi/accessibility": {
8 | "version": "6.1.3",
9 | "resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-6.1.3.tgz",
10 | "integrity": "sha512-JK6rtqfC2/rnJt1xLPznH2lNH0Jx9f2Py7uh50VM1sqoYrkyAAegenbOdyEzgB35Q4oQji3aBkTsWn2mrwXp/g=="
11 | },
12 | "@pixi/app": {
13 | "version": "6.1.3",
14 | "resolved": "https://registry.npmjs.org/@pixi/app/-/app-6.1.3.tgz",
15 | "integrity": "sha512-gryDVXuzErRIgY5G2CRQH6fZM7Pk3m1CFEInXEKa4rmVzfwRz+3OeU0YNSnD9atPAS5C2TaAzE4yOSHH2+wESQ=="
16 | },
17 | "@pixi/compressed-textures": {
18 | "version": "6.1.3",
19 | "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-6.1.3.tgz",
20 | "integrity": "sha512-FO2B7GhDMlZA0fnpH2PvNOh6ZlRxQoJnNlpjzNw+x1nvF9h3+V6dbFoG9oBC5zAisTfacdfoo1TdT789Oh+kTg=="
21 | },
22 | "@pixi/constants": {
23 | "version": "6.1.3",
24 | "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-6.1.3.tgz",
25 | "integrity": "sha512-Qvz/SIxw+dQ6P9niOEdILWX2DQ5FnGA0XZNFLW/3amekzad/+WqHobL+Mg5S6A4/a9mXTnqjyB0BqhhtLfpFkA=="
26 | },
27 | "@pixi/core": {
28 | "version": "6.1.3",
29 | "resolved": "https://registry.npmjs.org/@pixi/core/-/core-6.1.3.tgz",
30 | "integrity": "sha512-UQsR1Q7c+Zcvtu6HrYMidvoyF/j9n3b4WXPh3ojuNV6+ZIvps3rznoZYaIx6foEJNhj7HM9fMObsimGP+FB36A=="
31 | },
32 | "@pixi/display": {
33 | "version": "6.1.3",
34 | "resolved": "https://registry.npmjs.org/@pixi/display/-/display-6.1.3.tgz",
35 | "integrity": "sha512-8/GdapJVKfl6PUkxX/Et5zB1aXny+uy353cQX886KJ6dGle82fQAYjIn7I6Xm+JiZWOhWo0N6KE9cjotO0rroA=="
36 | },
37 | "@pixi/extract": {
38 | "version": "6.1.3",
39 | "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-6.1.3.tgz",
40 | "integrity": "sha512-yZOsXc9Lh+U59ayl+DoWDPmndrOJj5ft2nzENMAvz2rVEOHQjWxH73qCSP6Wa5VsoINyJLMmV4MTbI+U0SH7GA=="
41 | },
42 | "@pixi/filter-alpha": {
43 | "version": "6.1.3",
44 | "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-6.1.3.tgz",
45 | "integrity": "sha512-eubgEO/qlxQbuPXgwxTZxTBTWjA0EQbrs7TyPqyBK2Wj0eEvimaVQ8u4eiqfMFJCZLnuWDCAPJpP9bMHxBXXpQ=="
46 | },
47 | "@pixi/filter-blur": {
48 | "version": "6.1.3",
49 | "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-6.1.3.tgz",
50 | "integrity": "sha512-uo8FHpV+qm4SuXcDnWqZWrznHmLJ3b8ibgLAgi/e8VmwrFiC+EqGa4n4V8J+xtR5P/iA3lT5pRgWw09/xHN3dQ=="
51 | },
52 | "@pixi/filter-color-matrix": {
53 | "version": "6.1.3",
54 | "resolved": "https://registry.npmjs.org/@pixi/filter-color-matrix/-/filter-color-matrix-6.1.3.tgz",
55 | "integrity": "sha512-d1pyxmVrGDOrO5pINe+fTspj1NNxiIp2IZ+FGgT7e17xnxjXTvtk4n4KqXAZFS1NCoStImDAV5j+b8Lysdg5jQ=="
56 | },
57 | "@pixi/filter-displacement": {
58 | "version": "6.1.3",
59 | "resolved": "https://registry.npmjs.org/@pixi/filter-displacement/-/filter-displacement-6.1.3.tgz",
60 | "integrity": "sha512-tIXK8vXzb2unMxGmu4gjdlOwddnkHA0IJXFTOF25a5h36v/AHqWwWG4h5G775oPu37UuhuYjeD/j229t0Q9QNQ=="
61 | },
62 | "@pixi/filter-fxaa": {
63 | "version": "6.1.3",
64 | "resolved": "https://registry.npmjs.org/@pixi/filter-fxaa/-/filter-fxaa-6.1.3.tgz",
65 | "integrity": "sha512-yhKVxX5vFKQz3lxfqAGg4XoajFyIRR8XzWqEHgAsPMFRnIIQIbF25bMRygZj12P61z3vxwqAM/2bn7S46Ii1zQ=="
66 | },
67 | "@pixi/filter-noise": {
68 | "version": "6.1.3",
69 | "resolved": "https://registry.npmjs.org/@pixi/filter-noise/-/filter-noise-6.1.3.tgz",
70 | "integrity": "sha512-oVRtcJwbN6VnAnvXZuLEZ0c12JUzporao5AziXgRAUjTMA3bFVE0/7Dx193Kx/l6UAasmzhWQctuv6NMxy5Efw=="
71 | },
72 | "@pixi/graphics": {
73 | "version": "6.1.3",
74 | "resolved": "https://registry.npmjs.org/@pixi/graphics/-/graphics-6.1.3.tgz",
75 | "integrity": "sha512-e5O47yECRp5WXWIvKhLDQKpiak7CfIqJzuTuQIyE7jXp8QiJNw+aoWNlJEd4ksKbsDkP3EE39CxlmiaBpxNL3w=="
76 | },
77 | "@pixi/interaction": {
78 | "version": "6.1.3",
79 | "resolved": "https://registry.npmjs.org/@pixi/interaction/-/interaction-6.1.3.tgz",
80 | "integrity": "sha512-ju3fE/KnO6KZChnZzZAdY6bfjlSh7/igZcVcd/MZRkAdNozx4QoN5sYmwrcvTvA5llMYaThSIRWgIHQiSlbOfQ=="
81 | },
82 | "@pixi/loaders": {
83 | "version": "6.1.3",
84 | "resolved": "https://registry.npmjs.org/@pixi/loaders/-/loaders-6.1.3.tgz",
85 | "integrity": "sha512-qOvy72bsVGzCmWyoofm6dm1l//hd+bJneidngplwsovpqnnyMfuewCpQjeLRL6rLqcHR40V1+Qo4iJ+ElMdVZQ=="
86 | },
87 | "@pixi/math": {
88 | "version": "6.1.3",
89 | "resolved": "https://registry.npmjs.org/@pixi/math/-/math-6.1.3.tgz",
90 | "integrity": "sha512-1bLZeHpG38Bz6TESwxayNbL7tztOd7gpZDXS5OiBB9n8SFZeKlWfRQ/aJrvjoBz2qsZf9gGeVKsHpC/FJz0qnA=="
91 | },
92 | "@pixi/mesh": {
93 | "version": "6.1.3",
94 | "resolved": "https://registry.npmjs.org/@pixi/mesh/-/mesh-6.1.3.tgz",
95 | "integrity": "sha512-TF9eKNQdowozVOr4G05+Auku2EK8XwDXKYVvMYvt6Tsn2DLSrRhWl7xYyj4EuTjW/4eaP/c2QqY18cEMoMtJiQ=="
96 | },
97 | "@pixi/mesh-extras": {
98 | "version": "6.1.3",
99 | "resolved": "https://registry.npmjs.org/@pixi/mesh-extras/-/mesh-extras-6.1.3.tgz",
100 | "integrity": "sha512-HuTV8SkTQZDU1bmHmJWRo+4Hiz89oCuOonE3ckfqsoAoULfImgU72qqNIq7Vxmnu3kXoXAwV+fvOl49OzWl4+w=="
101 | },
102 | "@pixi/mixin-cache-as-bitmap": {
103 | "version": "6.1.3",
104 | "resolved": "https://registry.npmjs.org/@pixi/mixin-cache-as-bitmap/-/mixin-cache-as-bitmap-6.1.3.tgz",
105 | "integrity": "sha512-mEa0kn3Mou3KhbAUpaGnvmPz/ifI/41af1N6kVcTz1V8cu4BI/f74xLv5pKkQtp+xzWlquGo/2z9urkrRFD6qA=="
106 | },
107 | "@pixi/mixin-get-child-by-name": {
108 | "version": "6.1.3",
109 | "resolved": "https://registry.npmjs.org/@pixi/mixin-get-child-by-name/-/mixin-get-child-by-name-6.1.3.tgz",
110 | "integrity": "sha512-HHrnA1MtsMSyW0lOnBlklHp7j3JGYHIyick4b8F8p8eKqOFiAVdLzf4tmX/fKF4zs6i7DuYKE8G9Z7vpAhyrFg=="
111 | },
112 | "@pixi/mixin-get-global-position": {
113 | "version": "6.1.3",
114 | "resolved": "https://registry.npmjs.org/@pixi/mixin-get-global-position/-/mixin-get-global-position-6.1.3.tgz",
115 | "integrity": "sha512-XqhEyViMlGOS+p2LKW2tFjQy4ghbARKriwgY10MGvNApHHZbUDL3VKM1EmR6F2Xj8PPmycWRw/0oBu148O2KhQ=="
116 | },
117 | "@pixi/particle-container": {
118 | "version": "6.1.3",
119 | "resolved": "https://registry.npmjs.org/@pixi/particle-container/-/particle-container-6.1.3.tgz",
120 | "integrity": "sha512-pZqRRL5Yx2Yy30cdjsNEXRpTfl1WEf640ZLVHX2+fcKcWftPJaIXQZR+0aLvijyWF3VA4O/r/8IxhYgiMkqAUQ=="
121 | },
122 | "@pixi/polyfill": {
123 | "version": "6.1.3",
124 | "resolved": "https://registry.npmjs.org/@pixi/polyfill/-/polyfill-6.1.3.tgz",
125 | "integrity": "sha512-e+g2sHK/ORKDOrhJ86zZgdMSkQNzKdkaMw/UUFZ5wEUJgltoqF7H0zwNVPPO/1m7hfrN02PBMinYtXM+qFdY/A==",
126 | "requires": {
127 | "object-assign": "^4.1.1",
128 | "promise-polyfill": "^8.2.0"
129 | }
130 | },
131 | "@pixi/prepare": {
132 | "version": "6.1.3",
133 | "resolved": "https://registry.npmjs.org/@pixi/prepare/-/prepare-6.1.3.tgz",
134 | "integrity": "sha512-zjv81fPJjdQyWGCbA9Ij04GfwJUYA3j6/vFyJFaDKVMqEWzNDJwu40G00P23BXh3F5dYL638EXvyLYDQavjseg=="
135 | },
136 | "@pixi/runner": {
137 | "version": "6.1.3",
138 | "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-6.1.3.tgz",
139 | "integrity": "sha512-hJw7O9enlei7Cp5/j2REKuUjvyyC4BGqmVycmt01jTYyphRYMNQgyF+OjwrL7nidZMXnCVzfNKWi8e5+c4wssg=="
140 | },
141 | "@pixi/settings": {
142 | "version": "6.1.3",
143 | "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-6.1.3.tgz",
144 | "integrity": "sha512-laKwS4/R+bTQokKIeMeMO4orvSNTMWUpNRXJbDq7N29bCrA5pT6BW+LNZ+4gJs4TFK/s9bmP/xU5BlPVKHRoyg==",
145 | "requires": {
146 | "ismobilejs": "^1.1.0"
147 | }
148 | },
149 | "@pixi/sprite": {
150 | "version": "6.1.3",
151 | "resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-6.1.3.tgz",
152 | "integrity": "sha512-TzvqeRV+bbxFbucR74c28wcDsCbXic+5dONM+fy31ejAIraKbigzKbgHxH6opgLEMMh5APzmJPlwntYdEUGSXQ=="
153 | },
154 | "@pixi/sprite-animated": {
155 | "version": "6.1.3",
156 | "resolved": "https://registry.npmjs.org/@pixi/sprite-animated/-/sprite-animated-6.1.3.tgz",
157 | "integrity": "sha512-COrFkmcMPxyv3zGRJJrNB2nOdaeDEOYTkbxUcNxMSJ7eT3O3PUX5XEvfOW7bl2zHkt8XraIQ66uwWychqGHx7Q=="
158 | },
159 | "@pixi/sprite-tiling": {
160 | "version": "6.1.3",
161 | "resolved": "https://registry.npmjs.org/@pixi/sprite-tiling/-/sprite-tiling-6.1.3.tgz",
162 | "integrity": "sha512-om+RrModhNFljb8C1fhpGKtgt5k5AW9gCjFfeBPN+5pVdVjtc/luyO2Cbubpeow9YQldrUZri9it63GBo07Cfw=="
163 | },
164 | "@pixi/spritesheet": {
165 | "version": "6.1.3",
166 | "resolved": "https://registry.npmjs.org/@pixi/spritesheet/-/spritesheet-6.1.3.tgz",
167 | "integrity": "sha512-QUqAYUzn/+0JlzrLo7ASIFzJSteGZuNMxKwyFL29JtttUIjdJlXe3+jrfUMAu6gewYd9HVYkXJ0ODhH8PH6KpA=="
168 | },
169 | "@pixi/text": {
170 | "version": "6.1.3",
171 | "resolved": "https://registry.npmjs.org/@pixi/text/-/text-6.1.3.tgz",
172 | "integrity": "sha512-R0D3cbwwLbQOfobja4NGhq0bF7biCfNE3PXsOmTEsWOroVJqUexIob5XZXoT9Avy3B8nlrB2Hyl5imIQx60jFw=="
173 | },
174 | "@pixi/text-bitmap": {
175 | "version": "6.1.3",
176 | "resolved": "https://registry.npmjs.org/@pixi/text-bitmap/-/text-bitmap-6.1.3.tgz",
177 | "integrity": "sha512-x46qOVoosl67dBrG3mgd2eQx3A9NTxWUnzgRpk5vsNfLLNRu6XlM+YoscRMuHT5sLEEBLewjcVxzAAkrSW45eQ=="
178 | },
179 | "@pixi/ticker": {
180 | "version": "6.1.3",
181 | "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-6.1.3.tgz",
182 | "integrity": "sha512-ZSuhe5HrmkDoqSIZjETUGYCQr/EbtDQGngq0LQLAgblyhAJbi4p/B3uf2XGfRNZ7Tdxdl0j81BmUqBEu2+DeoA=="
183 | },
184 | "@pixi/utils": {
185 | "version": "6.1.3",
186 | "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-6.1.3.tgz",
187 | "integrity": "sha512-05mm9TBbpYorYO3ALC4CVgR5K6sA/0uhnwE/Zl4ZhNJZN699LrIr0OWFQhxhySeGUPMDaizeEZpn2rhx+CYYpg==",
188 | "requires": {
189 | "@types/earcut": "^2.1.0",
190 | "earcut": "^2.2.2",
191 | "eventemitter3": "^3.1.0",
192 | "url": "^0.11.0"
193 | }
194 | },
195 | "@types/earcut": {
196 | "version": "2.1.1",
197 | "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.1.tgz",
198 | "integrity": "sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ=="
199 | },
200 | "@types/matter-js": {
201 | "version": "0.17.6",
202 | "resolved": "https://registry.npmjs.org/@types/matter-js/-/matter-js-0.17.6.tgz",
203 | "integrity": "sha512-i6WLNuM7/89SLqO2aOyaUkom9tc3B/qo4ekh7BD99xQ8+wOVVZO0F4RzKNYZCaFwr+xp3pK3oIb6sSVjLpz+pA==",
204 | "dev": true
205 | },
206 | "@types/offscreencanvas": {
207 | "version": "2019.6.4",
208 | "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.6.4.tgz",
209 | "integrity": "sha512-u8SAgdZ8ROtkTF+mfZGOscl0or6BSj9A4g37e6nvxDc+YB/oDut0wHkK2PBBiC2bNR8TS0CPV+1gAk4fNisr1Q==",
210 | "dev": true
211 | },
212 | "earcut": {
213 | "version": "2.2.3",
214 | "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.3.tgz",
215 | "integrity": "sha512-iRDI1QeCQIhMCZk48DRDMVgQSSBDmbzzNhnxIo+pwx3swkfjMh6vh0nWLq1NdvGHLKH6wIrAM3vQWeTj6qeoug=="
216 | },
217 | "esbuild": {
218 | "version": "0.13.10",
219 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.10.tgz",
220 | "integrity": "sha512-0NfCsnAh5XatHIx6Cu93wpR2v6opPoOMxONYhaAoZKzGYqAE+INcDeX2wqMdcndvPQdWCuuCmvlnsh0zmbHcSQ==",
221 | "dev": true,
222 | "requires": {
223 | "esbuild-android-arm64": "0.13.10",
224 | "esbuild-darwin-64": "0.13.10",
225 | "esbuild-darwin-arm64": "0.13.10",
226 | "esbuild-freebsd-64": "0.13.10",
227 | "esbuild-freebsd-arm64": "0.13.10",
228 | "esbuild-linux-32": "0.13.10",
229 | "esbuild-linux-64": "0.13.10",
230 | "esbuild-linux-arm": "0.13.10",
231 | "esbuild-linux-arm64": "0.13.10",
232 | "esbuild-linux-mips64le": "0.13.10",
233 | "esbuild-linux-ppc64le": "0.13.10",
234 | "esbuild-netbsd-64": "0.13.10",
235 | "esbuild-openbsd-64": "0.13.10",
236 | "esbuild-sunos-64": "0.13.10",
237 | "esbuild-windows-32": "0.13.10",
238 | "esbuild-windows-64": "0.13.10",
239 | "esbuild-windows-arm64": "0.13.10"
240 | }
241 | },
242 | "esbuild-android-arm64": {
243 | "version": "0.13.10",
244 | "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.10.tgz",
245 | "integrity": "sha512-1sCdVAq64yMp2Uhlu+97/enFxpmrj31QHtThz7K+/QGjbHa7JZdBdBsZCzWJuntKHZ+EU178tHYkvjaI9z5sGg==",
246 | "dev": true,
247 | "optional": true
248 | },
249 | "esbuild-darwin-64": {
250 | "version": "0.13.10",
251 | "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.10.tgz",
252 | "integrity": "sha512-XlL+BYZ2h9cz3opHfFgSHGA+iy/mljBFIRU9q++f9SiBXEZTb4gTW/IENAD1l9oKH0FdO9rUpyAfV+lM4uAxrg==",
253 | "dev": true,
254 | "optional": true
255 | },
256 | "esbuild-darwin-arm64": {
257 | "version": "0.13.10",
258 | "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.10.tgz",
259 | "integrity": "sha512-RZMMqMTyActMrXKkW71IQO8B0tyQm0Bm+ZJQWNaHJchL5LlqazJi7rriwSocP+sKLszHhsyTEBBh6qPdw5g5yQ==",
260 | "dev": true,
261 | "optional": true
262 | },
263 | "esbuild-freebsd-64": {
264 | "version": "0.13.10",
265 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.10.tgz",
266 | "integrity": "sha512-pf4BEN9reF3jvZEZdxljVgOv5JS4kuYFCI78xk+2HWustbLvTP0b9XXfWI/OD0ZLWbyLYZYIA+VbVe4tdAklig==",
267 | "dev": true,
268 | "optional": true
269 | },
270 | "esbuild-freebsd-arm64": {
271 | "version": "0.13.10",
272 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.10.tgz",
273 | "integrity": "sha512-j9PUcuNWmlxr4/ry4dK/s6zKh42Jhh/N5qnAAj7tx3gMbkIHW0JBoVSbbgp97p88X9xgKbXx4lG2sJDhDWmsYQ==",
274 | "dev": true,
275 | "optional": true
276 | },
277 | "esbuild-linux-32": {
278 | "version": "0.13.10",
279 | "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.10.tgz",
280 | "integrity": "sha512-imtdHG5ru0xUUXuc2ofdtyw0fWlHYXV7JjF7oZHgmn0b+B4o4Nr6ZON3xxoo1IP8wIekW+7b9exIf/MYq0QV7w==",
281 | "dev": true,
282 | "optional": true
283 | },
284 | "esbuild-linux-64": {
285 | "version": "0.13.10",
286 | "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.10.tgz",
287 | "integrity": "sha512-O7fzQIH2e7GC98dvoTH0rad5BVLm9yU3cRWfEmryCEIFTwbNEWCEWOfsePuoGOHRtSwoVY1hPc21CJE4/9rWxQ==",
288 | "dev": true,
289 | "optional": true
290 | },
291 | "esbuild-linux-arm": {
292 | "version": "0.13.10",
293 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.10.tgz",
294 | "integrity": "sha512-R2Jij4A0K8BcmBehvQeUteQEcf24Y2YZ6mizlNFuJOBPxe3vZNmkZ4mCE7Pf1tbcqA65qZx8J3WSHeGJl9EsJA==",
295 | "dev": true,
296 | "optional": true
297 | },
298 | "esbuild-linux-arm64": {
299 | "version": "0.13.10",
300 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.10.tgz",
301 | "integrity": "sha512-bkGxN67S2n0PF4zhh87/92kBTsH2xXLuH6T5omReKhpXdJZF5SVDSk5XU/nngARzE+e6QK6isK060Dr5uobzNw==",
302 | "dev": true,
303 | "optional": true
304 | },
305 | "esbuild-linux-mips64le": {
306 | "version": "0.13.10",
307 | "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.10.tgz",
308 | "integrity": "sha512-UDNO5snJYOLWrA2uOUxM/PVbzzh2TR7Zf2i8zCCuFlYgvAb/81XO+Tasp3YAElDpp4VGqqcpBXLtofa9nrnJGA==",
309 | "dev": true,
310 | "optional": true
311 | },
312 | "esbuild-linux-ppc64le": {
313 | "version": "0.13.10",
314 | "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.10.tgz",
315 | "integrity": "sha512-xu6J9rMWu1TcEGuEmoc8gsTrJCEPsf+QtxK4IiUZNde9r4Q4nlRVah4JVZP3hJapZgZJcxsse0XiKXh1UFdOeA==",
316 | "dev": true,
317 | "optional": true
318 | },
319 | "esbuild-netbsd-64": {
320 | "version": "0.13.10",
321 | "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.10.tgz",
322 | "integrity": "sha512-d+Gr0ScMC2J83Bfx/ZvJHK0UAEMncctwgjRth9d4zppYGLk/xMfFKxv5z1ib8yZpQThafq8aPm8AqmFIJrEesw==",
323 | "dev": true,
324 | "optional": true
325 | },
326 | "esbuild-openbsd-64": {
327 | "version": "0.13.10",
328 | "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.10.tgz",
329 | "integrity": "sha512-OuCYc+bNKumBvxflga+nFzZvxsgmWQW+z4rMGIjM5XIW0nNbGgRc5p/0PSDv0rTdxAmwCpV69fezal0xjrDaaA==",
330 | "dev": true,
331 | "optional": true
332 | },
333 | "esbuild-sunos-64": {
334 | "version": "0.13.10",
335 | "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.10.tgz",
336 | "integrity": "sha512-gUkgivZK11bD56wDoLsnYrsOHD/zHzzLSdqKcIl3wRMulfHpRBpoX8gL0dbWr+8N9c+1HDdbNdvxSRmZ4RCVwg==",
337 | "dev": true,
338 | "optional": true
339 | },
340 | "esbuild-windows-32": {
341 | "version": "0.13.10",
342 | "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.10.tgz",
343 | "integrity": "sha512-C1xJ54E56dGWRaYcTnRy7amVZ9n1/D/D2/qVw7e5EtS7p+Fv/yZxxgqyb1hMGKXgtFYX4jMpU5eWBF/AsYrn+A==",
344 | "dev": true,
345 | "optional": true
346 | },
347 | "esbuild-windows-64": {
348 | "version": "0.13.10",
349 | "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.10.tgz",
350 | "integrity": "sha512-6+EXEXopEs3SvPFAHcps2Krp/FvqXXsOQV33cInmyilb0ZBEQew4MIoZtMIyB3YXoV6//dl3i6YbPrFZaWEinQ==",
351 | "dev": true,
352 | "optional": true
353 | },
354 | "esbuild-windows-arm64": {
355 | "version": "0.13.10",
356 | "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.10.tgz",
357 | "integrity": "sha512-xTqM/XKhORo6u9S5I0dNJWEdWoemFjogLUTVLkQMVyUV3ZuMChahVA+bCqKHdyX55pCFxD/8v2fm3/sfFMWN+g==",
358 | "dev": true,
359 | "optional": true
360 | },
361 | "eventemitter3": {
362 | "version": "3.1.2",
363 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
364 | "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
365 | },
366 | "fsevents": {
367 | "version": "2.3.2",
368 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
369 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
370 | "dev": true,
371 | "optional": true
372 | },
373 | "function-bind": {
374 | "version": "1.1.1",
375 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
376 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
377 | "dev": true
378 | },
379 | "has": {
380 | "version": "1.0.3",
381 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
382 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
383 | "dev": true,
384 | "requires": {
385 | "function-bind": "^1.1.1"
386 | }
387 | },
388 | "is-core-module": {
389 | "version": "2.8.0",
390 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz",
391 | "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==",
392 | "dev": true,
393 | "requires": {
394 | "has": "^1.0.3"
395 | }
396 | },
397 | "ismobilejs": {
398 | "version": "1.1.1",
399 | "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz",
400 | "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw=="
401 | },
402 | "matter-js": {
403 | "version": "0.17.1",
404 | "resolved": "https://registry.npmjs.org/matter-js/-/matter-js-0.17.1.tgz",
405 | "integrity": "sha512-pSquoENJgvSAlQGcA0s5UkmEohGXZaUww2g3B6qG87x0iEcVf+aigMXn5UkFPdnh6w3B+C4vXSLaYqhHwKrOLA=="
406 | },
407 | "nanoid": {
408 | "version": "3.1.30",
409 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz",
410 | "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==",
411 | "dev": true
412 | },
413 | "object-assign": {
414 | "version": "4.1.1",
415 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
416 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
417 | },
418 | "path-parse": {
419 | "version": "1.0.7",
420 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
421 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
422 | "dev": true
423 | },
424 | "picocolors": {
425 | "version": "1.0.0",
426 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
427 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
428 | "dev": true
429 | },
430 | "pixi.js": {
431 | "version": "6.1.3",
432 | "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-6.1.3.tgz",
433 | "integrity": "sha512-h8Y/YVgP4CSPoUQvXaQvQf5GyQxi0b1NtVD38bZQsrX4CQ3r85jBU+zPyHN0fAcvhCB+nNvdD2sEwhhqkNsuSw==",
434 | "requires": {
435 | "@pixi/accessibility": "6.1.3",
436 | "@pixi/app": "6.1.3",
437 | "@pixi/compressed-textures": "6.1.3",
438 | "@pixi/constants": "6.1.3",
439 | "@pixi/core": "6.1.3",
440 | "@pixi/display": "6.1.3",
441 | "@pixi/extract": "6.1.3",
442 | "@pixi/filter-alpha": "6.1.3",
443 | "@pixi/filter-blur": "6.1.3",
444 | "@pixi/filter-color-matrix": "6.1.3",
445 | "@pixi/filter-displacement": "6.1.3",
446 | "@pixi/filter-fxaa": "6.1.3",
447 | "@pixi/filter-noise": "6.1.3",
448 | "@pixi/graphics": "6.1.3",
449 | "@pixi/interaction": "6.1.3",
450 | "@pixi/loaders": "6.1.3",
451 | "@pixi/math": "6.1.3",
452 | "@pixi/mesh": "6.1.3",
453 | "@pixi/mesh-extras": "6.1.3",
454 | "@pixi/mixin-cache-as-bitmap": "6.1.3",
455 | "@pixi/mixin-get-child-by-name": "6.1.3",
456 | "@pixi/mixin-get-global-position": "6.1.3",
457 | "@pixi/particle-container": "6.1.3",
458 | "@pixi/polyfill": "6.1.3",
459 | "@pixi/prepare": "6.1.3",
460 | "@pixi/runner": "6.1.3",
461 | "@pixi/settings": "6.1.3",
462 | "@pixi/sprite": "6.1.3",
463 | "@pixi/sprite-animated": "6.1.3",
464 | "@pixi/sprite-tiling": "6.1.3",
465 | "@pixi/spritesheet": "6.1.3",
466 | "@pixi/text": "6.1.3",
467 | "@pixi/text-bitmap": "6.1.3",
468 | "@pixi/ticker": "6.1.3",
469 | "@pixi/utils": "6.1.3"
470 | }
471 | },
472 | "postcss": {
473 | "version": "8.3.11",
474 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.11.tgz",
475 | "integrity": "sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==",
476 | "dev": true,
477 | "requires": {
478 | "nanoid": "^3.1.30",
479 | "picocolors": "^1.0.0",
480 | "source-map-js": "^0.6.2"
481 | }
482 | },
483 | "promise-polyfill": {
484 | "version": "8.2.1",
485 | "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.1.tgz",
486 | "integrity": "sha512-3p9zj0cEHbp7NVUxEYUWjQlffXqnXaZIMPkAO7HhFh8u5636xLRDHOUo2vpWSK0T2mqm6fKLXYn1KP6PAZ2gKg=="
487 | },
488 | "punycode": {
489 | "version": "1.3.2",
490 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
491 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
492 | },
493 | "querystring": {
494 | "version": "0.2.0",
495 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
496 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
497 | },
498 | "resolve": {
499 | "version": "1.20.0",
500 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
501 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
502 | "dev": true,
503 | "requires": {
504 | "is-core-module": "^2.2.0",
505 | "path-parse": "^1.0.6"
506 | }
507 | },
508 | "rollup": {
509 | "version": "2.58.3",
510 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.58.3.tgz",
511 | "integrity": "sha512-ei27MSw1KhRur4p87Q0/Va2NAYqMXOX++FNEumMBcdreIRLURKy+cE2wcDJKBn0nfmhP2ZGrJkP1XPO+G8FJQw==",
512 | "dev": true,
513 | "requires": {
514 | "fsevents": "~2.3.2"
515 | }
516 | },
517 | "source-map-js": {
518 | "version": "0.6.2",
519 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz",
520 | "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==",
521 | "dev": true
522 | },
523 | "typescript": {
524 | "version": "4.4.4",
525 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
526 | "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
527 | "dev": true
528 | },
529 | "url": {
530 | "version": "0.11.0",
531 | "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
532 | "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
533 | "requires": {
534 | "punycode": "1.3.2",
535 | "querystring": "0.2.0"
536 | }
537 | },
538 | "vite": {
539 | "version": "2.6.13",
540 | "resolved": "https://registry.npmjs.org/vite/-/vite-2.6.13.tgz",
541 | "integrity": "sha512-+tGZ1OxozRirTudl4M3N3UTNJOlxdVo/qBl2IlDEy/ZpTFcskp+k5ncNjayR3bRYTCbqSOFz2JWGN1UmuDMScA==",
542 | "dev": true,
543 | "requires": {
544 | "esbuild": "^0.13.2",
545 | "fsevents": "~2.3.2",
546 | "postcss": "^8.3.8",
547 | "resolve": "^1.20.0",
548 | "rollup": "^2.57.0"
549 | }
550 | }
551 | }
552 | }
553 |
--------------------------------------------------------------------------------
/matterjs-pixi-worker/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "matterjs-pixi-worker",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite --config vite.config.js",
6 | "build": "tsc && vite build",
7 | "preview": "vite preview"
8 | },
9 | "devDependencies": {
10 | "@types/matter-js": "^0.17.6",
11 | "@types/offscreencanvas": "^2019.6.4",
12 | "typescript": "^4.3.2",
13 | "vite": "^2.6.4"
14 | },
15 | "dependencies": {
16 | "matter-js": "^0.17.1",
17 | "pixi.js": "^6.1.3"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/matterjs-pixi-worker/public/square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerzakm/gamedev-experiments/b482122293b6373d33d2f8ab91a9084cbc24d3e7/matterjs-pixi-worker/public/square.png
--------------------------------------------------------------------------------
/matterjs-pixi-worker/src/PhysicsMain.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Bodies,
3 | Body,
4 | Engine,
5 | IChamferableBodyDefinition,
6 | Render,
7 | World,
8 | } from "matter-js";
9 |
10 | export class PhysicsRunner {
11 | engine: Engine;
12 | world: World;
13 | render: Render | undefined;
14 | constructor() {
15 | const engine = Engine.create();
16 | const world = engine.world;
17 | engine.gravity.x = 0;
18 | engine.gravity.y = 0;
19 | this.engine = engine;
20 | this.world = world;
21 | }
22 |
23 | public addBody(
24 | x: number,
25 | y: number,
26 | width: number,
27 | height: number,
28 | options: IChamferableBodyDefinition
29 | ) {
30 | const body = Bodies.rectangle(x, y, width, height, options);
31 | World.addBody(this.world, body);
32 |
33 | return body;
34 | }
35 |
36 | public applyForceToRandomBody() {
37 | const bodyIndex = Math.round(Math.random() * this.world.bodies.length);
38 | const body = this.world.bodies[bodyIndex];
39 | if (!body) return;
40 | Body.applyForce(body, body.position, {
41 | x: (Math.random() - 0.5) * body.density * 25 * Math.random(),
42 | y: (Math.random() - 0.5) * body.density * 25 * Math.random(),
43 | });
44 | }
45 |
46 | public getBodySyncData() {
47 | const bodyData: any = {};
48 |
49 | for (let i = 0; i < this.world.bodies.length; i++) {
50 | bodyData[this.world.bodies[i].id] = {
51 | x: this.world.bodies[i].position.x,
52 | y: this.world.bodies[i].position.y,
53 | angle: this.world.bodies[i].angle,
54 | };
55 | }
56 |
57 | return bodyData;
58 | }
59 |
60 | public outOfBoundCheck() {
61 | for (let i = 0; i < this.world.bodies.length; i++) {
62 | if (
63 | this.world.bodies[i].position.x < -100 ||
64 | this.world.bodies[i].position.x > 3000 ||
65 | this.world.bodies[i].position.y < 0 ||
66 | this.world.bodies[i].position.y > 3000
67 | ) {
68 | Body.setStatic(this.world.bodies[i], true);
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/matterjs-pixi-worker/src/main.ts:
--------------------------------------------------------------------------------
1 | import "./style/global.css";
2 | import * as PIXI from "pixi.js";
3 | import { Renderer } from "./renderer";
4 | import PhysicsWorker from "./physicsWorker?worker";
5 | import { IChamferableBodyDefinition } from "matter-js";
6 |
7 | const spawnerAmount = 1;
8 | const spawnerTimer = 1000;
9 | const spawnAtStart = 4500;
10 |
11 | let bodySyncDelta = 0;
12 | let rendererFps = 0;
13 | let bodyCount = 0;
14 | let statsUpdateFrequency = 500;
15 |
16 | const initStats = () => {
17 | const statsDom = document.body.querySelector("#stats");
18 |
19 | if (!statsDom) return;
20 |
21 | statsDom.innerHTML = `
22 | Bodies ${bodyCount}
23 | renderer_fps ${rendererFps.toFixed(0)}
24 | physics_fps ${(1000 / bodySyncDelta).toFixed(0)}
25 | `;
26 |
27 | setTimeout(initStats, statsUpdateFrequency);
28 | };
29 |
30 | async function workerExample() {
31 | const worker = new PhysicsWorker();
32 |
33 | const { app, stage } = new Renderer();
34 | const container = new PIXI.Container();
35 |
36 | stage.addChild(container);
37 |
38 | const physicsObjects: IPhysicsSyncBody[] = [];
39 |
40 | const addBody = (
41 | x = 0,
42 | y = 0,
43 | width = 10,
44 | height = 10,
45 | options: IChamferableBodyDefinition = {
46 | restitution: 0,
47 | }
48 | ) => {
49 | const newBody = {
50 | x,
51 | y,
52 | width,
53 | height,
54 | options,
55 | };
56 |
57 | worker.postMessage({
58 | type: "ADD_BODY",
59 | data: newBody,
60 | });
61 | };
62 |
63 | const spawnRandomDynamicSquare = () => {
64 | const x =
65 | window.innerWidth / 2 + (Math.random() - 0.5) * window.innerWidth * 0.8;
66 | const y =
67 | window.innerHeight / 2 + (Math.random() - 0.5) * window.innerHeight * 0.8;
68 |
69 | const options = {
70 | restitution: 0,
71 | };
72 |
73 | const size = 4 + 10 * Math.random();
74 | addBody(x, y, size, size, options);
75 | };
76 |
77 | const setupWalls = () => {
78 | addBody(window.innerWidth / 2, 0, window.innerWidth, 50, {
79 | isStatic: true,
80 | });
81 | addBody(window.innerWidth / 2, window.innerHeight, window.innerWidth, 50, {
82 | isStatic: true,
83 | });
84 | addBody(0, window.innerHeight / 2, 50, window.innerHeight, {
85 | isStatic: true,
86 | });
87 | addBody(window.innerWidth, window.innerHeight / 2, 50, window.innerHeight, {
88 | isStatic: true,
89 | });
90 | };
91 |
92 | const initPhysicsHandler = () => {
93 | // Listener to handle data that worker passes to main thread
94 | worker.addEventListener("message", (e) => {
95 | if (e.data.type == "BODY_SYNC") {
96 | const physData = e.data.data;
97 |
98 | bodySyncDelta = e.data.delta;
99 |
100 | for (const obj of physicsObjects) {
101 | const { x, y, angle } = physData[obj.id];
102 | if (!obj.sprite) return;
103 | obj.sprite.position.x = x;
104 | obj.sprite.position.y = y;
105 | obj.sprite.rotation = angle;
106 | }
107 | }
108 | if (e.data.type == "BODY_CREATED") {
109 | const texture = PIXI.Texture.from("square.png");
110 | const sprite = new PIXI.Sprite(texture);
111 | const { x, y, width, height, id }: IPhysicsSyncBody = e.data.data;
112 | sprite.anchor.set(0.5);
113 | sprite.position.x = x;
114 | sprite.position.y = y;
115 | sprite.width = width;
116 | sprite.height = height;
117 | container.addChild(sprite);
118 |
119 | physicsObjects.push({
120 | id,
121 | x,
122 | y,
123 | width,
124 | height,
125 | angle: 0,
126 | sprite,
127 | });
128 | }
129 | });
130 | };
131 |
132 | const timedSpawner = () => {
133 | for (let i = 0; i < spawnerAmount; i++) {
134 | spawnRandomDynamicSquare();
135 | }
136 |
137 | setTimeout(() => {
138 | timedSpawner();
139 | }, spawnerTimer);
140 | };
141 |
142 | // Setup
143 | setupWalls();
144 |
145 | timedSpawner();
146 | initPhysicsHandler();
147 |
148 | // initial spawn
149 | for (let i = 0; i < spawnAtStart; i++) {
150 | spawnRandomDynamicSquare();
151 | }
152 |
153 | // gameloop
154 | let lastSpawnAttempt = 0;
155 | let delta = 0;
156 |
157 | app.ticker.stop();
158 |
159 | const gameLoop = () => {
160 | const start = performance.now();
161 | app.render();
162 | lastSpawnAttempt += delta;
163 |
164 | bodyCount = physicsObjects.length;
165 | rendererFps = 60 / delta;
166 | delta = performance.now() - start;
167 | console.log(delta);
168 | setTimeout(() => gameLoop(), 0);
169 | };
170 |
171 | gameLoop();
172 |
173 | app.ticker.add(() => {
174 | lastSpawnAttempt += delta;
175 | const start = performance.now();
176 |
177 | delta = performance.now() - start;
178 | bodyCount = physicsObjects.length;
179 | rendererFps = app.ticker.FPS;
180 | });
181 | }
182 |
183 | workerExample();
184 | initStats();
185 |
186 | interface IPhysicsSyncBody {
187 | id: string | number;
188 | x: number;
189 | y: number;
190 | width: number;
191 | height: number;
192 | angle: number;
193 | sprite: PIXI.Sprite | undefined;
194 | }
195 |
--------------------------------------------------------------------------------
/matterjs-pixi-worker/src/physicsWorker.ts:
--------------------------------------------------------------------------------
1 | import { Engine } from "matter-js";
2 | import { PhysicsRunner } from "./PhysicsMain";
3 |
4 | const physics = new PhysicsRunner();
5 |
6 | const maxFps = 60;
7 | const deltaGoal = 1000 / maxFps;
8 |
9 | const runner = (delta = 16) => {
10 | const startTs = performance.now();
11 |
12 | Engine.update(physics.engine, delta);
13 |
14 | if (Math.random() > 0.3) {
15 | physics.applyForceToRandomBody();
16 | }
17 |
18 | self.postMessage({
19 | type: "BODY_SYNC",
20 | data: physics.getBodySyncData(),
21 | delta,
22 | });
23 |
24 | const currentDelta = performance.now() - startTs;
25 |
26 | // this bit limits max FPS to 60
27 | const deltaGoalDifference = Math.max(0, deltaGoal - currentDelta);
28 | const d = Math.max(currentDelta, deltaGoal);
29 |
30 | setTimeout(() => runner(d), deltaGoalDifference);
31 | };
32 |
33 | runner();
34 |
35 | // once a second check for bodies out of bound
36 | setInterval(() => {
37 | physics.outOfBoundCheck();
38 | }, 1000);
39 |
40 | self.addEventListener("message", (e) => {
41 | const message = e.data || e;
42 |
43 | if (message.type == "ADD_BODY") {
44 | const { x, y, width, height, options } = message.data;
45 | const body = physics.addBody(x, y, width, height, options);
46 |
47 | self.postMessage({
48 | type: "BODY_CREATED",
49 | data: {
50 | id: body.id,
51 | x,
52 | y,
53 | width,
54 | height,
55 | angle: 0,
56 | sprite: undefined,
57 | },
58 | });
59 | }
60 | });
61 |
--------------------------------------------------------------------------------
/matterjs-pixi-worker/src/renderer.ts:
--------------------------------------------------------------------------------
1 | import * as PIXI from "pixi.js";
2 |
3 | export class Renderer {
4 | app: PIXI.Application;
5 | stage: PIXI.Container;
6 |
7 | constructor() {
8 | this.app = new PIXI.Application({
9 | width: window.innerWidth,
10 | height: window.innerHeight,
11 | backgroundColor: 0xcecece,
12 | resolution: 1,
13 | antialias: true,
14 | });
15 | // PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;
16 |
17 | document.body.appendChild(this.app.view);
18 | this.app.view.id = "pixi-view";
19 |
20 | this.stage = this.app.stage;
21 |
22 | this.resize();
23 | }
24 |
25 | private resize() {
26 | // resize canvas and webgl renderer when window sizeChanges
27 | window.addEventListener("resize", () => {
28 | this.app.view.width = window.innerWidth;
29 | this.app.view.height = window.innerHeight;
30 | });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/matterjs-pixi-worker/src/style/global.css:
--------------------------------------------------------------------------------
1 | #app {
2 | font-family: Avenir, Helvetica, Arial, sans-serif;
3 | -webkit-font-smoothing: antialiased;
4 | -moz-osx-font-smoothing: grayscale;
5 | text-align: center;
6 | color: #2c3e50;
7 | margin-top: 60px;
8 | }
9 |
10 | canvas {
11 | position: fixed;
12 | top: 0;
13 | left: 0;
14 | }
15 |
16 | #matter-canvas {
17 | z-index: 9999;
18 | mix-blend-mode: multiply;
19 | }
20 |
21 | #stats {
22 | z-index: 9999;
23 | position: fixed;
24 | top: 2rem;
25 | left: 2rem;
26 | display: grid;
27 | grid-template-columns: 1fr 1fr;
28 | gap: 0 1rem;
29 | background-color: #2c3e50;
30 | padding: 0.25rem;
31 | color: white;
32 | font-style: monospace;
33 | min-width: 150px;
34 | }
35 |
--------------------------------------------------------------------------------
/matterjs-pixi-worker/src/util.ts:
--------------------------------------------------------------------------------
1 | export const uuidv4 = () => {
2 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
3 | const r = (Math.random() * 16) | 0,
4 | v = c == "x" ? r : (r & 0x3) | 0x8;
5 | return v.toString(16);
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/matterjs-pixi-worker/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/matterjs-pixi-worker/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ESNext", "DOM"],
7 | "moduleResolution": "Node",
8 | "strict": true,
9 | "sourceMap": true,
10 | "resolveJsonModule": true,
11 | "esModuleInterop": true,
12 | "noEmit": true,
13 | "noUnusedLocals": false,
14 | "noUnusedParameters": false,
15 | "noImplicitReturns": false
16 | },
17 | "include": ["./src"]
18 | }
19 |
--------------------------------------------------------------------------------
/matterjs-pixi-worker/vite.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | server: {
3 | watch: {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/README.md:
--------------------------------------------------------------------------------
1 | In my previous game dev attempts with javascript I always struggled with physics engine performance. I always defaulted to matter.js - it's good documentation and plentiful examples outweighed the performance gains of other available libraries. I was very excited when I first learned about WASM and near-native performance it provides, but for the longest time Box2D was the only viable choice in that area and I truely hated using it. It had poor documentation and felt very archaic to use.
2 |
3 | Now, it seems like my woes might be over. In comes a new contender - Rapier.rs.
4 |
5 | 
6 | [Rapier home](https://rapier.rs/)
7 |
8 | Rapier.rs is a rust physics library compiled to WASM with javscript bindings and good documentation. I was able to set it up in around 30 minutes and it provided an massive, instant boost to app performance.
9 |
10 | Rapier remained more stable and allowed me to add thousands of more active physics bodies to the world.
11 |
12 | **Links:**
13 |
14 | - Example from my last article with Rapier.rs instead of matter +300% performance [LIVE](https://workerized-rapier-pixi.netlify.app/)
15 | - [Github repo](https://github.com/jerzakm/gamedev-experiments/tree/main/rapier-pixi-worker)
16 |
17 | | Active bodies | Matter FPS | Rapier FPS |
18 | | ------------- | ----------- | ---------- |
19 | | 4500 | 38 | 120 |
20 | | 6000 | 21 | 79 |
21 | | 7500 | 4 | 60 |
22 | | 9000 | 0 - crashed | 42 |
23 | | 10000 | 0 - crashed | 31 |
24 | | 12000 | 0 - crashed | 22 |
25 | | 15000 | 0 - crashed | 16 |
26 |
27 | ## Why you need to consider Rapier for your js physics needs
28 |
29 | ### 1. Performance
30 |
31 | Javascript can't compare to an optimized Rust library compiled to WASM
32 | [WASM is just this fast](https://medium.com/@torch2424/webassembly-is-fast-a-real-world-benchmark-of-webassembly-vs-es6-d85a23f8e193)
33 |
34 | ### 2. Documentation
35 |
36 | Rapier page provides a good overview of the key features, information how to get started and an in-depth API documentation. All of this for Rust, Rust+bevy and Javascript.
37 |
38 | ### 3. Modern developer experience
39 |
40 | I found Rapier API very intuitive to work with, imho making it by far the best choice out of the few performant. It comes with **typescript support**. Resulting code is readable and easy to reason with.
41 |
42 | ```js
43 | import("@dimforge/rapier2d").then((RAPIER) => {
44 | // Use the RAPIER module here.
45 | let gravity = { x: 0.0, y: 9.81 };
46 | let world = new RAPIER.World(gravity);
47 |
48 | // Create the ground
49 | let groundColliderDesc = RAPIER.ColliderDesc.cuboid(10.0, 0.1);
50 | world.createCollider(groundColliderDesc);
51 |
52 | // Create a dynamic rigid-body.
53 | let rigidBodyDesc = RAPIER.RigidBodyDesc.newDynamic().setTranslation(
54 | 0.0,
55 | 1.0
56 | );
57 | let rigidBody = world.createRigidBody(rigidBodyDesc);
58 |
59 | // Create a cuboid collider attached to the dynamic rigidBody.
60 | let colliderDesc = RAPIER.ColliderDesc.cuboid(0.5, 0.5);
61 | let collider = world.createCollider(colliderDesc, rigidBody.handle);
62 |
63 | // Game loop. Replace by your own game loop system.
64 | let gameLoop = () => {
65 | // Step the simulation forward.
66 | world.step();
67 |
68 | // Get and print the rigid-body's position.
69 | let position = rigidBody.translation();
70 | console.log("Rigid-body position: ", position.x, position.y);
71 |
72 | setTimeout(gameLoop, 16);
73 | };
74 |
75 | gameLoop();
76 | });
77 | ```
78 |
79 | ### 4. Cross-platform determinism & snapshotting
80 |
81 | - Running the **same simulation**, with the same initial conditions on different machines or distributions of Rapier (rust/bevy/js) **will yield the same result.**
82 |
83 | - **Easy data saving and restoring.** - _It is possible to take a snapshot of the whole physics world with `world.takeSnapshot`. This results in a byte array of type Uint8Array that may be saved on the disk, sent through the network, etc. The snapshot can then be restored with `let world = World.restoreSnapshot(snapshot);`_.
84 |
85 | ## What's next?
86 |
87 | I am excited to keep working with Rapier, but in the meanwhile I think a proper physics benchmark is in order. The ones I've found while doing research were a bit dated.
88 |
89 | ### Other: Vite usage errors
90 |
91 | I've ran into some issues adding Rapier to my Vite project, the solution can be found here: https://github.com/dimforge/rapier.js/issues/49
92 |
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pixijs + physics
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "matterjs-pixi-worker",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite --config vite.config.js",
6 | "build": "tsc && vite build",
7 | "preview": "vite preview"
8 | },
9 | "devDependencies": {
10 | "@types/offscreencanvas": "^2019.6.4",
11 | "express": "^4.17.1",
12 | "typescript": "^4.3.2",
13 | "vite": "^2.6.4"
14 | },
15 | "dependencies": {
16 | "@dimforge/rapier2d": "^0.7.6",
17 | "@dimforge/rapier2d-compat": "^0.7.6",
18 | "pixi.js": "^6.1.3"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/public/square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerzakm/gamedev-experiments/b482122293b6373d33d2f8ab91a9084cbc24d3e7/rapier-array-buffer-performance/public/square.png
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/server.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const { createServer: createViteServer } = require("vite");
3 |
4 | async function createServer() {
5 | const app = express();
6 |
7 | app.use((req, res, next) => {
8 | res.setHeader("Access-Control-Allow-Origin", "*");
9 | res.setHeader(
10 | "Access-Control-Allow-Headers",
11 | "Origin, X-Requested-With, Content, Accept, Content-Type, Authorization"
12 | );
13 | res.setHeader(
14 | "Access-Control-Allow-Methods",
15 | "GET, POST, PUT, DELETE, PATCH, OPTIONS"
16 | );
17 | res.setHeader("Cross-origin-Embedder-Policy", "require-corp");
18 | res.setHeader("Cross-origin-Opener-Policy", "same-origin");
19 |
20 | if (req.method === "OPTIONS") {
21 | res.sendStatus(200);
22 | } else {
23 | next();
24 | }
25 | });
26 |
27 | // Create Vite server in middleware mode.
28 | const vite = await createViteServer({
29 | server: { middlewareMode: "html" },
30 | });
31 | // Use vite's connect instance as middleware
32 | app.use(vite.middlewares);
33 |
34 | app.use("*", async (req, res) => {
35 | // If `middlewareMode` is `'ssr'`, should serve `index.html` here.
36 | // If `middlewareMode` is `'html'`, there is no need to serve `index.html`
37 | // because Vite will do that.
38 | });
39 |
40 | app.listen(3000);
41 | }
42 |
43 | createServer();
44 |
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/src/main.ts:
--------------------------------------------------------------------------------
1 | import "./style/global.css";
2 | import * as PIXI from "pixi.js";
3 | import { Renderer } from "./renderer";
4 | import PhysicsWorker from "./physicsWorker?worker";
5 | // import { IChamferableBodyDefinition } from "matter-js";
6 |
7 | const spawnerAmount = 250;
8 | const spawnerTimer = 1000;
9 | const spawnAtStart = 2000;
10 |
11 | let bodySyncDelta = 0;
12 | let rendererFps = 0;
13 | let bodyCount = 0;
14 | let statsUpdateFrequency = 500;
15 |
16 | const initStats = () => {
17 | const statsDom = document.body.querySelector("#stats");
18 |
19 | if (!statsDom) return;
20 |
21 | statsDom.innerHTML = `
22 | Bodies ${bodyCount}
23 | renderer_fps ${rendererFps.toFixed(0)}
24 | physics_fps ${(1000 / bodySyncDelta).toFixed(0)}
25 | `;
26 |
27 | setTimeout(initStats, statsUpdateFrequency);
28 | };
29 |
30 | async function workerExample() {
31 | const sab = new SharedArrayBuffer(1024);
32 | console.log(sab);
33 | const worker = new PhysicsWorker();
34 |
35 | const { app, stage } = new Renderer();
36 | const container = new PIXI.Container();
37 |
38 | stage.addChild(container);
39 |
40 | const physicsObjects: IPhysicsSyncBody[] = [];
41 |
42 | const addBody = (
43 | x = 0,
44 | y = 0,
45 | width = 10,
46 | height = 10,
47 | options: any = {
48 | restitution: 0,
49 | }
50 | ) => {
51 | const newBody = {
52 | x,
53 | y,
54 | width,
55 | height,
56 | options,
57 | };
58 |
59 | worker.postMessage({
60 | type: "ADD_BODY",
61 | data: newBody,
62 | });
63 | };
64 |
65 | const spawnRandomDynamicSquare = () => {
66 | const x =
67 | window.innerWidth / 2 + (Math.random() - 0.5) * window.innerWidth * 0.8;
68 | const y =
69 | window.innerHeight / 2 + (Math.random() - 0.5) * window.innerHeight * 0.8;
70 |
71 | const options = {
72 | restitution: 0,
73 | };
74 |
75 | const size = 4 + 10 * Math.random();
76 | addBody(x, y, size, size, options);
77 | };
78 |
79 | const setupWalls = () => {
80 | addBody(window.innerWidth / 2, 0, window.innerWidth, 50, {
81 | isStatic: true,
82 | });
83 | addBody(window.innerWidth / 2, window.innerHeight, window.innerWidth, 50, {
84 | isStatic: true,
85 | });
86 | addBody(0, window.innerHeight / 2, 50, window.innerHeight, {
87 | isStatic: true,
88 | });
89 | addBody(window.innerWidth, window.innerHeight / 2, 50, window.innerHeight, {
90 | isStatic: true,
91 | });
92 | };
93 |
94 | const initPhysicsHandler = () => {
95 | // Listener to handle data that worker passes to main thread
96 | worker.addEventListener("message", (e) => {
97 | if (e.data.type == "BODY_SYNC") {
98 | const physData = e.data.data;
99 |
100 | bodySyncDelta = e.data.delta;
101 |
102 | for (const obj of physicsObjects) {
103 | const { x, y, rotation } = physData[obj.id];
104 | if (!obj.sprite) return;
105 | obj.sprite.position.x = x;
106 | obj.sprite.position.y = y;
107 | obj.sprite.rotation = rotation;
108 | }
109 | const syncEnd = performance.now();
110 |
111 | // console.log(syncEnd - e.data.syncStart);
112 | }
113 | if (e.data.type == "BODY_SYNC_ARR") {
114 | let view = new Float32Array(e.data.syncArray);
115 | for (let i = 0; i < view.length; i += 4) {
116 | const id = view[i];
117 | const x = view[i + 1];
118 | const y = view[i + 2];
119 | const rotation = view[i + 3];
120 | const obj = physicsObjects.find((o) => {
121 | return o.id == id;
122 | });
123 | if (!obj || !obj.sprite) return;
124 | obj.sprite.position.x = x;
125 | obj.sprite.position.y = y;
126 | obj.sprite.rotation = rotation;
127 | }
128 | const syncEnd = performance.now();
129 |
130 | // console.log(syncEnd - e.data.syncStart);
131 | }
132 |
133 | if (e.data.type == "BODY_CREATED") {
134 | const texture = PIXI.Texture.from("square.png");
135 | const sprite = new PIXI.Sprite(texture);
136 | const { x, y, width, height, id }: IPhysicsSyncBody = e.data.data;
137 | sprite.anchor.set(0.5);
138 | sprite.position.x = x;
139 | sprite.position.y = y;
140 | sprite.width = width;
141 | sprite.height = height;
142 | container.addChild(sprite);
143 |
144 | physicsObjects.push({
145 | id,
146 | x,
147 | y,
148 | width,
149 | height,
150 | angle: 0,
151 | sprite,
152 | });
153 | }
154 | if (e.data.type == "PHYSICS_LOADED") {
155 | // initial spawn
156 | setupWalls();
157 | for (let i = 0; i < spawnAtStart; i++) {
158 | spawnRandomDynamicSquare();
159 | }
160 | }
161 | });
162 | };
163 |
164 | const timedSpawner = () => {
165 | for (let i = 0; i < spawnerAmount; i++) {
166 | spawnRandomDynamicSquare();
167 | }
168 |
169 | setTimeout(() => {
170 | timedSpawner();
171 | }, spawnerTimer);
172 | };
173 |
174 | timedSpawner();
175 | initPhysicsHandler();
176 |
177 | // gameloop
178 | let lastSpawnAttempt = 0;
179 | let delta = 0;
180 |
181 | app.ticker.stop();
182 |
183 | let start = performance.now();
184 | const gameLoop = () => {
185 | start = performance.now();
186 | // app.render();
187 | lastSpawnAttempt += delta;
188 |
189 | bodyCount = physicsObjects.length;
190 | delta = performance.now() - start;
191 | rendererFps = 60 / delta;
192 | setTimeout(() => gameLoop(), 0);
193 | };
194 |
195 | gameLoop();
196 | }
197 |
198 | workerExample();
199 | initStats();
200 |
201 | interface IPhysicsSyncBody {
202 | id: string | number;
203 | x: number;
204 | y: number;
205 | width: number;
206 | height: number;
207 | angle: number;
208 | sprite: PIXI.Sprite | undefined;
209 | }
210 |
211 | export type PositionSyncMap = {
212 | [key: number]: {
213 | x: number;
214 | y: number;
215 | rotation: number;
216 | };
217 | };
218 |
219 | export interface PhysicsObjectOptions {
220 | isStatic: boolean;
221 | }
222 |
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/src/physicsWorker.ts:
--------------------------------------------------------------------------------
1 | import { PositionSyncMap } from "./main";
2 | import { getRapier } from "./rapier";
3 |
4 | const maxFps = 500;
5 | const deltaGoal = 1000 / maxFps;
6 |
7 | const bodyAddQueue: any[] = [];
8 |
9 | async function init() {
10 | const RAPIER = await getRapier();
11 | // Use the RAPIER module here.
12 | let gravity = { x: 0.0, y: 0.0 };
13 | let world = new RAPIER.World(gravity);
14 |
15 | const applyForceToRandomBody = () => {
16 | const bodyCount = world.bodies.len();
17 |
18 | if (bodyCount == 0) return;
19 | const bodyIndex = Math.round(Math.random() * bodyCount);
20 |
21 | const body = world.getRigidBody(bodyIndex);
22 | if (!body) return;
23 | const mass = body.mass();
24 |
25 | body.applyImpulse(
26 | {
27 | x: (Math.random() - 0.5) * mass ** 2 * 0.5,
28 | y: (Math.random() - 0.5) * mass ** 2 * 0.5,
29 | },
30 | true
31 | );
32 | };
33 |
34 | const syncPositions = (delta: number) => {
35 | const syncStart = performance.now();
36 | const syncObj: PositionSyncMap = {};
37 |
38 | let count = 0;
39 |
40 | world.forEachRigidBody((body) => {
41 | const { x, y } = body.translation();
42 | const rotation = body.rotation();
43 | syncObj[body.handle] = { x, y, rotation };
44 | count++;
45 | });
46 |
47 | self.postMessage({
48 | type: "BODY_SYNC",
49 | data: syncObj,
50 | delta,
51 | syncStart,
52 | });
53 | };
54 |
55 | const syncPositionsArray = (delta: number) => {
56 | const syncStart = performance.now();
57 | let count = 0;
58 |
59 | const syncArray = new ArrayBuffer(world.bodies.len() * 4 * 4);
60 |
61 | let view = new Float32Array(syncArray);
62 |
63 | world.forEachRigidBody((body) => {
64 | const { x, y } = body.translation();
65 | const rotation = body.rotation();
66 |
67 | view[count * 4] = body.handle;
68 | view[count * 4 + 1] = x;
69 | view[count * 4 + 2] = y;
70 | view[count * 4 + 3] = rotation;
71 |
72 | count++;
73 | });
74 |
75 | //@ts-ignore
76 | //@ts-ignore
77 | self.postMessage({ type: "BODY_SYNC_ARR", syncArray, syncStart }, [
78 | syncArray,
79 | ]);
80 | };
81 |
82 | const outOfBoundCheck = () => {
83 | world.forEachRigidBody((body) => {
84 | const { x, y } = body.translation();
85 |
86 | if (Math.abs(x) + Math.abs(y) > 6000) {
87 | body.setTranslation(
88 | {
89 | x: 100,
90 | y: 100,
91 | },
92 | true
93 | );
94 | }
95 | });
96 | };
97 |
98 | let gameLoop = (delta = 16) => {
99 | const startTs = performance.now();
100 |
101 | if (Math.random() > 0.3) {
102 | applyForceToRandomBody();
103 | }
104 |
105 | while (bodyAddQueue.length > 0) {
106 | const { x, y, width, height, options } = bodyAddQueue[0];
107 |
108 | let rigidBody;
109 |
110 | if (options.isStatic) {
111 | rigidBody = world.createRigidBody(
112 | RAPIER.RigidBodyDesc.newStatic().setTranslation(x, y)
113 | );
114 | } else {
115 | rigidBody = world.createRigidBody(
116 | RAPIER.RigidBodyDesc.newDynamic().setTranslation(x, y)
117 | );
118 | }
119 |
120 | const colliderDesc = new RAPIER.ColliderDesc(
121 | new RAPIER.Cuboid(width / 2, height / 2)
122 | ).setTranslation(0, 0);
123 |
124 | const bodyCollider = world.createCollider(colliderDesc, rigidBody.handle);
125 |
126 | bodyAddQueue.shift();
127 |
128 | self.postMessage({
129 | type: "BODY_CREATED",
130 | data: {
131 | id: bodyCollider.handle,
132 | x,
133 | y,
134 | width,
135 | height,
136 | angle: 0,
137 | sprite: undefined,
138 | },
139 | });
140 | }
141 |
142 | world.timestep = delta;
143 |
144 | world.step();
145 | syncPositions(delta);
146 | // syncPositionsArray(delta);
147 |
148 | const currentDelta = performance.now() - startTs;
149 |
150 | // this bit limits max FPS to 60
151 | const deltaGoalDifference = Math.max(0, deltaGoal - currentDelta);
152 | const d = Math.max(currentDelta, deltaGoal);
153 |
154 | setTimeout(() => gameLoop(d), deltaGoalDifference);
155 | };
156 | gameLoop();
157 |
158 | self.postMessage({
159 | type: "PHYSICS_LOADED",
160 | });
161 |
162 | // once a second check for bodies out of bound
163 | setInterval(() => {
164 | // outOfBoundCheck();
165 | }, 1000);
166 |
167 | self.addEventListener("message", (e) => {
168 | const message = e.data || e;
169 |
170 | if (message.type == "ADD_BODY") {
171 | bodyAddQueue.push(message.data);
172 | }
173 | });
174 | }
175 |
176 | init();
177 |
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/src/rapier.ts:
--------------------------------------------------------------------------------
1 | import RAPIER from "@dimforge/rapier2d-compat";
2 | export type Rapier = typeof RAPIER;
3 |
4 | export function getRapier() {
5 | // eslint-disable-next-line import/no-named-as-default-member
6 | return RAPIER.init().then(() => RAPIER);
7 | }
8 |
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/src/renderer.ts:
--------------------------------------------------------------------------------
1 | import * as PIXI from "pixi.js";
2 |
3 | export class Renderer {
4 | app: PIXI.Application;
5 | stage: PIXI.Container;
6 |
7 | constructor() {
8 | this.app = new PIXI.Application({
9 | width: window.innerWidth,
10 | height: window.innerHeight,
11 | backgroundColor: 0xcecece,
12 | resolution: 1,
13 | antialias: true,
14 | });
15 | // PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;
16 |
17 | document.body.appendChild(this.app.view);
18 | this.app.view.id = "pixi-view";
19 |
20 | this.stage = this.app.stage;
21 |
22 | this.resize();
23 | }
24 |
25 | private resize() {
26 | // resize canvas and webgl renderer when window sizeChanges
27 | window.addEventListener("resize", () => {
28 | this.app.view.width = window.innerWidth;
29 | this.app.view.height = window.innerHeight;
30 | });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/src/style/global.css:
--------------------------------------------------------------------------------
1 | #app {
2 | font-family: Avenir, Helvetica, Arial, sans-serif;
3 | -webkit-font-smoothing: antialiased;
4 | -moz-osx-font-smoothing: grayscale;
5 | text-align: center;
6 | color: #2c3e50;
7 | margin-top: 60px;
8 | }
9 |
10 | canvas {
11 | position: fixed;
12 | top: 0;
13 | left: 0;
14 | }
15 |
16 | #matter-canvas {
17 | z-index: 9999;
18 | mix-blend-mode: multiply;
19 | }
20 |
21 | #stats {
22 | z-index: 9999;
23 | position: fixed;
24 | top: 2rem;
25 | left: 2rem;
26 | display: grid;
27 | grid-template-columns: 1fr 1fr;
28 | gap: 0 1rem;
29 | background-color: #2c3e50;
30 | padding: 0.25rem;
31 | color: white;
32 | font-style: monospace;
33 | min-width: 150px;
34 | }
35 |
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/src/util.ts:
--------------------------------------------------------------------------------
1 | export const uuidv4 = () => {
2 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
3 | const r = (Math.random() * 16) | 0,
4 | v = c == "x" ? r : (r & 0x3) | 0x8;
5 | return v.toString(16);
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ESNext", "DOM"],
7 | "moduleResolution": "Node",
8 | "strict": true,
9 | "sourceMap": true,
10 | "resolveJsonModule": true,
11 | "esModuleInterop": true,
12 | "noEmit": true,
13 | "noUnusedLocals": false,
14 | "noUnusedParameters": false,
15 | "noImplicitReturns": false
16 | },
17 | "include": ["./src"]
18 | }
19 |
--------------------------------------------------------------------------------
/rapier-array-buffer-performance/vite.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | server: {
3 | watch: {},
4 | cors: { origin: "same-origin" },
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/README.md:
--------------------------------------------------------------------------------
1 | Following up on my recent 'discovery' of the [Rapier.rs physics engine](https://rapier.rs) I make the first attempt at a character controller.
2 |
3 | Links:
4 |
5 | - [Github Repo](https://github.com/jerzakm/gamedev-experiments/tree/main/rapier-pixi-character-controller-dynamic)
6 | - [Live example](https://rapier-keyboard-character-controller.netlify.app)
7 |
8 | 
9 |
10 | ## Rigid-body choices for a character controller in Rapier.rs
11 |
12 | Except for `Static` all other body types seem viable to make a controller, namely:
13 |
14 | - `KinematicPositionBased`
15 | - `KinematicVelocityBased`
16 | - `Dynamic`
17 |
18 | Kinematic bodies allow us to set their Position and Velocity, so at a first glance, it sounds like they'd make a good controller. Unfortunately, they come with a few caveats, making them harder to use than you'd think. The biggest drawback for a quick and easy character controller is the fact that they don't interact with static bodies out of the gate and will clip through them. Not great if we want our characters to stick to walls and platforms. Rapier provides us with a lot of options to handle this drawback. Scene queries and hooks are quite robust, allowing the user to implement custom collision logic, but it's not something I want to get into before learning a bit more about the engine.
19 |
20 | The last remaining choice, `Dynamic` is a fully-fledged body that interacts with the entire world.
21 |
22 | ## Setup
23 |
24 | To not make this article unnecessarily long, I will skip the world and renderer setup and instead link the Github repo for the project. It should be easy enough to follow and you're always welcome to hit me up with any questions you might have.
25 |
26 | Before proceeding with character controller I setup:
27 |
28 | - rapier.rs physics world with gravity `{x: 0, y: 0}` - for the topdown experience
29 | - add walls to browser window bounds
30 | - spawn Dynamic objects for our character to interact with later, in this case, 100 randomly sized balls
31 | - render walls and balls with simple pixi.js graphics
32 |
33 | ## Step by step
34 |
35 | Steps to implement a simple keyboard and point to click controller:
36 |
37 | ### Player body setup
38 |
39 | 1. Create a player physics body and place it in the middle of the screen with `setTranslation`
40 |
41 | ```ts
42 | const body = world.createRigidBody(
43 | RAPIER.RigidBodyDesc.newDynamic().setTranslation(
44 | window.innerWidth / 2,
45 | window.innerHeight / 2
46 | )
47 | );
48 | ```
49 |
50 | 2. Make a collider description so the body has shape and size. It needs it to interact with the world. For this example, we're going with a simple circle. Translation in this step describes the collider's relative position to the body.
51 |
52 | ```ts
53 | const colliderDesc = new RAPIER.ColliderDesc(
54 | new RAPIER.Ball(12)
55 | ).setTranslation(0, 0);
56 | ```
57 |
58 | 3. Create a collider, attach it to the body and add the whole thing to the world.
59 |
60 | ```ts
61 | const collider = world.createCollider(colliderDesc, body.handle);
62 | ```
63 |
64 | ### Keyboard WASD control bindings
65 |
66 | In later steps, we will move the player's body based on the provided direction. To get that we're going to set up a basic WASD control scheme with listeners listening to `keydown` and `keyup`. They will manipulate a direction vector:
67 |
68 | ```ts
69 | const direction = {
70 | x: 0,
71 | y: 0,
72 | };
73 | ```
74 |
75 | When the key is pressed down, player begins to move:
76 |
77 | ```ts
78 | window.addEventListener("keydown", (e) => {
79 | switch (e.key) {
80 | case "w": {
81 | direction.y = -1;
82 | break;
83 | }
84 | case "s": {
85 | direction.y = 1;
86 | break;
87 | }
88 | case "a": {
89 | direction.x = -1;
90 | break;
91 | }
92 | case "d": {
93 | direction.x = 1;
94 | break;
95 | }
96 | }
97 | });
98 | ```
99 |
100 | Then, when the key is released, the movement on that particular axis (x or y) is set to 0.
101 |
102 | ```ts
103 | window.addEventListener("keyup", (e) => {
104 | switch (e.key) {
105 | case "w": {
106 | direction.y = 0;
107 | break;
108 | }
109 | case "s": {
110 | direction.y = 0;
111 | break;
112 | }
113 | case "a": {
114 | direction.x = 0;
115 | break;
116 | }
117 | case "d": {
118 | direction.x = 0;
119 | break;
120 | }
121 | }
122 | });
123 | ```
124 |
125 | ### Moving the body
126 |
127 | Now that we've made a way for us to input where the player has to go, it's time to make it happen. We will create an `updatePlayer` function that will have to be called every frame.
128 |
129 | The most basic approach is as simple as the snippet below, we simply set the body's velocity to the `direction`.
130 |
131 | ```ts
132 | const updatePlayer = () => {
133 | body.setLinvel(direction, true);
134 | };
135 | ```
136 |
137 | You might notice though, that the body isn't moving much. That's because we only set the direction vector to go from -1 to 1, and that isn't very fast. To combat that and make the code more reusable we add a `MOVE_SPEED` variable and multiply the x and y of the direction.
138 |
139 | ```ts
140 | const MOVE_SPEED = 80;
141 |
142 | const updatePlayer = () => {
143 | body.setLinvel(
144 | { x: direction.x * MOVE_SPEED, y: direction.y * MOVE_SPEED },
145 | true
146 | );
147 | };
148 | ```
149 |
150 | That's more like it!
151 |
152 | **Bonus method: Applying force to move the body**
153 | When I was playing around and writing this article I found another cool way to make our player's body move. Instead of setting the velocity directly, we "push" the body to make it go in the desired direction at the desired speed. It gives a smoother, more natural feeling movement right out of the gate.
154 |
155 | The whole thing is just these few lines of code but it's a little more complicated than the previous example.
156 |
157 | The concept is simple. We apply impulse in order to make the body move, but what if it starts going too fast or we want to stop?
158 |
159 | We check the body's current velocity with `const velocity = body.linvel();`.Then, to determine what impulse should be applied next, we take the difference of the desired and current velocity for both axis `direction.x * MOVE_SPEED - velocity.x `. If the body is moving too fast or in the wrong direction, a counteracting impulse is applied. We multiply it by `ACCELERATION` constant to.. drumroll - make the body accelerate faster or slower.
160 |
161 | 
162 |
163 | ```ts
164 | const MOVE_SPEED = 80;
165 | const ACCELERATION = 40;
166 |
167 | const velocity = body.linvel();
168 |
169 | const impulse = {
170 | x: (direction.x * MOVE_SPEED - velocity.x) * ACCELERATION,
171 | y: (direction.y * MOVE_SPEED - velocity.y) * ACCELERATION,
172 | };
173 | body.applyImpulse(impulse, true);
174 | ```
175 |
176 | You can achieve a similar effect by using the velocity method and applying some form of [easing](https://developers.google.com/web/fundamentals/design-and-ux/animations/the-basics-of-easing).
177 |
178 | Note: For simplicity, I use `VELOCITY` and `ACCELERATION` in relation to one value of the vector. So velocity with the value of `2` would look like this: `{x: 2, y: 2}`, where in reality velocity is almost always the length of such vector - `const velocity = Math.sqrt(2**2 + 2**2)` resulting in velocity of ~2.83!. This means that if we used my implementation in a game, moving diagonally would be 40% faster than going up and down!
179 | **TLDR; Use correct velocity, calculated for example with Pythagorem's theorem.**
180 |
181 | ### If you made it this far, thank you so much for reading. Let me know if you have any questions or maybe would like to see other things implemented.
182 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pixijs + physics
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "matterjs-pixi-worker",
3 | "version": "0.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@dimforge/rapier2d": {
8 | "version": "0.7.6",
9 | "resolved": "https://registry.npmjs.org/@dimforge/rapier2d/-/rapier2d-0.7.6.tgz",
10 | "integrity": "sha512-0tEIApb01XTXa/G476/3sjpNQ7/KdkO2wpQyC6CWXm5fna9KeNEWQp4/eEJYUdt+tDwhTE2WY66Ah6eQmWXS8Q=="
11 | },
12 | "@dimforge/rapier2d-compat": {
13 | "version": "0.7.6",
14 | "resolved": "https://registry.npmjs.org/@dimforge/rapier2d-compat/-/rapier2d-compat-0.7.6.tgz",
15 | "integrity": "sha512-WmKfOSaNM2VCyAAMa6+ow6/HTzD8QSnddwxYFI7W/CQp4I3a5iXOKXLXyUya+AJCTkPfb8Y0LuTfZBBbqqEy7w=="
16 | },
17 | "@pixi/accessibility": {
18 | "version": "6.1.3",
19 | "resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-6.1.3.tgz",
20 | "integrity": "sha512-JK6rtqfC2/rnJt1xLPznH2lNH0Jx9f2Py7uh50VM1sqoYrkyAAegenbOdyEzgB35Q4oQji3aBkTsWn2mrwXp/g=="
21 | },
22 | "@pixi/app": {
23 | "version": "6.1.3",
24 | "resolved": "https://registry.npmjs.org/@pixi/app/-/app-6.1.3.tgz",
25 | "integrity": "sha512-gryDVXuzErRIgY5G2CRQH6fZM7Pk3m1CFEInXEKa4rmVzfwRz+3OeU0YNSnD9atPAS5C2TaAzE4yOSHH2+wESQ=="
26 | },
27 | "@pixi/compressed-textures": {
28 | "version": "6.1.3",
29 | "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-6.1.3.tgz",
30 | "integrity": "sha512-FO2B7GhDMlZA0fnpH2PvNOh6ZlRxQoJnNlpjzNw+x1nvF9h3+V6dbFoG9oBC5zAisTfacdfoo1TdT789Oh+kTg=="
31 | },
32 | "@pixi/constants": {
33 | "version": "6.1.3",
34 | "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-6.1.3.tgz",
35 | "integrity": "sha512-Qvz/SIxw+dQ6P9niOEdILWX2DQ5FnGA0XZNFLW/3amekzad/+WqHobL+Mg5S6A4/a9mXTnqjyB0BqhhtLfpFkA=="
36 | },
37 | "@pixi/core": {
38 | "version": "6.1.3",
39 | "resolved": "https://registry.npmjs.org/@pixi/core/-/core-6.1.3.tgz",
40 | "integrity": "sha512-UQsR1Q7c+Zcvtu6HrYMidvoyF/j9n3b4WXPh3ojuNV6+ZIvps3rznoZYaIx6foEJNhj7HM9fMObsimGP+FB36A=="
41 | },
42 | "@pixi/display": {
43 | "version": "6.1.3",
44 | "resolved": "https://registry.npmjs.org/@pixi/display/-/display-6.1.3.tgz",
45 | "integrity": "sha512-8/GdapJVKfl6PUkxX/Et5zB1aXny+uy353cQX886KJ6dGle82fQAYjIn7I6Xm+JiZWOhWo0N6KE9cjotO0rroA=="
46 | },
47 | "@pixi/extract": {
48 | "version": "6.1.3",
49 | "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-6.1.3.tgz",
50 | "integrity": "sha512-yZOsXc9Lh+U59ayl+DoWDPmndrOJj5ft2nzENMAvz2rVEOHQjWxH73qCSP6Wa5VsoINyJLMmV4MTbI+U0SH7GA=="
51 | },
52 | "@pixi/filter-alpha": {
53 | "version": "6.1.3",
54 | "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-6.1.3.tgz",
55 | "integrity": "sha512-eubgEO/qlxQbuPXgwxTZxTBTWjA0EQbrs7TyPqyBK2Wj0eEvimaVQ8u4eiqfMFJCZLnuWDCAPJpP9bMHxBXXpQ=="
56 | },
57 | "@pixi/filter-blur": {
58 | "version": "6.1.3",
59 | "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-6.1.3.tgz",
60 | "integrity": "sha512-uo8FHpV+qm4SuXcDnWqZWrznHmLJ3b8ibgLAgi/e8VmwrFiC+EqGa4n4V8J+xtR5P/iA3lT5pRgWw09/xHN3dQ=="
61 | },
62 | "@pixi/filter-color-matrix": {
63 | "version": "6.1.3",
64 | "resolved": "https://registry.npmjs.org/@pixi/filter-color-matrix/-/filter-color-matrix-6.1.3.tgz",
65 | "integrity": "sha512-d1pyxmVrGDOrO5pINe+fTspj1NNxiIp2IZ+FGgT7e17xnxjXTvtk4n4KqXAZFS1NCoStImDAV5j+b8Lysdg5jQ=="
66 | },
67 | "@pixi/filter-displacement": {
68 | "version": "6.1.3",
69 | "resolved": "https://registry.npmjs.org/@pixi/filter-displacement/-/filter-displacement-6.1.3.tgz",
70 | "integrity": "sha512-tIXK8vXzb2unMxGmu4gjdlOwddnkHA0IJXFTOF25a5h36v/AHqWwWG4h5G775oPu37UuhuYjeD/j229t0Q9QNQ=="
71 | },
72 | "@pixi/filter-fxaa": {
73 | "version": "6.1.3",
74 | "resolved": "https://registry.npmjs.org/@pixi/filter-fxaa/-/filter-fxaa-6.1.3.tgz",
75 | "integrity": "sha512-yhKVxX5vFKQz3lxfqAGg4XoajFyIRR8XzWqEHgAsPMFRnIIQIbF25bMRygZj12P61z3vxwqAM/2bn7S46Ii1zQ=="
76 | },
77 | "@pixi/filter-noise": {
78 | "version": "6.1.3",
79 | "resolved": "https://registry.npmjs.org/@pixi/filter-noise/-/filter-noise-6.1.3.tgz",
80 | "integrity": "sha512-oVRtcJwbN6VnAnvXZuLEZ0c12JUzporao5AziXgRAUjTMA3bFVE0/7Dx193Kx/l6UAasmzhWQctuv6NMxy5Efw=="
81 | },
82 | "@pixi/graphics": {
83 | "version": "6.1.3",
84 | "resolved": "https://registry.npmjs.org/@pixi/graphics/-/graphics-6.1.3.tgz",
85 | "integrity": "sha512-e5O47yECRp5WXWIvKhLDQKpiak7CfIqJzuTuQIyE7jXp8QiJNw+aoWNlJEd4ksKbsDkP3EE39CxlmiaBpxNL3w=="
86 | },
87 | "@pixi/interaction": {
88 | "version": "6.1.3",
89 | "resolved": "https://registry.npmjs.org/@pixi/interaction/-/interaction-6.1.3.tgz",
90 | "integrity": "sha512-ju3fE/KnO6KZChnZzZAdY6bfjlSh7/igZcVcd/MZRkAdNozx4QoN5sYmwrcvTvA5llMYaThSIRWgIHQiSlbOfQ=="
91 | },
92 | "@pixi/loaders": {
93 | "version": "6.1.3",
94 | "resolved": "https://registry.npmjs.org/@pixi/loaders/-/loaders-6.1.3.tgz",
95 | "integrity": "sha512-qOvy72bsVGzCmWyoofm6dm1l//hd+bJneidngplwsovpqnnyMfuewCpQjeLRL6rLqcHR40V1+Qo4iJ+ElMdVZQ=="
96 | },
97 | "@pixi/math": {
98 | "version": "6.1.3",
99 | "resolved": "https://registry.npmjs.org/@pixi/math/-/math-6.1.3.tgz",
100 | "integrity": "sha512-1bLZeHpG38Bz6TESwxayNbL7tztOd7gpZDXS5OiBB9n8SFZeKlWfRQ/aJrvjoBz2qsZf9gGeVKsHpC/FJz0qnA=="
101 | },
102 | "@pixi/mesh": {
103 | "version": "6.1.3",
104 | "resolved": "https://registry.npmjs.org/@pixi/mesh/-/mesh-6.1.3.tgz",
105 | "integrity": "sha512-TF9eKNQdowozVOr4G05+Auku2EK8XwDXKYVvMYvt6Tsn2DLSrRhWl7xYyj4EuTjW/4eaP/c2QqY18cEMoMtJiQ=="
106 | },
107 | "@pixi/mesh-extras": {
108 | "version": "6.1.3",
109 | "resolved": "https://registry.npmjs.org/@pixi/mesh-extras/-/mesh-extras-6.1.3.tgz",
110 | "integrity": "sha512-HuTV8SkTQZDU1bmHmJWRo+4Hiz89oCuOonE3ckfqsoAoULfImgU72qqNIq7Vxmnu3kXoXAwV+fvOl49OzWl4+w=="
111 | },
112 | "@pixi/mixin-cache-as-bitmap": {
113 | "version": "6.1.3",
114 | "resolved": "https://registry.npmjs.org/@pixi/mixin-cache-as-bitmap/-/mixin-cache-as-bitmap-6.1.3.tgz",
115 | "integrity": "sha512-mEa0kn3Mou3KhbAUpaGnvmPz/ifI/41af1N6kVcTz1V8cu4BI/f74xLv5pKkQtp+xzWlquGo/2z9urkrRFD6qA=="
116 | },
117 | "@pixi/mixin-get-child-by-name": {
118 | "version": "6.1.3",
119 | "resolved": "https://registry.npmjs.org/@pixi/mixin-get-child-by-name/-/mixin-get-child-by-name-6.1.3.tgz",
120 | "integrity": "sha512-HHrnA1MtsMSyW0lOnBlklHp7j3JGYHIyick4b8F8p8eKqOFiAVdLzf4tmX/fKF4zs6i7DuYKE8G9Z7vpAhyrFg=="
121 | },
122 | "@pixi/mixin-get-global-position": {
123 | "version": "6.1.3",
124 | "resolved": "https://registry.npmjs.org/@pixi/mixin-get-global-position/-/mixin-get-global-position-6.1.3.tgz",
125 | "integrity": "sha512-XqhEyViMlGOS+p2LKW2tFjQy4ghbARKriwgY10MGvNApHHZbUDL3VKM1EmR6F2Xj8PPmycWRw/0oBu148O2KhQ=="
126 | },
127 | "@pixi/particle-container": {
128 | "version": "6.1.3",
129 | "resolved": "https://registry.npmjs.org/@pixi/particle-container/-/particle-container-6.1.3.tgz",
130 | "integrity": "sha512-pZqRRL5Yx2Yy30cdjsNEXRpTfl1WEf640ZLVHX2+fcKcWftPJaIXQZR+0aLvijyWF3VA4O/r/8IxhYgiMkqAUQ=="
131 | },
132 | "@pixi/polyfill": {
133 | "version": "6.1.3",
134 | "resolved": "https://registry.npmjs.org/@pixi/polyfill/-/polyfill-6.1.3.tgz",
135 | "integrity": "sha512-e+g2sHK/ORKDOrhJ86zZgdMSkQNzKdkaMw/UUFZ5wEUJgltoqF7H0zwNVPPO/1m7hfrN02PBMinYtXM+qFdY/A==",
136 | "requires": {
137 | "object-assign": "^4.1.1",
138 | "promise-polyfill": "^8.2.0"
139 | }
140 | },
141 | "@pixi/prepare": {
142 | "version": "6.1.3",
143 | "resolved": "https://registry.npmjs.org/@pixi/prepare/-/prepare-6.1.3.tgz",
144 | "integrity": "sha512-zjv81fPJjdQyWGCbA9Ij04GfwJUYA3j6/vFyJFaDKVMqEWzNDJwu40G00P23BXh3F5dYL638EXvyLYDQavjseg=="
145 | },
146 | "@pixi/runner": {
147 | "version": "6.1.3",
148 | "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-6.1.3.tgz",
149 | "integrity": "sha512-hJw7O9enlei7Cp5/j2REKuUjvyyC4BGqmVycmt01jTYyphRYMNQgyF+OjwrL7nidZMXnCVzfNKWi8e5+c4wssg=="
150 | },
151 | "@pixi/settings": {
152 | "version": "6.1.3",
153 | "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-6.1.3.tgz",
154 | "integrity": "sha512-laKwS4/R+bTQokKIeMeMO4orvSNTMWUpNRXJbDq7N29bCrA5pT6BW+LNZ+4gJs4TFK/s9bmP/xU5BlPVKHRoyg==",
155 | "requires": {
156 | "ismobilejs": "^1.1.0"
157 | }
158 | },
159 | "@pixi/sprite": {
160 | "version": "6.1.3",
161 | "resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-6.1.3.tgz",
162 | "integrity": "sha512-TzvqeRV+bbxFbucR74c28wcDsCbXic+5dONM+fy31ejAIraKbigzKbgHxH6opgLEMMh5APzmJPlwntYdEUGSXQ=="
163 | },
164 | "@pixi/sprite-animated": {
165 | "version": "6.1.3",
166 | "resolved": "https://registry.npmjs.org/@pixi/sprite-animated/-/sprite-animated-6.1.3.tgz",
167 | "integrity": "sha512-COrFkmcMPxyv3zGRJJrNB2nOdaeDEOYTkbxUcNxMSJ7eT3O3PUX5XEvfOW7bl2zHkt8XraIQ66uwWychqGHx7Q=="
168 | },
169 | "@pixi/sprite-tiling": {
170 | "version": "6.1.3",
171 | "resolved": "https://registry.npmjs.org/@pixi/sprite-tiling/-/sprite-tiling-6.1.3.tgz",
172 | "integrity": "sha512-om+RrModhNFljb8C1fhpGKtgt5k5AW9gCjFfeBPN+5pVdVjtc/luyO2Cbubpeow9YQldrUZri9it63GBo07Cfw=="
173 | },
174 | "@pixi/spritesheet": {
175 | "version": "6.1.3",
176 | "resolved": "https://registry.npmjs.org/@pixi/spritesheet/-/spritesheet-6.1.3.tgz",
177 | "integrity": "sha512-QUqAYUzn/+0JlzrLo7ASIFzJSteGZuNMxKwyFL29JtttUIjdJlXe3+jrfUMAu6gewYd9HVYkXJ0ODhH8PH6KpA=="
178 | },
179 | "@pixi/text": {
180 | "version": "6.1.3",
181 | "resolved": "https://registry.npmjs.org/@pixi/text/-/text-6.1.3.tgz",
182 | "integrity": "sha512-R0D3cbwwLbQOfobja4NGhq0bF7biCfNE3PXsOmTEsWOroVJqUexIob5XZXoT9Avy3B8nlrB2Hyl5imIQx60jFw=="
183 | },
184 | "@pixi/text-bitmap": {
185 | "version": "6.1.3",
186 | "resolved": "https://registry.npmjs.org/@pixi/text-bitmap/-/text-bitmap-6.1.3.tgz",
187 | "integrity": "sha512-x46qOVoosl67dBrG3mgd2eQx3A9NTxWUnzgRpk5vsNfLLNRu6XlM+YoscRMuHT5sLEEBLewjcVxzAAkrSW45eQ=="
188 | },
189 | "@pixi/ticker": {
190 | "version": "6.1.3",
191 | "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-6.1.3.tgz",
192 | "integrity": "sha512-ZSuhe5HrmkDoqSIZjETUGYCQr/EbtDQGngq0LQLAgblyhAJbi4p/B3uf2XGfRNZ7Tdxdl0j81BmUqBEu2+DeoA=="
193 | },
194 | "@pixi/utils": {
195 | "version": "6.1.3",
196 | "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-6.1.3.tgz",
197 | "integrity": "sha512-05mm9TBbpYorYO3ALC4CVgR5K6sA/0uhnwE/Zl4ZhNJZN699LrIr0OWFQhxhySeGUPMDaizeEZpn2rhx+CYYpg==",
198 | "requires": {
199 | "@types/earcut": "^2.1.0",
200 | "earcut": "^2.2.2",
201 | "eventemitter3": "^3.1.0",
202 | "url": "^0.11.0"
203 | }
204 | },
205 | "@types/combokeys": {
206 | "version": "2.4.6",
207 | "resolved": "https://registry.npmjs.org/@types/combokeys/-/combokeys-2.4.6.tgz",
208 | "integrity": "sha512-EIRXpjG8nXE1gcvsp9IwIWIbbbHePC9mnbXeAjnf9svgzSYFgZmEgA23Ca0rEyGXn86KCtznZedv+6c/YGkYTA==",
209 | "dev": true
210 | },
211 | "@types/earcut": {
212 | "version": "2.1.1",
213 | "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.1.tgz",
214 | "integrity": "sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ=="
215 | },
216 | "@types/offscreencanvas": {
217 | "version": "2019.6.4",
218 | "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.6.4.tgz",
219 | "integrity": "sha512-u8SAgdZ8ROtkTF+mfZGOscl0or6BSj9A4g37e6nvxDc+YB/oDut0wHkK2PBBiC2bNR8TS0CPV+1gAk4fNisr1Q==",
220 | "dev": true
221 | },
222 | "combokeys": {
223 | "version": "3.0.1",
224 | "resolved": "https://registry.npmjs.org/combokeys/-/combokeys-3.0.1.tgz",
225 | "integrity": "sha512-5nAfaLZ3oO3kA+/xdoL7t197UJTz2WWidyH3BBeU6hqHtvyFERICd0y3DQFrQkJFTKBrtUDck/xCLLoFpnjaCw=="
226 | },
227 | "earcut": {
228 | "version": "2.2.3",
229 | "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.3.tgz",
230 | "integrity": "sha512-iRDI1QeCQIhMCZk48DRDMVgQSSBDmbzzNhnxIo+pwx3swkfjMh6vh0nWLq1NdvGHLKH6wIrAM3vQWeTj6qeoug=="
231 | },
232 | "esbuild": {
233 | "version": "0.13.10",
234 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.10.tgz",
235 | "integrity": "sha512-0NfCsnAh5XatHIx6Cu93wpR2v6opPoOMxONYhaAoZKzGYqAE+INcDeX2wqMdcndvPQdWCuuCmvlnsh0zmbHcSQ==",
236 | "dev": true,
237 | "requires": {
238 | "esbuild-android-arm64": "0.13.10",
239 | "esbuild-darwin-64": "0.13.10",
240 | "esbuild-darwin-arm64": "0.13.10",
241 | "esbuild-freebsd-64": "0.13.10",
242 | "esbuild-freebsd-arm64": "0.13.10",
243 | "esbuild-linux-32": "0.13.10",
244 | "esbuild-linux-64": "0.13.10",
245 | "esbuild-linux-arm": "0.13.10",
246 | "esbuild-linux-arm64": "0.13.10",
247 | "esbuild-linux-mips64le": "0.13.10",
248 | "esbuild-linux-ppc64le": "0.13.10",
249 | "esbuild-netbsd-64": "0.13.10",
250 | "esbuild-openbsd-64": "0.13.10",
251 | "esbuild-sunos-64": "0.13.10",
252 | "esbuild-windows-32": "0.13.10",
253 | "esbuild-windows-64": "0.13.10",
254 | "esbuild-windows-arm64": "0.13.10"
255 | }
256 | },
257 | "esbuild-android-arm64": {
258 | "version": "0.13.10",
259 | "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.10.tgz",
260 | "integrity": "sha512-1sCdVAq64yMp2Uhlu+97/enFxpmrj31QHtThz7K+/QGjbHa7JZdBdBsZCzWJuntKHZ+EU178tHYkvjaI9z5sGg==",
261 | "dev": true,
262 | "optional": true
263 | },
264 | "esbuild-darwin-64": {
265 | "version": "0.13.10",
266 | "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.10.tgz",
267 | "integrity": "sha512-XlL+BYZ2h9cz3opHfFgSHGA+iy/mljBFIRU9q++f9SiBXEZTb4gTW/IENAD1l9oKH0FdO9rUpyAfV+lM4uAxrg==",
268 | "dev": true,
269 | "optional": true
270 | },
271 | "esbuild-darwin-arm64": {
272 | "version": "0.13.10",
273 | "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.10.tgz",
274 | "integrity": "sha512-RZMMqMTyActMrXKkW71IQO8B0tyQm0Bm+ZJQWNaHJchL5LlqazJi7rriwSocP+sKLszHhsyTEBBh6qPdw5g5yQ==",
275 | "dev": true,
276 | "optional": true
277 | },
278 | "esbuild-freebsd-64": {
279 | "version": "0.13.10",
280 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.10.tgz",
281 | "integrity": "sha512-pf4BEN9reF3jvZEZdxljVgOv5JS4kuYFCI78xk+2HWustbLvTP0b9XXfWI/OD0ZLWbyLYZYIA+VbVe4tdAklig==",
282 | "dev": true,
283 | "optional": true
284 | },
285 | "esbuild-freebsd-arm64": {
286 | "version": "0.13.10",
287 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.10.tgz",
288 | "integrity": "sha512-j9PUcuNWmlxr4/ry4dK/s6zKh42Jhh/N5qnAAj7tx3gMbkIHW0JBoVSbbgp97p88X9xgKbXx4lG2sJDhDWmsYQ==",
289 | "dev": true,
290 | "optional": true
291 | },
292 | "esbuild-linux-32": {
293 | "version": "0.13.10",
294 | "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.10.tgz",
295 | "integrity": "sha512-imtdHG5ru0xUUXuc2ofdtyw0fWlHYXV7JjF7oZHgmn0b+B4o4Nr6ZON3xxoo1IP8wIekW+7b9exIf/MYq0QV7w==",
296 | "dev": true,
297 | "optional": true
298 | },
299 | "esbuild-linux-64": {
300 | "version": "0.13.10",
301 | "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.10.tgz",
302 | "integrity": "sha512-O7fzQIH2e7GC98dvoTH0rad5BVLm9yU3cRWfEmryCEIFTwbNEWCEWOfsePuoGOHRtSwoVY1hPc21CJE4/9rWxQ==",
303 | "dev": true,
304 | "optional": true
305 | },
306 | "esbuild-linux-arm": {
307 | "version": "0.13.10",
308 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.10.tgz",
309 | "integrity": "sha512-R2Jij4A0K8BcmBehvQeUteQEcf24Y2YZ6mizlNFuJOBPxe3vZNmkZ4mCE7Pf1tbcqA65qZx8J3WSHeGJl9EsJA==",
310 | "dev": true,
311 | "optional": true
312 | },
313 | "esbuild-linux-arm64": {
314 | "version": "0.13.10",
315 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.10.tgz",
316 | "integrity": "sha512-bkGxN67S2n0PF4zhh87/92kBTsH2xXLuH6T5omReKhpXdJZF5SVDSk5XU/nngARzE+e6QK6isK060Dr5uobzNw==",
317 | "dev": true,
318 | "optional": true
319 | },
320 | "esbuild-linux-mips64le": {
321 | "version": "0.13.10",
322 | "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.10.tgz",
323 | "integrity": "sha512-UDNO5snJYOLWrA2uOUxM/PVbzzh2TR7Zf2i8zCCuFlYgvAb/81XO+Tasp3YAElDpp4VGqqcpBXLtofa9nrnJGA==",
324 | "dev": true,
325 | "optional": true
326 | },
327 | "esbuild-linux-ppc64le": {
328 | "version": "0.13.10",
329 | "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.10.tgz",
330 | "integrity": "sha512-xu6J9rMWu1TcEGuEmoc8gsTrJCEPsf+QtxK4IiUZNde9r4Q4nlRVah4JVZP3hJapZgZJcxsse0XiKXh1UFdOeA==",
331 | "dev": true,
332 | "optional": true
333 | },
334 | "esbuild-netbsd-64": {
335 | "version": "0.13.10",
336 | "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.10.tgz",
337 | "integrity": "sha512-d+Gr0ScMC2J83Bfx/ZvJHK0UAEMncctwgjRth9d4zppYGLk/xMfFKxv5z1ib8yZpQThafq8aPm8AqmFIJrEesw==",
338 | "dev": true,
339 | "optional": true
340 | },
341 | "esbuild-openbsd-64": {
342 | "version": "0.13.10",
343 | "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.10.tgz",
344 | "integrity": "sha512-OuCYc+bNKumBvxflga+nFzZvxsgmWQW+z4rMGIjM5XIW0nNbGgRc5p/0PSDv0rTdxAmwCpV69fezal0xjrDaaA==",
345 | "dev": true,
346 | "optional": true
347 | },
348 | "esbuild-sunos-64": {
349 | "version": "0.13.10",
350 | "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.10.tgz",
351 | "integrity": "sha512-gUkgivZK11bD56wDoLsnYrsOHD/zHzzLSdqKcIl3wRMulfHpRBpoX8gL0dbWr+8N9c+1HDdbNdvxSRmZ4RCVwg==",
352 | "dev": true,
353 | "optional": true
354 | },
355 | "esbuild-windows-32": {
356 | "version": "0.13.10",
357 | "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.10.tgz",
358 | "integrity": "sha512-C1xJ54E56dGWRaYcTnRy7amVZ9n1/D/D2/qVw7e5EtS7p+Fv/yZxxgqyb1hMGKXgtFYX4jMpU5eWBF/AsYrn+A==",
359 | "dev": true,
360 | "optional": true
361 | },
362 | "esbuild-windows-64": {
363 | "version": "0.13.10",
364 | "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.10.tgz",
365 | "integrity": "sha512-6+EXEXopEs3SvPFAHcps2Krp/FvqXXsOQV33cInmyilb0ZBEQew4MIoZtMIyB3YXoV6//dl3i6YbPrFZaWEinQ==",
366 | "dev": true,
367 | "optional": true
368 | },
369 | "esbuild-windows-arm64": {
370 | "version": "0.13.10",
371 | "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.10.tgz",
372 | "integrity": "sha512-xTqM/XKhORo6u9S5I0dNJWEdWoemFjogLUTVLkQMVyUV3ZuMChahVA+bCqKHdyX55pCFxD/8v2fm3/sfFMWN+g==",
373 | "dev": true,
374 | "optional": true
375 | },
376 | "eventemitter3": {
377 | "version": "3.1.2",
378 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
379 | "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
380 | },
381 | "fsevents": {
382 | "version": "2.3.2",
383 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
384 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
385 | "dev": true,
386 | "optional": true
387 | },
388 | "function-bind": {
389 | "version": "1.1.1",
390 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
391 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
392 | "dev": true
393 | },
394 | "has": {
395 | "version": "1.0.3",
396 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
397 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
398 | "dev": true,
399 | "requires": {
400 | "function-bind": "^1.1.1"
401 | }
402 | },
403 | "is-core-module": {
404 | "version": "2.8.0",
405 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz",
406 | "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==",
407 | "dev": true,
408 | "requires": {
409 | "has": "^1.0.3"
410 | }
411 | },
412 | "ismobilejs": {
413 | "version": "1.1.1",
414 | "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz",
415 | "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw=="
416 | },
417 | "nanoid": {
418 | "version": "3.1.30",
419 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz",
420 | "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==",
421 | "dev": true
422 | },
423 | "object-assign": {
424 | "version": "4.1.1",
425 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
426 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
427 | },
428 | "path-parse": {
429 | "version": "1.0.7",
430 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
431 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
432 | "dev": true
433 | },
434 | "picocolors": {
435 | "version": "1.0.0",
436 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
437 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
438 | "dev": true
439 | },
440 | "pixi.js": {
441 | "version": "6.1.3",
442 | "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-6.1.3.tgz",
443 | "integrity": "sha512-h8Y/YVgP4CSPoUQvXaQvQf5GyQxi0b1NtVD38bZQsrX4CQ3r85jBU+zPyHN0fAcvhCB+nNvdD2sEwhhqkNsuSw==",
444 | "requires": {
445 | "@pixi/accessibility": "6.1.3",
446 | "@pixi/app": "6.1.3",
447 | "@pixi/compressed-textures": "6.1.3",
448 | "@pixi/constants": "6.1.3",
449 | "@pixi/core": "6.1.3",
450 | "@pixi/display": "6.1.3",
451 | "@pixi/extract": "6.1.3",
452 | "@pixi/filter-alpha": "6.1.3",
453 | "@pixi/filter-blur": "6.1.3",
454 | "@pixi/filter-color-matrix": "6.1.3",
455 | "@pixi/filter-displacement": "6.1.3",
456 | "@pixi/filter-fxaa": "6.1.3",
457 | "@pixi/filter-noise": "6.1.3",
458 | "@pixi/graphics": "6.1.3",
459 | "@pixi/interaction": "6.1.3",
460 | "@pixi/loaders": "6.1.3",
461 | "@pixi/math": "6.1.3",
462 | "@pixi/mesh": "6.1.3",
463 | "@pixi/mesh-extras": "6.1.3",
464 | "@pixi/mixin-cache-as-bitmap": "6.1.3",
465 | "@pixi/mixin-get-child-by-name": "6.1.3",
466 | "@pixi/mixin-get-global-position": "6.1.3",
467 | "@pixi/particle-container": "6.1.3",
468 | "@pixi/polyfill": "6.1.3",
469 | "@pixi/prepare": "6.1.3",
470 | "@pixi/runner": "6.1.3",
471 | "@pixi/settings": "6.1.3",
472 | "@pixi/sprite": "6.1.3",
473 | "@pixi/sprite-animated": "6.1.3",
474 | "@pixi/sprite-tiling": "6.1.3",
475 | "@pixi/spritesheet": "6.1.3",
476 | "@pixi/text": "6.1.3",
477 | "@pixi/text-bitmap": "6.1.3",
478 | "@pixi/ticker": "6.1.3",
479 | "@pixi/utils": "6.1.3"
480 | }
481 | },
482 | "postcss": {
483 | "version": "8.3.11",
484 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.11.tgz",
485 | "integrity": "sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==",
486 | "dev": true,
487 | "requires": {
488 | "nanoid": "^3.1.30",
489 | "picocolors": "^1.0.0",
490 | "source-map-js": "^0.6.2"
491 | }
492 | },
493 | "promise-polyfill": {
494 | "version": "8.2.1",
495 | "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.1.tgz",
496 | "integrity": "sha512-3p9zj0cEHbp7NVUxEYUWjQlffXqnXaZIMPkAO7HhFh8u5636xLRDHOUo2vpWSK0T2mqm6fKLXYn1KP6PAZ2gKg=="
497 | },
498 | "punycode": {
499 | "version": "1.3.2",
500 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
501 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
502 | },
503 | "querystring": {
504 | "version": "0.2.0",
505 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
506 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
507 | },
508 | "resolve": {
509 | "version": "1.20.0",
510 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
511 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
512 | "dev": true,
513 | "requires": {
514 | "is-core-module": "^2.2.0",
515 | "path-parse": "^1.0.6"
516 | }
517 | },
518 | "rollup": {
519 | "version": "2.58.3",
520 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.58.3.tgz",
521 | "integrity": "sha512-ei27MSw1KhRur4p87Q0/Va2NAYqMXOX++FNEumMBcdreIRLURKy+cE2wcDJKBn0nfmhP2ZGrJkP1XPO+G8FJQw==",
522 | "dev": true,
523 | "requires": {
524 | "fsevents": "~2.3.2"
525 | }
526 | },
527 | "source-map-js": {
528 | "version": "0.6.2",
529 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz",
530 | "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==",
531 | "dev": true
532 | },
533 | "typescript": {
534 | "version": "4.4.4",
535 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
536 | "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
537 | "dev": true
538 | },
539 | "url": {
540 | "version": "0.11.0",
541 | "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
542 | "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
543 | "requires": {
544 | "punycode": "1.3.2",
545 | "querystring": "0.2.0"
546 | }
547 | },
548 | "vite": {
549 | "version": "2.6.13",
550 | "resolved": "https://registry.npmjs.org/vite/-/vite-2.6.13.tgz",
551 | "integrity": "sha512-+tGZ1OxozRirTudl4M3N3UTNJOlxdVo/qBl2IlDEy/ZpTFcskp+k5ncNjayR3bRYTCbqSOFz2JWGN1UmuDMScA==",
552 | "dev": true,
553 | "requires": {
554 | "esbuild": "^0.13.2",
555 | "fsevents": "~2.3.2",
556 | "postcss": "^8.3.8",
557 | "resolve": "^1.20.0",
558 | "rollup": "^2.57.0"
559 | }
560 | }
561 | }
562 | }
563 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "matterjs-pixi-worker",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite --config vite.config.js",
6 | "build": "tsc && vite build",
7 | "preview": "vite preview"
8 | },
9 | "devDependencies": {
10 | "@types/combokeys": "^2.4.6",
11 | "@types/offscreencanvas": "^2019.6.4",
12 | "typescript": "^4.3.2",
13 | "vite": "^2.6.4"
14 | },
15 | "dependencies": {
16 | "@dimforge/rapier2d": "^0.7.6",
17 | "@dimforge/rapier2d-compat": "^0.7.6",
18 | "combokeys": "^3.0.1",
19 | "pixi.js": "^6.1.3"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/public/square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerzakm/gamedev-experiments/b482122293b6373d33d2f8ab91a9084cbc24d3e7/rapier-pixi-character-controller-dynamic/public/square.png
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/src/draw/_colorTheme.ts:
--------------------------------------------------------------------------------
1 | export const WALL = {
2 | fill: 0x444444,
3 | alpha: 0.8,
4 | stroke: 0x00000,
5 | strokeWidth: 2,
6 | };
7 |
8 | export const ENV_BALL = {
9 | fill: 0xccaa22,
10 | alpha: 0.8,
11 | stroke: 0x00000,
12 | strokeWidth: 2,
13 | };
14 |
15 | export const PLAYER = {
16 | fill: 0xffffff,
17 | alpha: 1.0,
18 | stroke: 0x00000,
19 | strokeWidth: 2,
20 | };
21 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/src/draw/envBallGraphics.ts:
--------------------------------------------------------------------------------
1 | import { Collider, RigidBody } from "@dimforge/rapier2d-compat";
2 | import { Graphics } from "pixi.js";
3 | import { BallDefinition } from "../physics/ballFactory";
4 | import { ENV_BALL } from "./_colorTheme";
5 |
6 | export const initEnvBallGraphics = () => {
7 | const envBallGraphics = new Graphics();
8 |
9 | const drawEnvBalls = (
10 | balls: {
11 | body: RigidBody;
12 | collider: Collider;
13 | definition: BallDefinition;
14 | }[]
15 | ) => {
16 | envBallGraphics.clear();
17 | envBallGraphics.beginFill(ENV_BALL.fill, ENV_BALL.alpha);
18 | envBallGraphics.lineStyle({
19 | alpha: ENV_BALL.alpha,
20 | color: ENV_BALL.stroke,
21 | width: ENV_BALL.strokeWidth,
22 | });
23 | for (const ball of balls) {
24 | const { x, y } = ball.body.translation();
25 | const radius = ball.collider.radius();
26 | envBallGraphics.drawCircle(x, y, radius);
27 | }
28 | envBallGraphics.endFill();
29 | };
30 |
31 | return { envBallGraphics, drawEnvBalls };
32 | };
33 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/src/draw/wallGraphics.ts:
--------------------------------------------------------------------------------
1 | import { Collider, RigidBody } from "@dimforge/rapier2d-compat";
2 | import { Graphics } from "pixi.js";
3 | import { WallDefinition } from "../physics/wallFactory";
4 | import { WALL } from "./_colorTheme";
5 |
6 | export const initWallGraphics = () => {
7 | const wallGraphics = new Graphics();
8 |
9 | const drawWalls = (
10 | walls: {
11 | body: RigidBody;
12 | collider: Collider;
13 | definition: WallDefinition;
14 | }[]
15 | ) => {
16 | wallGraphics.clear();
17 | wallGraphics.beginFill(WALL.fill, WALL.alpha);
18 | wallGraphics.lineStyle({
19 | alpha: WALL.alpha,
20 | color: WALL.stroke,
21 | width: WALL.strokeWidth,
22 | });
23 | for (const wall of walls) {
24 | const { x, y } = wall.body.translation();
25 | const halfW = wall.collider.halfExtents().x;
26 | const halfH = wall.collider.halfExtents().y;
27 | wallGraphics.drawRect(
28 | x - halfW,
29 | y - halfH,
30 | wall.collider.halfExtents().x * 2,
31 | wall.collider.halfExtents().y * 2
32 | );
33 | }
34 | wallGraphics.endFill();
35 | };
36 |
37 | return { wallGraphics, drawWalls };
38 | };
39 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/src/main.ts:
--------------------------------------------------------------------------------
1 | import "./style/global.css";
2 | import * as PIXI from "pixi.js";
3 | import { Renderer } from "./renderer";
4 | import { initPhysics } from "./physics/core";
5 | import { wallScreenArea } from "./physics/wallFactory";
6 | import { initWallGraphics } from "./draw/wallGraphics";
7 | import { BallDefinition, spawnRandomBall } from "./physics/ballFactory";
8 | import { initEnvBallGraphics } from "./draw/envBallGraphics";
9 | import { RigidBody, Collider } from "@dimforge/rapier2d-compat";
10 | import { setupPlayer } from "./player";
11 |
12 | const BALL_COUNT = 100;
13 |
14 | async function start() {
15 | // setup the renderer
16 | const { app, stage } = new Renderer();
17 | const container = new PIXI.Container();
18 | stage.addChild(container);
19 |
20 | // individual graphics for walls and balls
21 | const { wallGraphics, drawWalls } = initWallGraphics();
22 | const { envBallGraphics, drawEnvBalls } = initEnvBallGraphics();
23 | container.addChild(wallGraphics);
24 | container.addChild(envBallGraphics);
25 |
26 | // physics setup
27 | const physics = await initPhysics({ x: 0, y: 0 });
28 | const { RAPIER, step, world } = physics;
29 |
30 | // add walls to the physics world
31 | const walls = wallScreenArea(world, RAPIER, 50);
32 |
33 | // add balls to the physics world
34 | const envBalls: {
35 | body: RigidBody;
36 | collider: Collider;
37 | definition: BallDefinition;
38 | }[] = [];
39 |
40 | for (let i = 0; i < BALL_COUNT; i++) {
41 | envBalls.push(spawnRandomBall(world, RAPIER));
42 | }
43 |
44 | // setup player - physics, graphics & update function
45 | const { playerGraphics, drawPlayer, updatePlayer } = setupPlayer(
46 | world,
47 | RAPIER
48 | );
49 | container.addChild(playerGraphics);
50 |
51 | app.ticker.add((delta) => {
52 | const d = delta * 0.1;
53 |
54 | // Uncomment to spawn more balls over time
55 | /*
56 | if (Math.random() > 0.99) {
57 | const bouncyBall = spawnRandomBall(world, RAPIER);
58 | bouncyBall.collider.setRestitution(1);
59 | envBalls.push(bouncyBall);
60 | }
61 | */
62 | updatePlayer(); // Player movement logic from player.ts happens here
63 | drawWalls(walls);
64 | drawEnvBalls(envBalls);
65 | drawPlayer(delta);
66 | step(d); // step physics
67 | app.render(); // pixi render
68 | });
69 | }
70 |
71 | start();
72 |
73 | export interface PhysicsObjectOptions {
74 | isStatic: boolean;
75 | }
76 |
77 | export enum MessageType {
78 | SPAWN_PLAYER,
79 | }
80 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/src/physics/ballFactory.ts:
--------------------------------------------------------------------------------
1 | import { Vector2, World } from "@dimforge/rapier2d-compat";
2 | import { RAPIER } from "./core";
3 |
4 | export const makeBall = (
5 | world: World,
6 | RAPIER: RAPIER,
7 | definition: BallDefinition
8 | ) => {
9 | const body = world.createRigidBody(
10 | RAPIER.RigidBodyDesc.newDynamic().setTranslation(
11 | definition.position.x,
12 | definition.position.y
13 | )
14 | );
15 | let colliderDesc = new RAPIER.ColliderDesc(new RAPIER.Ball(definition.radius))
16 | .setTranslation(0, 0)
17 | .setRestitution(1.0);
18 |
19 | const collider = world.createCollider(colliderDesc, body.handle);
20 |
21 | return { body, collider, definition };
22 | };
23 |
24 | export const spawnRandomBall = (
25 | world: World,
26 | RAPIER: RAPIER,
27 | maxRadius?: number
28 | ) => {
29 | if (!maxRadius) {
30 | maxRadius = Math.random();
31 | }
32 |
33 | const definition: BallDefinition = {
34 | position: {
35 | x: (0.05 + Math.random() * 0.8) * window.innerWidth,
36 | y: (0.05 + Math.random() * 0.8) * window.innerHeight,
37 | },
38 | radius: 10 + Math.random() * 10,
39 | };
40 |
41 | return makeBall(world, RAPIER, definition);
42 | };
43 |
44 | export interface BallDefinition {
45 | position: Vector2;
46 | radius: number;
47 | }
48 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/src/physics/core.ts:
--------------------------------------------------------------------------------
1 | import { Vector2 } from "@dimforge/rapier2d";
2 | import { getRapier } from "../rapier";
3 |
4 | export type RAPIER =
5 | //@ts-ignore
6 | typeof import("D:/gamedev-experiments/rapier-pixi-shooter/node_modules/@dimforge/rapier2d-compat/exports");
7 |
8 | export const initPhysics = async (gravity: Vector2) => {
9 | const RAPIER = await getRapier();
10 | // Use the RAPIER module here.
11 |
12 | const world = new RAPIER.World(gravity);
13 |
14 | const step = (delta?: number) => {
15 | if (delta) {
16 | world.timestep = delta;
17 | }
18 | world.step();
19 | };
20 |
21 | return { RAPIER, step, world };
22 | };
23 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/src/physics/wallFactory.ts:
--------------------------------------------------------------------------------
1 | import { Vector2, World } from "@dimforge/rapier2d-compat";
2 | import { RAPIER } from "./core";
3 |
4 | export const makeWall = (
5 | world: World,
6 | RAPIER: RAPIER,
7 | definition: WallDefinition
8 | ) => {
9 | const body = world.createRigidBody(
10 | RAPIER.RigidBodyDesc.newStatic().setTranslation(
11 | definition.position.x,
12 | definition.position.y
13 | )
14 | );
15 | let colliderDesc = new RAPIER.ColliderDesc(
16 | new RAPIER.Cuboid(definition.size.x / 2, definition.size.y / 2)
17 | ).setTranslation(0, 0);
18 | const collider = world.createCollider(colliderDesc, body.handle);
19 |
20 | return { body, collider, definition };
21 | };
22 |
23 | export const wallScreenArea = (
24 | world: World,
25 | RAPIER: RAPIER,
26 | thickness: number
27 | ) => {
28 | const walls = [];
29 | walls.push(
30 | makeWall(world, RAPIER, {
31 | angle: 0,
32 | size: { y: window.innerHeight, x: thickness },
33 | position: { x: 0, y: window.innerHeight / 2 },
34 | })
35 | );
36 | walls.push(
37 | makeWall(world, RAPIER, {
38 | angle: 0,
39 | size: { y: window.innerHeight, x: thickness },
40 |
41 | position: { x: window.innerWidth, y: window.innerHeight / 2 },
42 | })
43 | );
44 | walls.push(
45 | makeWall(world, RAPIER, {
46 | size: { y: thickness, x: window.innerWidth },
47 | position: { x: window.innerWidth / 2, y: window.innerHeight },
48 | angle: 0,
49 | })
50 | );
51 | walls.push(
52 | makeWall(world, RAPIER, {
53 | angle: 0,
54 | size: { y: thickness, x: window.innerWidth },
55 |
56 | position: { x: window.innerWidth / 2, y: 0 },
57 | })
58 | );
59 |
60 | return walls;
61 | };
62 |
63 | export interface WallDefinition {
64 | position: Vector2;
65 | size: Vector2;
66 | angle: number;
67 | }
68 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/src/player.ts:
--------------------------------------------------------------------------------
1 | import { World, Vector2 } from "@dimforge/rapier2d-compat";
2 | import { Graphics } from "pixi.js";
3 | import { PLAYER } from "./draw/_colorTheme";
4 | import { RAPIER } from "./physics/core";
5 |
6 | const MOVE_SPEED = 80;
7 | const ACCELERATION = 40;
8 |
9 | export const setupPlayer = (world: World, RAPIER: RAPIER) => {
10 | const updatePlayer = () => {
11 | // Mouse only logic start
12 | if (goal) {
13 | const position = body.translation();
14 |
15 | const distanceFromGoal = Math.sqrt(
16 | (position.x - goal.x) ** 2 + (position.y - goal.y) ** 2
17 | );
18 | if (distanceFromGoal < 3) {
19 | body.setTranslation(goal, true);
20 | direction.x = 0;
21 | direction.y = 0;
22 | goal = undefined;
23 | } else {
24 | const x = goal.x - position.x;
25 | const y = goal.y - position.y;
26 | const div = Math.max(Math.abs(x), Math.abs(y));
27 | direction.x = x / div;
28 | direction.y = y / div;
29 | }
30 | }
31 | // mouseonly logic end
32 |
33 | // Approach 1 - just setting velocity
34 | // body.setLinvel(
35 | // { x: direction.x * MOVE_SPEED, y: direction.y * MOVE_SPEED },
36 | // true
37 | // );
38 |
39 | // Approach 2 - Applying impulse to body
40 | const velocity = body.linvel();
41 | const impulse = {
42 | x: (direction.x * MOVE_SPEED - velocity.x) * ACCELERATION,
43 | y: (direction.y * MOVE_SPEED - velocity.y) * ACCELERATION,
44 | };
45 | body.applyImpulse(impulse, true);
46 | };
47 |
48 | const drawPlayer = (delta = 16) => {
49 | playerGraphics.clear();
50 | playerGraphics.beginFill(PLAYER.fill, PLAYER.alpha);
51 | playerGraphics.lineStyle({
52 | alpha: PLAYER.alpha,
53 | color: PLAYER.stroke,
54 | width: PLAYER.strokeWidth,
55 | });
56 | const { x, y } = body.translation();
57 | const radius = collider.radius();
58 | playerGraphics.drawCircle(x, y, radius);
59 | playerGraphics.endFill();
60 |
61 | if (goal) {
62 | playerGraphics.lineStyle({
63 | alpha: PLAYER.alpha,
64 | color: 0xff2222,
65 | width: PLAYER.strokeWidth,
66 | });
67 | playerGraphics.drawCircle(
68 | goal.x,
69 | goal.y,
70 | Math.sin(performance.now()) * 2 * delta + 20
71 | );
72 | }
73 | };
74 |
75 | const setupKeyMouseListeners = () => {
76 | // move on keydown
77 | window.addEventListener("keydown", (e) => {
78 | switch (e.key) {
79 | case "w": {
80 | direction.y = -1;
81 | break;
82 | }
83 | case "s": {
84 | direction.y = 1;
85 | break;
86 | }
87 | case "a": {
88 | direction.x = -1;
89 | break;
90 | }
91 | case "d": {
92 | direction.x = 1;
93 | break;
94 | }
95 | }
96 | });
97 |
98 | // stop on keyup
99 | window.addEventListener("keyup", (e) => {
100 | switch (e.key) {
101 | case "w": {
102 | direction.y = 0;
103 | break;
104 | }
105 | case "s": {
106 | direction.y = 0;
107 | break;
108 | }
109 | case "a": {
110 | direction.x = 0;
111 | break;
112 | }
113 | case "d": {
114 | direction.x = 0;
115 | break;
116 | }
117 | }
118 | });
119 |
120 | // click to move - set goal
121 | document.addEventListener("pointerdown", (e) => {
122 | const { clientX, clientY } = e;
123 |
124 | goal = {
125 | x: clientX,
126 | y: clientY,
127 | };
128 | });
129 | };
130 |
131 | const { body, collider } = makePlayerPhysicsBody(world, RAPIER);
132 |
133 | const playerGraphics = new Graphics();
134 |
135 | const direction: Vector2 = {
136 | x: 0,
137 | y: 0,
138 | };
139 |
140 | let goal: Vector2 | undefined;
141 |
142 | collider.setActiveHooks(RAPIER.ActiveHooks.FILTER_CONTACT_PAIRS);
143 |
144 | setupKeyMouseListeners();
145 |
146 | return { playerGraphics, drawPlayer, updatePlayer };
147 | };
148 |
149 | const makePlayerPhysicsBody = (world: World, RAPIER: RAPIER) => {
150 | // create player's body and place it in the middle of the screen
151 | const body = world.createRigidBody(
152 | RAPIER.RigidBodyDesc.newDynamic().setTranslation(
153 | window.innerWidth / 2,
154 | window.innerHeight / 2
155 | )
156 | );
157 | // create a collider that describes the shape of player's body
158 | let colliderDesc = new RAPIER.ColliderDesc(
159 | new RAPIER.Ball(12)
160 | ).setTranslation(0, 0);
161 |
162 | // attach the collider to the body and add it to the world
163 | const collider = world.createCollider(colliderDesc, body.handle);
164 |
165 | return { body, collider };
166 | };
167 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/src/rapier.ts:
--------------------------------------------------------------------------------
1 | import RAPIER from "@dimforge/rapier2d-compat";
2 | export type Rapier = typeof RAPIER;
3 |
4 | export function getRapier() {
5 | // eslint-disable-next-line import/no-named-as-default-member
6 | return RAPIER.init().then(() => RAPIER);
7 | }
8 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/src/renderer.ts:
--------------------------------------------------------------------------------
1 | import * as PIXI from "pixi.js";
2 |
3 | export class Renderer {
4 | app: PIXI.Application;
5 | stage: PIXI.Container;
6 |
7 | constructor() {
8 | this.app = new PIXI.Application({
9 | width: window.innerWidth,
10 | height: window.innerHeight,
11 | backgroundColor: 0xcecece,
12 | resolution: 1,
13 | antialias: true,
14 | });
15 | // PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;
16 |
17 | document.body.appendChild(this.app.view);
18 | this.app.view.id = "pixi-view";
19 |
20 | this.stage = this.app.stage;
21 |
22 | this.resize();
23 | }
24 |
25 | private resize() {
26 | // resize canvas and webgl renderer when window sizeChanges
27 | window.addEventListener("resize", () => {
28 | this.app.view.width = window.innerWidth;
29 | this.app.view.height = window.innerHeight;
30 | });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/src/style/global.css:
--------------------------------------------------------------------------------
1 | #app {
2 | font-family: Avenir, Helvetica, Arial, sans-serif;
3 | -webkit-font-smoothing: antialiased;
4 | -moz-osx-font-smoothing: grayscale;
5 | text-align: center;
6 | color: #2c3e50;
7 | margin-top: 60px;
8 | }
9 |
10 | canvas {
11 | position: fixed;
12 | top: 0;
13 | left: 0;
14 | }
15 |
16 | #matter-canvas {
17 | z-index: 9999;
18 | mix-blend-mode: multiply;
19 | }
20 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ESNext", "DOM"],
7 | "moduleResolution": "Node",
8 | "strict": true,
9 | "sourceMap": true,
10 | "resolveJsonModule": true,
11 | "esModuleInterop": true,
12 | "noEmit": true,
13 | "noUnusedLocals": false,
14 | "noUnusedParameters": false,
15 | "noImplicitReturns": false
16 | },
17 | "include": ["./src"]
18 | }
19 |
--------------------------------------------------------------------------------
/rapier-pixi-character-controller-dynamic/vite.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | server: {
3 | watch: {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/rapier-pixi-worker/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
--------------------------------------------------------------------------------
/rapier-pixi-worker/README.md:
--------------------------------------------------------------------------------
1 | In my previous game dev attempts with javascript I always struggled with physics engine performance. I always defaulted to matter.js - it's good documentation and plentiful examples outweighed the performance gains of other available libraries. I was very excited when I first learned about WASM and near-native performance it provides, but for the longest time Box2D was the only viable choice in that area and I truely hated using it. It had poor documentation and felt very archaic to use.
2 |
3 | Now, it seems like my woes might be over. In comes a new contender - Rapier.rs.
4 |
5 | 
6 | [Rapier home](https://rapier.rs/)
7 |
8 | Rapier.rs is a rust physics library compiled to WASM with javscript bindings and good documentation. I was able to set it up in around 30 minutes and it provided an massive, instant boost to app performance.
9 |
10 | Rapier remained more stable and allowed me to add thousands of more active physics bodies to the world.
11 |
12 | **Links:**
13 |
14 | - Example from my last article with Rapier.rs instead of matter +300% performance [LIVE](https://workerized-rapier-pixi.netlify.app/)
15 | - [Github repo](https://github.com/jerzakm/gamedev-experiments/tree/main/rapier-pixi-worker)
16 |
17 | | Active bodies | Matter FPS | Rapier FPS |
18 | | ------------- | ----------- | ---------- |
19 | | 4500 | 38 | 120 |
20 | | 6000 | 21 | 79 |
21 | | 7500 | 4 | 60 |
22 | | 9000 | 0 - crashed | 42 |
23 | | 10000 | 0 - crashed | 31 |
24 | | 12000 | 0 - crashed | 22 |
25 | | 15000 | 0 - crashed | 16 |
26 |
27 | ## Why you need to consider Rapier for your js physics needs
28 |
29 | ### 1. Performance
30 |
31 | Javascript can't compare to an optimized Rust library compiled to WASM
32 | [WASM is just this fast](https://medium.com/@torch2424/webassembly-is-fast-a-real-world-benchmark-of-webassembly-vs-es6-d85a23f8e193)
33 |
34 | ### 2. Documentation
35 |
36 | Rapier page provides a good overview of the key features, information how to get started and an in-depth API documentation. All of this for Rust, Rust+bevy and Javascript.
37 |
38 | ### 3. Modern developer experience
39 |
40 | I found Rapier API very intuitive to work with, imho making it by far the best choice out of the few performant. It comes with **typescript support**. Resulting code is readable and easy to reason with.
41 |
42 | ```js
43 | import("@dimforge/rapier2d").then((RAPIER) => {
44 | // Use the RAPIER module here.
45 | let gravity = { x: 0.0, y: 9.81 };
46 | let world = new RAPIER.World(gravity);
47 |
48 | // Create the ground
49 | let groundColliderDesc = RAPIER.ColliderDesc.cuboid(10.0, 0.1);
50 | world.createCollider(groundColliderDesc);
51 |
52 | // Create a dynamic rigid-body.
53 | let rigidBodyDesc = RAPIER.RigidBodyDesc.newDynamic().setTranslation(
54 | 0.0,
55 | 1.0
56 | );
57 | let rigidBody = world.createRigidBody(rigidBodyDesc);
58 |
59 | // Create a cuboid collider attached to the dynamic rigidBody.
60 | let colliderDesc = RAPIER.ColliderDesc.cuboid(0.5, 0.5);
61 | let collider = world.createCollider(colliderDesc, rigidBody.handle);
62 |
63 | // Game loop. Replace by your own game loop system.
64 | let gameLoop = () => {
65 | // Step the simulation forward.
66 | world.step();
67 |
68 | // Get and print the rigid-body's position.
69 | let position = rigidBody.translation();
70 | console.log("Rigid-body position: ", position.x, position.y);
71 |
72 | setTimeout(gameLoop, 16);
73 | };
74 |
75 | gameLoop();
76 | });
77 | ```
78 |
79 | ### 4. Cross-platform determinism & snapshotting
80 |
81 | - Running the **same simulation**, with the same initial conditions on different machines or distributions of Rapier (rust/bevy/js) **will yield the same result.**
82 |
83 | - **Easy data saving and restoring.** - _It is possible to take a snapshot of the whole physics world with `world.takeSnapshot`. This results in a byte array of type Uint8Array that may be saved on the disk, sent through the network, etc. The snapshot can then be restored with `let world = World.restoreSnapshot(snapshot);`_.
84 |
85 | ## What's next?
86 |
87 | I am excited to keep working with Rapier, but in the meanwhile I think a proper physics benchmark is in order. The ones I've found while doing research were a bit dated.
88 |
89 | ### Other: Vite usage errors
90 |
91 | I've ran into some issues adding Rapier to my Vite project, the solution can be found here: https://github.com/dimforge/rapier.js/issues/49
92 |
--------------------------------------------------------------------------------
/rapier-pixi-worker/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/rapier-pixi-worker/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pixijs + physics
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/rapier-pixi-worker/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "matterjs-pixi-worker",
3 | "version": "0.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@dimforge/rapier2d": {
8 | "version": "0.7.6",
9 | "resolved": "https://registry.npmjs.org/@dimforge/rapier2d/-/rapier2d-0.7.6.tgz",
10 | "integrity": "sha512-0tEIApb01XTXa/G476/3sjpNQ7/KdkO2wpQyC6CWXm5fna9KeNEWQp4/eEJYUdt+tDwhTE2WY66Ah6eQmWXS8Q=="
11 | },
12 | "@dimforge/rapier2d-compat": {
13 | "version": "0.7.6",
14 | "resolved": "https://registry.npmjs.org/@dimforge/rapier2d-compat/-/rapier2d-compat-0.7.6.tgz",
15 | "integrity": "sha512-WmKfOSaNM2VCyAAMa6+ow6/HTzD8QSnddwxYFI7W/CQp4I3a5iXOKXLXyUya+AJCTkPfb8Y0LuTfZBBbqqEy7w=="
16 | },
17 | "@pixi/accessibility": {
18 | "version": "6.1.3",
19 | "resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-6.1.3.tgz",
20 | "integrity": "sha512-JK6rtqfC2/rnJt1xLPznH2lNH0Jx9f2Py7uh50VM1sqoYrkyAAegenbOdyEzgB35Q4oQji3aBkTsWn2mrwXp/g=="
21 | },
22 | "@pixi/app": {
23 | "version": "6.1.3",
24 | "resolved": "https://registry.npmjs.org/@pixi/app/-/app-6.1.3.tgz",
25 | "integrity": "sha512-gryDVXuzErRIgY5G2CRQH6fZM7Pk3m1CFEInXEKa4rmVzfwRz+3OeU0YNSnD9atPAS5C2TaAzE4yOSHH2+wESQ=="
26 | },
27 | "@pixi/compressed-textures": {
28 | "version": "6.1.3",
29 | "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-6.1.3.tgz",
30 | "integrity": "sha512-FO2B7GhDMlZA0fnpH2PvNOh6ZlRxQoJnNlpjzNw+x1nvF9h3+V6dbFoG9oBC5zAisTfacdfoo1TdT789Oh+kTg=="
31 | },
32 | "@pixi/constants": {
33 | "version": "6.1.3",
34 | "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-6.1.3.tgz",
35 | "integrity": "sha512-Qvz/SIxw+dQ6P9niOEdILWX2DQ5FnGA0XZNFLW/3amekzad/+WqHobL+Mg5S6A4/a9mXTnqjyB0BqhhtLfpFkA=="
36 | },
37 | "@pixi/core": {
38 | "version": "6.1.3",
39 | "resolved": "https://registry.npmjs.org/@pixi/core/-/core-6.1.3.tgz",
40 | "integrity": "sha512-UQsR1Q7c+Zcvtu6HrYMidvoyF/j9n3b4WXPh3ojuNV6+ZIvps3rznoZYaIx6foEJNhj7HM9fMObsimGP+FB36A=="
41 | },
42 | "@pixi/display": {
43 | "version": "6.1.3",
44 | "resolved": "https://registry.npmjs.org/@pixi/display/-/display-6.1.3.tgz",
45 | "integrity": "sha512-8/GdapJVKfl6PUkxX/Et5zB1aXny+uy353cQX886KJ6dGle82fQAYjIn7I6Xm+JiZWOhWo0N6KE9cjotO0rroA=="
46 | },
47 | "@pixi/extract": {
48 | "version": "6.1.3",
49 | "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-6.1.3.tgz",
50 | "integrity": "sha512-yZOsXc9Lh+U59ayl+DoWDPmndrOJj5ft2nzENMAvz2rVEOHQjWxH73qCSP6Wa5VsoINyJLMmV4MTbI+U0SH7GA=="
51 | },
52 | "@pixi/filter-alpha": {
53 | "version": "6.1.3",
54 | "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-6.1.3.tgz",
55 | "integrity": "sha512-eubgEO/qlxQbuPXgwxTZxTBTWjA0EQbrs7TyPqyBK2Wj0eEvimaVQ8u4eiqfMFJCZLnuWDCAPJpP9bMHxBXXpQ=="
56 | },
57 | "@pixi/filter-blur": {
58 | "version": "6.1.3",
59 | "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-6.1.3.tgz",
60 | "integrity": "sha512-uo8FHpV+qm4SuXcDnWqZWrznHmLJ3b8ibgLAgi/e8VmwrFiC+EqGa4n4V8J+xtR5P/iA3lT5pRgWw09/xHN3dQ=="
61 | },
62 | "@pixi/filter-color-matrix": {
63 | "version": "6.1.3",
64 | "resolved": "https://registry.npmjs.org/@pixi/filter-color-matrix/-/filter-color-matrix-6.1.3.tgz",
65 | "integrity": "sha512-d1pyxmVrGDOrO5pINe+fTspj1NNxiIp2IZ+FGgT7e17xnxjXTvtk4n4KqXAZFS1NCoStImDAV5j+b8Lysdg5jQ=="
66 | },
67 | "@pixi/filter-displacement": {
68 | "version": "6.1.3",
69 | "resolved": "https://registry.npmjs.org/@pixi/filter-displacement/-/filter-displacement-6.1.3.tgz",
70 | "integrity": "sha512-tIXK8vXzb2unMxGmu4gjdlOwddnkHA0IJXFTOF25a5h36v/AHqWwWG4h5G775oPu37UuhuYjeD/j229t0Q9QNQ=="
71 | },
72 | "@pixi/filter-fxaa": {
73 | "version": "6.1.3",
74 | "resolved": "https://registry.npmjs.org/@pixi/filter-fxaa/-/filter-fxaa-6.1.3.tgz",
75 | "integrity": "sha512-yhKVxX5vFKQz3lxfqAGg4XoajFyIRR8XzWqEHgAsPMFRnIIQIbF25bMRygZj12P61z3vxwqAM/2bn7S46Ii1zQ=="
76 | },
77 | "@pixi/filter-noise": {
78 | "version": "6.1.3",
79 | "resolved": "https://registry.npmjs.org/@pixi/filter-noise/-/filter-noise-6.1.3.tgz",
80 | "integrity": "sha512-oVRtcJwbN6VnAnvXZuLEZ0c12JUzporao5AziXgRAUjTMA3bFVE0/7Dx193Kx/l6UAasmzhWQctuv6NMxy5Efw=="
81 | },
82 | "@pixi/graphics": {
83 | "version": "6.1.3",
84 | "resolved": "https://registry.npmjs.org/@pixi/graphics/-/graphics-6.1.3.tgz",
85 | "integrity": "sha512-e5O47yECRp5WXWIvKhLDQKpiak7CfIqJzuTuQIyE7jXp8QiJNw+aoWNlJEd4ksKbsDkP3EE39CxlmiaBpxNL3w=="
86 | },
87 | "@pixi/interaction": {
88 | "version": "6.1.3",
89 | "resolved": "https://registry.npmjs.org/@pixi/interaction/-/interaction-6.1.3.tgz",
90 | "integrity": "sha512-ju3fE/KnO6KZChnZzZAdY6bfjlSh7/igZcVcd/MZRkAdNozx4QoN5sYmwrcvTvA5llMYaThSIRWgIHQiSlbOfQ=="
91 | },
92 | "@pixi/loaders": {
93 | "version": "6.1.3",
94 | "resolved": "https://registry.npmjs.org/@pixi/loaders/-/loaders-6.1.3.tgz",
95 | "integrity": "sha512-qOvy72bsVGzCmWyoofm6dm1l//hd+bJneidngplwsovpqnnyMfuewCpQjeLRL6rLqcHR40V1+Qo4iJ+ElMdVZQ=="
96 | },
97 | "@pixi/math": {
98 | "version": "6.1.3",
99 | "resolved": "https://registry.npmjs.org/@pixi/math/-/math-6.1.3.tgz",
100 | "integrity": "sha512-1bLZeHpG38Bz6TESwxayNbL7tztOd7gpZDXS5OiBB9n8SFZeKlWfRQ/aJrvjoBz2qsZf9gGeVKsHpC/FJz0qnA=="
101 | },
102 | "@pixi/mesh": {
103 | "version": "6.1.3",
104 | "resolved": "https://registry.npmjs.org/@pixi/mesh/-/mesh-6.1.3.tgz",
105 | "integrity": "sha512-TF9eKNQdowozVOr4G05+Auku2EK8XwDXKYVvMYvt6Tsn2DLSrRhWl7xYyj4EuTjW/4eaP/c2QqY18cEMoMtJiQ=="
106 | },
107 | "@pixi/mesh-extras": {
108 | "version": "6.1.3",
109 | "resolved": "https://registry.npmjs.org/@pixi/mesh-extras/-/mesh-extras-6.1.3.tgz",
110 | "integrity": "sha512-HuTV8SkTQZDU1bmHmJWRo+4Hiz89oCuOonE3ckfqsoAoULfImgU72qqNIq7Vxmnu3kXoXAwV+fvOl49OzWl4+w=="
111 | },
112 | "@pixi/mixin-cache-as-bitmap": {
113 | "version": "6.1.3",
114 | "resolved": "https://registry.npmjs.org/@pixi/mixin-cache-as-bitmap/-/mixin-cache-as-bitmap-6.1.3.tgz",
115 | "integrity": "sha512-mEa0kn3Mou3KhbAUpaGnvmPz/ifI/41af1N6kVcTz1V8cu4BI/f74xLv5pKkQtp+xzWlquGo/2z9urkrRFD6qA=="
116 | },
117 | "@pixi/mixin-get-child-by-name": {
118 | "version": "6.1.3",
119 | "resolved": "https://registry.npmjs.org/@pixi/mixin-get-child-by-name/-/mixin-get-child-by-name-6.1.3.tgz",
120 | "integrity": "sha512-HHrnA1MtsMSyW0lOnBlklHp7j3JGYHIyick4b8F8p8eKqOFiAVdLzf4tmX/fKF4zs6i7DuYKE8G9Z7vpAhyrFg=="
121 | },
122 | "@pixi/mixin-get-global-position": {
123 | "version": "6.1.3",
124 | "resolved": "https://registry.npmjs.org/@pixi/mixin-get-global-position/-/mixin-get-global-position-6.1.3.tgz",
125 | "integrity": "sha512-XqhEyViMlGOS+p2LKW2tFjQy4ghbARKriwgY10MGvNApHHZbUDL3VKM1EmR6F2Xj8PPmycWRw/0oBu148O2KhQ=="
126 | },
127 | "@pixi/particle-container": {
128 | "version": "6.1.3",
129 | "resolved": "https://registry.npmjs.org/@pixi/particle-container/-/particle-container-6.1.3.tgz",
130 | "integrity": "sha512-pZqRRL5Yx2Yy30cdjsNEXRpTfl1WEf640ZLVHX2+fcKcWftPJaIXQZR+0aLvijyWF3VA4O/r/8IxhYgiMkqAUQ=="
131 | },
132 | "@pixi/polyfill": {
133 | "version": "6.1.3",
134 | "resolved": "https://registry.npmjs.org/@pixi/polyfill/-/polyfill-6.1.3.tgz",
135 | "integrity": "sha512-e+g2sHK/ORKDOrhJ86zZgdMSkQNzKdkaMw/UUFZ5wEUJgltoqF7H0zwNVPPO/1m7hfrN02PBMinYtXM+qFdY/A==",
136 | "requires": {
137 | "object-assign": "^4.1.1",
138 | "promise-polyfill": "^8.2.0"
139 | }
140 | },
141 | "@pixi/prepare": {
142 | "version": "6.1.3",
143 | "resolved": "https://registry.npmjs.org/@pixi/prepare/-/prepare-6.1.3.tgz",
144 | "integrity": "sha512-zjv81fPJjdQyWGCbA9Ij04GfwJUYA3j6/vFyJFaDKVMqEWzNDJwu40G00P23BXh3F5dYL638EXvyLYDQavjseg=="
145 | },
146 | "@pixi/runner": {
147 | "version": "6.1.3",
148 | "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-6.1.3.tgz",
149 | "integrity": "sha512-hJw7O9enlei7Cp5/j2REKuUjvyyC4BGqmVycmt01jTYyphRYMNQgyF+OjwrL7nidZMXnCVzfNKWi8e5+c4wssg=="
150 | },
151 | "@pixi/settings": {
152 | "version": "6.1.3",
153 | "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-6.1.3.tgz",
154 | "integrity": "sha512-laKwS4/R+bTQokKIeMeMO4orvSNTMWUpNRXJbDq7N29bCrA5pT6BW+LNZ+4gJs4TFK/s9bmP/xU5BlPVKHRoyg==",
155 | "requires": {
156 | "ismobilejs": "^1.1.0"
157 | }
158 | },
159 | "@pixi/sprite": {
160 | "version": "6.1.3",
161 | "resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-6.1.3.tgz",
162 | "integrity": "sha512-TzvqeRV+bbxFbucR74c28wcDsCbXic+5dONM+fy31ejAIraKbigzKbgHxH6opgLEMMh5APzmJPlwntYdEUGSXQ=="
163 | },
164 | "@pixi/sprite-animated": {
165 | "version": "6.1.3",
166 | "resolved": "https://registry.npmjs.org/@pixi/sprite-animated/-/sprite-animated-6.1.3.tgz",
167 | "integrity": "sha512-COrFkmcMPxyv3zGRJJrNB2nOdaeDEOYTkbxUcNxMSJ7eT3O3PUX5XEvfOW7bl2zHkt8XraIQ66uwWychqGHx7Q=="
168 | },
169 | "@pixi/sprite-tiling": {
170 | "version": "6.1.3",
171 | "resolved": "https://registry.npmjs.org/@pixi/sprite-tiling/-/sprite-tiling-6.1.3.tgz",
172 | "integrity": "sha512-om+RrModhNFljb8C1fhpGKtgt5k5AW9gCjFfeBPN+5pVdVjtc/luyO2Cbubpeow9YQldrUZri9it63GBo07Cfw=="
173 | },
174 | "@pixi/spritesheet": {
175 | "version": "6.1.3",
176 | "resolved": "https://registry.npmjs.org/@pixi/spritesheet/-/spritesheet-6.1.3.tgz",
177 | "integrity": "sha512-QUqAYUzn/+0JlzrLo7ASIFzJSteGZuNMxKwyFL29JtttUIjdJlXe3+jrfUMAu6gewYd9HVYkXJ0ODhH8PH6KpA=="
178 | },
179 | "@pixi/text": {
180 | "version": "6.1.3",
181 | "resolved": "https://registry.npmjs.org/@pixi/text/-/text-6.1.3.tgz",
182 | "integrity": "sha512-R0D3cbwwLbQOfobja4NGhq0bF7biCfNE3PXsOmTEsWOroVJqUexIob5XZXoT9Avy3B8nlrB2Hyl5imIQx60jFw=="
183 | },
184 | "@pixi/text-bitmap": {
185 | "version": "6.1.3",
186 | "resolved": "https://registry.npmjs.org/@pixi/text-bitmap/-/text-bitmap-6.1.3.tgz",
187 | "integrity": "sha512-x46qOVoosl67dBrG3mgd2eQx3A9NTxWUnzgRpk5vsNfLLNRu6XlM+YoscRMuHT5sLEEBLewjcVxzAAkrSW45eQ=="
188 | },
189 | "@pixi/ticker": {
190 | "version": "6.1.3",
191 | "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-6.1.3.tgz",
192 | "integrity": "sha512-ZSuhe5HrmkDoqSIZjETUGYCQr/EbtDQGngq0LQLAgblyhAJbi4p/B3uf2XGfRNZ7Tdxdl0j81BmUqBEu2+DeoA=="
193 | },
194 | "@pixi/utils": {
195 | "version": "6.1.3",
196 | "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-6.1.3.tgz",
197 | "integrity": "sha512-05mm9TBbpYorYO3ALC4CVgR5K6sA/0uhnwE/Zl4ZhNJZN699LrIr0OWFQhxhySeGUPMDaizeEZpn2rhx+CYYpg==",
198 | "requires": {
199 | "@types/earcut": "^2.1.0",
200 | "earcut": "^2.2.2",
201 | "eventemitter3": "^3.1.0",
202 | "url": "^0.11.0"
203 | }
204 | },
205 | "@types/earcut": {
206 | "version": "2.1.1",
207 | "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.1.tgz",
208 | "integrity": "sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ=="
209 | },
210 | "@types/offscreencanvas": {
211 | "version": "2019.6.4",
212 | "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.6.4.tgz",
213 | "integrity": "sha512-u8SAgdZ8ROtkTF+mfZGOscl0or6BSj9A4g37e6nvxDc+YB/oDut0wHkK2PBBiC2bNR8TS0CPV+1gAk4fNisr1Q==",
214 | "dev": true
215 | },
216 | "earcut": {
217 | "version": "2.2.3",
218 | "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.3.tgz",
219 | "integrity": "sha512-iRDI1QeCQIhMCZk48DRDMVgQSSBDmbzzNhnxIo+pwx3swkfjMh6vh0nWLq1NdvGHLKH6wIrAM3vQWeTj6qeoug=="
220 | },
221 | "esbuild": {
222 | "version": "0.13.10",
223 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.10.tgz",
224 | "integrity": "sha512-0NfCsnAh5XatHIx6Cu93wpR2v6opPoOMxONYhaAoZKzGYqAE+INcDeX2wqMdcndvPQdWCuuCmvlnsh0zmbHcSQ==",
225 | "dev": true,
226 | "requires": {
227 | "esbuild-android-arm64": "0.13.10",
228 | "esbuild-darwin-64": "0.13.10",
229 | "esbuild-darwin-arm64": "0.13.10",
230 | "esbuild-freebsd-64": "0.13.10",
231 | "esbuild-freebsd-arm64": "0.13.10",
232 | "esbuild-linux-32": "0.13.10",
233 | "esbuild-linux-64": "0.13.10",
234 | "esbuild-linux-arm": "0.13.10",
235 | "esbuild-linux-arm64": "0.13.10",
236 | "esbuild-linux-mips64le": "0.13.10",
237 | "esbuild-linux-ppc64le": "0.13.10",
238 | "esbuild-netbsd-64": "0.13.10",
239 | "esbuild-openbsd-64": "0.13.10",
240 | "esbuild-sunos-64": "0.13.10",
241 | "esbuild-windows-32": "0.13.10",
242 | "esbuild-windows-64": "0.13.10",
243 | "esbuild-windows-arm64": "0.13.10"
244 | }
245 | },
246 | "esbuild-android-arm64": {
247 | "version": "0.13.10",
248 | "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.10.tgz",
249 | "integrity": "sha512-1sCdVAq64yMp2Uhlu+97/enFxpmrj31QHtThz7K+/QGjbHa7JZdBdBsZCzWJuntKHZ+EU178tHYkvjaI9z5sGg==",
250 | "dev": true,
251 | "optional": true
252 | },
253 | "esbuild-darwin-64": {
254 | "version": "0.13.10",
255 | "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.10.tgz",
256 | "integrity": "sha512-XlL+BYZ2h9cz3opHfFgSHGA+iy/mljBFIRU9q++f9SiBXEZTb4gTW/IENAD1l9oKH0FdO9rUpyAfV+lM4uAxrg==",
257 | "dev": true,
258 | "optional": true
259 | },
260 | "esbuild-darwin-arm64": {
261 | "version": "0.13.10",
262 | "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.10.tgz",
263 | "integrity": "sha512-RZMMqMTyActMrXKkW71IQO8B0tyQm0Bm+ZJQWNaHJchL5LlqazJi7rriwSocP+sKLszHhsyTEBBh6qPdw5g5yQ==",
264 | "dev": true,
265 | "optional": true
266 | },
267 | "esbuild-freebsd-64": {
268 | "version": "0.13.10",
269 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.10.tgz",
270 | "integrity": "sha512-pf4BEN9reF3jvZEZdxljVgOv5JS4kuYFCI78xk+2HWustbLvTP0b9XXfWI/OD0ZLWbyLYZYIA+VbVe4tdAklig==",
271 | "dev": true,
272 | "optional": true
273 | },
274 | "esbuild-freebsd-arm64": {
275 | "version": "0.13.10",
276 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.10.tgz",
277 | "integrity": "sha512-j9PUcuNWmlxr4/ry4dK/s6zKh42Jhh/N5qnAAj7tx3gMbkIHW0JBoVSbbgp97p88X9xgKbXx4lG2sJDhDWmsYQ==",
278 | "dev": true,
279 | "optional": true
280 | },
281 | "esbuild-linux-32": {
282 | "version": "0.13.10",
283 | "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.10.tgz",
284 | "integrity": "sha512-imtdHG5ru0xUUXuc2ofdtyw0fWlHYXV7JjF7oZHgmn0b+B4o4Nr6ZON3xxoo1IP8wIekW+7b9exIf/MYq0QV7w==",
285 | "dev": true,
286 | "optional": true
287 | },
288 | "esbuild-linux-64": {
289 | "version": "0.13.10",
290 | "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.10.tgz",
291 | "integrity": "sha512-O7fzQIH2e7GC98dvoTH0rad5BVLm9yU3cRWfEmryCEIFTwbNEWCEWOfsePuoGOHRtSwoVY1hPc21CJE4/9rWxQ==",
292 | "dev": true,
293 | "optional": true
294 | },
295 | "esbuild-linux-arm": {
296 | "version": "0.13.10",
297 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.10.tgz",
298 | "integrity": "sha512-R2Jij4A0K8BcmBehvQeUteQEcf24Y2YZ6mizlNFuJOBPxe3vZNmkZ4mCE7Pf1tbcqA65qZx8J3WSHeGJl9EsJA==",
299 | "dev": true,
300 | "optional": true
301 | },
302 | "esbuild-linux-arm64": {
303 | "version": "0.13.10",
304 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.10.tgz",
305 | "integrity": "sha512-bkGxN67S2n0PF4zhh87/92kBTsH2xXLuH6T5omReKhpXdJZF5SVDSk5XU/nngARzE+e6QK6isK060Dr5uobzNw==",
306 | "dev": true,
307 | "optional": true
308 | },
309 | "esbuild-linux-mips64le": {
310 | "version": "0.13.10",
311 | "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.10.tgz",
312 | "integrity": "sha512-UDNO5snJYOLWrA2uOUxM/PVbzzh2TR7Zf2i8zCCuFlYgvAb/81XO+Tasp3YAElDpp4VGqqcpBXLtofa9nrnJGA==",
313 | "dev": true,
314 | "optional": true
315 | },
316 | "esbuild-linux-ppc64le": {
317 | "version": "0.13.10",
318 | "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.10.tgz",
319 | "integrity": "sha512-xu6J9rMWu1TcEGuEmoc8gsTrJCEPsf+QtxK4IiUZNde9r4Q4nlRVah4JVZP3hJapZgZJcxsse0XiKXh1UFdOeA==",
320 | "dev": true,
321 | "optional": true
322 | },
323 | "esbuild-netbsd-64": {
324 | "version": "0.13.10",
325 | "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.10.tgz",
326 | "integrity": "sha512-d+Gr0ScMC2J83Bfx/ZvJHK0UAEMncctwgjRth9d4zppYGLk/xMfFKxv5z1ib8yZpQThafq8aPm8AqmFIJrEesw==",
327 | "dev": true,
328 | "optional": true
329 | },
330 | "esbuild-openbsd-64": {
331 | "version": "0.13.10",
332 | "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.10.tgz",
333 | "integrity": "sha512-OuCYc+bNKumBvxflga+nFzZvxsgmWQW+z4rMGIjM5XIW0nNbGgRc5p/0PSDv0rTdxAmwCpV69fezal0xjrDaaA==",
334 | "dev": true,
335 | "optional": true
336 | },
337 | "esbuild-sunos-64": {
338 | "version": "0.13.10",
339 | "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.10.tgz",
340 | "integrity": "sha512-gUkgivZK11bD56wDoLsnYrsOHD/zHzzLSdqKcIl3wRMulfHpRBpoX8gL0dbWr+8N9c+1HDdbNdvxSRmZ4RCVwg==",
341 | "dev": true,
342 | "optional": true
343 | },
344 | "esbuild-windows-32": {
345 | "version": "0.13.10",
346 | "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.10.tgz",
347 | "integrity": "sha512-C1xJ54E56dGWRaYcTnRy7amVZ9n1/D/D2/qVw7e5EtS7p+Fv/yZxxgqyb1hMGKXgtFYX4jMpU5eWBF/AsYrn+A==",
348 | "dev": true,
349 | "optional": true
350 | },
351 | "esbuild-windows-64": {
352 | "version": "0.13.10",
353 | "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.10.tgz",
354 | "integrity": "sha512-6+EXEXopEs3SvPFAHcps2Krp/FvqXXsOQV33cInmyilb0ZBEQew4MIoZtMIyB3YXoV6//dl3i6YbPrFZaWEinQ==",
355 | "dev": true,
356 | "optional": true
357 | },
358 | "esbuild-windows-arm64": {
359 | "version": "0.13.10",
360 | "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.10.tgz",
361 | "integrity": "sha512-xTqM/XKhORo6u9S5I0dNJWEdWoemFjogLUTVLkQMVyUV3ZuMChahVA+bCqKHdyX55pCFxD/8v2fm3/sfFMWN+g==",
362 | "dev": true,
363 | "optional": true
364 | },
365 | "eventemitter3": {
366 | "version": "3.1.2",
367 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
368 | "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
369 | },
370 | "fsevents": {
371 | "version": "2.3.2",
372 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
373 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
374 | "dev": true,
375 | "optional": true
376 | },
377 | "function-bind": {
378 | "version": "1.1.1",
379 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
380 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
381 | "dev": true
382 | },
383 | "has": {
384 | "version": "1.0.3",
385 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
386 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
387 | "dev": true,
388 | "requires": {
389 | "function-bind": "^1.1.1"
390 | }
391 | },
392 | "is-core-module": {
393 | "version": "2.8.0",
394 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz",
395 | "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==",
396 | "dev": true,
397 | "requires": {
398 | "has": "^1.0.3"
399 | }
400 | },
401 | "ismobilejs": {
402 | "version": "1.1.1",
403 | "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz",
404 | "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw=="
405 | },
406 | "nanoid": {
407 | "version": "3.1.30",
408 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz",
409 | "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==",
410 | "dev": true
411 | },
412 | "object-assign": {
413 | "version": "4.1.1",
414 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
415 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
416 | },
417 | "path-parse": {
418 | "version": "1.0.7",
419 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
420 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
421 | "dev": true
422 | },
423 | "picocolors": {
424 | "version": "1.0.0",
425 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
426 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
427 | "dev": true
428 | },
429 | "pixi.js": {
430 | "version": "6.1.3",
431 | "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-6.1.3.tgz",
432 | "integrity": "sha512-h8Y/YVgP4CSPoUQvXaQvQf5GyQxi0b1NtVD38bZQsrX4CQ3r85jBU+zPyHN0fAcvhCB+nNvdD2sEwhhqkNsuSw==",
433 | "requires": {
434 | "@pixi/accessibility": "6.1.3",
435 | "@pixi/app": "6.1.3",
436 | "@pixi/compressed-textures": "6.1.3",
437 | "@pixi/constants": "6.1.3",
438 | "@pixi/core": "6.1.3",
439 | "@pixi/display": "6.1.3",
440 | "@pixi/extract": "6.1.3",
441 | "@pixi/filter-alpha": "6.1.3",
442 | "@pixi/filter-blur": "6.1.3",
443 | "@pixi/filter-color-matrix": "6.1.3",
444 | "@pixi/filter-displacement": "6.1.3",
445 | "@pixi/filter-fxaa": "6.1.3",
446 | "@pixi/filter-noise": "6.1.3",
447 | "@pixi/graphics": "6.1.3",
448 | "@pixi/interaction": "6.1.3",
449 | "@pixi/loaders": "6.1.3",
450 | "@pixi/math": "6.1.3",
451 | "@pixi/mesh": "6.1.3",
452 | "@pixi/mesh-extras": "6.1.3",
453 | "@pixi/mixin-cache-as-bitmap": "6.1.3",
454 | "@pixi/mixin-get-child-by-name": "6.1.3",
455 | "@pixi/mixin-get-global-position": "6.1.3",
456 | "@pixi/particle-container": "6.1.3",
457 | "@pixi/polyfill": "6.1.3",
458 | "@pixi/prepare": "6.1.3",
459 | "@pixi/runner": "6.1.3",
460 | "@pixi/settings": "6.1.3",
461 | "@pixi/sprite": "6.1.3",
462 | "@pixi/sprite-animated": "6.1.3",
463 | "@pixi/sprite-tiling": "6.1.3",
464 | "@pixi/spritesheet": "6.1.3",
465 | "@pixi/text": "6.1.3",
466 | "@pixi/text-bitmap": "6.1.3",
467 | "@pixi/ticker": "6.1.3",
468 | "@pixi/utils": "6.1.3"
469 | }
470 | },
471 | "postcss": {
472 | "version": "8.3.11",
473 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.11.tgz",
474 | "integrity": "sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==",
475 | "dev": true,
476 | "requires": {
477 | "nanoid": "^3.1.30",
478 | "picocolors": "^1.0.0",
479 | "source-map-js": "^0.6.2"
480 | }
481 | },
482 | "promise-polyfill": {
483 | "version": "8.2.1",
484 | "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.1.tgz",
485 | "integrity": "sha512-3p9zj0cEHbp7NVUxEYUWjQlffXqnXaZIMPkAO7HhFh8u5636xLRDHOUo2vpWSK0T2mqm6fKLXYn1KP6PAZ2gKg=="
486 | },
487 | "punycode": {
488 | "version": "1.3.2",
489 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
490 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
491 | },
492 | "querystring": {
493 | "version": "0.2.0",
494 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
495 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
496 | },
497 | "resolve": {
498 | "version": "1.20.0",
499 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
500 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
501 | "dev": true,
502 | "requires": {
503 | "is-core-module": "^2.2.0",
504 | "path-parse": "^1.0.6"
505 | }
506 | },
507 | "rollup": {
508 | "version": "2.58.3",
509 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.58.3.tgz",
510 | "integrity": "sha512-ei27MSw1KhRur4p87Q0/Va2NAYqMXOX++FNEumMBcdreIRLURKy+cE2wcDJKBn0nfmhP2ZGrJkP1XPO+G8FJQw==",
511 | "dev": true,
512 | "requires": {
513 | "fsevents": "~2.3.2"
514 | }
515 | },
516 | "source-map-js": {
517 | "version": "0.6.2",
518 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz",
519 | "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==",
520 | "dev": true
521 | },
522 | "typescript": {
523 | "version": "4.4.4",
524 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
525 | "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
526 | "dev": true
527 | },
528 | "url": {
529 | "version": "0.11.0",
530 | "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
531 | "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
532 | "requires": {
533 | "punycode": "1.3.2",
534 | "querystring": "0.2.0"
535 | }
536 | },
537 | "vite": {
538 | "version": "2.6.13",
539 | "resolved": "https://registry.npmjs.org/vite/-/vite-2.6.13.tgz",
540 | "integrity": "sha512-+tGZ1OxozRirTudl4M3N3UTNJOlxdVo/qBl2IlDEy/ZpTFcskp+k5ncNjayR3bRYTCbqSOFz2JWGN1UmuDMScA==",
541 | "dev": true,
542 | "requires": {
543 | "esbuild": "^0.13.2",
544 | "fsevents": "~2.3.2",
545 | "postcss": "^8.3.8",
546 | "resolve": "^1.20.0",
547 | "rollup": "^2.57.0"
548 | }
549 | }
550 | }
551 | }
552 |
--------------------------------------------------------------------------------
/rapier-pixi-worker/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "matterjs-pixi-worker",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite --config vite.config.js",
6 | "build": "tsc && vite build",
7 | "preview": "vite preview"
8 | },
9 | "devDependencies": {
10 | "@types/offscreencanvas": "^2019.6.4",
11 | "typescript": "^4.3.2",
12 | "vite": "^2.6.4"
13 | },
14 | "dependencies": {
15 | "@dimforge/rapier2d": "^0.7.6",
16 | "@dimforge/rapier2d-compat": "^0.7.6",
17 | "pixi.js": "^6.1.3"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/rapier-pixi-worker/public/square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerzakm/gamedev-experiments/b482122293b6373d33d2f8ab91a9084cbc24d3e7/rapier-pixi-worker/public/square.png
--------------------------------------------------------------------------------
/rapier-pixi-worker/src/main.ts:
--------------------------------------------------------------------------------
1 | import "./style/global.css";
2 | import * as PIXI from "pixi.js";
3 | import { Renderer } from "./renderer";
4 | import PhysicsWorker from "./physicsWorker?worker";
5 | // import { IChamferableBodyDefinition } from "matter-js";
6 |
7 | const spawnerAmount = 100;
8 | const spawnerTimer = 1000;
9 | const spawnAtStart = 4500;
10 |
11 | let bodySyncDelta = 0;
12 | let rendererFps = 0;
13 | let bodyCount = 0;
14 | let statsUpdateFrequency = 500;
15 |
16 | const initStats = () => {
17 | const statsDom = document.body.querySelector("#stats");
18 |
19 | if (!statsDom) return;
20 |
21 | statsDom.innerHTML = `
22 | Bodies ${bodyCount}
23 | renderer_fps ${rendererFps.toFixed(0)}
24 | physics_fps ${(1000 / bodySyncDelta).toFixed(0)}
25 | `;
26 |
27 | setTimeout(initStats, statsUpdateFrequency);
28 | };
29 |
30 | async function workerExample() {
31 | const worker = new PhysicsWorker();
32 |
33 | const { app, stage } = new Renderer();
34 | const container = new PIXI.Container();
35 |
36 | stage.addChild(container);
37 |
38 | const physicsObjects: IPhysicsSyncBody[] = [];
39 |
40 | const addBody = (
41 | x = 0,
42 | y = 0,
43 | width = 10,
44 | height = 10,
45 | options: any = {
46 | restitution: 0,
47 | }
48 | ) => {
49 | const newBody = {
50 | x,
51 | y,
52 | width,
53 | height,
54 | options,
55 | };
56 |
57 | worker.postMessage({
58 | type: "ADD_BODY",
59 | data: newBody,
60 | });
61 | };
62 |
63 | const spawnRandomDynamicSquare = () => {
64 | const x =
65 | window.innerWidth / 2 + (Math.random() - 0.5) * window.innerWidth * 0.8;
66 | const y =
67 | window.innerHeight / 2 + (Math.random() - 0.5) * window.innerHeight * 0.8;
68 |
69 | const options = {
70 | restitution: 0,
71 | };
72 |
73 | const size = 4 + 10 * Math.random();
74 | addBody(x, y, size, size, options);
75 | };
76 |
77 | const setupWalls = () => {
78 | addBody(window.innerWidth / 2, 0, window.innerWidth, 50, {
79 | isStatic: true,
80 | });
81 | addBody(window.innerWidth / 2, window.innerHeight, window.innerWidth, 50, {
82 | isStatic: true,
83 | });
84 | addBody(0, window.innerHeight / 2, 50, window.innerHeight, {
85 | isStatic: true,
86 | });
87 | addBody(window.innerWidth, window.innerHeight / 2, 50, window.innerHeight, {
88 | isStatic: true,
89 | });
90 | };
91 |
92 | const initPhysicsHandler = () => {
93 | // Listener to handle data that worker passes to main thread
94 | worker.addEventListener("message", (e) => {
95 | if (e.data.type == "BODY_SYNC") {
96 | const physData = e.data.data;
97 |
98 | bodySyncDelta = e.data.delta;
99 |
100 | for (const obj of physicsObjects) {
101 | const { x, y, rotation } = physData[obj.id];
102 | if (!obj.sprite) return;
103 | obj.sprite.position.x = x;
104 | obj.sprite.position.y = y;
105 | obj.sprite.rotation = rotation;
106 | }
107 | }
108 | if (e.data.type == "BODY_CREATED") {
109 | const texture = PIXI.Texture.from("square.png");
110 | const sprite = new PIXI.Sprite(texture);
111 | const { x, y, width, height, id }: IPhysicsSyncBody = e.data.data;
112 | sprite.anchor.set(0.5);
113 | sprite.position.x = x;
114 | sprite.position.y = y;
115 | sprite.width = width;
116 | sprite.height = height;
117 | container.addChild(sprite);
118 |
119 | physicsObjects.push({
120 | id,
121 | x,
122 | y,
123 | width,
124 | height,
125 | angle: 0,
126 | sprite,
127 | });
128 | }
129 | if (e.data.type == "PHYSICS_LOADED") {
130 | // initial spawn
131 | setupWalls();
132 | for (let i = 0; i < spawnAtStart; i++) {
133 | spawnRandomDynamicSquare();
134 | }
135 | }
136 | });
137 | };
138 |
139 | const timedSpawner = () => {
140 | for (let i = 0; i < spawnerAmount; i++) {
141 | spawnRandomDynamicSquare();
142 | }
143 |
144 | setTimeout(() => {
145 | timedSpawner();
146 | }, spawnerTimer);
147 | };
148 |
149 | timedSpawner();
150 | initPhysicsHandler();
151 |
152 | // gameloop
153 | let lastSpawnAttempt = 0;
154 | let delta = 0;
155 |
156 | app.ticker.stop();
157 |
158 | let start = performance.now();
159 | const gameLoop = () => {
160 | start = performance.now();
161 | app.render();
162 | lastSpawnAttempt += delta;
163 |
164 | bodyCount = physicsObjects.length;
165 | delta = performance.now() - start;
166 | rendererFps = 60 / delta;
167 | setTimeout(() => gameLoop(), 0);
168 | };
169 |
170 | gameLoop();
171 | }
172 |
173 | workerExample();
174 | initStats();
175 |
176 | interface IPhysicsSyncBody {
177 | id: string | number;
178 | x: number;
179 | y: number;
180 | width: number;
181 | height: number;
182 | angle: number;
183 | sprite: PIXI.Sprite | undefined;
184 | }
185 |
186 | export type PositionSyncMap = {
187 | [key: number]: {
188 | x: number;
189 | y: number;
190 | rotation: number;
191 | };
192 | };
193 |
194 | export interface PhysicsObjectOptions {
195 | isStatic: boolean;
196 | }
197 |
--------------------------------------------------------------------------------
/rapier-pixi-worker/src/physicsWorker.ts:
--------------------------------------------------------------------------------
1 | import { PositionSyncMap } from "./main";
2 | import { getRapier } from "./rapier";
3 |
4 | const maxFps = 500;
5 | const deltaGoal = 1000 / maxFps;
6 |
7 | const bodyAddQueue: any[] = [];
8 |
9 | async function init() {
10 | const RAPIER = await getRapier();
11 | // Use the RAPIER module here.
12 | let gravity = { x: 0.0, y: 0.0 };
13 | let world = new RAPIER.World(gravity);
14 |
15 | const applyForceToRandomBody = () => {
16 | const bodyCount = world.bodies.len();
17 |
18 | if (bodyCount == 0) return;
19 | const bodyIndex = Math.round(Math.random() * bodyCount);
20 |
21 | const body = world.getRigidBody(bodyIndex);
22 | if (!body) return;
23 | const mass = body.mass();
24 |
25 | body.applyImpulse(
26 | {
27 | x: (Math.random() - 0.5) * mass ** 2 * 0.5,
28 | y: (Math.random() - 0.5) * mass ** 2 * 0.5,
29 | },
30 | true
31 | );
32 | };
33 |
34 | const syncPositions = (delta: number) => {
35 | const syncObj: PositionSyncMap = {};
36 |
37 | let count = 0;
38 |
39 | world.forEachRigidBody((body) => {
40 | const { x, y } = body.translation();
41 | const rotation = body.rotation();
42 | syncObj[body.handle] = { x, y, rotation };
43 |
44 | count++;
45 | });
46 |
47 | self.postMessage({
48 | type: "BODY_SYNC",
49 | data: syncObj,
50 | delta,
51 | });
52 | };
53 |
54 | const outOfBoundCheck = () => {
55 | world.forEachRigidBody((body) => {
56 | const { x, y } = body.translation();
57 |
58 | if (Math.abs(x) + Math.abs(y) > 6000) {
59 | body.setTranslation(
60 | {
61 | x: 100,
62 | y: 100,
63 | },
64 | true
65 | );
66 | }
67 | });
68 | };
69 |
70 | let gameLoop = (delta = 16) => {
71 | const startTs = performance.now();
72 |
73 | if (Math.random() > 0.3) {
74 | applyForceToRandomBody();
75 | }
76 |
77 | while (bodyAddQueue.length > 0) {
78 | const { x, y, width, height, options } = bodyAddQueue[0];
79 |
80 | let rigidBody;
81 |
82 | if (options.isStatic) {
83 | rigidBody = world.createRigidBody(
84 | RAPIER.RigidBodyDesc.newStatic().setTranslation(x, y)
85 | );
86 | } else {
87 | rigidBody = world.createRigidBody(
88 | RAPIER.RigidBodyDesc.newDynamic().setTranslation(x, y)
89 | );
90 | }
91 |
92 | const colliderDesc = new RAPIER.ColliderDesc(
93 | new RAPIER.Cuboid(width / 2, height / 2)
94 | ).setTranslation(0, 0);
95 |
96 | const bodyCollider = world.createCollider(colliderDesc, rigidBody.handle);
97 |
98 | bodyAddQueue.shift();
99 |
100 | self.postMessage({
101 | type: "BODY_CREATED",
102 | data: {
103 | id: bodyCollider.handle,
104 | x,
105 | y,
106 | width,
107 | height,
108 | angle: 0,
109 | sprite: undefined,
110 | },
111 | });
112 | }
113 |
114 | world.timestep = delta;
115 |
116 | world.step();
117 | syncPositions(delta);
118 |
119 | const currentDelta = performance.now() - startTs;
120 |
121 | // this bit limits max FPS to 60
122 | const deltaGoalDifference = Math.max(0, deltaGoal - currentDelta);
123 | const d = Math.max(currentDelta, deltaGoal);
124 |
125 | setTimeout(() => gameLoop(d), deltaGoalDifference);
126 | };
127 | gameLoop();
128 |
129 | self.postMessage({
130 | type: "PHYSICS_LOADED",
131 | });
132 |
133 | // once a second check for bodies out of bound
134 | setInterval(() => {
135 | // outOfBoundCheck();
136 | }, 1000);
137 |
138 | self.addEventListener("message", (e) => {
139 | const message = e.data || e;
140 |
141 | if (message.type == "ADD_BODY") {
142 | bodyAddQueue.push(message.data);
143 | }
144 | });
145 | }
146 |
147 | init();
148 |
--------------------------------------------------------------------------------
/rapier-pixi-worker/src/rapier.ts:
--------------------------------------------------------------------------------
1 | import RAPIER from "@dimforge/rapier2d-compat";
2 | export type Rapier = typeof RAPIER;
3 |
4 | export function getRapier() {
5 | // eslint-disable-next-line import/no-named-as-default-member
6 | return RAPIER.init().then(() => RAPIER);
7 | }
8 |
--------------------------------------------------------------------------------
/rapier-pixi-worker/src/renderer.ts:
--------------------------------------------------------------------------------
1 | import * as PIXI from "pixi.js";
2 |
3 | export class Renderer {
4 | app: PIXI.Application;
5 | stage: PIXI.Container;
6 |
7 | constructor() {
8 | this.app = new PIXI.Application({
9 | width: window.innerWidth,
10 | height: window.innerHeight,
11 | backgroundColor: 0xcecece,
12 | resolution: 1,
13 | antialias: true,
14 | });
15 | // PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;
16 |
17 | document.body.appendChild(this.app.view);
18 | this.app.view.id = "pixi-view";
19 |
20 | this.stage = this.app.stage;
21 |
22 | this.resize();
23 | }
24 |
25 | private resize() {
26 | // resize canvas and webgl renderer when window sizeChanges
27 | window.addEventListener("resize", () => {
28 | this.app.view.width = window.innerWidth;
29 | this.app.view.height = window.innerHeight;
30 | });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/rapier-pixi-worker/src/style/global.css:
--------------------------------------------------------------------------------
1 | #app {
2 | font-family: Avenir, Helvetica, Arial, sans-serif;
3 | -webkit-font-smoothing: antialiased;
4 | -moz-osx-font-smoothing: grayscale;
5 | text-align: center;
6 | color: #2c3e50;
7 | margin-top: 60px;
8 | }
9 |
10 | canvas {
11 | position: fixed;
12 | top: 0;
13 | left: 0;
14 | }
15 |
16 | #matter-canvas {
17 | z-index: 9999;
18 | mix-blend-mode: multiply;
19 | }
20 |
21 | #stats {
22 | z-index: 9999;
23 | position: fixed;
24 | top: 2rem;
25 | left: 2rem;
26 | display: grid;
27 | grid-template-columns: 1fr 1fr;
28 | gap: 0 1rem;
29 | background-color: #2c3e50;
30 | padding: 0.25rem;
31 | color: white;
32 | font-style: monospace;
33 | min-width: 150px;
34 | }
35 |
--------------------------------------------------------------------------------
/rapier-pixi-worker/src/util.ts:
--------------------------------------------------------------------------------
1 | export const uuidv4 = () => {
2 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
3 | const r = (Math.random() * 16) | 0,
4 | v = c == "x" ? r : (r & 0x3) | 0x8;
5 | return v.toString(16);
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/rapier-pixi-worker/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/rapier-pixi-worker/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ESNext", "DOM"],
7 | "moduleResolution": "Node",
8 | "strict": true,
9 | "sourceMap": true,
10 | "resolveJsonModule": true,
11 | "esModuleInterop": true,
12 | "noEmit": true,
13 | "noUnusedLocals": false,
14 | "noUnusedParameters": false,
15 | "noImplicitReturns": false
16 | },
17 | "include": ["./src"]
18 | }
19 |
--------------------------------------------------------------------------------
/rapier-pixi-worker/vite.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | server: {
3 | watch: {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------