Spaces:
Running
Running
Upload 27 files
Browse files- .github/workflows/main.yml +28 -0
- .gitignore +5 -0
- .prettierrc +1 -0
- .vscode/extensions.json +3 -0
- LICENSE +21 -0
- README.md +24 -10
- client/assets/controls.png +0 -0
- client/assets/fullscreen.png +0 -0
- client/assets/player.png +0 -0
- client/client.js +22 -0
- client/components/controls.js +95 -0
- client/components/cursors.js +41 -0
- client/components/fullscreenButton.js +22 -0
- client/components/fullscreenEvent.js +17 -0
- client/components/player.js +12 -0
- client/scenes/bootScene.js +18 -0
- client/scenes/gameScene.js +120 -0
- index.html +16 -17
- package-lock.json +0 -0
- package.json +39 -0
- server/game/components/player.js +80 -0
- server/game/config.js +20 -0
- server/game/game.js +8 -0
- server/game/gameScene.js +114 -0
- server/server.js +37 -0
- test/test.js +64 -0
- webpack.config.cjs +12 -0
.github/workflows/main.yml
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# read: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
2 |
+
|
3 |
+
name: CI
|
4 |
+
|
5 |
+
on: [push]
|
6 |
+
|
7 |
+
jobs:
|
8 |
+
build:
|
9 |
+
runs-on: ubuntu-latest
|
10 |
+
|
11 |
+
strategy:
|
12 |
+
matrix:
|
13 |
+
node-version: [14.x, 16.x, 18.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
|
.gitignore
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/.cache
|
2 |
+
/.parcel-cache
|
3 |
+
/client/bundle.js
|
4 |
+
/dist
|
5 |
+
/node_modules
|
.prettierrc
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
"@yandeu/prettier-config"
|
.vscode/extensions.json
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"recommendations": ["esbenp.prettier-vscode"]
|
3 |
+
}
|
LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2022 Yannick Deubel (https://github.com/yandeu)
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
README.md
CHANGED
@@ -1,10 +1,24 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Phaser 3 Multiplayer Game Example with geckos.io
|
2 |
+
|
3 |
+
## How To Start
|
4 |
+
|
5 |
+
To clone and run this game, 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. From your command line:
|
6 |
+
|
7 |
+
**Note:** Test it on Chrome. On some browsers like Firefox you need to add a STUN server to make it work.
|
8 |
+
|
9 |
+
```bash
|
10 |
+
# Clone this repository
|
11 |
+
$ npx gitget https://github.com/geckosio/phaser3-multiplayer-game-example phaser3-multiplayer-game
|
12 |
+
|
13 |
+
# Go into the repository
|
14 |
+
$ cd phaser3-multiplayer-game
|
15 |
+
|
16 |
+
# Install dependencies
|
17 |
+
$ npm install
|
18 |
+
|
19 |
+
# Start the local development server (on port 1444)
|
20 |
+
$ npm run start
|
21 |
+
|
22 |
+
# Add bots to the game (via puppeteer) to test it
|
23 |
+
$ npm run test
|
24 |
+
```
|
client/assets/controls.png
ADDED
![]() |
client/assets/fullscreen.png
ADDED
![]() |
client/assets/player.png
ADDED
![]() |
client/client.js
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
//// <reference path="../phaser.d.ts" />
|
2 |
+
|
3 |
+
import Phaser, { Game } from 'phaser'
|
4 |
+
import BootScene from './scenes/bootScene.js'
|
5 |
+
import GameScene from './scenes/gameScene.js'
|
6 |
+
import FullScreenEvent from './components/fullscreenEvent.js'
|
7 |
+
|
8 |
+
const config = {
|
9 |
+
type: Phaser.AUTO,
|
10 |
+
scale: {
|
11 |
+
mode: Phaser.Scale.FIT,
|
12 |
+
autoCenter: Phaser.Scale.CENTER_BOTH,
|
13 |
+
width: 896,
|
14 |
+
height: 504
|
15 |
+
},
|
16 |
+
scene: [BootScene, GameScene]
|
17 |
+
}
|
18 |
+
|
19 |
+
window.addEventListener('load', () => {
|
20 |
+
const game = new Game(config)
|
21 |
+
FullScreenEvent(() => resize(game))
|
22 |
+
})
|
client/components/controls.js
ADDED
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export default class Controls {
|
2 |
+
constructor(scene, channel) {
|
3 |
+
this.scene = scene
|
4 |
+
this.channel = channel
|
5 |
+
this.left = false
|
6 |
+
this.right = false
|
7 |
+
this.up = false
|
8 |
+
this.controls = []
|
9 |
+
this.none = true
|
10 |
+
this.prevNone = true
|
11 |
+
|
12 |
+
// add a second pointer
|
13 |
+
scene.input.addPointer()
|
14 |
+
|
15 |
+
const detectPointer = (gameObject, down) => {
|
16 |
+
if (gameObject.btn) {
|
17 |
+
switch (gameObject.btn) {
|
18 |
+
case 'left':
|
19 |
+
this.left = down
|
20 |
+
break
|
21 |
+
case 'right':
|
22 |
+
this.right = down
|
23 |
+
break
|
24 |
+
case 'up':
|
25 |
+
this.up = down
|
26 |
+
break
|
27 |
+
}
|
28 |
+
}
|
29 |
+
}
|
30 |
+
scene.input.on('gameobjectdown', (pointer, gameObject) => detectPointer(gameObject, true))
|
31 |
+
scene.input.on('gameobjectup', (pointer, gameObject) => detectPointer(gameObject, false))
|
32 |
+
|
33 |
+
let left = new Control(scene, 0, 0, 'left').setRotation(-0.5 * Math.PI)
|
34 |
+
let right = new Control(scene, 0, 0, 'right').setRotation(0.5 * Math.PI)
|
35 |
+
let up = new Control(scene, 0, 0, 'up')
|
36 |
+
this.controls.push(left, right, up)
|
37 |
+
this.resize()
|
38 |
+
|
39 |
+
this.scene.events.on('update', this.update, this)
|
40 |
+
}
|
41 |
+
|
42 |
+
controlsDown() {
|
43 |
+
return { left: this.left, right: this.right, up: this.up, none: this.none }
|
44 |
+
}
|
45 |
+
|
46 |
+
resize() {
|
47 |
+
const SCALE = 1
|
48 |
+
const controlsRadius = (192 / 2) * SCALE
|
49 |
+
const w = this.scene.cameras.main.width - 10 - controlsRadius
|
50 |
+
const h = this.scene.cameras.main.height - 10 - controlsRadius
|
51 |
+
|
52 |
+
let positchannelns = [
|
53 |
+
{
|
54 |
+
x: controlsRadius + 10,
|
55 |
+
y: h
|
56 |
+
},
|
57 |
+
{ x: controlsRadius + 214, y: h },
|
58 |
+
{ x: w, y: h }
|
59 |
+
]
|
60 |
+
|
61 |
+
this.controls.forEach((ctl, i) => {
|
62 |
+
ctl.setPosition(positchannelns[i].x, positchannelns[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 |
+
let str36 = total.toString(36)
|
76 |
+
|
77 |
+
this.channel.emit('playerMove', str36)
|
78 |
+
}
|
79 |
+
|
80 |
+
this.prevNone = this.none
|
81 |
+
}
|
82 |
+
}
|
83 |
+
|
84 |
+
class Control extends Phaser.GameObjects.Image {
|
85 |
+
constructor(scene, x, y, btn) {
|
86 |
+
super(scene, x, y, 'controls')
|
87 |
+
scene.add.existing(this)
|
88 |
+
|
89 |
+
this.btn = btn
|
90 |
+
|
91 |
+
this.setInteractive().setScrollFactor(0).setAlpha(0.2).setDepth(2)
|
92 |
+
|
93 |
+
// if (!scene.sys.game.device.input.touch) this.setAlpha(0)
|
94 |
+
}
|
95 |
+
}
|
client/components/cursors.js
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export default class Cursors {
|
2 |
+
constructor(scene, channel) {
|
3 |
+
this.channel = channel
|
4 |
+
this.cursors = scene.input.keyboard.createCursorKeys()
|
5 |
+
|
6 |
+
scene.events.on('update', this.update, this)
|
7 |
+
}
|
8 |
+
|
9 |
+
update() {
|
10 |
+
let move = {
|
11 |
+
left: false,
|
12 |
+
right: false,
|
13 |
+
up: false,
|
14 |
+
none: true
|
15 |
+
}
|
16 |
+
if (this.cursors.left.isDown) {
|
17 |
+
move.left = true
|
18 |
+
move.none = false
|
19 |
+
} else if (this.cursors.right.isDown) {
|
20 |
+
move.right = true
|
21 |
+
move.none = false
|
22 |
+
}
|
23 |
+
|
24 |
+
if (this.cursors.up.isDown) {
|
25 |
+
move.up = true
|
26 |
+
move.none = false
|
27 |
+
}
|
28 |
+
|
29 |
+
if (move.left || move.right || move.up || move.none !== this.prevNoMovement) {
|
30 |
+
let total = 0
|
31 |
+
if (move.left) total += 1
|
32 |
+
if (move.right) total += 2
|
33 |
+
if (move.up) total += 4
|
34 |
+
let str36 = total.toString(36)
|
35 |
+
|
36 |
+
this.channel.emit('playerMove', str36)
|
37 |
+
}
|
38 |
+
|
39 |
+
this.prevNoMovement = move.none
|
40 |
+
}
|
41 |
+
}
|
client/components/fullscreenButton.js
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const FullscreenButton = scene => {
|
2 |
+
let button = scene.add
|
3 |
+
.image(scene.cameras.main.width - 20, 20, 'fullscreen', 0)
|
4 |
+
.setOrigin(1, 0)
|
5 |
+
.setInteractive()
|
6 |
+
.setScrollFactor(0)
|
7 |
+
.setDepth(100)
|
8 |
+
.setAlpha(0.2)
|
9 |
+
|
10 |
+
button.on('pointerup', () => {
|
11 |
+
if (scene.scale.isFullscreen) {
|
12 |
+
button.setFrame(0)
|
13 |
+
scene.scale.stopFullscreen()
|
14 |
+
} else {
|
15 |
+
button.setFrame(1)
|
16 |
+
scene.scale.startFullscreen()
|
17 |
+
}
|
18 |
+
})
|
19 |
+
return button
|
20 |
+
}
|
21 |
+
|
22 |
+
export default FullscreenButton
|
client/components/fullscreenEvent.js
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// listen for fullscreen change event
|
2 |
+
const FullScreenEvent = callback => {
|
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
|
client/components/player.js
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Phaser from 'phaser'
|
2 |
+
|
3 |
+
export default class Player extends Phaser.GameObjects.Sprite {
|
4 |
+
constructor(scene, channelId, x, y) {
|
5 |
+
super(scene, x, y, 'player')
|
6 |
+
scene.add.existing(this)
|
7 |
+
|
8 |
+
this.channelId = channelId
|
9 |
+
|
10 |
+
this.setFrame(4)
|
11 |
+
}
|
12 |
+
}
|
client/scenes/bootScene.js
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Scene } from 'phaser'
|
2 |
+
import geckos from '@geckos.io/client'
|
3 |
+
|
4 |
+
export default class BootScene extends Scene {
|
5 |
+
constructor() {
|
6 |
+
super({ key: 'BootScene' })
|
7 |
+
|
8 |
+
const channel = geckos({ port: 1444 })
|
9 |
+
|
10 |
+
channel.onConnect(error => {
|
11 |
+
if (error) console.error(error.message)
|
12 |
+
|
13 |
+
channel.on('ready', () => {
|
14 |
+
this.scene.start('GameScene', { channel: channel })
|
15 |
+
})
|
16 |
+
})
|
17 |
+
}
|
18 |
+
}
|
client/scenes/gameScene.js
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Scene } from 'phaser'
|
2 |
+
import axios from 'axios'
|
3 |
+
import Player from '../components/player.js'
|
4 |
+
import Cursors from '../components/cursors.js'
|
5 |
+
import Controls from '../components/controls.js'
|
6 |
+
import FullscreenButton from '../components/fullscreenButton.js'
|
7 |
+
|
8 |
+
export default class GameScene extends Scene {
|
9 |
+
constructor() {
|
10 |
+
super({ key: 'GameScene' })
|
11 |
+
this.objects = {}
|
12 |
+
this.playerId
|
13 |
+
}
|
14 |
+
|
15 |
+
init({ channel }) {
|
16 |
+
this.channel = channel
|
17 |
+
}
|
18 |
+
|
19 |
+
preload() {
|
20 |
+
this.load.image('controls', 'assets/controls.png')
|
21 |
+
this.load.spritesheet('fullscreen', 'assets/fullscreen.png', {
|
22 |
+
frameWidth: 64,
|
23 |
+
frameHeight: 64
|
24 |
+
})
|
25 |
+
this.load.spritesheet('player', 'assets/player.png', {
|
26 |
+
frameWidth: 32,
|
27 |
+
frameHeight: 48
|
28 |
+
})
|
29 |
+
}
|
30 |
+
|
31 |
+
async create() {
|
32 |
+
new Cursors(this, this.channel)
|
33 |
+
new Controls(this, this.channel)
|
34 |
+
|
35 |
+
FullscreenButton(this)
|
36 |
+
|
37 |
+
let addDummyDude = this.add
|
38 |
+
.text(this.cameras.main.width / 2, this.cameras.main.height / 2 - 100, 'CLICK ME', { fontSize: 48 })
|
39 |
+
.setOrigin(0.5)
|
40 |
+
addDummyDude.setInteractive().on('pointerdown', () => {
|
41 |
+
this.channel.emit('addDummy')
|
42 |
+
})
|
43 |
+
|
44 |
+
const parseUpdates = updates => {
|
45 |
+
if (typeof updates === undefined || updates === '') return []
|
46 |
+
|
47 |
+
// parse
|
48 |
+
let u = updates.split(',')
|
49 |
+
u.pop()
|
50 |
+
|
51 |
+
let u2 = []
|
52 |
+
|
53 |
+
u.forEach((el, i) => {
|
54 |
+
if (i % 4 === 0) {
|
55 |
+
u2.push({
|
56 |
+
playerId: u[i + 0],
|
57 |
+
x: parseInt(u[i + 1], 36),
|
58 |
+
y: parseInt(u[i + 2], 36),
|
59 |
+
dead: parseInt(u[i + 3]) === 1 ? true : false
|
60 |
+
})
|
61 |
+
}
|
62 |
+
})
|
63 |
+
return u2
|
64 |
+
}
|
65 |
+
|
66 |
+
const updatesHandler = updates => {
|
67 |
+
updates.forEach(gameObject => {
|
68 |
+
const { playerId, x, y, dead } = gameObject
|
69 |
+
const alpha = dead ? 0 : 1
|
70 |
+
|
71 |
+
if (Object.keys(this.objects).includes(playerId)) {
|
72 |
+
// if the gameObject does already exist,
|
73 |
+
// update the gameObject
|
74 |
+
let sprite = this.objects[playerId].sprite
|
75 |
+
sprite.setAlpha(alpha)
|
76 |
+
sprite.setPosition(x, y)
|
77 |
+
} else {
|
78 |
+
// if the gameObject does NOT exist,
|
79 |
+
// create a new gameObject
|
80 |
+
let newGameObject = {
|
81 |
+
sprite: new Player(this, playerId, x || 200, y || 200),
|
82 |
+
playerId: playerId
|
83 |
+
}
|
84 |
+
newGameObject.sprite.setAlpha(alpha)
|
85 |
+
this.objects = { ...this.objects, [playerId]: newGameObject }
|
86 |
+
}
|
87 |
+
})
|
88 |
+
}
|
89 |
+
|
90 |
+
this.channel.on('updateObjects', updates => {
|
91 |
+
let parsedUpdates = parseUpdates(updates[0])
|
92 |
+
updatesHandler(parsedUpdates)
|
93 |
+
})
|
94 |
+
|
95 |
+
this.channel.on('removePlayer', playerId => {
|
96 |
+
try {
|
97 |
+
this.objects[playerId].sprite.destroy()
|
98 |
+
delete this.objects[playerId]
|
99 |
+
} catch (error) {
|
100 |
+
console.error(error.message)
|
101 |
+
}
|
102 |
+
})
|
103 |
+
|
104 |
+
try {
|
105 |
+
let res = await axios.get(`${location.protocol}//${location.hostname}:1444/getState`)
|
106 |
+
|
107 |
+
let parsedUpdates = parseUpdates(res.data.state)
|
108 |
+
updatesHandler(parsedUpdates)
|
109 |
+
|
110 |
+
this.channel.on('getId', playerId36 => {
|
111 |
+
this.playerId = parseInt(playerId36, 36)
|
112 |
+
this.channel.emit('addPlayer')
|
113 |
+
})
|
114 |
+
|
115 |
+
this.channel.emit('getId')
|
116 |
+
} catch (error) {
|
117 |
+
console.error(error.message)
|
118 |
+
}
|
119 |
+
}
|
120 |
+
}
|
index.html
CHANGED
@@ -1,19 +1,18 @@
|
|
1 |
<!DOCTYPE html>
|
2 |
-
<html>
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
</body>
|
19 |
</html>
|
|
|
1 |
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6 |
+
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
7 |
+
<script type="module" src="bundle.js"></script>
|
8 |
+
<style>
|
9 |
+
html,
|
10 |
+
body {
|
11 |
+
margin: 0;
|
12 |
+
padding: 0;
|
13 |
+
}
|
14 |
+
</style>
|
15 |
+
<title>Phaser 3 Game</title>
|
16 |
+
</head>
|
17 |
+
<body></body>
|
|
|
18 |
</html>
|
package-lock.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
package.json
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "phaser3-multiplayer-example-with-geckos.io",
|
3 |
+
"version": "1.0.0",
|
4 |
+
"description": "",
|
5 |
+
"type": "module",
|
6 |
+
"engines": {
|
7 |
+
"node": "^14.15 || >=16"
|
8 |
+
},
|
9 |
+
"scripts": {
|
10 |
+
"start": "npm run dev",
|
11 |
+
"dev": "npm-run-all --parallel dev:*",
|
12 |
+
"build": "webpack -c webpack.config.cjs",
|
13 |
+
"play": "cross-env-shell NODE_ENV=production node server/server.js",
|
14 |
+
"test": "node test/test.js",
|
15 |
+
"dev:webpack": "webpack -c webpack.config.cjs --watch",
|
16 |
+
"dev:nodemon": "nodemon --delay 500ms server/server.js"
|
17 |
+
},
|
18 |
+
"keywords": [],
|
19 |
+
"author": "",
|
20 |
+
"license": "MIT",
|
21 |
+
"dependencies": {
|
22 |
+
"@geckos.io/client": "^2.1.3",
|
23 |
+
"@geckos.io/phaser-on-nodejs": "^1.2.8",
|
24 |
+
"@geckos.io/server": "^2.1.3",
|
25 |
+
"axios": "^0.21.1",
|
26 |
+
"cors": "^2.8.5",
|
27 |
+
"express": "^4.17.1",
|
28 |
+
"phaser": "3.55.2"
|
29 |
+
},
|
30 |
+
"devDependencies": {
|
31 |
+
"@yandeu/prettier-config": "^0.0.2",
|
32 |
+
"cross-env": "^7.0.3",
|
33 |
+
"nodemon": "^2.0.3",
|
34 |
+
"npm-run-all": "^4.1.5",
|
35 |
+
"puppeteer": "^19.4.1",
|
36 |
+
"webpack": "^5.75.0",
|
37 |
+
"webpack-cli": "^4.9.1"
|
38 |
+
}
|
39 |
+
}
|
server/game/components/player.js
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export class Player extends Phaser.Physics.Arcade.Sprite {
|
2 |
+
constructor(scene, playerId, x = 200, y = 200, dummy = false) {
|
3 |
+
super(scene, x, y, '')
|
4 |
+
scene.add.existing(this)
|
5 |
+
scene.physics.add.existing(this)
|
6 |
+
|
7 |
+
this.scene = scene
|
8 |
+
|
9 |
+
this.prevX = -1
|
10 |
+
this.prevY = -1
|
11 |
+
|
12 |
+
this.dead = false
|
13 |
+
this.prevDead = false
|
14 |
+
|
15 |
+
this.playerId = playerId
|
16 |
+
this.move = {}
|
17 |
+
|
18 |
+
this.setDummy(dummy)
|
19 |
+
|
20 |
+
this.body.setSize(32, 48)
|
21 |
+
|
22 |
+
this.prevNoMovement = true
|
23 |
+
|
24 |
+
this.setCollideWorldBounds(true)
|
25 |
+
|
26 |
+
scene.events.on('update', this.update, this)
|
27 |
+
}
|
28 |
+
|
29 |
+
setDummy(dummy) {
|
30 |
+
if (dummy) {
|
31 |
+
this.body.setBounce(1)
|
32 |
+
this.scene.time.addEvent({
|
33 |
+
delay: Phaser.Math.RND.integerInRange(45, 90) * 1000,
|
34 |
+
callback: () => this.kill()
|
35 |
+
})
|
36 |
+
} else {
|
37 |
+
this.body.setBounce(0)
|
38 |
+
}
|
39 |
+
}
|
40 |
+
|
41 |
+
kill() {
|
42 |
+
this.dead = true
|
43 |
+
this.setActive(false)
|
44 |
+
}
|
45 |
+
|
46 |
+
revive(playerId, dummy) {
|
47 |
+
this.playerId = playerId
|
48 |
+
this.dead = false
|
49 |
+
this.setActive(true)
|
50 |
+
this.setDummy(dummy)
|
51 |
+
this.setVelocity(0)
|
52 |
+
}
|
53 |
+
|
54 |
+
setMove(data) {
|
55 |
+
let int = parseInt(data, 36)
|
56 |
+
|
57 |
+
let move = {
|
58 |
+
left: int === 1 || int === 5,
|
59 |
+
right: int === 2 || int === 6,
|
60 |
+
up: int === 4 || int === 6 || int === 5,
|
61 |
+
none: int === 8
|
62 |
+
}
|
63 |
+
|
64 |
+
this.move = move
|
65 |
+
}
|
66 |
+
|
67 |
+
update() {
|
68 |
+
if (this.move.left) this.setVelocityX(-160)
|
69 |
+
else if (this.move.right) this.setVelocityX(160)
|
70 |
+
else this.setVelocityX(0)
|
71 |
+
|
72 |
+
if (this.move.up && this.body.onFloor()) this.setVelocityY(-550)
|
73 |
+
}
|
74 |
+
|
75 |
+
postUpdate() {
|
76 |
+
this.prevX = this.x
|
77 |
+
this.prevY = this.y
|
78 |
+
this.prevDead = this.dead
|
79 |
+
}
|
80 |
+
}
|
server/game/config.js
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import '@geckos.io/phaser-on-nodejs'
|
2 |
+
|
3 |
+
import Phaser from 'phaser'
|
4 |
+
import { GameScene } from './gameScene.js'
|
5 |
+
|
6 |
+
export const config = {
|
7 |
+
type: Phaser.HEADLESS,
|
8 |
+
parent: 'phaser-game',
|
9 |
+
width: 896,
|
10 |
+
height: 504,
|
11 |
+
banner: false,
|
12 |
+
audio: false,
|
13 |
+
scene: [GameScene],
|
14 |
+
physics: {
|
15 |
+
default: 'arcade',
|
16 |
+
arcade: {
|
17 |
+
gravity: { y: 1200 }
|
18 |
+
}
|
19 |
+
}
|
20 |
+
}
|
server/game/game.js
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { config } from './config.js'
|
2 |
+
|
3 |
+
export class PhaserGame extends Phaser.Game {
|
4 |
+
constructor(server) {
|
5 |
+
super(config)
|
6 |
+
this.server = server
|
7 |
+
}
|
8 |
+
}
|
server/game/gameScene.js
ADDED
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import geckos from '@geckos.io/server'
|
2 |
+
import { iceServers } from '@geckos.io/server'
|
3 |
+
|
4 |
+
import pkg from 'phaser'
|
5 |
+
const { Scene } = pkg
|
6 |
+
|
7 |
+
import { Player } from './components/player.js'
|
8 |
+
|
9 |
+
export class GameScene extends Scene {
|
10 |
+
constructor() {
|
11 |
+
super({ key: 'GameScene' })
|
12 |
+
this.playerId = 0
|
13 |
+
}
|
14 |
+
|
15 |
+
init() {
|
16 |
+
this.io = geckos({
|
17 |
+
iceServers: process.env.NODE_ENV === 'production' ? iceServers : []
|
18 |
+
})
|
19 |
+
this.io.addServer(this.game.server)
|
20 |
+
}
|
21 |
+
|
22 |
+
getId() {
|
23 |
+
return this.playerId++
|
24 |
+
}
|
25 |
+
|
26 |
+
prepareToSync(player) {
|
27 |
+
return `${player.playerId},${Math.round(player.x).toString(36)},${Math.round(player.y).toString(36)},${
|
28 |
+
player.dead === true ? 1 : 0
|
29 |
+
},`
|
30 |
+
}
|
31 |
+
|
32 |
+
getState() {
|
33 |
+
let state = ''
|
34 |
+
this.playersGroup.children.iterate(player => {
|
35 |
+
state += this.prepareToSync(player)
|
36 |
+
})
|
37 |
+
return state
|
38 |
+
}
|
39 |
+
|
40 |
+
create() {
|
41 |
+
this.playersGroup = this.add.group()
|
42 |
+
|
43 |
+
const addDummy = () => {
|
44 |
+
let x = Phaser.Math.RND.integerInRange(50, 800)
|
45 |
+
let y = Phaser.Math.RND.integerInRange(100, 400)
|
46 |
+
let id = Math.random()
|
47 |
+
|
48 |
+
let dead = this.playersGroup.getFirstDead()
|
49 |
+
if (dead) {
|
50 |
+
dead.revive(id, true)
|
51 |
+
dead.setPosition(x, y)
|
52 |
+
} else {
|
53 |
+
this.playersGroup.add(new Player(this, id, x, y, true))
|
54 |
+
}
|
55 |
+
}
|
56 |
+
|
57 |
+
this.io.onConnection(channel => {
|
58 |
+
channel.onDisconnect(() => {
|
59 |
+
console.log('Disconnect user ' + channel.id)
|
60 |
+
this.playersGroup.children.each(player => {
|
61 |
+
if (player.playerId === channel.playerId) {
|
62 |
+
player.kill()
|
63 |
+
}
|
64 |
+
})
|
65 |
+
channel.room.emit('removePlayer', channel.playerId)
|
66 |
+
})
|
67 |
+
|
68 |
+
channel.on('addDummy', addDummy)
|
69 |
+
|
70 |
+
channel.on('getId', () => {
|
71 |
+
channel.playerId = this.getId()
|
72 |
+
channel.emit('getId', channel.playerId.toString(36))
|
73 |
+
})
|
74 |
+
|
75 |
+
channel.on('playerMove', data => {
|
76 |
+
this.playersGroup.children.iterate(player => {
|
77 |
+
if (player.playerId === channel.playerId) {
|
78 |
+
player.setMove(data)
|
79 |
+
}
|
80 |
+
})
|
81 |
+
})
|
82 |
+
|
83 |
+
channel.on('addPlayer', data => {
|
84 |
+
let dead = this.playersGroup.getFirstDead()
|
85 |
+
if (dead) {
|
86 |
+
dead.revive(channel.playerId, false)
|
87 |
+
} else {
|
88 |
+
this.playersGroup.add(new Player(this, channel.playerId, Phaser.Math.RND.integerInRange(100, 700)))
|
89 |
+
}
|
90 |
+
})
|
91 |
+
|
92 |
+
channel.emit('ready')
|
93 |
+
})
|
94 |
+
}
|
95 |
+
|
96 |
+
update() {
|
97 |
+
let updates = ''
|
98 |
+
this.playersGroup.children.iterate(player => {
|
99 |
+
let x = Math.abs(player.x - player.prevX) > 0.5
|
100 |
+
let y = Math.abs(player.y - player.prevY) > 0.5
|
101 |
+
let dead = player.dead != player.prevDead
|
102 |
+
if (x || y || dead) {
|
103 |
+
if (dead || !player.dead) {
|
104 |
+
updates += this.prepareToSync(player)
|
105 |
+
}
|
106 |
+
}
|
107 |
+
player.postUpdate()
|
108 |
+
})
|
109 |
+
|
110 |
+
if (updates.length > 0) {
|
111 |
+
this.io.room().emit('updateObjects', [updates])
|
112 |
+
}
|
113 |
+
}
|
114 |
+
}
|
server/server.js
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import express from 'express'
|
2 |
+
import http from 'http'
|
3 |
+
import cors from 'cors'
|
4 |
+
import path from 'path'
|
5 |
+
import { PhaserGame } from './game/game.js'
|
6 |
+
|
7 |
+
import { dirname } from 'path'
|
8 |
+
import { fileURLToPath } from 'url'
|
9 |
+
const __filename = fileURLToPath(import.meta.url)
|
10 |
+
const __dirname = dirname(__filename)
|
11 |
+
|
12 |
+
const app = express()
|
13 |
+
const server = http.createServer(app)
|
14 |
+
|
15 |
+
const game = new PhaserGame(server)
|
16 |
+
const port = 1444
|
17 |
+
|
18 |
+
app.use(cors())
|
19 |
+
|
20 |
+
app.use('/', express.static(path.join(__dirname, '../client')))
|
21 |
+
|
22 |
+
app.get('/', (req, res) => {
|
23 |
+
res.sendFile(path.join(__dirname, '../index.html'))
|
24 |
+
})
|
25 |
+
|
26 |
+
app.get('/getState', (req, res) => {
|
27 |
+
try {
|
28 |
+
let gameScene = game.scene.keys['GameScene']
|
29 |
+
return res.json({ state: gameScene.getState() })
|
30 |
+
} catch (error) {
|
31 |
+
return res.status(500).json({ error: error.message })
|
32 |
+
}
|
33 |
+
})
|
34 |
+
|
35 |
+
server.listen(port, () => {
|
36 |
+
console.log('Express is listening on http://localhost:' + port)
|
37 |
+
})
|
test/test.js
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Adds 10 player to the scene
|
3 |
+
*/
|
4 |
+
|
5 |
+
import puppeteer from 'puppeteer'
|
6 |
+
|
7 |
+
const browser = await puppeteer.launch({
|
8 |
+
defaultViewport: { width: 896, height: 504 }
|
9 |
+
})
|
10 |
+
|
11 |
+
const wait = ms => {
|
12 |
+
return new Promise(resolve => {
|
13 |
+
setTimeout(() => {
|
14 |
+
resolve()
|
15 |
+
}, ms)
|
16 |
+
})
|
17 |
+
}
|
18 |
+
|
19 |
+
const randomTime = () => {
|
20 |
+
return Math.random() * 2000 + 2000
|
21 |
+
}
|
22 |
+
|
23 |
+
const goRight = async page => {
|
24 |
+
await page.keyboard.up('ArrowRight')
|
25 |
+
await page.keyboard.down('ArrowLeft')
|
26 |
+
await wait(randomTime())
|
27 |
+
}
|
28 |
+
|
29 |
+
const goLeft = async page => {
|
30 |
+
await page.keyboard.up('ArrowLeft')
|
31 |
+
await page.keyboard.down('ArrowRight')
|
32 |
+
await wait(randomTime())
|
33 |
+
}
|
34 |
+
|
35 |
+
const newPage = async () => {
|
36 |
+
try {
|
37 |
+
const page = await browser.newPage()
|
38 |
+
await page.goto('http://localhost:1444/')
|
39 |
+
|
40 |
+
await wait(randomTime() + 5000)
|
41 |
+
await page.keyboard.down('ArrowUp')
|
42 |
+
await wait(randomTime())
|
43 |
+
|
44 |
+
await goLeft(page)
|
45 |
+
await goRight(page)
|
46 |
+
await goLeft(page)
|
47 |
+
await goRight(page)
|
48 |
+
await goLeft(page)
|
49 |
+
await goRight(page)
|
50 |
+
await goLeft(page)
|
51 |
+
await goRight(page)
|
52 |
+
await goLeft(page)
|
53 |
+
await goRight(page)
|
54 |
+
|
55 |
+
await browser.close()
|
56 |
+
} catch (error) {
|
57 |
+
console.error(error.message)
|
58 |
+
}
|
59 |
+
process.exit()
|
60 |
+
}
|
61 |
+
|
62 |
+
for (let i = 0; i < 10; i++) {
|
63 |
+
newPage()
|
64 |
+
}
|
webpack.config.cjs
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const path = require('path')
|
2 |
+
|
3 |
+
module.exports = {
|
4 |
+
mode: 'development',
|
5 |
+
devtool: 'eval-cheap-source-map',
|
6 |
+
stats: 'minimal',
|
7 |
+
entry: './client/client.js',
|
8 |
+
output: {
|
9 |
+
filename: 'bundle.js',
|
10 |
+
path: path.resolve(__dirname, 'client')
|
11 |
+
}
|
12 |
+
}
|