Omnibus commited on
Commit
c679a93
·
1 Parent(s): d9eb22e

Upload 27 files

Browse files
.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
- title: Game Multi Test
3
- emoji: 📉
4
- colorFrom: pink
5
- colorTo: green
6
- sdk: static
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
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
+ }