├── .github ├── dependabot.yml ├── linters │ └── .markdownlint.yml └── workflows │ ├── code.yml │ ├── lint-toml.yml │ ├── markdown-lint.yml │ └── website.yml ├── .gitignore ├── .markdownlint.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── bevy_matchbox ├── Cargo.toml ├── README.md ├── examples │ ├── hello.rs │ ├── hello_host.rs │ └── hello_signaling.rs └── src │ ├── lib.rs │ ├── signaling.rs │ └── socket.rs ├── examples ├── async_example │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── bevy_ggrs │ ├── .cargo │ │ └── config.toml │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── assets │ │ └── fonts │ │ │ ├── quicksand-light.ttf │ │ │ └── quicksand-light.txt │ └── src │ │ ├── args.rs │ │ ├── box_game.rs │ │ └── main.rs ├── custom_signaller │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── direct_message.rs │ │ ├── get_browser_url.rs │ │ ├── iroh_gossip_signaller.rs │ │ ├── lib.rs │ │ └── main.rs ├── error_handling │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs └── simple │ ├── .cargo │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ └── main.rs ├── images ├── connection.excalidraw.svg └── matchbox_logo.png ├── matchbox_protocol ├── Cargo.toml └── src │ └── lib.rs ├── matchbox_server ├── .gitignore ├── Cargo.toml ├── Dockerfile ├── README.md └── src │ ├── args.rs │ ├── main.rs │ ├── state.rs │ └── topology.rs ├── matchbox_signaling ├── Cargo.toml ├── README.md ├── examples │ ├── client_server.rs │ └── full_mesh.rs ├── src │ ├── error.rs │ ├── lib.rs │ ├── signaling_server │ │ ├── builder.rs │ │ ├── callbacks.rs │ │ ├── error.rs │ │ ├── handlers.rs │ │ ├── mod.rs │ │ └── server.rs │ └── topologies │ │ ├── client_server.rs │ │ ├── full_mesh.rs │ │ └── mod.rs └── tests │ ├── client_server.rs │ └── full_mesh.rs ├── matchbox_socket ├── .gitignore ├── Cargo.toml ├── README.md └── src │ ├── error.rs │ ├── ggrs_socket.rs │ ├── lib.rs │ └── webrtc_socket │ ├── error.rs │ ├── messages.rs │ ├── mod.rs │ ├── native.rs │ ├── signal_peer.rs │ ├── socket.rs │ └── wasm.rs ├── rustfmt.toml └── website └── index.html /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: docker 8 | directory: matchbox_server/ 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/linters/.markdownlint.yml: -------------------------------------------------------------------------------- 1 | ../../.markdownlint.yml -------------------------------------------------------------------------------- /.github/workflows/code.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | name: Code 8 | 9 | jobs: 10 | test-native: 11 | name: Test Native 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, windows-latest, macos-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - name: Checkout sources 18 | uses: actions/checkout@v4 19 | 20 | - name: Install stable toolchain 21 | uses: dtolnay/rust-toolchain@stable 22 | with: 23 | toolchain: stable 24 | 25 | - name: Rust Cache 26 | uses: Swatinem/rust-cache@v2 27 | 28 | - name: Run cargo test 29 | run: cargo test --features signaling --all-targets 30 | 31 | # Should be upgraded to test when possible 32 | check-wasm: 33 | name: Check Wasm 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout sources 37 | uses: actions/checkout@v4 38 | 39 | - name: Install stable toolchain 40 | uses: dtolnay/rust-toolchain@stable 41 | with: 42 | toolchain: stable 43 | target: wasm32-unknown-unknown 44 | 45 | - name: Rust Cache 46 | uses: Swatinem/rust-cache@v2 47 | 48 | - name: Run cargo check 49 | # getrandom_backend="wasm_js" is needed for tracing to compile 50 | run: RUSTFLAGS='--cfg=web_sys_unstable_apis --cfg=getrandom_backend="wasm_js"' cargo check > 51 | --all-targets 52 | --target wasm32-unknown-unknown 53 | -p matchbox_socket 54 | -p bevy_matchbox 55 | -p bevy_ggrs_example 56 | -p simple_example 57 | -p custom_signaller 58 | 59 | format: 60 | name: Format 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout sources 64 | uses: actions/checkout@v4 65 | 66 | - name: Install stable toolchain 67 | uses: dtolnay/rust-toolchain@stable 68 | with: 69 | toolchain: stable 70 | components: rustfmt 71 | 72 | - name: Rust Cache 73 | uses: Swatinem/rust-cache@v2 74 | 75 | - name: Run cargo clippy 76 | run: cargo fmt --all --check 77 | 78 | 79 | lint-native: 80 | name: Lints native 81 | strategy: 82 | matrix: 83 | os: [ubuntu-latest, windows-latest, macos-latest] 84 | runs-on: ${{ matrix.os }} 85 | steps: 86 | - name: Checkout sources 87 | uses: actions/checkout@v4 88 | 89 | - name: Install stable toolchain 90 | uses: dtolnay/rust-toolchain@stable 91 | with: 92 | toolchain: stable 93 | components: clippy 94 | 95 | - name: Rust Cache 96 | uses: Swatinem/rust-cache@v2 97 | 98 | - name: Run cargo clippy 99 | run: cargo clippy --features signaling --all-targets -- -D warnings 100 | 101 | lint-wasm: 102 | name: Clippy wasm 103 | runs-on: ubuntu-latest 104 | steps: 105 | - name: Checkout sources 106 | uses: actions/checkout@v4 107 | 108 | - name: Install stable toolchain 109 | uses: dtolnay/rust-toolchain@stable 110 | with: 111 | toolchain: stable 112 | target: wasm32-unknown-unknown 113 | components: clippy 114 | 115 | - name: Rust Cache 116 | uses: Swatinem/rust-cache@v2 117 | 118 | - name: Run cargo clippy 119 | run: RUSTFLAGS='--cfg=web_sys_unstable_apis --cfg=getrandom_backend="wasm_js"' cargo clippy > 120 | --all-targets 121 | --target wasm32-unknown-unknown 122 | -p matchbox_socket 123 | -p bevy_matchbox 124 | -p bevy_ggrs_example 125 | -p simple_example 126 | -p custom_signaller 127 | -- 128 | -D warnings 129 | 130 | server-container: 131 | name: Build & Push Server Container 132 | needs: [test-native, check-wasm, lint-native, lint-wasm] 133 | runs-on: ubuntu-latest 134 | permissions: 135 | packages: write 136 | steps: 137 | - name: Checkout Repository 138 | uses: actions/checkout@v4 139 | 140 | - name: Generate Image Name 141 | run: echo IMAGE_REPOSITORY=ghcr.io/$(tr '[:upper:]' '[:lower:]' <<< "${{ github.repository }}")/matchbox_server >> $GITHUB_ENV 142 | 143 | - name: Log in to GitHub Docker Registry 144 | if: github.event_name != 'pull_request' 145 | uses: docker/login-action@v3 146 | with: 147 | registry: ghcr.io 148 | username: ${{ github.repository_owner }} 149 | password: ${{ secrets.GITHUB_TOKEN }} 150 | 151 | - name: Docker Metadata 152 | id: meta 153 | uses: docker/metadata-action@v5 154 | with: 155 | images: ${{ env.IMAGE_REPOSITORY }} 156 | tags: | 157 | type=ref,event=tag 158 | type=raw,value=latest 159 | 160 | - name: Set up Docker Buildx 161 | uses: docker/setup-buildx-action@v3 162 | 163 | - name: Build Image 164 | uses: docker/build-push-action@v6 165 | with: 166 | context: "." 167 | file: "matchbox_server/Dockerfile" 168 | push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} 169 | load: ${{ ! (github.event_name == 'push' && startsWith(github.ref, 'refs/tags')) }} 170 | tags: ${{ steps.meta.outputs.tags }} 171 | labels: ${{ steps.meta.outputs.labels }} 172 | cache-from: type=gha 173 | cache-to: type=gha,mode=max 174 | -------------------------------------------------------------------------------- /.github/workflows/lint-toml.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | name: Lint TOML 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout sources 14 | uses: actions/checkout@v4 15 | 16 | - name: Install stable toolchain 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | profile: minimal 20 | toolchain: stable 21 | override: true 22 | 23 | - name: Rust Cache 24 | uses: Swatinem/rust-cache@v2.7.3 25 | 26 | - name: Install Taplo 27 | uses: actions-rs/cargo@v1 28 | with: 29 | command: install 30 | args: taplo-cli --locked 31 | 32 | - name: Lint 33 | run: | 34 | taplo check --default-schema-catalogs 35 | taplo fmt --check --diff 36 | -------------------------------------------------------------------------------- /.github/workflows/markdown-lint.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | name: Markdown Lint 8 | 9 | jobs: 10 | markdownlint: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 30 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | # full git history is needed to get a proper list of changed files within `super-linter` 17 | fetch-depth: 0 18 | - name: Run Markdown Lint 19 | uses: docker://ghcr.io/github/super-linter:slim-v4 20 | env: 21 | MULTI_STATUS: false 22 | VALIDATE_ALL_CODEBASE: false 23 | VALIDATE_MARKDOWN: true 24 | DEFAULT_BRANCH: main 25 | -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | name: Website 7 | 8 | jobs: 9 | build: 10 | name: Build Examples 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout sources 14 | uses: actions/checkout@v4 15 | 16 | - name: Install stable toolchain 17 | uses: dtolnay/rust-toolchain@stable 18 | with: 19 | toolchain: stable 20 | target: wasm32-unknown-unknown 21 | 22 | #- name: Rust Cache 23 | # uses: Swatinem/rust-cache@v2.5.0 24 | 25 | - name: Install bevy_wasm_pack 26 | run: cargo install --git https://github.com/johanhelsing/bevy_wasm_pack 27 | 28 | - name: Build bevy_ggrs example 29 | run: bevy_wasm_pack dist bevy_ggrs_example --dir-name bevy_ggrs 30 | 31 | - name: Copy index.html 32 | run: cp website/index.html dist/index.html 33 | 34 | - name: Copy assets 35 | run: cp -r examples/bevy_ggrs/assets dist/assets 36 | 37 | - name: Upload GitHub Pages artifact 38 | uses: actions/upload-pages-artifact@v3.0.1 39 | with: 40 | path: dist 41 | 42 | deploy: 43 | name: Deploy Pages 44 | needs: 45 | - build 46 | permissions: 47 | pages: write 48 | id-token: write 49 | environment: 50 | name: github-pages 51 | url: ${{ steps.deployment.outputs.page_url }} 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: Deploy GitHub Pages site 55 | id: deployment 56 | uses: actions/deploy-pages@v4.0.5 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | crates/*/target 3 | **/*.rs.bk 4 | .cargo/config 5 | /.idea 6 | /.vscode 7 | /benches/target 8 | *.code-workspace 9 | .DS_Store -------------------------------------------------------------------------------- /.markdownlint.yml: -------------------------------------------------------------------------------- 1 | { 2 | "MD026": false, # Trailing punctuation in heading 3 | "MD013": false, # Line length 4 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "bevy_matchbox", 4 | "matchbox_protocol", 5 | "matchbox_socket", 6 | "matchbox_server", 7 | "matchbox_signaling", 8 | "examples/*", 9 | ] 10 | resolver = "2" # Important! Bevy/WGPU needs this! 11 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![Matchbox](https://raw.githubusercontent.com/johanhelsing/matchbox/main/images/matchbox_logo.png)](https://github.com/johanhelsing/matchbox) 2 | 3 | [![crates.io](https://img.shields.io/crates/v/matchbox_socket.svg)](https://crates.io/crates/matchbox_socket) 4 | ![MIT/Apache 2.0](https://img.shields.io/badge/license-MIT%2FApache-blue.svg) 5 | [![crates.io](https://img.shields.io/crates/d/matchbox_socket.svg)](https://crates.io/crates/matchbox_socket) 6 | [![docs.rs](https://img.shields.io/docsrs/matchbox_socket)](https://docs.rs/matchbox_socket) 7 | 8 | Painless peer-to-peer WebRTC networking for rust's native and wasm applications. 9 | 10 | The goal of the Matchbox project is to enable udp-like, unordered, unreliable p2p connections in web browsers or native to facilitate low-latency multiplayer games. 11 | 12 | Matchbox supports both unreliable and reliable data channels, with configurable ordering guarantees and variable packet retransmits. 13 | 14 | - [Tutorial for usage with Bevy and GGRS](https://johanhelsing.studio/posts/extreme-bevy) 15 | 16 | The Matchbox project contains: 17 | 18 | - [matchbox_socket](https://github.com/johanhelsing/matchbox/tree/main/matchbox_socket): A socket abstraction for Wasm or Native, with: 19 | - `ggrs`: A feature providing a [ggrs](https://github.com/gschup/ggrs) compatible socket. 20 | - [matchbox_signaling](https://github.com/johanhelsing/matchbox/tree/main/matchbox_signaling): A signaling server library, with ready to use examples 21 | - [matchbox_server](https://github.com/johanhelsing/matchbox/tree/main/matchbox_server): A ready to use full-mesh signalling server 22 | - [bevy_matchbox](https://github.com/johanhelsing/matchbox/tree/main/bevy_matchbox): A `matchbox_socket` integration for the [Bevy](https://bevyengine.org/) game engine 23 | 24 | | bevy | bevy_matchbox | 25 | | ----- | ------------- | 26 | | 0.16 | 0.12, main | 27 | | 0.15 | 0.11 | 28 | | 0.14 | 0.10 | 29 | | 0.13 | 0.9 | 30 | | 0.12 | 0.8 | 31 | | 0.11 | 0.7 | 32 | | 0.10 | 0.6 | 33 | | < 0.9 | Unsupported | 34 | 35 | ## Examples 36 | 37 | - [simple](examples/simple): A simple communication loop using matchbox_socket 38 | - [bevy_ggrs](examples/bevy_ggrs): An example browser game, using `bevy` and `bevy_ggrs` 39 | - Live 2-player demo: 40 | - Live 4-player demo: 41 | 42 | ## How it works 43 | 44 | ![Connection](https://raw.githubusercontent.com/johanhelsing/matchbox/main/images/connection.excalidraw.svg) 45 | 46 | WebRTC allows direct connections between peers, but in order to establish those connections, some kind of signaling service is needed. `matchbox_server` is such a service. Once the connections are established, however, data will flow directly between peers, and no traffic will go through the signaling server. 47 | 48 | The signaling service needs to run somewhere all clients can reach it over http or https connections. In production, this usually means the public internet. 49 | 50 | When a client wants to join a p2p (mesh) network, it connects to the signaling service. The signaling server then notifies the peers that have already connected about the new peer (sends a `NewPeer` event). 51 | 52 | Peers then negotiate a connection through the signaling server. The initiator sends an "offer" and the recipient responds with an "answer." Once peers have enough information relayed, a [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) is established for each peer, which comes with one or more data channels. 53 | 54 | All of this, however, is hidden from rust application code. All you will need to do on the client side, is: 55 | 56 | - Create a new socket, and give it a signaling server url 57 | - `.await` the message loop future that processes new messages. 58 | - If you are using [Bevy](https://bevyengine.org), this is done automatically by `bevy_matchbox` (see the [`bevy_ggrs`](examples/bevy_ggrs/) example). 59 | - Otherwise, if you are using WASM, `wasm-bindgen-futures` can help (see the [`simple`](examples/simple/) example). 60 | - Alternatively, the future can be polled manually, i.e. once per frame. 61 | 62 | You can hook into the lifecycle of your socket through the socket's API, such as connection state changes. Similarly, you can send packets to peers using the socket through a simple, non-blocking method. 63 | 64 | ## Showcase 65 | 66 | Projects using Matchbox: 67 | 68 | - [NES Bundler](https://github.com/tedsteen/nes-bundler) - Transform your NES game into a single executable targeting your favorite OS! 69 | - [Cargo Space](https://helsing.studio/cargospace) (in development) - A coop 2D space game about building and flying a ship together 70 | - [Extreme Bevy](https://helsing.studio/extreme) - Simple 2-player arcade shooter 71 | - [Matchbox demo](https://helsing.studio/box_game/) 72 | - [A Janitors Nightmare](https://gorktheork.itch.io/bevy-jam-1-submission) - 2-player jam game 73 | - [Lavagna](https://github.com/alepez/lavagna) - collaborative blackboard for online meetings 74 | 75 | ## Contributing 76 | 77 | PRs welcome! 78 | 79 | If you have questions or suggestions, feel free to make an [issue](https://github.com/johanhelsing/matchbox/issues). There's also a [Discord channel](https://discord.gg/ye9UDNvqQD) if you want to get in touch. 80 | 81 | ## Thanks 82 | 83 | - A huge thanks to Ernest Wong for his [Dango Tribute experiment](https://github.com/ErnWong/dango-tribute)! `matchbox_socket` is heavily inspired its wasm-bindgen server_socket and Matchbox would probably not exist without it. 84 | 85 | ## License 86 | 87 | All code in this repository dual-licensed under either: 88 | 89 | - [MIT License](LICENSE-MIT) or 90 | - [Apache License, Version 2.0](LICENSE-APACHE) or 91 | 92 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 93 | -------------------------------------------------------------------------------- /bevy_matchbox/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_matchbox" 3 | version = "0.12.0" 4 | authors = [ 5 | "Johan Helsing ", 6 | "Garry O'Donnell ) { 28 | let peers: Vec<_> = socket.connected_peers().collect(); 29 | 30 | for peer in peers { 31 | let message = "Hello"; 32 | info!("Sending message: {message:?} to {peer}"); 33 | socket 34 | .channel_mut(CHANNEL_ID) 35 | .send(message.as_bytes().into(), peer); 36 | } 37 | } 38 | 39 | fn receive_messages(mut socket: ResMut) { 40 | for (peer, state) in socket.update_peers() { 41 | info!("{peer}: {state:?}"); 42 | } 43 | 44 | for (_id, message) in socket.channel_mut(CHANNEL_ID).receive() { 45 | match std::str::from_utf8(&message) { 46 | Ok(message) => info!("Received message: {message:?}"), 47 | Err(e) => error!("Failed to convert message to string: {e}"), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /bevy_matchbox/examples/hello_host.rs: -------------------------------------------------------------------------------- 1 | //! Runs both signaling with server/client topology and runs the host in the same process 2 | //! 3 | //! Sends messages periodically to all connected clients. 4 | //! 5 | //! Note: When building a signaling server make sure you depend on 6 | //! `bevy_matchbox` with the `signaling` feature enabled. 7 | //! 8 | //! ```toml 9 | //! bevy_matchbox = { version = "0.x", features = ["signaling"] } 10 | //! ``` 11 | 12 | use bevy::{ 13 | app::ScheduleRunnerPlugin, log::LogPlugin, prelude::*, time::common_conditions::on_timer, 14 | }; 15 | use bevy_matchbox::{matchbox_signaling::SignalingServer, prelude::*}; 16 | use core::time::Duration; 17 | use std::net::{Ipv4Addr, SocketAddrV4}; 18 | 19 | fn main() { 20 | App::new() 21 | // .add_plugins(DefaultPlugins) 22 | .add_plugins(( 23 | MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f32( 24 | 1. / 120., // be nice to the CPU 25 | ))), 26 | LogPlugin::default(), 27 | )) 28 | .add_systems(Startup, (start_signaling_server, start_host_socket).chain()) 29 | .add_systems(Update, receive_messages) 30 | .add_systems( 31 | Update, 32 | send_message.run_if(on_timer(Duration::from_secs(5))), 33 | ) 34 | .run(); 35 | } 36 | 37 | fn start_signaling_server(mut commands: Commands) { 38 | info!("Starting signaling server"); 39 | let addr = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 3536); 40 | let signaling_server = MatchboxServer::from( 41 | SignalingServer::client_server_builder(addr) 42 | .on_connection_request(|connection| { 43 | info!("Connecting: {connection:?}"); 44 | Ok(true) // Allow all connections 45 | }) 46 | .on_id_assignment(|(socket, id)| info!("{socket} received {id}")) 47 | .on_host_connected(|id| info!("Host joined: {id}")) 48 | .on_host_disconnected(|id| info!("Host left: {id}")) 49 | .on_client_connected(|id| info!("Client joined: {id}")) 50 | .on_client_disconnected(|id| info!("Client left: {id}")) 51 | .cors() 52 | .trace() 53 | .build(), 54 | ); 55 | commands.insert_resource(signaling_server); 56 | } 57 | 58 | fn start_host_socket(mut commands: Commands) { 59 | let socket = MatchboxSocket::new_reliable("ws://localhost:3536/hello"); 60 | commands.insert_resource(socket); 61 | } 62 | 63 | fn send_message(mut socket: ResMut) { 64 | let peers: Vec<_> = socket.connected_peers().collect(); 65 | 66 | for peer in peers { 67 | let message = "Hello, I'm the host"; 68 | info!("Sending message: {message:?} to {peer}"); 69 | socket.channel_mut(0).send(message.as_bytes().into(), peer); 70 | } 71 | } 72 | 73 | fn receive_messages(mut socket: ResMut) { 74 | for (peer, state) in socket.update_peers() { 75 | info!("{peer}: {state:?}"); 76 | } 77 | 78 | for (_id, message) in socket.channel_mut(0).receive() { 79 | match std::str::from_utf8(&message) { 80 | Ok(message) => info!("Received message: {message:?}"), 81 | Err(e) => error!("Failed to convert message to string: {e}"), 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /bevy_matchbox/examples/hello_signaling.rs: -------------------------------------------------------------------------------- 1 | //! Runs a signaling server with server/client topology as a headless bevy 2 | //! application. 3 | //! 4 | //! Note: When building a signaling server make sure you depend on 5 | //! `bevy_matchbox` with the `signaling` feature enabled. 6 | //! 7 | //! ```toml 8 | //! bevy_matchbox = { version = "0.x", features = ["signaling"] } 9 | //! ``` 10 | 11 | use bevy::{app::ScheduleRunnerPlugin, log::LogPlugin, prelude::*}; 12 | use bevy_matchbox::{matchbox_signaling::SignalingServer, prelude::*}; 13 | use core::time::Duration; 14 | use std::net::{Ipv4Addr, SocketAddrV4}; 15 | 16 | fn main() { 17 | App::new() 18 | .add_plugins(( 19 | MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f32( 20 | 1. / 120., // be nice to the CPU 21 | ))), 22 | LogPlugin::default(), 23 | )) 24 | .add_systems(Startup, (start_signaling_server).chain()) 25 | .run(); 26 | } 27 | 28 | fn start_signaling_server(mut commands: Commands) { 29 | info!("Starting signaling server"); 30 | let addr = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 3536); 31 | let signaling_server = MatchboxServer::from( 32 | SignalingServer::client_server_builder(addr) 33 | .on_connection_request(|connection| { 34 | info!("Connecting: {connection:?}"); 35 | Ok(true) // Allow all connections 36 | }) 37 | .on_id_assignment(|(socket, id)| info!("{socket} received {id}")) 38 | .on_host_connected(|id| info!("Host joined: {id}")) 39 | .on_host_disconnected(|id| info!("Host left: {id}")) 40 | .on_client_connected(|id| info!("Client joined: {id}")) 41 | .on_client_disconnected(|id| info!("Client left: {id}")) 42 | .cors() 43 | .trace() 44 | .build(), 45 | ); 46 | commands.insert_resource(signaling_server); 47 | } 48 | -------------------------------------------------------------------------------- /bevy_matchbox/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | #![doc = include_str!("../README.md")] 3 | #![forbid(unsafe_code)] 4 | 5 | use cfg_if::cfg_if; 6 | 7 | mod socket; 8 | pub use socket::*; 9 | 10 | cfg_if! { 11 | if #[cfg(all(not(target_arch = "wasm32"), feature = "signaling"))] { 12 | mod signaling; 13 | pub use signaling::*; 14 | } 15 | } 16 | 17 | /// use `bevy_matchbox::prelude::*;` to import common resources and commands 18 | pub mod prelude { 19 | pub use crate::{CloseSocketExt, MatchboxSocket, OpenSocketExt}; 20 | use cfg_if::cfg_if; 21 | pub use matchbox_socket::{ChannelConfig, PeerId, PeerState, WebRtcSocketBuilder}; 22 | 23 | cfg_if! { 24 | if #[cfg(all(not(target_arch = "wasm32"), feature = "signaling"))] { 25 | pub use crate::signaling::{MatchboxServer, StartServerExt, StopServerExt}; 26 | pub use matchbox_signaling::SignalingServerBuilder; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /bevy_matchbox/src/signaling.rs: -------------------------------------------------------------------------------- 1 | use async_compat::CompatExt; 2 | use bevy::{ 3 | prelude::{Command, Commands, Resource}, 4 | tasks::{IoTaskPool, Task}, 5 | }; 6 | pub use matchbox_signaling; 7 | use matchbox_signaling::{ 8 | Error, SignalingCallbacks, SignalingServer, SignalingServerBuilder, SignalingState, 9 | topologies::{ 10 | SignalingTopology, 11 | client_server::{ClientServer, ClientServerCallbacks, ClientServerState}, 12 | full_mesh::{FullMesh, FullMeshCallbacks, FullMeshState}, 13 | }, 14 | }; 15 | use std::net::SocketAddr; 16 | 17 | /// A [`SignalingServer`] as a [`Resource`]. 18 | /// 19 | /// As a [`Resource`], with [`Commands`] 20 | /// ``` 21 | /// use std::net::Ipv4Addr; 22 | /// use bevy_matchbox::{ 23 | /// prelude::*, 24 | /// matchbox_signaling::topologies::full_mesh::{FullMesh, FullMeshState} 25 | /// }; 26 | /// use bevy::prelude::*; 27 | /// 28 | /// fn start_server_system(mut commands: Commands) { 29 | /// let builder = SignalingServerBuilder::new( 30 | /// (Ipv4Addr::UNSPECIFIED, 3536), 31 | /// FullMesh, 32 | /// FullMeshState::default(), 33 | /// ); 34 | /// commands.start_server(builder); 35 | /// } 36 | /// 37 | /// fn stop_server_system(mut commands: Commands) { 38 | /// commands.stop_server(); 39 | /// } 40 | /// ``` 41 | /// 42 | /// As a [`Resource`], directly 43 | /// ``` 44 | /// use std::net::Ipv4Addr; 45 | /// use bevy_matchbox::{ 46 | /// prelude::*, 47 | /// matchbox_signaling::topologies::full_mesh::{FullMesh, FullMeshState} 48 | /// }; 49 | /// use bevy::prelude::*; 50 | /// 51 | /// fn start_server_system(mut commands: Commands) { 52 | /// let server: MatchboxServer = SignalingServerBuilder::new( 53 | /// (Ipv4Addr::UNSPECIFIED, 3536), 54 | /// FullMesh, 55 | /// FullMeshState::default(), 56 | /// ).into(); 57 | /// 58 | /// commands.insert_resource(MatchboxServer::from(server)); 59 | /// } 60 | /// 61 | /// fn stop_server_system(mut commands: Commands) { 62 | /// commands.remove_resource::(); 63 | /// } 64 | /// ``` 65 | #[derive(Debug, Resource)] 66 | #[allow(dead_code)] // we take ownership of the task to not drop it 67 | pub struct MatchboxServer(Task>); 68 | 69 | impl From> for MatchboxServer 70 | where 71 | Topology: SignalingTopology, 72 | Cb: SignalingCallbacks, 73 | S: SignalingState, 74 | { 75 | fn from(value: SignalingServerBuilder) -> Self { 76 | MatchboxServer::from(value.build()) 77 | } 78 | } 79 | 80 | impl From for MatchboxServer { 81 | fn from(server: SignalingServer) -> Self { 82 | let task_pool = IoTaskPool::get(); 83 | let task = task_pool.spawn(server.serve().compat()); 84 | MatchboxServer(task) 85 | } 86 | } 87 | 88 | struct StartServer(SignalingServerBuilder) 89 | where 90 | Topology: SignalingTopology, 91 | Cb: SignalingCallbacks, 92 | S: SignalingState; 93 | 94 | impl Command for StartServer 95 | where 96 | Topology: SignalingTopology + Send + 'static, 97 | Cb: SignalingCallbacks, 98 | S: SignalingState, 99 | { 100 | fn apply(self, world: &mut bevy::prelude::World) { 101 | world.insert_resource(MatchboxServer::from(self.0)) 102 | } 103 | } 104 | 105 | /// A [`Commands`] extension used to start a [`MatchboxServer`]. 106 | pub trait StartServerExt< 107 | Topology: SignalingTopology, 108 | Cb: SignalingCallbacks, 109 | S: SignalingState, 110 | > 111 | { 112 | /// Starts a [`MatchboxServer`] and allocates it as a resource. 113 | fn start_server(&mut self, builder: SignalingServerBuilder); 114 | } 115 | 116 | impl StartServerExt for Commands<'_, '_> 117 | where 118 | Topology: SignalingTopology + Send + 'static, 119 | Cb: SignalingCallbacks, 120 | S: SignalingState, 121 | { 122 | fn start_server(&mut self, builder: SignalingServerBuilder) { 123 | self.queue(StartServer(builder)) 124 | } 125 | } 126 | 127 | struct StopServer; 128 | 129 | impl Command for StopServer { 130 | fn apply(self, world: &mut bevy::prelude::World) { 131 | world.remove_resource::(); 132 | } 133 | } 134 | 135 | /// A [`Commands`] extension used to stop a [`MatchboxServer`]. 136 | pub trait StopServerExt { 137 | /// Delete the [`MatchboxServer`] resource. 138 | fn stop_server(&mut self); 139 | } 140 | 141 | impl StopServerExt for Commands<'_, '_> { 142 | fn stop_server(&mut self) { 143 | self.queue(StopServer) 144 | } 145 | } 146 | 147 | impl MatchboxServer { 148 | /// Creates a new builder for a [`SignalingServer`] with full-mesh topology. 149 | pub fn full_mesh_builder( 150 | socket_addr: impl Into, 151 | ) -> SignalingServerBuilder { 152 | SignalingServer::full_mesh_builder(socket_addr) 153 | } 154 | 155 | /// Creates a new builder for a [`SignalingServer`] with client-server topology. 156 | pub fn client_server_builder( 157 | socket_addr: impl Into, 158 | ) -> SignalingServerBuilder { 159 | SignalingServer::client_server_builder(socket_addr) 160 | } 161 | } 162 | 163 | #[cfg(test)] 164 | mod tests { 165 | use crate::{ 166 | matchbox_signaling::topologies::client_server::{ClientServer, ClientServerState}, 167 | prelude::*, 168 | }; 169 | use bevy::prelude::*; 170 | use std::net::Ipv4Addr; 171 | 172 | fn start_signaling(mut commands: Commands) { 173 | let server: MatchboxServer = SignalingServerBuilder::new( 174 | (Ipv4Addr::UNSPECIFIED, 3536), 175 | ClientServer, 176 | ClientServerState::default(), 177 | ) 178 | .into(); 179 | 180 | commands.insert_resource(server); 181 | } 182 | 183 | #[test] 184 | // https://github.com/johanhelsing/matchbox/issues/350 185 | fn start_signaling_without_panics() { 186 | let mut app = App::new(); 187 | 188 | app.add_plugins(MinimalPlugins) 189 | .add_systems(Startup, start_signaling); 190 | 191 | app.update(); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /bevy_matchbox/src/socket.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | prelude::{Command, Commands, Component, Resource, World}, 3 | tasks::IoTaskPool, 4 | }; 5 | pub use matchbox_socket; 6 | use matchbox_socket::{MessageLoopFuture, WebRtcSocket, WebRtcSocketBuilder}; 7 | use std::{ 8 | fmt::Debug, 9 | ops::{Deref, DerefMut}, 10 | }; 11 | 12 | /// A [`WebRtcSocket`] as a [`Component`] or [`Resource`]. 13 | /// 14 | /// As a [`Component`], directly 15 | /// ``` 16 | /// use bevy_matchbox::prelude::*; 17 | /// use bevy::prelude::*; 18 | /// 19 | /// fn open_socket_system(mut commands: Commands) { 20 | /// let room_url = "wss://matchbox.example.com"; 21 | /// let builder = WebRtcSocketBuilder::new(room_url).add_channel(ChannelConfig::reliable()); 22 | /// commands.spawn(MatchboxSocket::from(builder)); 23 | /// } 24 | /// 25 | /// fn close_socket_system( 26 | /// mut commands: Commands, 27 | /// socket: Query> 28 | /// ) { 29 | /// let socket = socket.single(); 30 | /// commands.entity(socket).despawn(); 31 | /// } 32 | /// ``` 33 | /// 34 | /// As a [`Resource`], with [`Commands`] 35 | /// ``` 36 | /// use bevy_matchbox::prelude::*; 37 | /// use bevy::prelude::*; 38 | /// 39 | /// fn open_socket_system(mut commands: Commands) { 40 | /// let room_url = "wss://matchbox.example.com"; 41 | /// commands.open_socket(WebRtcSocketBuilder::new(room_url).add_channel(ChannelConfig::reliable())); 42 | /// } 43 | /// 44 | /// fn close_socket_system(mut commands: Commands) { 45 | /// commands.close_socket(); 46 | /// } 47 | /// ``` 48 | /// 49 | /// As a [`Resource`], directly 50 | /// ``` 51 | /// use bevy_matchbox::prelude::*; 52 | /// use bevy::prelude::*; 53 | /// 54 | /// fn open_socket_system(mut commands: Commands) { 55 | /// let room_url = "wss://matchbox.example.com"; 56 | /// 57 | /// let socket: MatchboxSocket = WebRtcSocketBuilder::new(room_url) 58 | /// .add_channel(ChannelConfig::reliable()) 59 | /// .into(); 60 | /// 61 | /// commands.insert_resource(socket); 62 | /// } 63 | /// 64 | /// fn close_socket_system(mut commands: Commands) { 65 | /// commands.remove_resource::(); 66 | /// } 67 | /// ``` 68 | #[derive(Resource, Component, Debug)] 69 | #[allow(dead_code)] // keep the task alive so it doesn't drop before the socket 70 | pub struct MatchboxSocket(WebRtcSocket, Box); 71 | 72 | impl Deref for MatchboxSocket { 73 | type Target = WebRtcSocket; 74 | 75 | fn deref(&self) -> &Self::Target { 76 | &self.0 77 | } 78 | } 79 | 80 | impl DerefMut for MatchboxSocket { 81 | fn deref_mut(&mut self) -> &mut Self::Target { 82 | &mut self.0 83 | } 84 | } 85 | 86 | impl From for MatchboxSocket { 87 | fn from(builder: WebRtcSocketBuilder) -> Self { 88 | Self::from(builder.build()) 89 | } 90 | } 91 | 92 | impl From<(WebRtcSocket, MessageLoopFuture)> for MatchboxSocket { 93 | fn from((socket, message_loop_fut): (WebRtcSocket, MessageLoopFuture)) -> Self { 94 | let task_pool = IoTaskPool::get(); 95 | let task = task_pool.spawn(message_loop_fut); 96 | MatchboxSocket(socket, Box::new(task)) 97 | } 98 | } 99 | 100 | /// A [`Command`] used to open a [`MatchboxSocket`] and allocate it as a resource. 101 | struct OpenSocket(WebRtcSocketBuilder); 102 | 103 | impl Command for OpenSocket { 104 | fn apply(self, world: &mut World) { 105 | world.insert_resource(MatchboxSocket::from(self.0)); 106 | } 107 | } 108 | 109 | /// A [`Commands`] extension used to open a [`MatchboxSocket`] and allocate it as a resource. 110 | pub trait OpenSocketExt { 111 | /// Opens a [`MatchboxSocket`] and allocates it as a resource. 112 | fn open_socket(&mut self, socket_builder: WebRtcSocketBuilder); 113 | } 114 | 115 | impl OpenSocketExt for Commands<'_, '_> { 116 | fn open_socket(&mut self, socket_builder: WebRtcSocketBuilder) { 117 | self.queue(OpenSocket(socket_builder)) 118 | } 119 | } 120 | 121 | /// A [`Command`] used to close a [`WebRtcSocket`], deleting the [`MatchboxSocket`] resource. 122 | struct CloseSocket; 123 | 124 | impl Command for CloseSocket { 125 | fn apply(self, world: &mut World) { 126 | world.remove_resource::(); 127 | } 128 | } 129 | 130 | /// A [`Commands`] extension used to close a [`WebRtcSocket`], deleting the [`MatchboxSocket`] 131 | /// resource. 132 | pub trait CloseSocketExt { 133 | /// Delete the [`MatchboxSocket`] resource. 134 | fn close_socket(&mut self); 135 | } 136 | 137 | impl CloseSocketExt for Commands<'_, '_> { 138 | fn close_socket(&mut self) { 139 | self.queue(CloseSocket) 140 | } 141 | } 142 | 143 | impl MatchboxSocket { 144 | /// Create a new socket with a single unreliable channel 145 | /// 146 | /// ```rust 147 | /// use bevy_matchbox::prelude::*; 148 | /// use bevy::prelude::*; 149 | /// 150 | /// fn open_channel_system(mut commands: Commands) { 151 | /// let room_url = "wss://matchbox.example.com"; 152 | /// let socket = MatchboxSocket::new_unreliable(room_url); 153 | /// commands.spawn(socket); 154 | /// } 155 | /// ``` 156 | pub fn new_unreliable(room_url: impl Into) -> MatchboxSocket { 157 | Self::from(WebRtcSocket::new_unreliable(room_url)) 158 | } 159 | 160 | /// Create a new socket with a single reliable channel 161 | /// 162 | /// ```rust 163 | /// use bevy_matchbox::prelude::*; 164 | /// use bevy::prelude::*; 165 | /// 166 | /// fn open_channel_system(mut commands: Commands) { 167 | /// let room_url = "wss://matchbox.example.com"; 168 | /// let socket = MatchboxSocket::new_reliable(room_url); 169 | /// commands.spawn(socket); 170 | /// } 171 | /// ``` 172 | pub fn new_reliable(room_url: impl Into) -> MatchboxSocket { 173 | Self::from(WebRtcSocket::new_reliable(room_url)) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /examples/async_example/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | runner = "wasm-server-runner" 3 | -------------------------------------------------------------------------------- /examples/async_example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async_example" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | matchbox_socket = { path = "../../matchbox_socket" } 8 | 9 | futures-timer = { version = "3", features = ["wasm-bindgen"] } 10 | n0-future = { version = "0.1.2", features = [] } 11 | uuid = { version = "1.16", features = ["v4", "rng-getrandom"] } 12 | anyhow = "1.0" 13 | serde = { version = "1.0", features = ["derive"] } 14 | serde_json = "1.0" 15 | web-time = "1.1.0" 16 | js-sys = "0.3.77" 17 | tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } 18 | tracing = { version = "0.1" } 19 | async-broadcast = "0.7" 20 | log = "0.4" 21 | 22 | [target.'cfg(target_arch = "wasm32")'.dependencies] 23 | console_error_panic_hook = "0.1.7" 24 | console_log = "1.0" 25 | futures = { version = "0.3", default-features = false } 26 | wasm-bindgen-futures = "0.4.29" 27 | tokio = { version = "1.32", default-features = false, features = [ 28 | "time", 29 | "sync", 30 | ] } 31 | getrandom = { version = "0.3", features = ["wasm_js"] } 32 | tracing-wasm = "0.2.1" 33 | wasm-bindgen = "0.2" 34 | web-sys = { version = "0.3", features = ["Window", "Location"] } 35 | 36 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 37 | futures = "0.3" 38 | tokio = "1.32" 39 | -------------------------------------------------------------------------------- /examples/async_example/src/main.rs: -------------------------------------------------------------------------------- 1 | use futures::{FutureExt, SinkExt, StreamExt}; 2 | use matchbox_socket::{Packet, PeerId, PeerState, WebRtcSocket}; 3 | use n0_future::task::{AbortOnDropHandle, spawn}; 4 | use std::{collections::BTreeMap, sync::Arc, time::Duration}; 5 | use tokio::sync::{ 6 | RwLock, 7 | mpsc::{Receiver, Sender, channel}, 8 | }; 9 | use tracing::{info, warn}; 10 | 11 | const CHANNEL_ID: usize = 0; 12 | 13 | fn get_timestamp() -> u128 { 14 | web_time::SystemTime::now() 15 | .duration_since(web_time::UNIX_EPOCH) 16 | .unwrap() 17 | .as_micros() 18 | } 19 | 20 | #[cfg(target_arch = "wasm32")] 21 | fn main() { 22 | // Setup logging 23 | console_error_panic_hook::set_once(); 24 | console_log::init_with_level(log::Level::Debug).unwrap(); 25 | 26 | wasm_bindgen_futures::spawn_local(async_main()); 27 | } 28 | 29 | #[cfg(not(target_arch = "wasm32"))] 30 | #[tokio::main] 31 | async fn main() { 32 | // Setup logging 33 | use tracing_subscriber::prelude::*; 34 | tracing_subscriber::registry() 35 | .with( 36 | tracing_subscriber::EnvFilter::try_from_default_env() 37 | .unwrap_or_else(|_| "async_example=info,matchbox_socket=info".into()), 38 | ) 39 | .with(tracing_subscriber::fmt::layer()) 40 | .init(); 41 | 42 | async_main().await 43 | } 44 | 45 | async fn async_main() { 46 | let (mut socket, loop_fut) = WebRtcSocket::new_reliable("ws://localhost:3536/"); 47 | 48 | let loop_fut = loop_fut.fuse(); 49 | futures::pin_mut!(loop_fut); 50 | 51 | let (tx0, rx0) = socket.take_channel(CHANNEL_ID).unwrap().split(); 52 | 53 | #[allow(clippy::type_complexity)] 54 | let tasks: Arc< 55 | RwLock< 56 | BTreeMap< 57 | PeerId, 58 | ( 59 | (AbortOnDropHandle<()>, AbortOnDropHandle<()>), 60 | Sender>, 61 | ), 62 | >, 63 | >, 64 | > = Arc::new(RwLock::new(BTreeMap::new())); 65 | let tasks_ = tasks.clone(); 66 | let _task_rx_route = spawn(async move { 67 | futures::pin_mut!(rx0); 68 | while let Some((peer, packet)) = rx0.next().await { 69 | let tx = { 70 | let r = tasks_.read().await; 71 | let Some((_, tx)) = r.get(&peer) else { 72 | warn!("Received packet from unknown peer: {peer}"); 73 | continue; 74 | }; 75 | tx.clone() 76 | }; 77 | tx.send(packet).await.unwrap(); 78 | } 79 | }); 80 | 81 | let tasks_ = tasks.clone(); 82 | let _dispatch_task = AbortOnDropHandle::new(spawn(async move { 83 | // Handle any new peers 84 | while let Some((peer, state)) = socket.next().await { 85 | let mut tx0 = tx0.clone(); 86 | match state { 87 | PeerState::Connected => { 88 | info!("Peer joined: {peer}"); 89 | let (rx_sender, rx) = channel(1); 90 | let (tx, mut tx_recv) = channel(1); 91 | let _task_tx_combine = AbortOnDropHandle::new(spawn(async move { 92 | while let Some(packet) = tx_recv.recv().await { 93 | tx0.send((peer, packet)).await.unwrap(); 94 | } 95 | })); 96 | let task = AbortOnDropHandle::new(spawn(socket_task(peer, tx, rx))); 97 | { 98 | tasks_ 99 | .write() 100 | .await 101 | .insert(peer, ((task, _task_tx_combine), rx_sender)); 102 | } 103 | } 104 | PeerState::Disconnected => { 105 | info!("Peer left: {peer}"); 106 | { 107 | tasks_.write().await.remove(&peer); 108 | } 109 | } 110 | } 111 | } 112 | })); 113 | 114 | let _ = loop_fut.await; 115 | tasks.write().await.clear(); 116 | } 117 | 118 | async fn socket_task(peer: PeerId, tx: Sender, mut rx: Receiver) { 119 | let writer = tx.clone(); 120 | let ping_task = spawn(async move { 121 | for _i in 0..20 { 122 | n0_future::time::sleep(Duration::from_secs_f32(0.25)).await; 123 | if _i == 0 { 124 | writer.send(b"hello friend!".to_vec().into()).await.unwrap(); 125 | } 126 | writer 127 | .send( 128 | format!("ping {}", get_timestamp()) 129 | .as_bytes() 130 | .to_vec() 131 | .into(), 132 | ) 133 | .await 134 | .unwrap(); 135 | } 136 | }); 137 | let writer = tx.clone(); 138 | let pong_task = spawn(async move { 139 | while let Some(packet) = rx.recv().await { 140 | let message = String::from_utf8_lossy(&packet); 141 | if message.starts_with("ping") { 142 | let ts = message.split(" ").nth(1).unwrap().parse::().unwrap(); 143 | let packet = format!("pong {}", ts).as_bytes().to_vec(); 144 | writer.send(packet.into()).await.unwrap(); 145 | } else if message.starts_with("pong") { 146 | let ts = message.split(" ").nth(1).unwrap().parse::().unwrap(); 147 | let now = get_timestamp(); 148 | let diff = now - ts; 149 | info!("Ping from {peer} took {}ms", diff as f32 / 1000.0); 150 | } else { 151 | info!("Message from {peer}: \n\n {message:?} \n"); 152 | } 153 | } 154 | }); 155 | ping_task.await.unwrap(); 156 | pong_task.await.unwrap(); 157 | } 158 | -------------------------------------------------------------------------------- /examples/bevy_ggrs/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | runner = "wasm-server-runner" 3 | rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] 4 | -------------------------------------------------------------------------------- /examples/bevy_ggrs/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | crates/*/target 3 | **/*.rs.bk 4 | .cargo/config 5 | /.idea 6 | /.vscode 7 | /benches/target 8 | /dist 9 | -------------------------------------------------------------------------------- /examples/bevy_ggrs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_ggrs_example" 3 | version = "0.7.0" 4 | authors = ["Johan Helsing "] 5 | description = "A demo game where two web browser can connect and move boxes around" 6 | edition = "2024" 7 | repository = "https://github.com/johanhelsing/matchbox" 8 | keywords = ["gamedev", "webrtc", "peer-to-peer", "networking", "wasm"] 9 | license = "MIT OR Apache-2.0" 10 | 11 | [target.'cfg(target_arch = "wasm32")'.dependencies] 12 | web-sys = { version = "0.3", features = [ 13 | "Document", 14 | "Location", # for getting args from query string 15 | ] } 16 | serde_qs = "0.15" 17 | wasm-bindgen = "0.2" 18 | bevy_ggrs = { version = "0.18", features = ["wasm-bindgen"] } 19 | 20 | [dependencies] 21 | bevy_matchbox = { path = "../../bevy_matchbox", features = ["ggrs"] } 22 | bevy = { version = "0.16", default-features = false, features = [ 23 | "bevy_winit", 24 | "bevy_render", 25 | "bevy_pbr", 26 | "bevy_core_pipeline", 27 | "bevy_ui", 28 | "bevy_text", 29 | "bevy_asset", 30 | "bevy_sprite", 31 | "bevy_state", 32 | "multi_threaded", 33 | "png", 34 | "webgl2", 35 | "tonemapping_luts", 36 | # gh actions runners don't like wayland 37 | "x11", 38 | ] } 39 | bevy_ggrs = "0.18" 40 | clap = { version = "4.3", features = ["derive"] } 41 | serde = "1.0" 42 | -------------------------------------------------------------------------------- /examples/bevy_ggrs/README.md: -------------------------------------------------------------------------------- 1 | # Bevy + GGRS 2 | 3 | Shows how to use `matchbox_socket` with `bevy` and `ggrs` using `bevy_matchbox` and `bevy_ggrs`, to create a simple working browser "game" (if moving cubes around on a plane can be called a game). 4 | 5 | ## Live Demo 6 | 7 | There is a live version here (move the cube with WASD): 8 | 9 | - 2-Player: 10 | - 3-Player: 11 | - N-player: Edit the link above. 12 | 13 | When enough players have joined, you should see a couple of boxes, one of which 14 | you can move around using the `WASD` keys. 15 | 16 | You can open the browser console to get some rough idea about what's happening 17 | (or not happening if that's the unfortunate case). 18 | 19 | ## Instructions 20 | 21 | - Run the matchbox-provided [`matchbox_server`](../../matchbox_server/) ([help](../../matchbox_server/README.md)), or run your own on `ws://localhost:3536/`. 22 | - Run the demo (enough clients must connect before the game stats) 23 | - [on Native](#run-on-native) 24 | - [on WASM](#run-on-wasm) 25 | 26 | ## Run on Native 27 | 28 | ```sh 29 | cargo run -- [--matchbox ws://127.0.0.1:3536] [--players 2] [--room ] 30 | ``` 31 | 32 | ## Run on WASM 33 | 34 | ### Prerequisites 35 | 36 | Install the `wasm32-unknown-unknown` target 37 | 38 | ```sh 39 | rustup target install wasm32-unknown-unknown 40 | ``` 41 | 42 | Install a lightweight web server 43 | 44 | ```sh 45 | cargo install wasm-server-runner 46 | ``` 47 | 48 | ### Serve 49 | 50 | ```sh 51 | cargo run --target wasm32-unknown-unknown 52 | ``` 53 | 54 | ### Run 55 | 56 | - Use a web browser and navigate to 57 | - Open the console to see execution logs 58 | -------------------------------------------------------------------------------- /examples/bevy_ggrs/assets/fonts/quicksand-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johanhelsing/matchbox/b5d8be77ff7f80db18b4c5532e28f6d9d6ad34da/examples/bevy_ggrs/assets/fonts/quicksand-light.ttf -------------------------------------------------------------------------------- /examples/bevy_ggrs/assets/fonts/quicksand-light.txt: -------------------------------------------------------------------------------- 1 | Copyright 2011 The Quicksand Project Authors (https://github.com/andrew-paglinawan/QuicksandFamily), with Reserved Font Name “Quicksand”. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /examples/bevy_ggrs/src/args.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use clap::Parser; 3 | use serde::Deserialize; 4 | use std::ffi::OsString; 5 | 6 | #[derive(Parser, Debug, Clone, Deserialize, Resource)] 7 | #[serde(default)] 8 | #[clap( 9 | name = "box_game_web", 10 | rename_all = "kebab-case", 11 | rename_all_env = "screaming-snake" 12 | )] 13 | pub struct Args { 14 | #[clap(long, default_value = "ws://127.0.0.1:3536")] 15 | pub matchbox: String, 16 | 17 | #[clap(long)] 18 | pub room: Option, 19 | 20 | #[clap(long, short, default_value = "2")] 21 | pub players: usize, 22 | } 23 | 24 | impl Default for Args { 25 | fn default() -> Self { 26 | let args = Vec::::new(); 27 | Args::parse_from(args) 28 | } 29 | } 30 | 31 | impl Args { 32 | pub fn get() -> Self { 33 | #[cfg(target_arch = "wasm32")] 34 | { 35 | let qs = web_sys::window() 36 | .unwrap() 37 | .location() 38 | .search() 39 | .unwrap() 40 | .trim_start_matches('?') 41 | .to_owned(); 42 | 43 | Args::from_query(&qs) 44 | } 45 | #[cfg(not(target_arch = "wasm32"))] 46 | { 47 | Args::parse() 48 | } 49 | } 50 | 51 | // #[allow(dead_code)] 52 | #[cfg(target_arch = "wasm32")] 53 | fn from_query(query: &str) -> Self { 54 | // TODO: result? 55 | serde_qs::from_str(query).unwrap() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/bevy_ggrs/src/box_game.rs: -------------------------------------------------------------------------------- 1 | use bevy::{platform::collections::HashMap, prelude::*}; 2 | use bevy_ggrs::{LocalInputs, LocalPlayers, prelude::*}; 3 | use bevy_matchbox::prelude::PeerId; 4 | use serde::{Deserialize, Serialize}; 5 | use std::hash::Hash; 6 | 7 | const BLUE: Color = Color::srgb(0.8, 0.6, 0.2); 8 | const ORANGE: Color = Color::srgb(0., 0.35, 0.8); 9 | const MAGENTA: Color = Color::srgb(0.9, 0.2, 0.2); 10 | const GREEN: Color = Color::srgb(0.35, 0.7, 0.35); 11 | const PLAYER_COLORS: [Color; 4] = [BLUE, ORANGE, MAGENTA, GREEN]; 12 | 13 | const INPUT_UP: u8 = 1 << 0; 14 | const INPUT_DOWN: u8 = 1 << 1; 15 | const INPUT_LEFT: u8 = 1 << 2; 16 | const INPUT_RIGHT: u8 = 1 << 3; 17 | 18 | const ACCELERATION: f32 = 18.0; 19 | const MAX_SPEED: f32 = 3.0; 20 | const FRICTION: f32 = 0.0018; 21 | const PLANE_SIZE: f32 = 5.0; 22 | const CUBE_SIZE: f32 = 0.2; 23 | 24 | // You need to define a config struct to bundle all the generics of GGRS. bevy_ggrs provides a 25 | // sensible default in `GgrsConfig`. (optional) You can define a type here for brevity. 26 | pub type BoxConfig = GgrsConfig; 27 | 28 | #[repr(C)] 29 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] 30 | pub struct BoxInput { 31 | pub inp: u8, 32 | } 33 | 34 | #[derive(Default, Component)] 35 | pub struct Player { 36 | pub handle: usize, 37 | } 38 | 39 | // Components that should be saved/loaded need to support snapshotting. The built-in options are: 40 | // - Clone (Recommended) 41 | // - Copy 42 | // - Reflect 43 | // See `bevy_ggrs::Strategy` for custom alternatives 44 | #[derive(Default, Reflect, Component, Clone)] 45 | pub struct Velocity { 46 | pub x: f32, 47 | pub y: f32, 48 | pub z: f32, 49 | } 50 | 51 | // You can also register resources. 52 | #[derive(Resource, Default, Reflect, Hash, Clone, Copy)] 53 | #[reflect(Hash)] 54 | pub struct FrameCount { 55 | pub frame: u32, 56 | } 57 | 58 | /// Collects player inputs during [`ReadInputs`](`bevy_ggrs::ReadInputs`) and creates a 59 | /// [`LocalInputs`] resource. 60 | pub fn read_local_inputs( 61 | mut commands: Commands, 62 | keyboard_input: Res>, 63 | local_players: Res, 64 | ) { 65 | let mut local_inputs = HashMap::new(); 66 | 67 | for handle in &local_players.0 { 68 | let mut input: u8 = 0; 69 | 70 | if keyboard_input.pressed(KeyCode::KeyW) { 71 | input |= INPUT_UP; 72 | } 73 | if keyboard_input.pressed(KeyCode::KeyA) { 74 | input |= INPUT_LEFT; 75 | } 76 | if keyboard_input.pressed(KeyCode::KeyS) { 77 | input |= INPUT_DOWN; 78 | } 79 | if keyboard_input.pressed(KeyCode::KeyD) { 80 | input |= INPUT_RIGHT; 81 | } 82 | 83 | local_inputs.insert(*handle, BoxInput { inp: input }); 84 | } 85 | 86 | commands.insert_resource(LocalInputs::(local_inputs)); 87 | } 88 | 89 | pub fn setup_scene( 90 | mut commands: Commands, 91 | mut meshes: ResMut>, 92 | mut materials: ResMut>, 93 | session: Res>, 94 | mut camera_query: Query<&mut Transform, With>, 95 | ) { 96 | let num_players = match &*session { 97 | Session::SyncTest(s) => s.num_players(), 98 | Session::P2P(s) => s.num_players(), 99 | Session::Spectator(s) => s.num_players(), 100 | }; 101 | 102 | // A ground plane 103 | commands.spawn(( 104 | Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(PLANE_SIZE / 2.0)))), 105 | MeshMaterial3d(materials.add(StandardMaterial::from(Color::srgb(0.3, 0.5, 0.3)))), 106 | )); 107 | 108 | let r = PLANE_SIZE / 4.; 109 | let mesh = meshes.add(Mesh::from(Cuboid::from_size(Vec3::splat(CUBE_SIZE)))); 110 | 111 | for handle in 0..num_players { 112 | let rot = handle as f32 / num_players as f32 * 2. * std::f32::consts::PI; 113 | let x = r * rot.cos(); 114 | let z = r * rot.sin(); 115 | 116 | let mut transform = Transform::default(); 117 | transform.translation.x = x; 118 | transform.translation.y = CUBE_SIZE / 2.; 119 | transform.translation.z = z; 120 | let color = PLAYER_COLORS[handle % PLAYER_COLORS.len()]; 121 | 122 | // Entities which will be rolled back can be created just like any other... 123 | commands 124 | .spawn(( 125 | // ...add visual information... 126 | Mesh3d(mesh.clone()), 127 | MeshMaterial3d(materials.add(StandardMaterial::from(color))), 128 | transform, 129 | // ...flags... 130 | Player { handle }, 131 | // ...and components which will be rolled-back... 132 | Velocity::default(), 133 | )) 134 | // ...just ensure you call `add_rollback()` 135 | // This ensures a stable ID is available for the rollback system to refer to 136 | .add_rollback(); 137 | } 138 | 139 | // light 140 | commands.spawn((PointLight::default(), Transform::from_xyz(-4.0, 8.0, 4.0))); 141 | // camera 142 | for mut transform in camera_query.iter_mut() { 143 | *transform = Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y); 144 | } 145 | } 146 | 147 | // Example system, manipulating a resource, will be added to the rollback schedule. 148 | // Increases the frame count by 1 every update step. If loading and saving resources works 149 | // correctly, you should see this resource rolling back, counting back up and finally increasing by 150 | // 1 every update step 151 | #[allow(dead_code)] 152 | pub fn increase_frame_system(mut frame_count: ResMut) { 153 | frame_count.frame += 1; 154 | } 155 | 156 | // Example system that moves the cubes, will be added to the rollback schedule. 157 | // Filtering for the rollback component is a good way to make sure your game logic systems 158 | // only mutate components that are being saved/loaded. 159 | #[allow(dead_code)] 160 | pub fn move_cube_system( 161 | mut query: Query<(&mut Transform, &mut Velocity, &Player), With>, 162 | // ^------^ Added by 163 | // `add_rollback` earlier 164 | inputs: Res>, 165 | // Thanks to RollbackTimePlugin, this is rollback safe 166 | time: Res