├── .dockerignore
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .prettierrc
├── .vscode
└── settings.json
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yml
├── package-lock.json
├── package.json
├── readme
└── phaser-with-nodejs.png
├── src
├── client
│ ├── assets
│ │ ├── box.png
│ │ ├── bug.png
│ │ ├── controls.png
│ │ ├── dude.png
│ │ ├── fullscreen.png
│ │ ├── mummy37x45.png
│ │ ├── star.png
│ │ └── starfield.jpg
│ ├── components
│ │ ├── animations.ts
│ │ ├── controls.ts
│ │ ├── cursors.ts
│ │ ├── fullscreenButton.ts
│ │ ├── fullscreenEvent.ts
│ │ ├── resize.ts
│ │ └── texts.ts
│ ├── config.ts
│ ├── game.ts
│ ├── index.html
│ ├── index.ts
│ └── scenes
│ │ ├── mainScene.ts
│ │ ├── menuScene.ts
│ │ └── preloadScene.ts
├── constants.ts
├── physics
│ ├── game.ts
│ ├── index.html
│ └── index.ts
├── server
│ ├── game
│ │ ├── arcadeObjects
│ │ │ ├── box.ts
│ │ │ ├── dude.ts
│ │ │ ├── map.ts
│ │ │ ├── mummy.ts
│ │ │ └── star.ts
│ │ ├── config.ts
│ │ ├── game.ts
│ │ ├── matterObjects
│ │ │ ├── box.ts
│ │ │ ├── dude.ts
│ │ │ ├── matterGameObject.ts
│ │ │ ├── matterGameObjectGroup.ts
│ │ │ └── star.ts
│ │ └── scenes
│ │ │ ├── arcadeScene.ts
│ │ │ └── matterScene.ts
│ ├── managers
│ │ ├── roomManager.ts
│ │ └── syncManager.ts
│ ├── routes
│ │ └── routes.ts
│ ├── server.ts
│ └── socket
│ │ ├── ioGame.ts
│ │ └── ioStats.ts
└── stats
│ ├── index.html
│ └── index.ts
├── tsconfig.json
├── typings
└── custom.d.ts
└── webpack
├── webpack.client.cjs
├── webpack.client.prod.cjs
├── webpack.physics.cjs
├── webpack.physics.prod.cjs
├── webpack.server.cjs
├── webpack.stats.cjs
└── webpack.stats.prod.cjs
/.dockerignore:
--------------------------------------------------------------------------------
1 | /*
2 | !/dist
3 | !package*.json
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # read: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
2 |
3 | name: CI
4 |
5 | on: [push, pull_request]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 |
11 | strategy:
12 | matrix:
13 | node-version: [16.x, 18.x, 19.x]
14 |
15 | steps:
16 | - name: Checkout repository
17 | uses: actions/checkout@v3
18 |
19 | - name: Use Node.js ${{ matrix.node-version }}
20 | uses: actions/setup-node@v3
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 |
24 | - name: Install Dependencies
25 | run: npm install
26 |
27 | - name: Build Packages
28 | run: npm run build
29 |
30 | # - name: Run Tests
31 | # run: npm test
32 |
33 | # - name: Run Prettier
34 | # run: npm run format
35 |
36 | # - name: Run ESLint
37 | # run: npm run lint
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "semi": false,
4 | "singleQuote": true,
5 | "trailingComma": "none"
6 | }
7 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "git.enabled": true
3 | }
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:10
2 |
3 | WORKDIR /usr/src/app
4 |
5 | # https://www.npmjs.com/package/canvas
6 | RUN apt-get update -y && apt-get install -y build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
7 |
8 | COPY package*.json ./
9 | COPY dist/ dist/
10 |
11 | EXPOSE 3000
12 |
13 | CMD [ "npm", "run", "docker:start" ]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Yannick Deubel (https://github.com/yandeu); Project Url: https://github.com/yandeu/phaser3-multiplayer-with-physics
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Phaser 3 - Real-Time Multiplayer example with Physics
6 |
7 |
8 |
9 |
10 | A Real-Time Multiplayer example using Phaser 3 with Physics (MatterJS and Arcade) on a NodeJS server with Express and Socket.io
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ---
25 |
26 | ## ⚠️ Note
27 |
28 | This example is using [`@geckos.io/phaser-on-nodejs`](https://github.com/geckosio/phaser-on-nodejs#readme) which is very slow tedious. If you need to run Phaser's Arcade Physics on the Server, I recommend using [`arcade-physics`](https://github.com/yandeu/arcade-physics#readme). I will not update this example anymore.
29 |
30 | ## Play It
31 |
32 | _This example is running on NodeJS on Heroku (**Free Dyno** in **Europe**) which causes the example sometimes to take **about 1 minute to load**._
33 |
34 | - Play it here - [phaser3-multiplayer-example](http://phaser3-multiplayer-example.herokuapp.com/)
35 | _It works best if your latency is below 100ms, which should the case if you are located in Europe._
36 |
37 | ## Key Features
38 |
39 | - The physics is entirely calculated on the Server
40 | - Automatically manages Rooms (new Phaser instances)
41 | - Physics debugging version
42 | - A nice Stats page
43 |
44 | ## Geckos.io
45 |
46 | Why does this game example not use geckos.io?
47 | Well there are two reasons:
48 |
49 | - Geckos.io did not exist back then.
50 | - This example is deployed on heroku, which does not allow to forward UDP ports, geckos.io depends on.
51 |
52 | ## Video
53 |
54 | Watch it on YouTube
55 |
56 | [](https://youtu.be/n8gJQEfA18s)
57 |
58 | ## Matter Physics vs Arcade Physics
59 |
60 | This example includes 2 different games. One with MatterJS and the other with Arcade. The MatterJS game has only one level. The Arcade one is a simple platformer game with 3 levels.
61 |
62 | So in total, you can play 4 different levels. For each level, the server creates a new room, which creates a new Phaser instance, which are completely isolated from each other. There can be up to 4 players per room.
63 |
64 | ## Structure
65 |
66 | ```bash
67 | ├── src
68 | │ ├── client # Contains all the code the client will need
69 | │ ├── physics # This is for debugging the physics (Arcade and MatterJS)
70 | │ ├── server # Contains the code running on the NodeJS server
71 | │ └── stats # The stats page will show useful information about the server
72 | ```
73 |
74 | ## How To Use
75 |
76 | To clone and run this template, you'll need [Git](https://git-scm.com) and [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer.
77 |
78 | You also need to install some additional packages since this example uses [Node-Canvas](https://www.npmjs.com/package/canvas). See [here](https://www.npmjs.com/package/canvas#compiling).
79 |
80 | From your command line:
81 |
82 | ```bash
83 | # Clone this repository
84 | $ git clone --depth 1 https://github.com/yandeu/phaser3-multiplayer-with-physics.git phaser3-example
85 |
86 | # Go into the repository
87 | $ cd phaser3-example
88 |
89 | # Install dependencies
90 | $ npm install
91 |
92 | # Start the local development server (on port 3000)
93 | $ npm run dev
94 |
95 | # To publish a production build using docker use docker:publish
96 | # This needs docker and docker-compose to be installed on your machine
97 | $ npm run docker:publish
98 | ```
99 |
100 | ## Other Multiplayer Examples
101 |
102 | Looking for a simpler multiplayer example? Take a look at [Phaser 3 - Multiplayer Game Example](https://github.com/geckosio/phaser3-multiplayer-game-example).
103 |
104 | ## License
105 |
106 | The MIT License (MIT) 2019 - [Yannick Deubel](https://github.com/yandeu). Please have a look at the [LICENSE](LICENSE) for more details.
107 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | phaser3-coop-game:
5 | container_name: phaser3-coop-game
6 | build: .
7 | ports:
8 | - '8080:3000'
9 | restart: always
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "phaser3-multiplayer-with-physics",
3 | "version": "1.1.0",
4 | "description": "Phaser 3 - Real-Time Multiplayer Game with MatterJS Physics",
5 | "homepage": "https://github.com/yandeu/phaser3-multiplayer-with-physics#readme",
6 | "main": "server.js",
7 | "scripts": {
8 | "start": "npm run dev",
9 | "dev": "npm-run-all --parallel server client physics stats",
10 | "server": "npm-run-all --parallel server:*",
11 | "server:webpack": "webpack --config webpack/webpack.server.cjs --watch",
12 | "server:nodemon": "nodemon dist/server/server.js",
13 | "stats": "webpack --config webpack/webpack.stats.cjs --watch",
14 | "client": "webpack --config webpack/webpack.client.cjs --watch",
15 | "physics": "webpack --config webpack/webpack.physics.cjs --watch",
16 | "build": "webpack --config webpack/webpack.client.prod.cjs && webpack --config webpack/webpack.physics.prod.cjs && webpack --config webpack/webpack.server.cjs && webpack --config webpack/webpack.stats.prod.cjs",
17 | "serve": "node dist/server/server.js",
18 | "prettier": "prettier --write 'src/**/*.ts'",
19 | "docker:start": "npm i --only=production && npm run serve",
20 | "docker:publish": "npm run build && docker-compose up -d --build",
21 | "docker:up": "docker-compose up -d --build",
22 | "postinstall": "webpack --config webpack/webpack.server.cjs && webpack --config webpack/webpack.stats.cjs && webpack --config webpack/webpack.client.cjs && webpack --config webpack/webpack.physics.cjs"
23 | },
24 | "author": {
25 | "name": "Yannick Deubel",
26 | "url": "https://github.com/yandeu"
27 | },
28 | "repository": {
29 | "type": "git",
30 | "url": "https://github.com/yandeu/phaser3-multiplayer-with-physics.git"
31 | },
32 | "template": {
33 | "name": "phaser3-multiplayer-with-physics",
34 | "description": "This game is based on the phaser3-multiplayer-with-physics",
35 | "url": "https://github.com/yandeu/phaser3-multiplayer-with-physics",
36 | "author": "Yannick Deubel (https://github.com/yandeu)"
37 | },
38 | "license": "MIT",
39 | "dependencies": {
40 | "@geckos.io/phaser-on-nodejs": "^1.2.8",
41 | "axios": "^1.2.1",
42 | "compression": "^1.7.4",
43 | "express": "^4.17.1",
44 | "helmet": "^3.23.1",
45 | "moment": "^2.27.0",
46 | "phaser": "3.55.2",
47 | "pidusage": "^2.0.20",
48 | "socket.io": "^2.3.0",
49 | "socket.io-client": "^2.3.0",
50 | "source-map-support": "^0.5.19",
51 | "uuid": "^8.1.0"
52 | },
53 | "devDependencies": {
54 | "@types/compression": "^1.7.0",
55 | "@types/express": "^4.17.6",
56 | "@types/helmet": "^0.0.47",
57 | "@types/matter-js": "^0.14.4",
58 | "@types/node": "^18",
59 | "@types/pidusage": "^2.0.1",
60 | "@types/socket.io": "^2.1.8",
61 | "@types/socket.io-client": "^1.4.33",
62 | "@types/uuid": "^3.4.9",
63 | "copy-webpack-plugin": "^11.0.0",
64 | "html-webpack-plugin": "^5.5.0",
65 | "nodemon": "^2.0.20",
66 | "npm-run-all": "^4.1.5",
67 | "object-sizeof": "^1.6.0",
68 | "ts-loader": "^9.4.2",
69 | "typescript": "^4.9.4",
70 | "webpack": "^5.75.0",
71 | "webpack-cli": "^5.0.1",
72 | "webpack-merge": "^5.8.0",
73 | "webpack-node-externals": "^3.0.0"
74 | }
75 | }
--------------------------------------------------------------------------------
/readme/phaser-with-nodejs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandeu/phaser3-multiplayer-with-physics/ea494f835cfe50edefd86b7af378a6c35fa3dcca/readme/phaser-with-nodejs.png
--------------------------------------------------------------------------------
/src/client/assets/box.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandeu/phaser3-multiplayer-with-physics/ea494f835cfe50edefd86b7af378a6c35fa3dcca/src/client/assets/box.png
--------------------------------------------------------------------------------
/src/client/assets/bug.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandeu/phaser3-multiplayer-with-physics/ea494f835cfe50edefd86b7af378a6c35fa3dcca/src/client/assets/bug.png
--------------------------------------------------------------------------------
/src/client/assets/controls.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandeu/phaser3-multiplayer-with-physics/ea494f835cfe50edefd86b7af378a6c35fa3dcca/src/client/assets/controls.png
--------------------------------------------------------------------------------
/src/client/assets/dude.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandeu/phaser3-multiplayer-with-physics/ea494f835cfe50edefd86b7af378a6c35fa3dcca/src/client/assets/dude.png
--------------------------------------------------------------------------------
/src/client/assets/fullscreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandeu/phaser3-multiplayer-with-physics/ea494f835cfe50edefd86b7af378a6c35fa3dcca/src/client/assets/fullscreen.png
--------------------------------------------------------------------------------
/src/client/assets/mummy37x45.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandeu/phaser3-multiplayer-with-physics/ea494f835cfe50edefd86b7af378a6c35fa3dcca/src/client/assets/mummy37x45.png
--------------------------------------------------------------------------------
/src/client/assets/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandeu/phaser3-multiplayer-with-physics/ea494f835cfe50edefd86b7af378a6c35fa3dcca/src/client/assets/star.png
--------------------------------------------------------------------------------
/src/client/assets/starfield.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandeu/phaser3-multiplayer-with-physics/ea494f835cfe50edefd86b7af378a6c35fa3dcca/src/client/assets/starfield.jpg
--------------------------------------------------------------------------------
/src/client/components/animations.ts:
--------------------------------------------------------------------------------
1 | import { SKINS } from '../../constants'
2 |
3 | export const createDudeAnimations = (scene: Phaser.Scene) => {
4 | scene.anims.create({
5 | key: 'left',
6 | frames: scene.anims.generateFrameNumbers(SKINS.DUDE.toString(), { start: 0, end: 3 }),
7 | frameRate: 10,
8 | repeat: -1
9 | })
10 |
11 | scene.anims.create({
12 | key: 'idle',
13 | frames: [{ key: SKINS.DUDE.toString(), frame: 4 }],
14 | frameRate: 20
15 | })
16 |
17 | scene.anims.create({
18 | key: 'right',
19 | frames: scene.anims.generateFrameNumbers(SKINS.DUDE.toString(), { start: 5, end: 8 }),
20 | frameRate: 10,
21 | repeat: -1
22 | })
23 | }
24 |
25 | export const setDudeAnimation = (sprite: Phaser.GameObjects.Sprite, animation: string = 'idle') => {
26 | if (!sprite.anims.isPlaying) sprite.play(animation)
27 | else if (sprite.anims.isPlaying && sprite.anims.getName() !== animation) sprite.play(animation)
28 | }
29 |
30 | export const createMummyAnimation = (scene: Phaser.Scene) => {
31 | scene.anims.create({
32 | key: 'walk',
33 | frames: scene.anims.generateFrameNumbers(SKINS.MUMMY.toString(), {}),
34 | frameRate: 16,
35 | repeat: 7
36 | })
37 | }
38 |
39 | export const setMummyAnimation = (sprite: Phaser.GameObjects.Sprite, direction: string) => {
40 | if (!sprite.anims.isPlaying) sprite.anims.play('walk')
41 | let flipX = direction === 'left' ? true : false
42 | sprite.setFlipX(flipX)
43 | }
44 |
--------------------------------------------------------------------------------
/src/client/components/controls.ts:
--------------------------------------------------------------------------------
1 | export default class Controls {
2 | left = false
3 | right = false
4 | up = false
5 | controls: Control[] = []
6 | none = true
7 | prevNone = true
8 |
9 | constructor(public scene: Phaser.Scene, public socket: SocketIOClient.Socket) {
10 | // add a second pointer
11 | scene.input.addPointer()
12 |
13 | const detectPointer = (gameObject: Control, down: boolean) => {
14 | if (gameObject.btn) {
15 | switch (gameObject.btn) {
16 | case 'left':
17 | this.left = down
18 | break
19 | case 'right':
20 | this.right = down
21 | break
22 | case 'up':
23 | this.up = down
24 | break
25 | }
26 | }
27 | }
28 | scene.input.on('gameobjectdown', (pointer: Phaser.Input.Pointer, gameObject: Control) =>
29 | detectPointer(gameObject, true)
30 | )
31 | scene.input.on('gameobjectup', (pointer: Phaser.Input.Pointer, gameObject: Control) =>
32 | detectPointer(gameObject, false)
33 | )
34 |
35 | let left = new Control(scene, 0, 0, 'left').setRotation(-0.5 * Math.PI)
36 | let right = new Control(scene, 0, 0, 'right').setRotation(0.5 * Math.PI)
37 | let up = new Control(scene, 0, 0, 'up')
38 | this.controls.push(left, right, up)
39 | this.resize()
40 |
41 | this.scene.events.on('update', this.update, this)
42 | }
43 |
44 | controlsDown() {
45 | return { left: this.left, right: this.right, up: this.up, none: this.none }
46 | }
47 |
48 | resize() {
49 | const SCALE = 1
50 | const controlsRadius = (192 / 2) * SCALE
51 | const w = this.scene.cameras.main.width - 10 - controlsRadius
52 | const h = this.scene.cameras.main.height - 10 - controlsRadius
53 | let positions = [
54 | {
55 | x: controlsRadius + 10,
56 | y: h
57 | },
58 | { x: controlsRadius + 214, y: h },
59 | { x: w, y: h }
60 | ]
61 | this.controls.forEach((ctl, i) => {
62 | ctl.setPosition(positions[i].x, positions[i].y)
63 | ctl.setScale(SCALE)
64 | })
65 | }
66 |
67 | update() {
68 | this.none = this.left || this.right || this.up ? false : true
69 |
70 | if (!this.none || this.none !== this.prevNone) {
71 | let total = 0
72 | if (this.left) total += 1
73 | if (this.right) total += 2
74 | if (this.up) total += 4
75 | if (this.none) total += 8
76 | this.socket.emit('U' /* short for updateDude */, total)
77 | }
78 |
79 | this.prevNone = this.none
80 | }
81 | }
82 |
83 | class Control extends Phaser.GameObjects.Image {
84 | constructor(scene: Phaser.Scene, x: number, y: number, public btn: string) {
85 | super(scene, x, y, 'controls')
86 | scene.add.existing(this)
87 |
88 | this.setInteractive()
89 | .setScrollFactor(0)
90 | .setAlpha(0.5)
91 | .setDepth(2)
92 |
93 | if (!scene.sys.game.device.input.touch) this.setAlpha(0)
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/client/components/cursors.ts:
--------------------------------------------------------------------------------
1 | export default class Cursors {
2 | cursors: Phaser.Types.Input.Keyboard.CursorKeys
3 |
4 | none = true
5 | prevNone = true
6 |
7 | left = false
8 | right = false
9 | up = false
10 |
11 | constructor(public scene: Phaser.Scene, public socket: SocketIOClient.Socket) {
12 | this.cursors = scene.input.keyboard.createCursorKeys()
13 |
14 | this.scene.events.on('update', this.update, this)
15 | }
16 |
17 | cursorsDown() {
18 | return { left: this.left, right: this.right, up: this.up, none: this.none }
19 | }
20 |
21 | update() {
22 | if (!this.cursors.left || !this.cursors.right || !this.cursors.up) return
23 |
24 | this.none = this.cursors.left.isDown || this.cursors.right.isDown || this.cursors.up.isDown ? false : true
25 |
26 | if (!this.none || this.none !== this.prevNone) {
27 | this.left = false
28 | this.right = false
29 | this.up = false
30 |
31 | if (this.cursors.left.isDown) {
32 | this.left = true
33 | } else if (this.cursors.right.isDown) {
34 | this.right = true
35 | }
36 |
37 | if (this.cursors.up.isDown) {
38 | this.up = true
39 | }
40 |
41 | if (!PHYSICS_DEBUG) {
42 | let total = 0
43 | if (this.left) total += 1
44 | if (this.right) total += 2
45 | if (this.up) total += 4
46 | if (this.none) total += 8
47 | this.socket.emit('U' /* short for updateDude */, total)
48 | }
49 | }
50 |
51 | this.prevNone = this.none
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/client/components/fullscreenButton.ts:
--------------------------------------------------------------------------------
1 | const fullscreenButton = (scene: Phaser.Scene) => {
2 | let button = scene.add
3 | .image(0, 0, 'fullscreen', 0)
4 | .setOrigin(1, 0)
5 | .setInteractive()
6 | .setScrollFactor(0)
7 | .setDepth(100)
8 |
9 | button.on('pointerup', () => {
10 | if (scene.scale.isFullscreen) {
11 | button.setFrame(0)
12 | scene.scale.stopFullscreen()
13 | } else {
14 | button.setFrame(1)
15 | scene.scale.startFullscreen()
16 | }
17 | })
18 | return button
19 | }
20 |
21 | export default fullscreenButton
22 |
--------------------------------------------------------------------------------
/src/client/components/fullscreenEvent.ts:
--------------------------------------------------------------------------------
1 | // listen for fullscreen change event
2 | const FullScreenEvent = (callback: Function) => {
3 | const fullScreenChange = () => {
4 | let times = [50, 100, 200, 500, 1000, 2000, 5000]
5 | times.forEach(time => {
6 | window.setTimeout(() => {
7 | callback()
8 | }, time)
9 | })
10 | }
11 | var vendors = ['webkit', 'moz', 'ms', '']
12 | vendors.forEach(prefix => {
13 | document.addEventListener(prefix + 'fullscreenchange', fullScreenChange, false)
14 | })
15 | document.addEventListener('MSFullscreenChange', fullScreenChange, false)
16 | }
17 | export default FullScreenEvent
18 |
--------------------------------------------------------------------------------
/src/client/components/resize.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This is a phaser 3 scaling strategy implementation from
3 | * https://github.com/yandeu/phaser3-scaling-resizing-example
4 | */
5 |
6 | type scaleMode = 'FIT' | 'SMOOTH'
7 |
8 | const DEFAULT_WIDTH: number = 896
9 | const DEFAULT_HEIGHT: number = 504
10 | const MAX_WIDTH: number = DEFAULT_WIDTH * 1.5
11 | const MAX_HEIGHT: number = DEFAULT_HEIGHT * 1.5
12 | let SCALE_MODE: scaleMode = 'SMOOTH' // FIT OR SMOOTH
13 |
14 | const resize = (game: Phaser.Game) => {
15 | const w = window.innerWidth
16 | const h = window.innerHeight
17 |
18 | let width = DEFAULT_WIDTH
19 | let height = DEFAULT_HEIGHT
20 | let maxWidth = MAX_WIDTH
21 | let maxHeight = MAX_HEIGHT
22 | let scaleMode = SCALE_MODE
23 |
24 | let scale = Math.min(w / width, h / height)
25 | let newWidth = Math.min(w / scale, maxWidth)
26 | let newHeight = Math.min(h / scale, maxHeight)
27 |
28 | let defaultRatio = DEFAULT_WIDTH / DEFAULT_HEIGHT
29 | let maxRatioWidth = MAX_WIDTH / DEFAULT_HEIGHT
30 | let maxRatioHeight = DEFAULT_WIDTH / MAX_HEIGHT
31 |
32 | // smooth scaling
33 | let smooth = 1
34 | if (scaleMode === 'SMOOTH') {
35 | const maxSmoothScale = 1.15
36 | const normalize = (value: number, min: number, max: number) => {
37 | return (value - min) / (max - min)
38 | }
39 | if (width / height < w / h) {
40 | smooth =
41 | -normalize(newWidth / newHeight, defaultRatio, maxRatioWidth) / (1 / (maxSmoothScale - 1)) + maxSmoothScale
42 | } else {
43 | smooth =
44 | -normalize(newWidth / newHeight, defaultRatio, maxRatioHeight) / (1 / (maxSmoothScale - 1)) + maxSmoothScale
45 | }
46 | }
47 |
48 | // resize the game
49 | game.scale.resize(newWidth * smooth, newHeight * smooth)
50 |
51 | // scale the width and height of the css
52 | game.canvas.style.width = newWidth * scale + 'px'
53 | game.canvas.style.height = newHeight * scale + 'px'
54 |
55 | // center the game with css margin
56 | game.canvas.style.marginTop = `${(h - newHeight * scale) / 2}px`
57 | game.canvas.style.marginLeft = `${(w - newWidth * scale) / 2}px`
58 | }
59 |
60 | export default resize
61 |
--------------------------------------------------------------------------------
/src/client/components/texts.ts:
--------------------------------------------------------------------------------
1 | import { MAX_PLAYERS_PER_ROOM } from '../../constants'
2 |
3 | const texts = [
4 | {
5 | y: 230,
6 | fontSize: 28,
7 | type: 'server_running_time'
8 | },
9 | {
10 | y: 260,
11 | fontSize: 28,
12 | type: 'the_room_id'
13 | },
14 | {
15 | y: 290,
16 | fontSize: 28,
17 | type: 'show_connected_users'
18 | },
19 | {
20 | y: 320,
21 | fontSize: 28,
22 | type: 'show_latency'
23 | },
24 | {
25 | y: 350,
26 | fontSize: 28,
27 | type: 'show_fps'
28 | }
29 | ]
30 |
31 | export default class Texts {
32 | textObjects: { [key: string]: Phaser.GameObjects.Text } = {}
33 | hidden = false
34 | bug: Phaser.GameObjects.Image | undefined
35 |
36 | constructor(public scene: Phaser.Scene) {
37 | texts.forEach(text => {
38 | let theText = scene.add
39 | .text(scene.cameras.main.width / 2, text.y, '', {
40 | color: '#ffffff',
41 | fontSize: text.fontSize
42 | })
43 | .setOrigin(0.5)
44 | .setResolution(window.devicePixelRatio)
45 | .setScrollFactor(0)
46 | .setDepth(100)
47 |
48 | this.textObjects[text.type] = theText
49 | })
50 |
51 | this.makeBug()
52 | this.resize()
53 | this.toggleHidden()
54 | this.scene.events.on('update', this.update, this)
55 | }
56 |
57 | update() {
58 | if (this.hidden) return
59 | this.setFps(this.scene.game.loop.actualFps)
60 | }
61 |
62 | makeBug() {
63 | this.bug = this.scene.add
64 | .image(0, 0, 'bug')
65 | .setOrigin(0)
66 | .setScrollFactor(0)
67 | .setDepth(100)
68 | this.bug.setInteractive().on('pointerdown', () => {
69 | this.toggleHidden()
70 | })
71 | }
72 |
73 | toggleHidden() {
74 | this.hidden = !this.hidden
75 | for (const key in this.textObjects) {
76 | this.textObjects[key].setAlpha(this.hidden ? 0 : 1)
77 | }
78 | }
79 |
80 | resize() {
81 | texts.forEach(text => {
82 | const textObj = this.textObjects[text.type]
83 | textObj.setPosition(this.scene.cameras.main.width / 2, text.y)
84 | })
85 | if (this.bug) this.bug.setPosition(16, 16)
86 | }
87 |
88 | setConnectCounter(connectCounter: number) {
89 | this.textObjects['show_connected_users'].setText(`Connected users: ${connectCounter}/${MAX_PLAYERS_PER_ROOM}`)
90 | }
91 |
92 | setRoomId(roomId: string) {
93 | this.textObjects['the_room_id'].setText(`RoomId ${roomId}`)
94 | }
95 |
96 | setTime(time: number) {
97 | this.textObjects['server_running_time'].setText(`Server is running since ${new Date(time).toUTCString()}`)
98 | }
99 |
100 | setFps(fps: number) {
101 | this.textObjects['show_fps'].setText(`fps: ${Math.round(fps)}`)
102 | }
103 |
104 | setLatency(latency: Latency) {
105 | if (isNaN(latency.current)) return
106 | if (isNaN(latency.high) || latency.current > latency.high) latency.high = latency.current
107 | if (isNaN(latency.low) || latency.current < latency.low) latency.low = latency.current
108 |
109 | let sum = latency.history.reduce((previous, current) => (current += previous))
110 | let avg = sum / latency.history.length
111 |
112 | this.textObjects['show_latency'].setText(
113 | `Latency ${latency.current}ms (avg ${Math.round(avg)}ms / low ${latency.low}ms / high ${latency.high}ms)`
114 | )
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/client/config.ts:
--------------------------------------------------------------------------------
1 | import PreloadScene from './scenes/preloadScene'
2 | import MenuScene from './scenes/menuScene'
3 | import MainScene from './scenes/mainScene'
4 |
5 | const DEFAULT_WIDTH = 1280
6 | const DEFAULT_HEIGHT = 720
7 |
8 | // the size of the world
9 | export const world = {
10 | x: 0,
11 | y: 0,
12 | width: 2560,
13 | height: 864
14 | }
15 |
16 | const config = {
17 | type: Phaser.WEBGL,
18 | backgroundColor: '#ffffff',
19 | scale: {
20 | parent: 'phaser-game',
21 | mode: Phaser.Scale.NONE,
22 | width: DEFAULT_WIDTH,
23 | height: DEFAULT_HEIGHT
24 | },
25 | scene: [PreloadScene, MenuScene, MainScene],
26 | physics: {
27 | default: 'matter',
28 | matter: {
29 | gravity: {
30 | y: 0.8
31 | },
32 | debug: false,
33 | debugBodyColor: 0xff00ff
34 | }
35 | }
36 | }
37 | export default config
38 |
--------------------------------------------------------------------------------
/src/client/game.ts:
--------------------------------------------------------------------------------
1 | import config from './config'
2 |
3 | export default class Game extends Phaser.Game {
4 | constructor() {
5 | super(config)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 | Document
11 |
12 |
13 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/client/index.ts:
--------------------------------------------------------------------------------
1 | import 'phaser'
2 | import resize from './components/resize'
3 | import Game from './game'
4 | import FullScreenEvent from './components/fullscreenEvent'
5 |
6 | window.addEventListener('load', () => {
7 | let game = new Game()
8 |
9 | window.addEventListener('resize', () => {
10 | resize(game)
11 | })
12 |
13 | FullScreenEvent(() => resize(game))
14 |
15 | resize(game)
16 | })
17 |
--------------------------------------------------------------------------------
/src/client/scenes/mainScene.ts:
--------------------------------------------------------------------------------
1 | import Texts from '../components/texts'
2 | import Cursors from '../components/cursors'
3 | import { setDudeAnimation, setMummyAnimation } from '../components/animations'
4 | import fullscreenButton from '../components/fullscreenButton'
5 | import Controls from '../components/controls'
6 | import { world } from '../config'
7 | import Resize from '../components/resize'
8 |
9 | import SyncManager from '../../server/managers/syncManager'
10 | import { SKINS } from '../../constants'
11 |
12 | interface Objects {
13 | [key: string]: any
14 | }
15 |
16 | export default class MainScene extends Phaser.Scene {
17 | objects: Objects = {}
18 | sync: { initialState: boolean; objects: any[] } = {
19 | initialState: false,
20 | objects: []
21 | }
22 |
23 | latency: Latency = {
24 | current: NaN,
25 | high: NaN,
26 | low: NaN,
27 | ping: NaN,
28 | id: '',
29 | canSend: true,
30 | history: []
31 | }
32 | socket: Socket
33 |
34 | cursors: Cursors | undefined
35 | controls: Controls | undefined
36 | level: number = 0
37 |
38 | constructor() {
39 | super({ key: 'MainScene' })
40 | }
41 |
42 | init(props: { scene: string; level: number; socket: Socket }) {
43 | const { scene, level = 0, socket } = props
44 | this.level = level
45 | this.socket = socket
46 | this.socket.emit('joinRoom', { scene, level })
47 | }
48 |
49 | create() {
50 | const socket = this.socket
51 |
52 | let levelText = this.add
53 | .text(0, 0, `Level ${this.level + 1}`, {
54 | color: '#ffffff',
55 | fontSize: 42
56 | })
57 | .setOrigin(0.5, 0)
58 | .setDepth(100)
59 | .setScrollFactor(0)
60 |
61 | let starfield = this.add.tileSprite(world.x, world.y, world.width, world.height, 'starfield').setOrigin(0)
62 | this.cursors = new Cursors(this, socket)
63 | this.controls = new Controls(this, socket)
64 | let texts = new Texts(this)
65 | let fullscreenBtn = fullscreenButton(this)
66 |
67 | this.cameras.main.setBounds(world.x, world.y, world.width, world.height)
68 |
69 | socket.on('getPong', (id: string) => {
70 | if (this.latency.id !== id) return
71 | this.latency.canSend = true
72 | this.latency.current = new Date().getTime() - this.latency.ping
73 | if (this.latency.history.length >= 200) this.latency.history.shift()
74 | this.latency.history.push(this.latency.current)
75 | texts.setLatency(this.latency)
76 | })
77 | this.time.addEvent({
78 | delay: 250, // max 4 times per second
79 | loop: true,
80 | callback: () => {
81 | if (!this.latency.canSend) return
82 | if (texts.hidden) return
83 | this.latency.ping = new Date().getTime()
84 | this.latency.id = Phaser.Math.RND.uuid()
85 | this.latency.canSend = false
86 | socket.emit('sendPing', this.latency.id)
87 | }
88 | })
89 |
90 | socket.on('changingRoom', (data: { scene: string; level: number }) => {
91 | console.log('You are changing room')
92 | // destroy all objects and get new onces
93 | Object.keys(this.objects).forEach((key: string) => {
94 | this.objects[key].sprite.destroy()
95 | delete this.objects[key]
96 | })
97 | socket.emit('getInitialState')
98 | this.level = data.level | 0
99 | levelText.setText(`Level ${this.level + 1}`)
100 | })
101 |
102 | socket.on('S' /* short for syncGame */, (res: any) => {
103 | if (res.connectCounter) texts.setConnectCounter(res.connectCounter)
104 | if (res.time) texts.setTime(res.time)
105 | if (res.roomId) texts.setRoomId(res.roomId)
106 |
107 | // res.O (objects) contains only the objects that need to be updated
108 | if (res.O /* short for objects */) {
109 | res.O = SyncManager.decode(res.O)
110 |
111 | this.sync.objects = [...this.sync.objects, ...res.O]
112 | this.sync.objects.forEach((obj: any) => {
113 | // the if the player's dude is in the objects list the camera follows it sprite
114 | if (this.objects[obj.id] && obj.skin === SKINS.DUDE && obj.clientId && +obj.clientId === +socket.clientId) {
115 | this.cameras.main.setScroll(obj.x - this.cameras.main.width / 2, obj.y - this.cameras.main.height / 2)
116 | }
117 |
118 | // if the object does not exist, create a new one
119 | if (!this.objects[obj.id]) {
120 | let sprite = this.add
121 | .sprite(obj.x, obj.y, obj.skin.toString())
122 | .setOrigin(0.5)
123 | .setRotation(obj.angle || 0)
124 |
125 | // add the sprite by id to the objects object
126 | this.objects[obj.id] = {
127 | sprite: sprite
128 | }
129 | }
130 |
131 | // set some properties to the sprite
132 | let sprite = this.objects[obj.id].sprite
133 | // set scale
134 | if (obj.scale) {
135 | sprite.setScale(obj.scale)
136 | }
137 | // set scale
138 | if (obj.tint) {
139 | sprite.setTint(obj.tint)
140 | }
141 | // set visibility
142 | sprite.setVisible(!obj.dead)
143 | })
144 | }
145 | })
146 | // request the initial state
147 | socket.emit('getInitialState')
148 |
149 | // request the initial state every 15 seconds
150 | // to make sure all objects are up to date
151 | // in case we missed one (network issues)
152 | // should be sent from the server side not the client
153 | // this.time.addEvent({
154 | // delay: 15000,
155 | // loop: true,
156 | // callback: () => {
157 | // socket.emit('getInitialState')
158 | // }
159 | // })
160 |
161 | // request the initial state if the game gets focus
162 | // e.g. if the users comes from another tab or window
163 | this.game.events.on('focus', () => socket.emit('getInitialState'))
164 |
165 | // this helps debugging
166 | this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
167 | console.log(pointer.worldX, pointer.worldY)
168 | })
169 |
170 | const resize = () => {
171 | starfield.setScale(Math.max(this.cameras.main.height / starfield.height, 1))
172 | texts.resize()
173 | if (this.controls) this.controls.resize()
174 | fullscreenBtn.setPosition(this.cameras.main.width - 16, 16)
175 | this.cameras.main.setScroll(this.cameras.main.worldView.x, world.height)
176 | levelText.setPosition(this.cameras.main.width / 2, 20)
177 | }
178 |
179 | this.scale.on('resize', (gameSize: any, baseSize: any, displaySize: any, resolution: any) => {
180 | this.cameras.resize(gameSize.width, gameSize.height)
181 | resize()
182 | })
183 | Resize(this.game)
184 | }
185 |
186 | update(time: number, delta: number) {
187 | // update all objects
188 | if (this.sync.objects.length > 0) {
189 | this.sync.objects.forEach(obj => {
190 | if (this.objects[obj.id]) {
191 | let sprite = this.objects[obj.id].sprite
192 | if (obj.dead !== null) sprite.setVisible(!obj.dead)
193 | if (obj.x !== null) sprite.x = obj.x
194 | if (obj.y !== null) sprite.y = obj.y
195 | if (obj.angle !== null && typeof obj.angle !== 'undefined') sprite.angle = obj.angle
196 | if (obj.skin !== null) {
197 | if (obj.skin === SKINS.MUMMY) {
198 | if (obj.direction !== null) setMummyAnimation(sprite, obj.direction)
199 | }
200 | if (obj.skin === SKINS.DUDE) {
201 | if (obj.animation !== null) setDudeAnimation(sprite, obj.animation)
202 | }
203 | }
204 | }
205 | })
206 | }
207 | this.sync.objects = []
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/client/scenes/menuScene.ts:
--------------------------------------------------------------------------------
1 | import Resize from '../components/resize'
2 |
3 | export default class MenuScene extends Phaser.Scene {
4 | socket: Socket
5 | constructor() {
6 | super({ key: 'MenuScene' })
7 | }
8 |
9 | init(props: { socket: Socket }) {
10 | const { socket } = props
11 | this.socket = socket
12 | }
13 |
14 | create() {
15 | const styles = {
16 | color: '#000000',
17 | align: 'center',
18 | fontSize: 52
19 | }
20 |
21 | let texts: any[] = []
22 |
23 | texts.push(this.add.text(0, 0, 'Choose which Level\nyou want to play', styles).setOrigin(0.5, 0))
24 |
25 | texts.push(
26 | this.add
27 | .text(0, 0, 'Matter Physics', styles)
28 | .setOrigin(0.5, 0)
29 | .setInteractive()
30 | .on('pointerdown', () => {
31 | this.scene.start('MainScene', { scene: 'MatterScene', level: 0, socket: this.socket })
32 | })
33 | )
34 |
35 | texts.push(
36 | this.add
37 | .text(0, 0, 'Arcade Physics (Level 1)', styles)
38 | .setOrigin(0.5, 0)
39 | .setInteractive()
40 | .on('pointerdown', () => {
41 | this.scene.start('MainScene', { scene: 'ArcadeScene', level: 0, socket: this.socket })
42 | })
43 | )
44 |
45 | texts.push(
46 | this.add
47 | .text(0, 0, 'Arcade Physics (Level 2)', styles)
48 | .setOrigin(0.5, 0)
49 | .setInteractive()
50 | .on('pointerdown', () => {
51 | this.scene.stop()
52 | this.scene.start('MainScene', { scene: 'ArcadeScene', level: 1, socket: this.socket })
53 | })
54 | )
55 |
56 | texts.push(
57 | this.add
58 | .text(0, 0, 'Arcade Physics (Level 3)', styles)
59 | .setOrigin(0.5, 0)
60 | .setInteractive()
61 | .on('pointerdown', () => {
62 | this.scene.stop()
63 | this.scene.start('MainScene', { scene: 'ArcadeScene', level: 2, socket: this.socket })
64 | })
65 | )
66 |
67 | const resize = () => {
68 | const { centerX, centerY } = this.cameras.main
69 | let posY = [20, centerY - 100, centerY - 10, centerY + 60, centerY + 130]
70 | texts.forEach((text, i) => {
71 | text.setPosition(centerX, posY[i])
72 | })
73 | }
74 |
75 | this.scale.on('resize', (gameSize: any, baseSize: any, displaySize: any, resolution: any) => {
76 | if (!this.scene.isActive()) return
77 | this.cameras.resize(gameSize.width, gameSize.height)
78 | resize()
79 | })
80 | resize()
81 | Resize(this.game)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/client/scenes/preloadScene.ts:
--------------------------------------------------------------------------------
1 | import io from 'socket.io-client'
2 | import { SKINS } from '../../constants'
3 | import { createDudeAnimations, createMummyAnimation } from '../components/animations'
4 |
5 | export default class PreloadScene extends Phaser.Scene {
6 | constructor() {
7 | super({ key: 'PreloadScene' })
8 | }
9 |
10 | preload() {
11 | this.load.setBaseURL('static/client')
12 | this.load.image(SKINS.BOX.toString(), 'assets/box.png')
13 | this.load.image(SKINS.STAR.toString(), 'assets/star.png')
14 | this.load.image('bug', 'assets/bug.png')
15 | this.load.image('starfield', 'assets/starfield.jpg')
16 | this.load.image('controls', 'assets/controls.png')
17 | this.load.spritesheet(SKINS.DUDE.toString(), 'assets/dude.png', {
18 | frameWidth: 32,
19 | frameHeight: 48
20 | })
21 | this.load.spritesheet('fullscreen', 'assets/fullscreen.png', {
22 | frameWidth: 64,
23 | frameHeight: 64
24 | })
25 | this.load.spritesheet(SKINS.MUMMY.toString(), 'assets/mummy37x45.png', { frameWidth: 37, frameHeight: 45 })
26 | }
27 |
28 | create() {
29 | createDudeAnimations(this)
30 | createMummyAnimation(this)
31 |
32 | // connecting to socket.io
33 | const url = `${location.origin}/G` /* short for stats */
34 |
35 | let socket = io.connect(url, { transports: ['websocket'] }) as Socket
36 |
37 | // on reconnection, reset the transports option, as the Websocket
38 | // connection may have failed (caused by proxy, firewall, browser, ...)
39 | socket.on('reconnect_attempt', () => {
40 | socket.io.opts.transports = ['polling', 'websocket']
41 | })
42 |
43 | socket.on('connect', () => {
44 | console.log("You're connected to socket.io")
45 | })
46 |
47 | // we wait until we have a valid clientId, then start the MainScene
48 | socket.on('clientId', (clientId: number) => {
49 | socket.clientId = clientId
50 | console.log('Your client id', clientId)
51 | this.scene.start('MenuScene', { socket })
52 | })
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const SKINS = {
2 | DUDE: 0,
3 | BOX: 1,
4 | STAR: 2,
5 | MUMMY: 3
6 | }
7 |
8 | export const MAX_PLAYERS_PER_ROOM = 4
9 | export const USER_KICK_TIMEOUT = 60_000 // 1 minute
10 |
--------------------------------------------------------------------------------
/src/physics/game.ts:
--------------------------------------------------------------------------------
1 | import commonConfig, { arcadePhysics, matterPhysics } from '../server/game/config'
2 |
3 | import ArcadeScene from '../server/game/scenes/arcadeScene'
4 | import MatterScene from '../server/game/scenes/matterScene'
5 |
6 | class PhaserGame extends Phaser.Game {
7 | debug = true
8 |
9 | constructor(public io: SocketIO.Namespace, config: Phaser.Types.Core.GameConfig) {
10 | super(config)
11 | }
12 | }
13 |
14 | const Game = (io: SocketIO.Namespace) => {
15 | let config = { ...commonConfig }
16 | let href = location.href
17 |
18 | config.type = Phaser.AUTO
19 | config.scale = {
20 | mode: Phaser.Scale.FIT,
21 | autoCenter: Phaser.Scale.CENTER_BOTH
22 | }
23 |
24 | if (/arcade/.test(href)) {
25 | config.scene = [ArcadeScene]
26 | config.physics = arcadePhysics
27 | // @ts-ignore
28 | config.physics.arcade.debug = true
29 | }
30 | if (/matter/.test(href)) {
31 | config.scene = [MatterScene]
32 | config.physics = matterPhysics
33 | if (config.physics.matter) config.physics.matter.debug = true
34 | }
35 |
36 | return new PhaserGame(io, config)
37 | }
38 | export default Game
39 |
--------------------------------------------------------------------------------
/src/physics/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/physics/index.ts:
--------------------------------------------------------------------------------
1 | import Game from './game'
2 |
3 | // mock the socket.io
4 | const ioMock = {
5 | emit: () => {},
6 | on: () => {},
7 | in: () => {},
8 | connected: 'connected'
9 | }
10 |
11 | window.addEventListener('load', () => {
12 | // @ts-ignore
13 | window.game = Game(ioMock)
14 | })
15 |
--------------------------------------------------------------------------------
/src/server/game/arcadeObjects/box.ts:
--------------------------------------------------------------------------------
1 | import { SKINS } from '../../../constants'
2 |
3 | export default class Box extends Phaser.Physics.Arcade.Sprite {
4 | skin = SKINS.BOX
5 | id: string
6 | sync = true
7 |
8 | constructor(scene: Phaser.Scene, id: number, x: number, y: number) {
9 | super(scene, x, y, '')
10 | scene.add.existing(this)
11 | scene.physics.add.existing(this, true)
12 |
13 | // @ts-ignore
14 | this.body
15 | .setSize(95, 95)
16 | // 32 is the default width an height for an sprite if the texture can not be loaded
17 | .setOffset(-32, -32)
18 |
19 | this.id = id.toString()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/server/game/arcadeObjects/dude.ts:
--------------------------------------------------------------------------------
1 | import { SKINS } from '../../../constants'
2 |
3 | export default class Dude extends Phaser.Physics.Arcade.Sprite {
4 | skin = SKINS.DUDE
5 | clientId: number
6 | socketId: string
7 | id: string
8 | private updates: any = {}
9 | private shouldUpdate = true
10 | prevPosition = {
11 | x: -1,
12 | y: -1
13 | }
14 | dead = false
15 | prevDead = false
16 | color: number = 0xffffff
17 | prevColor: number = 0xffffff
18 | animation: string | undefined = undefined
19 | hit = false
20 |
21 | constructor(scene: Phaser.Scene, id: number, options: { socketId: string; clientId: number }) {
22 | super(scene, 0, 0, '')
23 | scene.add.existing(this)
24 | scene.physics.add.existing(this)
25 |
26 | this.setFrame(0)
27 |
28 | this.socketId = options.socketId
29 | this.clientId = options.clientId
30 |
31 | this.setNewPosition()
32 | this.setCollideWorldBounds(true).setOrigin(0)
33 |
34 | // @ts-ignore
35 | this.body.setSize(32, 48)
36 |
37 | // matterJS uses an id per object, so I do the same here to be consistent
38 | // @ts-ignore
39 | this.id = id.toString()
40 | }
41 |
42 | setNewPosition() {
43 | this.setPosition(Phaser.Math.RND.integerInRange(0, 1000), Phaser.Math.RND.integerInRange(100, 300))
44 | }
45 |
46 | postUpdate() {
47 | this.prevPosition = { ...this.body.position }
48 | this.prevDead = this.dead
49 | this.prevColor = this.color
50 | }
51 |
52 | gotHit() {
53 | if (this.hit) return
54 | this.hit = true
55 | this.color = 0xff0000
56 |
57 | this.scene.time.addEvent({
58 | delay: 3500,
59 | callback: () => {
60 | this.hit = false
61 | this.color = 0xffffff
62 | }
63 | })
64 | }
65 |
66 | update() {
67 | if (!this.active) return
68 | if (!this.shouldUpdate) return
69 | this.shouldUpdate = false
70 |
71 | if (this.updates.left) this.setVelocityX(-400)
72 | else if (this.updates.right) this.setVelocityX(400)
73 | else this.setVelocityX(0)
74 |
75 | if (this.updates.up && this.body.blocked.down) this.setVelocityY(-600)
76 |
77 | this.animation = this.body.velocity.x >= 0.5 ? 'right' : this.body.velocity.x <= -0.5 ? 'left' : 'idle'
78 |
79 | this.updates = {}
80 | }
81 |
82 | revive(clientId: number, socketId: string) {
83 | this.setActive(true)
84 | this.dead = false
85 | this.setNewPosition()
86 | this.clientId = clientId
87 | this.socketId = socketId
88 | }
89 |
90 | kill() {
91 | this.setActive(false)
92 | this.dead = true
93 | }
94 |
95 | setUpdates(updates: any) {
96 | this.shouldUpdate = true
97 | this.updates = updates
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/server/game/arcadeObjects/map.ts:
--------------------------------------------------------------------------------
1 | export default class Map {
2 | margin: { x: number; y: number }
3 | tileSize = 95
4 | levels = [
5 | [
6 | ' XXX M M XXXXXX',
7 | ' XX X X ',
8 | ' XX X X XXXXX X ',
9 | ' M M M X G ',
10 | 'XX XXXX XXX X ',
11 | 'XXXX X X X '
12 | ],
13 | [
14 | ' ',
15 | ' M X X ',
16 | ' XX X XXXXX XXX X ',
17 | ' G M X ',
18 | 'XX XXXX M XXX X ',
19 | 'XXXX XXXXXXXXX X X '
20 | ],
21 | [
22 | ' M G ',
23 | ' XXXXXXX X X ',
24 | ' XX X XXXXX XXX X ',
25 | ' M X ',
26 | 'XX XXXX XXX X ',
27 | 'XXXX XXXXXXXXX X X '
28 | ]
29 | ]
30 |
31 | constructor(public scene: Phaser.Scene, public world: any, public level: number) {
32 | this.margin = {
33 | y: 3 * this.tileSize + 11 + 45, // 45 is the half of a box
34 | x: world.x + 45
35 | }
36 | }
37 |
38 | private collideRect(
39 | rect1: { x: number; y: number; width: number; height: number },
40 | rect2: { x: number; y: number; width: number; height: number }
41 | ) {
42 | return (
43 | rect1.x < rect2.x + rect2.width &&
44 | rect1.x + rect1.width > rect2.x &&
45 | rect1.y < rect2.y + rect2.height &&
46 | rect1.y + rect1.height > rect2.y
47 | )
48 | }
49 |
50 | getTileByCoordinates(coordinates: { x: number; y: number }) {
51 | let { x: x1, y: y1 } = coordinates
52 |
53 | let tile = { tile: '', x: -1, y: -1 }
54 |
55 | this.getLevel().forEach((row, y) => {
56 | for (let x = 0; x < row.length; x++) {
57 | let x2 = x * this.tileSize + this.margin.x
58 | let y2 = y * this.tileSize + this.margin.y
59 | x2 -= 45 // minus the half of a the box
60 | y2 -= 45 // minus the half of a the box
61 | if (
62 | this.collideRect(
63 | { x: x1, y: y1, width: 1, height: 1 },
64 | { x: x2, y: y2, width: this.tileSize, height: this.tileSize }
65 | )
66 | ) {
67 | tile = { tile: row[x], x: x2, y: y2 }
68 | break
69 | }
70 | }
71 | })
72 |
73 | return tile
74 | }
75 |
76 | countTotalLevels() {
77 | return this.levels.length
78 | }
79 |
80 | getLevel() {
81 | return this.levels[this.level]
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/server/game/arcadeObjects/mummy.ts:
--------------------------------------------------------------------------------
1 | import { SKINS } from '../../../constants'
2 |
3 | export default class Mummy extends Phaser.Physics.Arcade.Sprite {
4 | skin = SKINS.MUMMY
5 | id: string
6 | direction: 'left' | 'right'
7 | dead: boolean = false
8 |
9 | constructor(scene: Phaser.Scene, id: number, x: number, y: number) {
10 | super(scene, x, y, '')
11 | scene.add.existing(this)
12 | scene.physics.add.existing(this)
13 |
14 | this.setFrame(0)
15 |
16 | this.direction = Math.random() > 0.5 ? 'left' : 'right'
17 |
18 | // @ts-ignore
19 | this.body.setSize(37, 45)
20 |
21 | this.id = id.toString()
22 | }
23 |
24 | kill() {
25 | if (this.dead) return
26 | this.dead = true
27 | this.scene.time.addEvent({
28 | delay: 5000,
29 | callback: () => (this.dead = false)
30 | })
31 | }
32 |
33 | getLookAhead() {
34 | let x = this.direction === 'right' ? this.body.right + 5 : this.body.left - 5
35 | let y = this.body.bottom + 10
36 | return { x, y }
37 | }
38 |
39 | changeDirection(tile: { tile: string; x: number; y: number }) {
40 | if (tile.tile !== 'X') {
41 | this.direction = this.direction === 'right' ? 'left' : 'right'
42 | }
43 | }
44 |
45 | move() {
46 | let velocity = this.direction === 'right' ? 35 : -35
47 | if (this.dead) velocity = 0
48 | this.setVelocityX(velocity)
49 | }
50 |
51 | update() {
52 | this.move()
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/server/game/arcadeObjects/star.ts:
--------------------------------------------------------------------------------
1 | import { SKINS } from '../../../constants'
2 |
3 | export default class Star extends Phaser.Physics.Arcade.Sprite {
4 | skin = SKINS.STAR
5 | id: string
6 | sync = true
7 | tint = 0x00ff00
8 |
9 | constructor(scene: Phaser.Scene, id: number, x: number, y: number) {
10 | super(scene, x, y, '')
11 | scene.add.existing(this)
12 | scene.physics.add.existing(this, true)
13 |
14 | // @ts-ignore
15 | this.body.setSize(24, 22, false)
16 |
17 | this.id = id.toString()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/server/game/config.ts:
--------------------------------------------------------------------------------
1 | import 'phaser'
2 |
3 | const config: Phaser.Types.Core.GameConfig = {
4 | type: Phaser.HEADLESS,
5 | parent: 'phaser-game',
6 | width: 1280,
7 | height: 720,
8 | banner: false,
9 | // @ts-ignore
10 | audio: false
11 | }
12 | export default config
13 |
14 | export const arcadePhysics = {
15 | default: 'arcade',
16 | arcade: {
17 | gravity: { y: 1500 }
18 | }
19 | }
20 |
21 | export const matterPhysics = {
22 | default: 'matter',
23 | matter: {
24 | gravity: {
25 | y: 2
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/server/game/game.ts:
--------------------------------------------------------------------------------
1 | import '@geckos.io/phaser-on-nodejs'
2 | import commonConfig, { arcadePhysics, matterPhysics } from './config'
3 |
4 | import ArcadeScene from './scenes/arcadeScene'
5 | import MatterScene from './scenes/matterScene'
6 | import RoomManager from '../managers/roomManager'
7 |
8 | export class PhaserGame extends Phaser.Game {
9 | constructor(config: Phaser.Types.Core.GameConfig) {
10 | super(config)
11 | }
12 | }
13 |
14 | const Game = (roomManager: RoomManager, roomId: string, options: { scene: string; level: number }) => {
15 | let config = { ...commonConfig }
16 |
17 | if (options.scene === 'ArcadeScene') {
18 | config.scene = [ArcadeScene]
19 | config.physics = arcadePhysics
20 | }
21 | if (options.scene === 'MatterScene') {
22 | config.scene = [MatterScene]
23 | config.physics = matterPhysics
24 | }
25 |
26 | // @ts-ignore
27 | config.customEnvironment = true
28 |
29 | // a very hackie trick to pass some custom data
30 | // but it work well :)
31 | config.callbacks = {
32 | preBoot: () => {
33 | return { level: +options.level, roomManager, roomId }
34 | }
35 | }
36 |
37 | return new PhaserGame(config)
38 | }
39 | export default Game
40 |
--------------------------------------------------------------------------------
/src/server/game/matterObjects/box.ts:
--------------------------------------------------------------------------------
1 | import MatterGameObject from './matterGameObject'
2 | import { SKINS } from '../../../constants'
3 |
4 | export default class Box extends MatterGameObject {
5 | lifeTime: number
6 |
7 | constructor(public scene: Phaser.Scene, x: number, y: number) {
8 | super(scene, SKINS.BOX)
9 |
10 | this.addBody(
11 | this.Matter.Bodies.rectangle(x, y, 95, 95, {
12 | friction: 0.1,
13 | chamfer: { radius: 14 },
14 | label: 'box',
15 | density: 0.000125
16 | })
17 | )
18 |
19 | this.lifeTime = Phaser.Math.RND.integerInRange(1000 * 15, 1000 * 45)
20 | this.setTimer()
21 | }
22 |
23 | setTimer() {
24 | this.scene.time.addEvent({
25 | delay: this.lifeTime,
26 | callback: () => {
27 | this.kill()
28 | }
29 | })
30 | }
31 |
32 | revive(x: number, y: number) {
33 | super.revive(x, y)
34 | this.setTimer()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/server/game/matterObjects/dude.ts:
--------------------------------------------------------------------------------
1 | import MatterGameObject from './matterGameObject'
2 | import { SKINS } from '../../../constants'
3 |
4 | export default class Dude extends MatterGameObject {
5 | maxVelocity = {
6 | x: 6,
7 | y: 12
8 | }
9 | width = 32
10 | height = 48
11 |
12 | shouldUpdate = true
13 |
14 | sensors: any
15 | mainBody: any
16 | translateX = 0
17 | translateY = 0
18 |
19 | jumpLocked = false
20 |
21 | move = {
22 | leftAllowed: true,
23 | rightAllowed: true
24 | }
25 | touching = {
26 | left: false,
27 | right: false,
28 | bottom: false
29 | }
30 | updates: any = {}
31 |
32 | constructor(scene: Phaser.Scene, x: number, y: number, public clientId: number, public socketId: string) {
33 | super(scene, SKINS.DUDE)
34 |
35 | let h = this.height
36 | let w = this.width - 4
37 |
38 | console.log('clientId', clientId)
39 |
40 | this.mainBody = this.Matter.Bodies.rectangle(x, y, w, h, {
41 | density: 0.001,
42 | friction: 0.1,
43 | frictionStatic: 0.1,
44 | label: 'dude',
45 | chamfer: { radius: 10 }
46 | })
47 | this.sensors = {
48 | bottom: this.Matter.Bodies.rectangle(x, y + h / 2 + 2 / 2, w * 0.35, 4, {
49 | isSensor: true
50 | }),
51 | left: this.Matter.Bodies.rectangle(x - w / 2 - 4 / 2, y + 0, 4, h * 0.9, {
52 | isSensor: true
53 | }),
54 | right: this.Matter.Bodies.rectangle(x + w / 2 + 4 / 2, y + 0, 4, h * 0.9, {
55 | isSensor: true
56 | })
57 | }
58 | this.addBodies([this.mainBody, this.sensors.bottom, this.sensors.left, this.sensors.right])
59 |
60 | this.setSensorLabel()
61 |
62 | this.Matter.Body.setInertia(this.body, Infinity) // setFixedRotation
63 | }
64 |
65 | setTranslate(x: number, y: number = 0) {
66 | this.translateX = x
67 | this.translateY = y
68 | }
69 |
70 | translate() {
71 | if (this.translateX !== 0 || this.translateY !== 0) {
72 | this.Matter.Body.setPosition(this.body, {
73 | x: this.body.position.x + this.translateX,
74 | y: this.body.position.y + this.translateY
75 | })
76 | this.translateX = 0
77 | this.translateY = 0
78 | }
79 | }
80 |
81 | setSensorLabel() {
82 | this.sensors.bottom.label = `dudeBottomSensor_${this.clientId}`
83 | this.sensors.left.label = `dudeLeftSensor_${this.clientId}`
84 | this.sensors.right.label = `dudeRightSensor_${this.clientId}`
85 | }
86 |
87 | revive(x: number, y: number, clientId: number, socketId: string) {
88 | super.revive(x, y)
89 | this.clientId = clientId
90 | this.socketId = socketId
91 | this.setSensorLabel()
92 | }
93 |
94 | lockJump() {
95 | this.jumpLocked = true
96 | this.scene.time.addEvent({
97 | delay: 250,
98 | callback: () => (this.jumpLocked = false)
99 | })
100 | }
101 |
102 | setUpdates(updates: any) {
103 | this.shouldUpdate = true
104 | this.updates = updates
105 | }
106 |
107 | update(force = false) {
108 | this.animation = 'idle'
109 |
110 | if (!force && !this.shouldUpdate) return
111 |
112 | const updates = this.updates
113 |
114 | let x = updates.left && this.move.leftAllowed ? -0.01 : updates.right && this.move.rightAllowed ? 0.01 : 0
115 | let y = !this.jumpLocked && updates.up && this.touching.bottom ? -this.maxVelocity.y : 0
116 | if (y !== 0) this.lockJump()
117 |
118 | // We use setVelocity to jump and applyForce to move right and left
119 |
120 | // Jump
121 | if (y !== 0) this.Matter.Body.setVelocity(this.body, { x: this.body.velocity.x, y })
122 |
123 | // Move
124 | this.Matter.Body.applyForce(this.body, { x: 0, y: 0 }, { x, y: 0 })
125 |
126 | // check max velocity
127 | let maxVelocityX =
128 | this.body.velocity.x > this.maxVelocity.x ? 1 : this.body.velocity.x < -this.maxVelocity.x ? -1 : null
129 | if (maxVelocityX)
130 | this.Matter.Body.setVelocity(this.body, { x: this.maxVelocity.x * maxVelocityX, y: this.body.velocity.y })
131 |
132 | // set velocity X to zero
133 | if (!updates.left && !updates.right) {
134 | this.Matter.Body.setVelocity(this.body, { x: this.body.velocity.x * 0.5, y: this.body.velocity.y })
135 | }
136 |
137 | this.animation = this.body.velocity.x >= 0.5 ? 'right' : this.body.velocity.x <= -0.5 ? 'left' : 'idle'
138 |
139 | this.translate()
140 |
141 | this.touching = {
142 | left: false,
143 | right: false,
144 | bottom: false
145 | }
146 | this.move = {
147 | leftAllowed: true,
148 | rightAllowed: true
149 | }
150 | this.updates = {}
151 | this.shouldUpdate = false
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/server/game/matterObjects/matterGameObject.ts:
--------------------------------------------------------------------------------
1 | export default class MatterGameObject {
2 | Matter: any
3 | body: any
4 | clientId: number | undefined = undefined
5 | dead = false
6 | prevDead = false
7 | angle = 0 // in DEG
8 | prevAngle = -1 // in DEG
9 | animation: string = 'idle'
10 | prevAnimation: string = 'idle'
11 | tint = 0x000000
12 |
13 | constructor(public scene: Phaser.Scene, public skin: number) {
14 | this.Matter = Phaser.Physics.Matter.Matter
15 | }
16 |
17 | protected addBody(body: any) {
18 | this.body = body
19 | this.body.prevVelocity = { x: 0, y: 0 }
20 | this.scene.matter.world.add(this.body)
21 | }
22 |
23 | protected addBodies(bodies: any[]) {
24 | this.body = this.Matter.Body.create({
25 | parts: bodies.map(body => body)
26 | })
27 | this.body.prevVelocity = { x: 0, y: 0 }
28 | this.scene.matter.world.add(this.body)
29 | }
30 |
31 | preUpdate(arg: any = undefined) {
32 | this.angle = Phaser.Math.RadToDeg(this.body.angle)
33 | }
34 |
35 | update(arg: any = undefined) {}
36 |
37 | postUpdate(arg: any = undefined) {
38 | if (this.dead && !this.prevDead) this.prevDead = true
39 | else if (!this.dead && this.prevDead) this.prevDead = false
40 |
41 | this.body.prevVelocity = { ...this.body.velocity }
42 | this.prevAngle = this.angle
43 | this.prevAnimation = this.animation
44 | }
45 |
46 | revive(x: number, y: number, clientId: number | undefined = undefined, socketId: string | undefined = undefined) {
47 | this.kill(false)
48 | this.Matter.Body.setPosition(this.body, { x, y })
49 | }
50 |
51 | kill(dead: boolean = true) {
52 | this.dead = dead
53 | if (dead) this.Matter.Body.setPosition(this.body, { x: -1000, y: -1000 })
54 | this.Matter.Sleeping.set(this.body, dead)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/server/game/matterObjects/matterGameObjectGroup.ts:
--------------------------------------------------------------------------------
1 | import Dude from './dude'
2 | import Box from './box'
3 | import MatterGameObject from './matterGameObject'
4 | import Star from './star'
5 | import { SKINS } from '../../../constants'
6 |
7 | interface GameObjectGroupAddOptions {
8 | socketId?: string
9 | clientId?: number
10 | category?: string
11 | }
12 |
13 | export default class GameObjectGroup {
14 | Matter: any
15 |
16 | constructor(public scene: Phaser.Scene, public objects: MatterGameObject[]) {
17 | this.Matter = Phaser.Physics.Matter.Matter
18 | }
19 |
20 | killById(id: string) {
21 | this.objects.forEach((obj: any) => {
22 | if (obj.body.id === id) obj.kill()
23 | })
24 | }
25 |
26 | getObjectById(id: string) {
27 | let object = undefined
28 | this.objects.forEach((obj: any) => {
29 | if (obj.body.id === id) object = obj
30 | })
31 | return object
32 | }
33 |
34 | add(x: number, y: number, skin: number, options: GameObjectGroupAddOptions = {}) {
35 | let dead = this.objects.filter(obj => obj.dead && obj.skin === skin)
36 | let alive = this.objects.filter(obj => !obj.dead && obj.skin === skin)
37 |
38 | const { clientId, socketId, category } = options
39 |
40 | // allow not more than 100 alive objects per skin
41 | if (alive.length >= 100) return
42 |
43 | let object: MatterGameObject | null = null
44 |
45 | if (dead.length > 0) {
46 | // revive the first dead object and set its x and y
47 | object = dead[0]
48 | object.revive(x, y, clientId, socketId)
49 | } else {
50 | // create a new object and add it to the objects array
51 | if (skin === SKINS.BOX) object = new Box(this.scene, x, y)
52 | else if (skin === SKINS.STAR) object = new Star(this.scene, x, y, category)
53 | else if (typeof clientId !== 'undefined' && typeof socketId !== 'undefined')
54 | object = new Dude(this.scene, x, y, clientId, socketId)
55 | if (object) this.objects.push(object)
56 | }
57 |
58 | // Rotate the box
59 | // TODO(yandeu) this should be inside the boxObject class
60 | if (skin === SKINS.BOX && object) this.Matter.Body.rotate(object.body, Math.random() * 2)
61 |
62 | return object
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/server/game/matterObjects/star.ts:
--------------------------------------------------------------------------------
1 | import MatterGameObject from './matterGameObject'
2 | import { SKINS } from '../../../constants'
3 |
4 | export default class Star extends MatterGameObject {
5 | scale: number = 1
6 |
7 | constructor(
8 | public scene: Phaser.Scene,
9 | public x: number,
10 | public y: number,
11 | public category: string | undefined = undefined
12 | ) {
13 | super(scene, SKINS.STAR)
14 |
15 | if (category === 'big') {
16 | this.tint = 0xff7200
17 | this.scale = 3
18 | }
19 | if (category === 'medium') {
20 | this.scale = 2
21 | }
22 |
23 | this.addBody(
24 | this.Matter.Bodies.rectangle(x, y, 24 * this.scale, 22 * this.scale, {
25 | chamfer: { radius: 14 },
26 | label: 'star',
27 | isStatic: true,
28 | isSensor: true
29 | })
30 | )
31 | }
32 |
33 | setReviveTimer() {
34 | this.scene.time.addEvent({
35 | delay: 15000,
36 | callback: () => {
37 | super.revive(this.x, this.y)
38 | }
39 | })
40 | }
41 |
42 | kill(dead: boolean = true) {
43 | this.dead = dead
44 | if (dead) this.Matter.Body.setPosition(this.body, { x: -1000, y: -1000 })
45 | this.Matter.Sleeping.set(this.body, dead)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/server/game/scenes/arcadeScene.ts:
--------------------------------------------------------------------------------
1 | import { world } from '../../../client/config'
2 | import Box from '../arcadeObjects/box'
3 | import Dude from '../arcadeObjects/dude'
4 | import Cursors from '../../../client/components/cursors'
5 | import Star from '../arcadeObjects/star'
6 | import Mummy from '../arcadeObjects/mummy'
7 | import Map from '../arcadeObjects/map'
8 | import SyncManager from '../../managers/syncManager'
9 | import RoomManager from '../../managers/roomManager'
10 | import { SKINS } from '../../../constants'
11 |
12 | export default class MainScene extends Phaser.Scene {
13 | id = 0
14 | dudeGroup: Phaser.GameObjects.Group
15 | boxGroup: Phaser.GameObjects.Group
16 | mummyGroup: Phaser.GameObjects.Group
17 | star: Star
18 | debug: any = {}
19 | level = 0
20 | map: Map
21 | objectsToSync: any = {}
22 | tick = 0
23 | roomManager: RoomManager
24 | roomId: string
25 |
26 | constructor() {
27 | // @ts-ignore
28 | super({ key: 'MainScene', plugins: PHYSICS_DEBUG ? null : ['Clock'], active: false, cameras: null })
29 | // see all scene plugins:
30 | // Phaser.Plugins.DefaultScene
31 | // https://github.com/photonstorm/phaser/blob/master/src/plugins/DefaultPlugins.js#L76
32 | }
33 |
34 | /** Create a new object id */
35 | newId() {
36 | return this.id++
37 | }
38 |
39 | init() {
40 | try {
41 | //@ts-ignore
42 | const { level = 0, roomId, roomManager } = this.game.config.preBoot()
43 | this.level = level
44 | this.roomManager = roomManager
45 | this.roomId = roomId
46 | } catch (error) {
47 | if (!PHYSICS_DEBUG) console.error('onInit() failed!')
48 | }
49 | }
50 |
51 | create() {
52 | // this will stop the scene
53 | this.events.addListener('stopScene', () => {
54 | this.roomManager.stats.removeTotalObjects(this.roomId)
55 | this.scene.stop()
56 | this.roomManager.stats.log(`Scene in roomId ${this.roomId} has stopped!`)
57 | })
58 |
59 | this.physics.world.setBounds(world.x, world.y, world.width, world.height)
60 | this.dudeGroup = this.add.group()
61 | this.boxGroup = this.add.group()
62 | this.mummyGroup = this.add.group()
63 | this.map = new Map(this, world, this.level)
64 | const level = this.map.getLevel()
65 |
66 | // generate the level
67 | level.forEach((row, y) => {
68 | for (let x = 0; x < row.length; x++) {
69 | const xx = x * this.map.tileSize + this.map.margin.x
70 | const yy = y * this.map.tileSize + this.map.margin.y
71 | if (row[x] === 'X') this.boxGroup.add(new Box(this, this.newId(), xx, yy))
72 | if (row[x] === 'G') this.star = new Star(this, this.newId(), xx, yy)
73 | if (row[x] === 'M') this.mummyGroup.add(new Mummy(this, this.newId(), xx, yy))
74 | }
75 | })
76 |
77 | if (PHYSICS_DEBUG) {
78 | this.add
79 | .text(24, 24, 'Physics Debugging Version\nMove with Arrow Keys', {
80 | fontSize: 36
81 | })
82 | .setScrollFactor(0)
83 | .setOrigin(0)
84 | .setAlpha(0.6)
85 | // mock socket
86 | this.debug.socket = { emit: () => {} }
87 | this.debug.cursors = new Cursors(this, this.debug.socket)
88 | this.debug.dude = new Dude(this, this.newId(), { clientId: 55555, socketId: 'some-socket-id' })
89 | this.dudeGroup.add(this.debug.dude)
90 |
91 | // this helps debugging
92 | this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
93 | console.log(pointer.worldX, pointer.worldY)
94 | console.log(this.map.getTileByCoordinates({ x: pointer.worldX, y: pointer.worldY }))
95 | })
96 | }
97 |
98 | this.events.addListener('createDude', (clientId: number, socketId: string) => {
99 | let dude: Dude = this.dudeGroup.getFirstDead()
100 | if (dude) {
101 | dude.revive(clientId, socketId)
102 | } else {
103 | dude = new Dude(this, this.newId(), { clientId, socketId })
104 | this.dudeGroup.add(dude)
105 | }
106 | })
107 |
108 | this.events.addListener('U' /* short for updateDude */, (res: any) => {
109 | // @ts-ignore
110 | let dudes: Dude[] = this.dudeGroup.children.getArray().filter((dude: Dude) => {
111 | return dude.clientId && dude.clientId === res.clientId
112 | })
113 | if (dudes[0]) {
114 | let b = res.updates
115 | let updates = {
116 | left: b === 1 || b === 5 ? true : false,
117 | right: b === 2 || b === 6 ? true : false,
118 | up: b === 4 || b === 6 || b === 5 ? true : false,
119 | none: b === 8 ? true : false
120 | }
121 | dudes[0].setUpdates(updates)
122 | }
123 | })
124 |
125 | this.events.addListener('removeDude', (clientId: number) => {
126 | // @ts-ignore
127 | this.dudeGroup.children.iterate((dude: Dude) => {
128 | if (dude.clientId === clientId) {
129 | dude.kill()
130 | }
131 | })
132 | })
133 |
134 | this.physics.add.collider(this.dudeGroup, this.boxGroup)
135 | this.physics.add.collider(this.mummyGroup, this.boxGroup)
136 | // @ts-ignore
137 | this.physics.add.overlap(this.mummyGroup, this.dudeGroup, (mummy: Mummy, dude: Dude) => {
138 | if (mummy.dead) return
139 | if (mummy.body.touching.up && dude.body.touching.down) {
140 | dude.setVelocityY(-300)
141 | mummy.kill()
142 | } else {
143 | dude.gotHit()
144 | }
145 | })
146 | // @ts-ignore
147 | this.physics.add.overlap(this.dudeGroup, this.star, (dude: Dude, star: Star) => {
148 | if (dude.dead) return
149 | dude.kill()
150 |
151 | let nextLevel = this.level + 1 >= this.map.countTotalLevels() ? 0 : this.level + 1
152 | let socket = this.roomManager.ioNspGame.sockets[dude.socketId] as any
153 |
154 | this.roomManager.changeRoom(socket, 'ArcadeScene', nextLevel)
155 | })
156 | }
157 |
158 | /** Sends the initial state to the client */
159 | getInitialState() {
160 | let objects: any[] = []
161 |
162 | SyncManager.prepareFromPhaserGroup(this.boxGroup, objects)
163 | SyncManager.prepareFromPhaserGroup(this.dudeGroup, objects)
164 | SyncManager.prepareFromPhaserSprite(this.star, objects)
165 |
166 | return SyncManager.encode(objects)
167 | }
168 |
169 | update() {
170 | this.tick++
171 | if (this.tick > 1000000) this.tick = 0
172 |
173 | // @ts-ignore
174 | this.mummyGroup.children.iterate((mummy: Mummy) => {
175 | let coordinates = mummy.getLookAhead()
176 | let tile = this.map.getTileByCoordinates(coordinates)
177 | mummy.changeDirection(tile)
178 | mummy.update()
179 | })
180 |
181 | if (PHYSICS_DEBUG) {
182 | this.debug.cursors.update()
183 | let cursorsDown = this.debug.cursors.cursorsDown()
184 | let dude: Dude = this.debug.dude
185 | dude.setUpdates(cursorsDown)
186 | dude.update()
187 | this.cameras.main.setScroll(
188 | dude.body.position.x - this.cameras.main.width / 2,
189 | dude.body.position.y - this.cameras.main.height * 0.8
190 | )
191 | }
192 |
193 | if (PHYSICS_DEBUG) return
194 |
195 | const prepareObjectToSync = (obj: any) => {
196 | let cleanObjectToSync = SyncManager.cleanObjectToSync(obj)
197 | this.objectsToSync = SyncManager.mergeObjectToSync(cleanObjectToSync, this.objectsToSync)
198 | }
199 |
200 | if (this.star && this.star.sync) {
201 | let starObj = {
202 | skin: this.star.skin,
203 | tint: this.star.tint,
204 | id: this.star.id,
205 | x: this.star.body.position.x + this.star.body.width / 2,
206 | y: this.star.body.position.y + this.star.body.height / 2
207 | }
208 | prepareObjectToSync(starObj)
209 | this.star.sync = false
210 | }
211 |
212 | // @ts-ignore
213 | this.mummyGroup.children.iterate((child: Mummy) => {
214 | let object = {
215 | skin: child.skin,
216 | direction: child.direction,
217 | id: child.id,
218 | x: child.body.position.x + child.body.width / 2,
219 | y: child.body.position.y + child.body.height / 2
220 | }
221 | prepareObjectToSync(object)
222 | })
223 |
224 | // @ts-ignore
225 | this.boxGroup.children.iterate((child: Box) => {
226 | if (child.sync) {
227 | let object = {
228 | skin: child.skin,
229 | id: child.id,
230 | x: child.body.position.x + child.body.width / 2,
231 | y: child.body.position.y + child.body.height / 2
232 | }
233 | prepareObjectToSync(object)
234 | }
235 | child.sync = false
236 | })
237 | // @ts-ignore
238 | this.dudeGroup.children.iterate((child: Dude) => {
239 | child.update()
240 | // we only update the dude if one if the 4 properties below have changed
241 | let x = child.prevPosition.x.toFixed(0) !== child.body.position.x.toFixed(0)
242 | let y = child.prevPosition.y.toFixed(0) !== child.body.position.y.toFixed(0)
243 | let dead = child.prevDead !== child.dead
244 | let color = child.prevColor.toString() !== child.color.toString()
245 | if (x || y || dead || color) {
246 | let object = {
247 | animation: child.animation,
248 | dead: child.dead,
249 | clientId: child.clientId,
250 | skin: child.skin,
251 | tint: child.color,
252 | id: child.id,
253 | x: child.body.position.x + child.body.width / 2,
254 | y: child.body.position.y + child.body.height / 2
255 | }
256 | prepareObjectToSync(object)
257 | }
258 | child.postUpdate()
259 | })
260 |
261 | let send: any[] = []
262 |
263 | Object.keys(this.objectsToSync).forEach(key => {
264 | // we only sync the mummies on every 3th frame
265 | if (this.objectsToSync[key].skin === SKINS.MUMMY) {
266 | if (this.tick % 3 === 0) {
267 | send.push(this.objectsToSync[key])
268 | delete this.objectsToSync[key]
269 | }
270 | } else {
271 | send.push(this.objectsToSync[key])
272 | delete this.objectsToSync[key]
273 | }
274 | })
275 |
276 | if (send.length > 0) {
277 | // send the objects to sync to all connected clients in this.roomId
278 | this.roomManager.ioNspGame
279 | .in(this.roomId)
280 | .emit('S' /* short for syncGame */, { O /* short for objects */: SyncManager.encode(send) })
281 | }
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/src/server/game/scenes/matterScene.ts:
--------------------------------------------------------------------------------
1 | import GameObjectGroup from '../matterObjects/matterGameObjectGroup'
2 | import MatterGameObject from '../matterObjects/matterGameObject'
3 | import Dude from '../matterObjects/dude'
4 | import Star from '../matterObjects/star'
5 | import { world } from '../../../client/config'
6 |
7 | // PHYSICS_DEBUG
8 | import Cursors from '../../../client/components/cursors'
9 | import SyncManager from '../../managers/syncManager'
10 | import RoomManager from '../../managers/roomManager'
11 | import { SKINS } from '../../../constants'
12 |
13 | export default class MainScene extends Phaser.Scene {
14 | objects: MatterGameObject[] = []
15 | objectsToSync: any = {}
16 | debug: any = {}
17 | tick = 0
18 | level: number
19 | roomManager: RoomManager
20 | roomId: string
21 |
22 | constructor() {
23 | super({ key: 'MainScene', plugins: PHYSICS_DEBUG ? null : ['Clock'] })
24 | // see all scene plugins:
25 | // Phaser.Plugins.DefaultScene
26 | // https://github.com/photonstorm/phaser/blob/master/src/plugins/DefaultPlugins.js#L76
27 | }
28 |
29 | init() {
30 | try {
31 | //@ts-ignore
32 | const { level = 0, roomId, roomManager } = this.game.config.preBoot()
33 | this.level = level
34 | this.roomManager = roomManager
35 | this.roomId = roomId
36 | } catch (error) {
37 | if (!PHYSICS_DEBUG) console.error('onInit() failed!')
38 | }
39 | }
40 |
41 | create() {
42 | const Matter = Phaser.Physics.Matter.Matter
43 | const worldCenterX = (world.x + world.width) / 2
44 |
45 | // add and modify the world bounds
46 | let bounds: any = this.matter.world.setBounds(world.x, world.y, world.width, world.height)
47 | Object.keys(bounds.walls).forEach((key: any) => {
48 | let body = bounds.walls[key]
49 | Matter.Body.set(body, { friction: 0.05, frictionStatic: 0.05, frictionAir: 0.01 })
50 | // we do not need the top, so we set it to isSensor
51 | if (key === 'top') Matter.Body.set(body, { isSensor: true })
52 | })
53 |
54 | // instantiate the GameObjectGroup
55 | let gameObjectGroup = new GameObjectGroup(this, this.objects)
56 |
57 | // this will stop the scene
58 | this.events.addListener('stopScene', () => {
59 | this.objects.forEach(obj => {
60 | this.matter.world.remove(this.matter.world, obj.body)
61 | })
62 | this.roomManager.stats.removeTotalObjects(this.roomId)
63 | this.scene.stop()
64 | this.roomManager.stats.log(`Scene in roomId ${this.roomId} has stopped!`)
65 | })
66 |
67 | // creates a new dude, when a new user connects
68 | this.events.addListener('createDude', (clientId: number, socketId: string) => {
69 | let leftX = Phaser.Math.RND.integerInRange(world.x + 100, this.cameras.main.width / 2 - 640)
70 | let rightX = Phaser.Math.RND.integerInRange(this.cameras.main.width / 2 + 640, world.x + world.width - 100)
71 | let x = Math.random() > 0.5 ? leftX : rightX
72 | let y = -50
73 | gameObjectGroup.add(x, y, SKINS.DUDE, { clientId, socketId })
74 | })
75 |
76 | // updates the position of a dude
77 | this.events.addListener('U' /* short for updateDude */, (res: any) => {
78 | let dudes: Dude[] = this.objects.filter(obj => obj.clientId && obj.clientId === res.clientId) as any
79 | if (dudes[0]) {
80 | let b = res.updates
81 | let updates = {
82 | left: b === 1 || b === 5 ? true : false,
83 | right: b === 2 || b === 6 ? true : false,
84 | up: b === 4 || b === 6 || b === 5 ? true : false,
85 | none: b === 8 ? true : false
86 | }
87 | dudes[0].setUpdates(updates)
88 | }
89 | })
90 |
91 | // removes a dude
92 | this.events.addListener('removeDude', (clientId: number) => {
93 | let dudes = this.objects.filter(obj => obj.clientId && obj.clientId === clientId)
94 | dudes.forEach(dude => dude.kill())
95 | })
96 |
97 | // adds another box every 1.2 seconds
98 | this.time.addEvent({
99 | delay: 1200,
100 | loop: true,
101 | callback: () => {
102 | let x = Phaser.Math.RND.integerInRange(worldCenterX - 250 - 640, worldCenterX + 640 + 250)
103 | let y = 100
104 | gameObjectGroup.add(x, y, SKINS.BOX)
105 | }
106 | })
107 |
108 | if (PHYSICS_DEBUG) {
109 | this.add
110 | .text(24, 24, 'Physics Debugging Version\nMove with Arrow Keys', {
111 | fontSize: 36
112 | })
113 | .setScrollFactor(0)
114 | .setOrigin(0)
115 | .setAlpha(0.6)
116 | this.debug.socket = { emit: () => {} } // mock socket
117 | this.debug.cursors = new Cursors(this, this.debug.socket)
118 | this.debug.dude = gameObjectGroup.add(400, 400, SKINS.DUDE, { clientId: 55555, socketId: 'some-socket-id' })
119 | }
120 |
121 | if (!PHYSICS_DEBUG) {
122 | this.time.addEvent({
123 | delay: 5000,
124 | loop: true,
125 | callback: () => {
126 | this.roomManager.stats.setTotalObjects(this.roomId, this.objects.length)
127 | }
128 | })
129 | }
130 |
131 | // add the big star
132 | gameObjectGroup.add(worldCenterX, world.height - 320 - 100 - 115, SKINS.STAR, {
133 | category: 'big'
134 | })
135 |
136 | // add medium stars
137 | for (let x = worldCenterX - 128; x < worldCenterX + 128 + 64; x += 128)
138 | gameObjectGroup.add(x, world.height - 320 - 100, SKINS.STAR, { category: 'medium' })
139 |
140 | // add yellow stars
141 | for (let x = worldCenterX - 160 - 80; x < worldCenterX + 320 + 80; x += 160)
142 | gameObjectGroup.add(x, world.height - 320, SKINS.STAR)
143 |
144 | // create 4 boxes at server start
145 | gameObjectGroup.add(1280, 640, SKINS.BOX)
146 | gameObjectGroup.add(1280, 640, SKINS.BOX)
147 | gameObjectGroup.add(1280, 640, SKINS.BOX)
148 | gameObjectGroup.add(1280, 640, SKINS.BOX)
149 |
150 | // check for collisions
151 | const collisionEvent = (event: any) => {
152 | event.pairs.forEach((pair: any) => {
153 | const { bodyA, bodyB } = pair
154 | const labels: string[] = [bodyA.label, bodyB.label]
155 |
156 | // Dude hits star
157 | if (labels.includes('dude') && labels.includes('star')) {
158 | let starBody = bodyA.label === 'star' ? bodyA : bodyB
159 | let star: Star = gameObjectGroup.getObjectById(starBody.id) as any
160 | if (star) {
161 | star.kill()
162 | star.setReviveTimer()
163 | }
164 | }
165 |
166 | // Dude's sensor hits another body
167 | if (/Sensor/.test(bodyA.label) || /Sensor/.test(bodyB.label)) {
168 | let sensorBody = /Sensor/.test(bodyA.label) ? bodyA : bodyB
169 | let otherBody = /Sensor/.test(bodyA.label) ? bodyB : bodyA
170 | if (otherBody.isSensor) return
171 |
172 | let dude: Dude = gameObjectGroup.getObjectById(sensorBody.parent.id) as any
173 | if (dude) {
174 | let sepPadding = 2
175 | if (otherBody.isStatic) {
176 | sepPadding = 0.1
177 | }
178 |
179 | let sep = pair.separation - sepPadding
180 |
181 | if (sensorBody === dude.sensors.left) {
182 | dude.move.leftAllowed = !otherBody.isStatic
183 | dude.touching.left = true
184 | if (pair.separation > sepPadding) {
185 | dude.setTranslate(sep)
186 | dude.translate()
187 | }
188 | } else if (sensorBody === dude.sensors.right) {
189 | dude.move.rightAllowed = !otherBody.isStatic
190 | dude.touching.right = true
191 | if (pair.separation > sepPadding) {
192 | dude.setTranslate(-sep)
193 | dude.translate()
194 | }
195 | } else if (sensorBody === dude.sensors.bottom) {
196 | dude.touching.bottom = true
197 | }
198 | }
199 | }
200 | })
201 | }
202 | // https://itnext.io/modular-game-worlds-in-phaser-3-tilemaps-5-matter-physics-platformer-d14d1f614557
203 | this.matter.world.on('collisionstart', collisionEvent)
204 | this.matter.world.on('collisionactive', collisionEvent)
205 | }
206 |
207 | /** Sends the initial state to the client */
208 | getInitialState() {
209 | let objects: any[] = []
210 | SyncManager.prepareFromMatterGameObject(this.objects, objects)
211 | return SyncManager.encode(objects)
212 | }
213 |
214 | update(time: number, delta: number) {
215 | this.tick++
216 | if (this.tick > 1000000) this.tick = 0
217 |
218 | if (PHYSICS_DEBUG) {
219 | this.debug.cursors.update()
220 | let cursorsDown = this.debug.cursors.cursorsDown()
221 | let dude: Dude = this.debug.dude
222 | dude.setUpdates(cursorsDown)
223 | dude.update()
224 | this.cameras.main.setScroll(
225 | dude.body.position.x - this.cameras.main.width / 2,
226 | dude.body.position.y - this.cameras.main.height * 0.8
227 | )
228 | }
229 |
230 | if (!PHYSICS_DEBUG) {
231 | this.objects.forEach(obj => {
232 | if (obj.body.position.y > world.height) obj.kill()
233 |
234 | obj.preUpdate()
235 | obj.update()
236 |
237 | const roundToEvenNumber = (number: number) => {
238 | try {
239 | return +(Math.round(number / 2) * 2).toFixed(0)
240 | } catch (e) {
241 | return 0
242 | }
243 | }
244 |
245 | // only send the object to the client if one of these properties have changed
246 | let dead = obj.dead != obj.prevDead
247 | let x = obj.body.position.x.toFixed(0) != obj.body.positionPrev.x.toFixed(0)
248 | let y = obj.body.position.y.toFixed(0) != obj.body.positionPrev.y.toFixed(0)
249 | let angle = roundToEvenNumber(obj.angle) != roundToEvenNumber(obj.prevAngle)
250 | let animation = obj.animation !== obj.prevAnimation
251 | if (dead || x || y || angle || animation) {
252 | let theObj: { [key: string]: any } = {
253 | // it always needs to have an id!
254 | id: obj.body.id,
255 | x: +obj.body.position.x.toFixed(0),
256 | y: +obj.body.position.y.toFixed(0),
257 | angle: angle ? roundToEvenNumber(obj.angle) : null,
258 | dead: dead ? obj.dead : null,
259 | animation: obj.animation ? obj.animation : null,
260 | clientId: obj.clientId ? obj.clientId : null,
261 | skin: obj.skin
262 | }
263 | let cleanObjectToSync = SyncManager.cleanObjectToSync(theObj)
264 | this.objectsToSync = SyncManager.mergeObjectToSync(cleanObjectToSync, this.objectsToSync)
265 | }
266 |
267 | // call the postUpdate function on all gameObjects
268 | obj.postUpdate()
269 | })
270 |
271 | let send: any[] = []
272 | Object.keys(this.objectsToSync).forEach(key => {
273 | // this syncs the dude on every frame
274 | // but the boxes only on every second frame
275 | // (safes a lot of bandwidth)
276 | if (this.objectsToSync[key].skin === SKINS.BOX) {
277 | if (this.tick % 2 === 0) {
278 | send.push(this.objectsToSync[key])
279 | delete this.objectsToSync[key]
280 | }
281 | } else {
282 | send.push(this.objectsToSync[key])
283 | delete this.objectsToSync[key]
284 | }
285 | })
286 |
287 | if (send.length > 0) {
288 | // send the objects to sync to all connected clients in this.roomId
289 | this.roomManager.ioNspGame
290 | .in(this.roomId)
291 | .emit('S' /* short for syncGame */, { O /* short for objects */: SyncManager.encode(send) })
292 | }
293 | }
294 | }
295 | }
296 |
--------------------------------------------------------------------------------
/src/server/managers/roomManager.ts:
--------------------------------------------------------------------------------
1 | interface GameScene extends Phaser.Scene {
2 | objects: any
3 | }
4 | interface User {
5 | id: string
6 | lastUpdate: number
7 | clientId: number
8 | roomId: string
9 | }
10 | interface Users {
11 | [userId: string]: User
12 | }
13 | interface Room {
14 | roomId: string
15 | // I am not sure if it is safe to publish the roomId to the client
16 | // so we only create a second id
17 | // - If you know if it is safe or not, please tell me :) -
18 | //publicRoomId: string
19 | game: Phaser.Game
20 | scene: GameScene
21 | removing: boolean
22 | users: Users
23 | level: number
24 | sceneKey: string
25 | }
26 | interface Rooms {
27 | [room: string]: Room
28 | }
29 | import Game, { PhaserGame } from '../game/game'
30 | import { Math as phaserMath } from 'phaser'
31 | import { MAX_PLAYERS_PER_ROOM, USER_KICK_TIMEOUT } from '../../constants'
32 |
33 | let randomDataGenerator = new phaserMath.RandomDataGenerator()
34 |
35 | import { v4 as uuidv4 } from 'uuid'
36 | import Stats from '../socket/ioStats'
37 |
38 | export default class RoomManager {
39 | rooms: Rooms = {}
40 |
41 | constructor(public ioNspGame: SocketIO.Namespace, public stats: Stats) {
42 | setInterval(() => {
43 | this.removeInactiveRooms()
44 | this.removeInactiveUsers()
45 | }, 10000)
46 | }
47 |
48 | generateClientId(socket: Socket) {
49 | let clientId = randomDataGenerator.integerInRange(100000, 100000000)
50 | socket.clientId = clientId
51 | socket.emit('clientId', clientId)
52 | }
53 |
54 | // the 2 functions below should be better
55 | async joinRoom(socket: Socket, scene: string, level: number) {
56 | if (typeof scene !== 'string' || typeof level !== 'number') {
57 | console.error('level or scene is not defined in ioGame.ts')
58 | return
59 | }
60 | socket.room = this.chooseRoom({ scene: scene, level: +level })
61 |
62 | // create a new game instance if this room does not exist yet
63 | if (!this.rooms[socket.room]) {
64 | await this.createRoom(socket.room, scene, +level)
65 | }
66 |
67 | this.addUser(socket)
68 | this.rooms[socket.room].scene.events.emit('createDude', socket.clientId, socket.id)
69 | }
70 |
71 | leaveRoom(socket: Socket) {
72 | this.removeUser(socket.room, socket.id)
73 | this.ioNspGame
74 | .in(socket.room)
75 | .emit('S' /* short for syncGame */, { connectCounter: this.getRoomUsersArray(socket.room).length })
76 |
77 | if (this.isRemoving(socket.room)) return
78 | this.rooms[socket.room].scene.events.emit('removeDude', socket.clientId)
79 | }
80 |
81 | async changeRoom(socket: Socket, scene: string, level: number) {
82 | this.leaveRoom(socket)
83 | await this.joinRoom(socket, scene, +level)
84 | socket.emit('changingRoom', { scene: scene, level: +level })
85 | }
86 |
87 | addUser(socket: Socket) {
88 | let newUsers: Users = {
89 | [socket.id]: {
90 | roomId: socket.room,
91 | lastUpdate: Date.now(),
92 | clientId: socket.clientId,
93 | id: socket.id
94 | }
95 | }
96 |
97 | this.rooms[socket.room].users = {
98 | ...this.rooms[socket.room].users,
99 | ...newUsers
100 | }
101 | // join the socket room
102 | socket.join(socket.room)
103 | }
104 |
105 | /** Removed the user from the room */
106 | removeUser(roomId: string, userId: string, log: boolean = true) {
107 | if (this.ioNspGame.sockets[userId]) this.ioNspGame.sockets[userId].leave(roomId)
108 |
109 | if (this.userExists(roomId, userId)) {
110 | delete this.rooms[roomId].users[userId]
111 | if (log) this.stats.log(`User ${userId} disconnected!`)
112 | return true
113 | }
114 | return false
115 | }
116 |
117 | /** Check if this user exists */
118 | userExists(roomId: string, userId: string) {
119 | if (this.roomExists(roomId) && this.rooms[roomId].users && this.rooms[roomId].users[userId]) return true
120 | return false
121 | }
122 |
123 | /** Check if this room exists */
124 | roomExists(roomId: string) {
125 | if (this.rooms && this.rooms[roomId]) return true
126 | return false
127 | }
128 |
129 | isRemoving(roomId: string) {
130 | if (!!!this.rooms[roomId] || this.rooms[roomId].removing) return true
131 | else return false
132 | }
133 |
134 | createRoom = async (roomId: string, scene: string, level: number) => {
135 | this.stats.log(`Create new room ${roomId}`)
136 |
137 | let game: PhaserGame = await Game(this, roomId, { scene, level })
138 |
139 | this.rooms[roomId] = {
140 | sceneKey: scene,
141 | level: +level,
142 | roomId: roomId,
143 | users: {},
144 | game: game,
145 | // @ts-ignore
146 | scene: game.scene.keys['MainScene'],
147 | removing: false
148 | }
149 |
150 | this.stats.log(`Room ${roomId} created!`)
151 | }
152 |
153 | removeRoom = async (roomId: string) => {
154 | if (this.rooms[roomId].removing) return
155 | this.stats.log(`Removing room ${roomId}`)
156 | this.rooms[roomId].removing = true
157 | this.rooms[roomId].scene.events.emit('stopScene')
158 |
159 | setTimeout(async () => {
160 | await this.rooms[roomId].game.destroy(true, true)
161 | // @ts-ignore
162 | this.rooms[roomId].game = null
163 | delete this.rooms[roomId]
164 |
165 | this.stats.log(`Room ${roomId} has been removed!`)
166 | this.stats.log(`Remaining rooms: ${Object.keys(this.rooms).length}`)
167 | }, 5000)
168 | }
169 |
170 | chooseRoom = (props: { scene: string; level: number }): string => {
171 | const { scene, level } = props
172 |
173 | let rooms = Object.keys(this.rooms)
174 |
175 | if (rooms.length === 0) return uuidv4()
176 |
177 | // check for the next room with 1 or more free spaces
178 | let chosenRoom = null
179 | for (let i = 0; i < Object.keys(this.rooms).length; i++) {
180 | let room = this.rooms[rooms[i]]
181 | let count = Object.keys(room.users).length
182 | if (
183 | count < MAX_PLAYERS_PER_ROOM &&
184 | room.sceneKey === scene &&
185 | room.level === level &&
186 | !this.isRemoving(rooms[i])
187 | ) {
188 | chosenRoom = rooms[i]
189 | break
190 | }
191 | }
192 | if (chosenRoom) return chosenRoom
193 |
194 | // create a new room with a new uuidv4 id
195 | return uuidv4()
196 | }
197 |
198 | getRoomsArray() {
199 | let rooms: Room[] = []
200 | Object.keys(this.rooms).forEach((roomId) => {
201 | rooms.push(this.rooms[roomId])
202 | })
203 | return rooms
204 | }
205 |
206 | /** Returns an Array of all users in a specific room */
207 | getRoomUsersArray(roomId: string) {
208 | let users: User[] = []
209 |
210 | if (!this.roomExists(roomId)) return users
211 |
212 | Object.keys(this.rooms[roomId].users).forEach((userId) => {
213 | users.push(this.rooms[roomId].users[userId])
214 | })
215 | return users
216 | }
217 |
218 | /** Returns an Array of all users in all rooms */
219 | getAllUsersArray() {
220 | let users: User[] = []
221 | Object.keys(this.rooms).forEach((roomId) => {
222 | Object.keys(this.rooms[roomId].users).forEach((userId) => {
223 | users.push(this.rooms[roomId].users[userId])
224 | })
225 | })
226 | return users
227 | }
228 |
229 | disconnectUser(userId: string) {
230 | if (this.ioNspGame.connected && this.ioNspGame.connected[userId]) {
231 | this.ioNspGame.connected[userId].disconnect(true)
232 | return true
233 | }
234 | return false
235 | }
236 |
237 | removeInactiveRooms() {
238 | this.getRoomsArray().forEach((room: Room) => {
239 | if (!room.users || Object.keys(room.users).length === 0) this.removeRoom(room.roomId)
240 | })
241 | }
242 |
243 | removeInactiveUsers() {
244 | this.getAllUsersArray().forEach((user: User) => {
245 | if (Date.now() - user.lastUpdate > USER_KICK_TIMEOUT) {
246 | let removed = this.removeUser(user.roomId, user.id, false)
247 | let disconnected = this.disconnectUser(user.id)
248 | if (removed && disconnected) {
249 | this.stats.log(`Kick user ${user.clientId} from room ${user.roomId}`)
250 | }
251 | }
252 | })
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/src/server/managers/syncManager.ts:
--------------------------------------------------------------------------------
1 | import MatterGameObject from '../game/matterObjects/matterGameObject'
2 |
3 | /** Helps preparing the object to sync with the client */
4 | export default class SyncManager {
5 | constructor() {}
6 |
7 | static prepareFromPhaserGroup(group: Phaser.GameObjects.Group, objects: any) {
8 | group.children.iterate((sprite: any) => {
9 | SyncManager.prepareFromPhaserSprite(sprite, objects)
10 | })
11 | }
12 |
13 | static prepareFromPhaserSprite(sprite: any, objects: any) {
14 | let obj = {
15 | ...sprite,
16 | ...this.getXY(sprite)
17 | }
18 | objects.push(SyncManager.cleanObjectToSync(obj))
19 | }
20 |
21 | static prepareFromMatterGameObject(gameObjects: MatterGameObject[], objects: any) {
22 | gameObjects.forEach(obj => {
23 | objects.push(SyncManager.cleanObjectToSync(obj))
24 | })
25 | }
26 |
27 | static getXY(child: any) {
28 | return { x: child.body.position.x + child.body.width / 2, y: child.body.position.y + child.body.height / 2 }
29 | }
30 |
31 | static mergeObjectToSync(obj: any, mergeTo: any[]) {
32 | let merged = false
33 | Object.keys(mergeTo).forEach(o => {
34 | if (o === obj.id) {
35 | mergeTo[obj.id] = {
36 | ...mergeTo[obj.id],
37 | ...obj
38 | }
39 | merged = true
40 | }
41 | })
42 | if (!merged)
43 | mergeTo = {
44 | ...mergeTo,
45 | [obj.id]: obj
46 | }
47 | return mergeTo
48 | }
49 |
50 | static cleanObjectToSync(obj: any) {
51 | const addToObjectToSync = (key: string, prop: any) => {
52 | if (prop !== null) objectToSync = { ...objectToSync, [key]: prop }
53 | }
54 |
55 | let objectToSync: { [key: string]: any } = {}
56 |
57 | addToObjectToSync('id', obj.id || obj.body.id)
58 | addToObjectToSync('x', obj.x || obj.body.position.x || null)
59 | addToObjectToSync('y', obj.y || obj.body.position.y || null)
60 | addToObjectToSync('angle', obj.angle !== 'undefined' ? obj.angle : null)
61 | addToObjectToSync('dead', obj.dead !== 'undefined' ? obj.dead : null)
62 | addToObjectToSync('skin', obj.skin !== 'undefined' ? obj.skin : null)
63 | addToObjectToSync('animation', obj.animation || null)
64 | addToObjectToSync('direction', obj.direction || null)
65 | addToObjectToSync('scale', obj.scale && obj.scale !== 1 ? obj.scale : null)
66 | addToObjectToSync('tint', obj.tint ? obj.tint : null)
67 | addToObjectToSync('clientId', obj.clientId || null)
68 | addToObjectToSync('category', obj.category || null)
69 |
70 | // Object.keys(objectToSync).forEach(key => objectToSync[key] == null && delete objectToSync[key])
71 |
72 | return objectToSync
73 | }
74 |
75 | static get keys() {
76 | // sort these based on most used
77 | return ['id', 'x', 'y', 'angle', 'dead', 'skin', 'animation', 'direction', 'scale', 'tint', 'clientId', 'category']
78 | }
79 |
80 | static decode(data: any) {
81 | const keys = SyncManager.keys
82 | let decodedArray: any[] = []
83 |
84 | let obj: any = {}
85 | data.split(',').forEach((value: string, index: number) => {
86 | let key = keys[index % keys.length]
87 |
88 | // id (radix 36)
89 | if (key === 'id') {
90 | obj[key] = parseInt(value, 36).toString()
91 | }
92 | // numbers
93 | else if (['skin', 'scale'].includes(key)) {
94 | obj[key] = value !== '' ? parseInt(value) : null
95 | }
96 | // numbers (radix 36)
97 | else if (['x', 'y', 'angle', 'clientId'].includes(key)) {
98 | obj[key] = value !== '' ? parseInt(value, 36) : null
99 | }
100 | // booleans
101 | else if (['dead'].includes(key)) {
102 | obj[key] = value === '0' ? false : value === '1' ? true : null
103 | }
104 | // strings
105 | else obj[key] = value !== '' ? value : null
106 |
107 | if (index % keys.length === keys.length - 1) {
108 | decodedArray.push({ ...obj })
109 | obj = {}
110 | }
111 | })
112 |
113 | return decodedArray
114 | }
115 |
116 | static encode(objs: any[]) {
117 | const keys = SyncManager.keys
118 |
119 | let encodedString = ''
120 | objs.forEach((obj: any) => {
121 | keys.forEach(key => {
122 | if (typeof obj[key] !== 'undefined') {
123 | let value = obj[key]
124 |
125 | // booleans
126 | if (typeof obj[key] === 'boolean') value = obj[key] === false ? 0 : 1
127 | // some numbers to radix 36
128 | else if (['id', 'x', 'y', 'angle', 'clientId'].includes(key)) {
129 | value = +value
130 | value = +value.toFixed(0)
131 | value = value.toString(36)
132 | }
133 |
134 | encodedString += `${value},`
135 | } else encodedString += ','
136 | })
137 | })
138 |
139 | return encodedString.slice(0, -1)
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/server/routes/routes.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import pidusage from 'pidusage'
3 | import path from 'path'
4 | import RoomManager from '../managers/roomManager'
5 | import IoStats from '../socket/ioStats'
6 |
7 | export default class Routes {
8 | router: express.Router
9 | time = new Date()
10 |
11 | constructor(public roomManager: RoomManager, public ioStats: IoStats) {
12 | this.router = express.Router()
13 |
14 | this.router.get('/', (req, res) => {
15 | res.send(`
16 |
17 |
18 |
19 |
20 |
21 |
22 | Phaser 3: Multiplayer Example
23 |
24 |
25 |
43 | Phaser 3: Real-Time Multiplayer Game with Physics
44 | Play the Game
45 | Debug the Physics
46 | View Server Stats
47 | `)
48 | })
49 |
50 | this.router.get('/play', (req, res) => {
51 | res.sendFile(path.join(__dirname, '../../dist/client/index.html'))
52 | })
53 |
54 | this.router.get('/physics', (req, res) => {
55 | res.send(`
56 |
57 |
58 |
59 |
60 |
61 |
62 | Document
63 |
64 |
65 |
73 |
77 | `)
78 | })
79 |
80 | this.router.get('/matter', (req, res) => {
81 | res.sendFile(path.join(__dirname, '../../dist/physics/index.html'))
82 | })
83 | this.router.get('/arcade', (req, res) => {
84 | res.sendFile(path.join(__dirname, '../../dist/physics/index.html'))
85 | })
86 |
87 | this.router.get('/stats', (req, res) => {
88 | res.sendFile(path.join(__dirname, '../../dist/stats/index.html'))
89 | })
90 |
91 | this.router.get('/stats/get', (req, res) => {
92 | pidusage(process.pid, (err, stats) => {
93 | if (err) return res.status(500).json({ err: err })
94 |
95 | let objects = ioStats.getTotalObjects()
96 |
97 | let payload = {
98 | ...stats,
99 | users: roomManager.getAllUsersArray().length,
100 | rooms: roomManager.getRoomsArray().length,
101 | objects: objects,
102 | time: this.time
103 | }
104 | res.json({ payload })
105 | })
106 | })
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/server/server.ts:
--------------------------------------------------------------------------------
1 | import 'source-map-support/register'
2 |
3 | import express from 'express'
4 | import helmet from 'helmet'
5 | import compression from 'compression'
6 | import path from 'path'
7 |
8 | const app = express()
9 | const server = require('http').Server(app)
10 | import SocketIOStatic from 'socket.io'
11 | const io = SocketIOStatic(server)
12 |
13 | import RoomManager from './managers/roomManager'
14 | import Routes from './routes/routes'
15 | import IoStats from './socket/ioStats'
16 | import IoGame from './socket/ioGame'
17 |
18 | const port = process.env.PORT || 3000
19 |
20 | // create 2 socket.io namespaces
21 | const ioNspGame = io.of('/G' /* short for stats */)
22 | const ioNspStats = io.of('/S' /* short for stats */)
23 |
24 | const ioStats = new IoStats(ioNspStats)
25 | const roomManager = new RoomManager(ioNspGame, ioStats)
26 | const ioGame = new IoGame(ioNspGame, ioStats, roomManager)
27 |
28 | app.use(helmet())
29 | app.use(compression())
30 |
31 | app.use('/static', express.static(path.join(__dirname, '../')))
32 | app.use('/', new Routes(roomManager, ioStats).router)
33 |
34 | server.listen(port, () => {
35 | console.log('App is listening on http://localhost:' + port)
36 | })
37 |
--------------------------------------------------------------------------------
/src/server/socket/ioGame.ts:
--------------------------------------------------------------------------------
1 | import RoomManager from '../managers/roomManager'
2 | import IoStats from './ioStats'
3 |
4 | /** Handles all the communication for /game namespace (ioNspGame) */
5 | export default class IoGame {
6 | time = new Date()
7 |
8 | constructor(public ioNspGame: SocketIO.Namespace, public ioStats: IoStats, public roomManager: RoomManager) {
9 | ioNspGame.on('connection', async (socket: Socket) => {
10 | roomManager.generateClientId(socket)
11 |
12 | socket.on('joinRoom', async (data: { scene: string; level: number }) => {
13 | const { scene, level } = data
14 | await roomManager.joinRoom(socket, scene, +level)
15 | ioStats.log(`New user ${socket.id} connected! to room ${socket.room}`)
16 | })
17 |
18 | socket.on('disconnect', () => {
19 | roomManager.leaveRoom(socket)
20 | })
21 |
22 | socket.on('changeRoom', (data: { scene: string; level: number }) => {
23 | roomManager.changeRoom(socket, data.scene, +data.level)
24 | })
25 |
26 | socket.on('sendPing', (id: string) => {
27 | socket.emit('getPong', id)
28 | })
29 |
30 | socket.on('U' /* short for updateDude */, (updates: any) => {
31 | if (roomManager.isRemoving(socket.room)) return
32 | if (!roomManager.userExists(socket.room, socket.id)) return
33 |
34 | roomManager.rooms[socket.room].users[socket.id].lastUpdate = Date.now()
35 | roomManager.rooms[socket.room].scene.events.emit('U' /* short for updateDude */, {
36 | clientId: socket.clientId,
37 | updates
38 | })
39 | })
40 |
41 | socket.on('getInitialState', () => {
42 | if (roomManager.isRemoving(socket.room)) return
43 | if (!roomManager.roomExists(socket.room)) return
44 |
45 | let payload = {
46 | time: this.time,
47 | // @ts-ignore
48 | O /* short for objects */: roomManager.rooms[socket.room].scene.getInitialState(),
49 | connectCounter: roomManager.getRoomUsersArray(socket.room).length,
50 | initialState: true,
51 | roomId: socket.room
52 | }
53 |
54 | socket.emit('S' /* short for syncGame */, payload)
55 | // ioNspGame.in(socket.room).emit('S' /* short for syncGame */, payload)
56 | })
57 | })
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/server/socket/ioStats.ts:
--------------------------------------------------------------------------------
1 | /** Handles all the communication for /stats namespace (ioNspGame) */
2 | export default class IoStats {
3 | totalObjects: { [roomId: string]: { count: number } } = {}
4 |
5 | constructor(public ioNspStats: SocketIO.Namespace) {}
6 |
7 | /** This function will console.log and send it to the ioStats */
8 | log(log: string, logInNode = false) {
9 | if (logInNode) console.log('LOG: ' + log)
10 | this.ioNspStats.emit('getLog', { date: new Date(), log: log })
11 | }
12 |
13 | /** Get the total of objects in the game */
14 | getTotalObjects() {
15 | let count = 0
16 | Object.keys(this.totalObjects).forEach(roomId => {
17 | count += this.totalObjects[roomId].count
18 | })
19 | return count
20 | }
21 |
22 | setTotalObjects(roomId: string, count: number) {
23 | this.totalObjects = { ...this.totalObjects, [roomId]: { count: count } }
24 | }
25 |
26 | removeTotalObjects(roomId: string) {
27 | if (this.totalObjects && this.totalObjects[roomId]) {
28 | delete this.totalObjects[roomId]
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/stats/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Stats Page
8 |
9 |
10 |
24 | Stats
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | Logs
34 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/stats/index.ts:
--------------------------------------------------------------------------------
1 | import io from 'socket.io-client'
2 | import axios from 'axios'
3 | import moment from 'moment'
4 |
5 | let url = `${location.origin}/S` /* short for stats */
6 | let stats = io.connect(url, { transports: ['websocket'] })
7 |
8 | let statsUl = document.getElementById('logs-ul')
9 |
10 | // on reconnection, reset the transports option, as the Websocket
11 | // connection may have failed (caused by proxy, firewall, browser, ...)
12 | stats.on('reconnect_attempt', () => {
13 | stats.io.opts.transports = ['polling', 'websocket']
14 | })
15 |
16 | stats.on('connect', () => {
17 | console.log("You're connected")
18 | })
19 |
20 | stats.on('getLog', (res: { date: Date; log: string }) => {
21 | if (statsUl) {
22 | let li = document.createElement('li')
23 | li.innerHTML = `${moment(res.date).format('h:mm:ss a')}: ${res.log}`
24 | statsUl.appendChild(li)
25 | }
26 | })
27 |
28 | const setInnerHTML = (id: string, text: string | number) => {
29 | let el = document.getElementById(id)
30 | if (el) el.innerHTML = text.toString()
31 | }
32 |
33 | const getNewServerStats = async () => {
34 | try {
35 | let res = await axios.get('/stats/get')
36 | if (!res || !res.data) throw new Error()
37 |
38 | const { payload } = res.data
39 |
40 | const { time, cpu, memory, rooms, users, objects } = payload
41 | setInnerHTML('cpu', `CPU: ${Math.round(cpu)}%`)
42 | setInnerHTML('memory', `Memory: ${Math.round(memory / 1000000)}mb`)
43 | setInnerHTML('rooms', `Rooms: ${rooms}`)
44 | setInnerHTML('users', `Users: ${users}`)
45 | setInnerHTML('objects', `Objects: ${objects}`)
46 | setInnerHTML('time', `Server started ${moment(time).fromNow()}`)
47 | } catch (error) {
48 | console.error(error.message)
49 | }
50 | }
51 | setInterval(getNewServerStats, 2000)
52 | getNewServerStats()
53 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "esnext",
5 | "lib": ["esnext", "dom"],
6 | "allowJs": false,
7 | "checkJs": false,
8 | "strict": true,
9 | "sourceMap": true,
10 | "noImplicitAny": true,
11 | "esModuleInterop": true,
12 | "moduleResolution": "node",
13 | "strictPropertyInitialization": false,
14 | "skipLibCheck": true
15 | },
16 | "include": ["src/**/*"],
17 | "exclude": ["node_modules"]
18 | }
19 |
--------------------------------------------------------------------------------
/typings/custom.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace Phaser {
2 | namespace Physics {
3 | namespace Matter {
4 | export const Matter: any
5 | export const matter: any
6 | }
7 | }
8 | }
9 |
10 | interface Window {
11 | game: Phaser.Game
12 | }
13 |
14 | interface Socket extends SocketIOClient.Socket {
15 | join: (roomId: string) => {}
16 | leave: (roomId: string) => {}
17 | scene: Phaser.Scene
18 | id: string
19 | clientId: number
20 | room: string
21 | }
22 |
23 | declare const PHYSICS_DEBUG: boolean
24 |
25 | interface Latency {
26 | current: number
27 | high: number
28 | low: number
29 | ping: number
30 | id: string
31 | canSend: boolean
32 | history: any[]
33 | }
34 |
--------------------------------------------------------------------------------
/webpack/webpack.client.cjs:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const HtmlWebpackPlugin = require('html-webpack-plugin')
3 | const CopyWebpackPlugin = require('copy-webpack-plugin')
4 | const webpack = require('webpack')
5 |
6 | module.exports = {
7 | mode: 'development',
8 | stats: 'errors-warnings',
9 | entry: ['./src/client/index.ts'],
10 | output: {
11 | publicPath: 'static/client',
12 | path: path.resolve(__dirname, '../dist/client'),
13 | filename: '[name].bundle.js',
14 | chunkFilename: '[name].chunk.js'
15 | },
16 | resolve: {
17 | extensions: ['.ts', '.tsx', '.js']
18 | },
19 | module: {
20 | rules: [
21 | {
22 | test: /\.tsx?$/,
23 | loader: 'ts-loader',
24 | options: {
25 | transpileOnly: true
26 | }
27 | }
28 | ]
29 | },
30 | optimization: {
31 | splitChunks: {
32 | cacheGroups: {
33 | commons: {
34 | test: /[\\/]node_modules[\\/]/,
35 | name: 'vendors',
36 | chunks: 'all',
37 | filename: '[name].bundle.js'
38 | }
39 | }
40 | }
41 | },
42 | plugins: [
43 | new HtmlWebpackPlugin({
44 | template: 'src/client/index.html'
45 | }),
46 | new CopyWebpackPlugin({ patterns: [{ from: 'src/client/assets', to: 'assets' }] }),
47 | new webpack.DefinePlugin({
48 | PHYSICS_DEBUG: JSON.stringify(false)
49 | })
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/webpack/webpack.client.prod.cjs:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge')
2 | const common = require('./webpack.client')
3 | const webpack = require('webpack')
4 |
5 | const prod = {
6 | mode: 'production',
7 | output: {
8 | filename: '[name].[contenthash].bundle.js',
9 | chunkFilename: '[name].[contenthash].chunk.js'
10 | },
11 | optimization: {
12 | splitChunks: {
13 | cacheGroups: {
14 | commons: {
15 | filename: '[name].[contenthash].bundle.js'
16 | }
17 | }
18 | }
19 | },
20 | plugins: [
21 | new webpack.DefinePlugin({
22 | 'process.env.NODE_ENV': JSON.stringify('production'),
23 | PHYSICS_DEBUG: JSON.stringify(false)
24 | })
25 | ]
26 | }
27 |
28 | module.exports = merge(common, prod)
29 |
--------------------------------------------------------------------------------
/webpack/webpack.physics.cjs:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const HtmlWebpackPlugin = require('html-webpack-plugin')
4 |
5 | module.exports = {
6 | mode: 'development',
7 | stats: 'errors-warnings',
8 | entry: ['./src/physics/index.ts'],
9 | output: {
10 | publicPath: 'static/physics',
11 | path: path.resolve(__dirname, '../dist/physics'),
12 | filename: '[name].bundle.js',
13 | chunkFilename: '[name].chunk.js'
14 | },
15 | resolve: {
16 | extensions: ['.ts', '.tsx', '.js']
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.tsx?$/,
22 | loader: 'ts-loader',
23 | options: {
24 | transpileOnly: true
25 | }
26 | }
27 | ]
28 | },
29 | optimization: {
30 | splitChunks: {
31 | cacheGroups: {
32 | commons: {
33 | test: /[\\/]node_modules[\\/]/,
34 | name: 'vendors',
35 | chunks: 'all',
36 | filename: '[name].bundle.js'
37 | }
38 | }
39 | }
40 | },
41 | plugins: [
42 | new HtmlWebpackPlugin({
43 | template: 'src/physics/index.html'
44 | }),
45 | new webpack.DefinePlugin({
46 | PHYSICS_DEBUG: JSON.stringify(true)
47 | })
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/webpack/webpack.physics.prod.cjs:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge')
2 | const common = require('./webpack.physics')
3 | const webpack = require('webpack')
4 |
5 | const prod = {
6 | mode: 'production',
7 | output: {
8 | filename: '[name].[contenthash].bundle.js',
9 | chunkFilename: '[name].[contenthash].chunk.js'
10 | },
11 | optimization: {
12 | splitChunks: {
13 | cacheGroups: {
14 | commons: {
15 | filename: '[name].[contenthash].bundle.js'
16 | }
17 | }
18 | }
19 | },
20 | plugins: [
21 | new webpack.DefinePlugin({
22 | 'process.env.NODE_ENV': JSON.stringify('production'),
23 | PHYSICS_DEBUG: JSON.stringify(true)
24 | })
25 | ]
26 | }
27 |
28 | module.exports = merge(common, prod)
29 |
--------------------------------------------------------------------------------
/webpack/webpack.server.cjs:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const nodeExternals = require('webpack-node-externals')
3 | const webpack = require('webpack')
4 |
5 | module.exports = {
6 | mode: 'development',
7 | stats: 'errors-warnings',
8 | devtool: 'inline-source-map',
9 | target: 'node',
10 | node: {
11 | __dirname: false
12 | },
13 | entry: './src/server/server.ts',
14 | output: {
15 | filename: 'server.js',
16 | path: path.resolve(__dirname, '../dist/server')
17 | },
18 | resolve: {
19 | extensions: ['.ts', '.tsx', '.js']
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.tsx?$/,
25 | include: path.join(__dirname, '../src'),
26 | loader: 'ts-loader',
27 | options: {
28 | transpileOnly: true
29 | }
30 | }
31 | ]
32 | },
33 | plugins: [
34 | new webpack.DefinePlugin({
35 | 'process.env.NODE_ENV': JSON.stringify('production'),
36 | PHYSICS_DEBUG: JSON.stringify(false)
37 | })
38 | ],
39 | externals: [nodeExternals()]
40 | }
41 |
--------------------------------------------------------------------------------
/webpack/webpack.stats.cjs:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const HtmlWebpackPlugin = require('html-webpack-plugin')
3 |
4 | module.exports = {
5 | mode: 'development',
6 | stats: 'errors-warnings',
7 | entry: ['./src/stats/index.ts'],
8 | output: {
9 | publicPath: 'static/stats',
10 | path: path.resolve(__dirname, '../dist/stats'),
11 | filename: '[name].bundle.js',
12 | chunkFilename: '[name].chunk.js'
13 | },
14 | resolve: {
15 | extensions: ['.ts', '.tsx', '.js']
16 | },
17 | module: {
18 | rules: [
19 | {
20 | test: /\.tsx?$/,
21 | loader: 'ts-loader',
22 | options: {
23 | transpileOnly: true
24 | }
25 | }
26 | ]
27 | },
28 | optimization: {
29 | splitChunks: {
30 | cacheGroups: {
31 | commons: {
32 | test: /[\\/]node_modules[\\/]/,
33 | name: 'vendors',
34 | chunks: 'all',
35 | filename: '[name].bundle.js'
36 | }
37 | }
38 | }
39 | },
40 | plugins: [
41 | new HtmlWebpackPlugin({
42 | template: 'src/stats/index.html'
43 | })
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/webpack/webpack.stats.prod.cjs:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const HtmlWebpackPlugin = require('html-webpack-plugin')
3 |
4 | module.exports = {
5 | mode: 'production',
6 | stats: 'errors-warnings',
7 | entry: ['./src/stats/index.ts'],
8 | output: {
9 | publicPath: 'static/stats',
10 | path: path.resolve(__dirname, '../dist/stats'),
11 | filename: '[name].[contenthash].bundle.js',
12 | chunkFilename: '[name].[contenthash].chunk.js'
13 | },
14 | resolve: {
15 | extensions: ['.ts', '.tsx', '.js']
16 | },
17 | module: {
18 | rules: [
19 | {
20 | test: /\.tsx?$/,
21 | loader: 'ts-loader',
22 | options: {
23 | transpileOnly: true
24 | }
25 | }
26 | ]
27 | },
28 | optimization: {
29 | splitChunks: {
30 | cacheGroups: {
31 | commons: {
32 | test: /[\\/]node_modules[\\/]/,
33 | name: 'vendors',
34 | chunks: 'all',
35 | filename: '[name].[contenthash].bundle.js'
36 | }
37 | }
38 | }
39 | },
40 | plugins: [
41 | new HtmlWebpackPlugin({
42 | template: 'src/stats/index.html'
43 | })
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------