├── 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 | ![nbrpJOCJQu.gif](https://media.graphcms.com/6buI1RaOvW2KIklhMDAz) 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 | ![Untitled.png](https://media.graphcms.com/gqr7vUh4SmuwgE5Rjs5c) 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 | ![nbrpJOCJQu.gif](https://media.graphcms.com/6buI1RaOvW2KIklhMDAz) 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 | ![Rapier.rs logo](https://media.graphcms.com/IxHiH6ZYRLuYapuZHFTT) 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 | ![RipHok4WkN.gif](https://media.graphcms.com/clfiTnXlSbOQgUgThJ6m) 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 | ![Moving with impulse.png](https://media.graphcms.com/3CtKK59cSQWohAcACggc) 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 | ![Rapier.rs logo](https://media.graphcms.com/IxHiH6ZYRLuYapuZHFTT) 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 | --------------------------------------------------------------------------------