├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .prettierrc
├── .vscode
└── extensions.json
├── LICENSE
├── README.md
├── client
├── assets
│ ├── controls.png
│ ├── fullscreen.png
│ └── player.png
├── client.js
├── components
│ ├── controls.js
│ ├── cursors.js
│ ├── fullscreenButton.js
│ ├── fullscreenEvent.js
│ └── player.js
└── scenes
│ ├── bootScene.js
│ └── gameScene.js
├── index.html
├── package-lock.json
├── package.json
├── server
├── game
│ ├── components
│ │ └── player.js
│ ├── config.js
│ ├── game.js
│ └── gameScene.js
└── server.js
├── test
└── test.js
└── webpack.config.cjs
/.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]
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:
--------------------------------------------------------------------------------
1 | /.cache
2 | /.parcel-cache
3 | /client/bundle.js
4 | /dist
5 | /node_modules
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | "@yandeu/prettier-config"
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
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 | ```
25 |
--------------------------------------------------------------------------------
/client/assets/controls.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geckosio/phaser3-multiplayer-game-example/0b0591777eb7a1a82e5ca9456b92edd329dc8e12/client/assets/controls.png
--------------------------------------------------------------------------------
/client/assets/fullscreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geckosio/phaser3-multiplayer-game-example/0b0591777eb7a1a82e5ca9456b92edd329dc8e12/client/assets/fullscreen.png
--------------------------------------------------------------------------------
/client/assets/player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geckosio/phaser3-multiplayer-game-example/0b0591777eb7a1a82e5ca9456b92edd329dc8e12/client/assets/player.png
--------------------------------------------------------------------------------
/client/client.js:
--------------------------------------------------------------------------------
1 | ////
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 | })
23 |
--------------------------------------------------------------------------------
/client/components/controls.js:
--------------------------------------------------------------------------------
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 | }
96 |
--------------------------------------------------------------------------------
/client/components/cursors.js:
--------------------------------------------------------------------------------
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 | }
42 |
--------------------------------------------------------------------------------
/client/components/fullscreenButton.js:
--------------------------------------------------------------------------------
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
23 |
--------------------------------------------------------------------------------
/client/components/fullscreenEvent.js:
--------------------------------------------------------------------------------
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
18 |
--------------------------------------------------------------------------------
/client/components/player.js:
--------------------------------------------------------------------------------
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 | }
13 |
--------------------------------------------------------------------------------
/client/scenes/bootScene.js:
--------------------------------------------------------------------------------
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 | }
19 |
--------------------------------------------------------------------------------
/client/scenes/gameScene.js:
--------------------------------------------------------------------------------
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 | }
121 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 | Phaser 3 Game
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
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 | }
40 |
--------------------------------------------------------------------------------
/server/game/components/player.js:
--------------------------------------------------------------------------------
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 | }
81 |
--------------------------------------------------------------------------------
/server/game/config.js:
--------------------------------------------------------------------------------
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 | }
21 |
--------------------------------------------------------------------------------
/server/game/game.js:
--------------------------------------------------------------------------------
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 | }
9 |
--------------------------------------------------------------------------------
/server/game/gameScene.js:
--------------------------------------------------------------------------------
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 | }
115 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
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 | })
38 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
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 | }
65 |
--------------------------------------------------------------------------------
/webpack.config.cjs:
--------------------------------------------------------------------------------
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 | }
13 |
--------------------------------------------------------------------------------