├── .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 | --------------------------------------------------------------------------------