├── .gitignore ├── .prettierrc ├── README.md ├── electron ├── app.js ├── index.html ├── src │ ├── game.ts │ └── mainScene.ts ├── tsconfig.json └── webpack.dev.js ├── package-lock.json ├── package.json ├── readme └── screenshot.png ├── tsconfig.json └── www ├── src ├── app.ts ├── assets │ ├── dude.png │ ├── platform.png │ └── sky.png ├── index.html └── mainScene.ts ├── tsconfig.json └── webpack.dev.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | **dist/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phaser 3 Multiplayer Game Example 2 | 3 | Uses electron on the server side to better debug the game. 4 | 5 | Use `npm i && npm start`. 6 | 7 | To start it without the visual server (electron window) add `show: false` to `electron/app.js` 8 | 9 | [![screenshot](/readme/screenshot.png)](/readme/screenshot.png) 10 | -------------------------------------------------------------------------------- /electron/app.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron') 2 | 3 | function createWindow() { 4 | // Create the browser window. 5 | const win = new BrowserWindow({ 6 | width: 1280, 7 | height: 720, 8 | // show: false, // uncomment if you want to hide the electron window 9 | webPreferences: { 10 | nodeIntegration: true, 11 | }, 12 | }) 13 | 14 | // and load the index.html of the app. 15 | win.loadFile('index.html') 16 | 17 | // Open the DevTools. 18 | win.webContents.openDevTools() 19 | } 20 | 21 | // This method will be called when Electron has finished 22 | // initialization and is ready to create browser windows. 23 | // Some APIs can only be used after this event occurs. 24 | app.whenReady().then(createWindow) 25 | 26 | // Quit when all windows are closed. 27 | app.on('window-all-closed', () => { 28 | // On macOS it is common for applications and their menu bar 29 | // to stay active until the user quits explicitly with Cmd + Q 30 | if (process.platform !== 'darwin') { 31 | app.quit() 32 | } 33 | }) 34 | 35 | app.on('activate', () => { 36 | // On macOS it's common to re-create a window in the app when the 37 | // dock icon is clicked and there are no other windows open. 38 | if (BrowserWindow.getAllWindows().length === 0) { 39 | createWindow() 40 | } 41 | }) 42 | 43 | // In this file you can include the rest of your app's specific main process 44 | // code. You can also put them in separate files and require them here. 45 | -------------------------------------------------------------------------------- /electron/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World! 6 | 7 | 11 | 12 | 13 | 14 | 15 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /electron/src/game.ts: -------------------------------------------------------------------------------- 1 | import { Game } from 'phaser' 2 | import MainScene from './mainScene' 3 | 4 | new Game({ 5 | width: 800, 6 | height: 600, 7 | scale: { 8 | mode: Phaser.Scale.FIT, 9 | autoCenter: Phaser.Scale.CENTER_BOTH, 10 | }, 11 | scene: [MainScene], 12 | physics: { 13 | default: 'arcade', 14 | 15 | arcade: { 16 | gravity: { y: 300 }, 17 | debug: false, 18 | }, 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /electron/src/mainScene.ts: -------------------------------------------------------------------------------- 1 | import geckos from '@geckos.io/server' 2 | 3 | const DEBUG = true 4 | 5 | class MainScene extends Phaser.Scene { 6 | players = new Map() 7 | level: { key: string; x: number; y: number; scale: number }[] = [] 8 | io 9 | controls 10 | staticGroup 11 | 12 | constructor() { 13 | super('MainScene') 14 | } 15 | 16 | geckos() { 17 | const io = geckos() 18 | 19 | io.listen(3000) 20 | 21 | io.onConnection((channel) => { 22 | console.log('new player connected') 23 | 24 | const dude = this.physics.add.sprite(Math.random() * 300 + 50, 10, 'dude') 25 | dude.setBounce(0.2) 26 | dude.setCollideWorldBounds(true) 27 | this.players.set(channel.id, { dude }) 28 | 29 | // make this player collide with the level (staticGroup in this case) 30 | this.physics.add.collider(dude, this.staticGroup) 31 | 32 | // make sure the client gets the current level 33 | channel.emit('level', this.level, { reliable: true }) 34 | 35 | channel.onDisconnect(() => { 36 | const player = this.players.get(channel.id) 37 | if (player) { 38 | const { dude } = player 39 | if (dude) { 40 | dude.destroy() 41 | console.log('dude destroyed') 42 | io.emit('destroy', { id: channel.id }, { reliable: true }) 43 | } 44 | this.players.delete(channel.id) 45 | } 46 | }) 47 | 48 | channel.on('move', (data) => { 49 | const dude = this.players.get(channel.id).dude 50 | if (dude) { 51 | if (data === 'jump') { 52 | dude.body.setVelocityY(-300) 53 | } else { 54 | const speed = 150 55 | dude.body.setVelocityX(data === 'right' ? speed : -speed) 56 | } 57 | } 58 | }) 59 | }) 60 | 61 | return io 62 | } 63 | 64 | init() { 65 | this.level = [ 66 | { key: 'ground', x: 400, y: 568, scale: 2 }, 67 | { key: 'ground', x: 600, y: 400, scale: 1 }, 68 | { key: 'ground', x: 50, y: 250, scale: 1 }, 69 | { key: 'ground', x: 750, y: 220, scale: 1 }, 70 | ] 71 | } 72 | 73 | preload() { 74 | this.load.image('ground', '../www/src/assets/platform.png') 75 | this.load.spritesheet('dude', '../www/src/assets/dude.png', { 76 | frameWidth: 32, 77 | frameHeight: 48, 78 | }) 79 | } 80 | 81 | create() { 82 | this.io = this.geckos() 83 | 84 | if (DEBUG) { 85 | const cursors = this.input.keyboard.createCursorKeys() 86 | const controlConfig = { 87 | camera: this.cameras.main, 88 | left: cursors.left, 89 | right: cursors.right, 90 | up: cursors.up, 91 | down: cursors.down, 92 | zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q), 93 | zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E), 94 | acceleration: 0.04, 95 | drag: 0.0005, 96 | maxSpeed: 0.5, 97 | } 98 | this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl( 99 | controlConfig 100 | ) 101 | } 102 | 103 | // generate level 104 | this.staticGroup = this.physics.add.staticGroup() 105 | this.level.forEach((el) => { 106 | this.staticGroup 107 | .create(el.x, el.y, el.key) 108 | .setScale(el.scale) 109 | .refreshBody() 110 | }) 111 | 112 | this.add 113 | .text( 114 | 20, 115 | 20, 116 | 'This what the server sees.\nOpen your browser at http://localhost:8080/ to play.\nUse your arrow keys to move the camera.', 117 | { fontSize: 20 } 118 | ) 119 | .setScrollFactor(0) 120 | } 121 | 122 | update(time, delta) { 123 | if (DEBUG) this.controls.update(delta) 124 | 125 | if (this.io && this.players.size >= 1) { 126 | const update: any = [] 127 | this.players.forEach((player, id) => { 128 | const animation = player.dude.body.velocity.x > 0 ? 'right' : 'left' 129 | update.push({ id, x: player.dude.x, y: player.dude.y, animation }) 130 | }) 131 | this.io.emit('update', update) 132 | } 133 | } 134 | } 135 | 136 | export default MainScene 137 | -------------------------------------------------------------------------------- /electron/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /electron/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const nodeExternals = require('webpack-node-externals') 3 | 4 | module.exports = { 5 | mode: 'development', 6 | target: 'node', 7 | entry: ['./electron/src/game.ts'], 8 | output: { 9 | publicPath: '', 10 | path: path.resolve(__dirname, './dist'), 11 | filename: '[name].bundle.js', 12 | chunkFilename: '[name].chunk.js', 13 | }, 14 | resolve: { 15 | extensions: ['.ts', '.tsx', '.js'], 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.tsx?$/, 21 | loader: 'ts-loader', 22 | }, 23 | ], 24 | }, 25 | externals: [nodeExternals()], 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "private": true, 7 | "scripts": { 8 | "start": "npm-run-all --parallel electron:* www", 9 | "electron:app": "electron electron/app.js", 10 | "electron:webpack": "webpack --watch --config electron/webpack.dev.js", 11 | "www": "webpack-dev-server --config www/webpack.dev.js", 12 | "format": "prettier --write www/** electron/** --loglevel silent", 13 | "postinstall": "webpack --config electron/webpack.dev.js && webpack --config www/webpack.dev.js" 14 | }, 15 | "keywords": [], 16 | "author": "Yannick Deubel", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@geckos.io/client": "^1.4.0", 20 | "@geckos.io/server": "^1.4.0", 21 | "phaser": "3.22.0" 22 | }, 23 | "devDependencies": { 24 | "copy-webpack-plugin": "^5.1.1", 25 | "electron": "^8.2.3", 26 | "html-webpack-plugin": "^4.2.0", 27 | "npm-run-all": "^4.1.5", 28 | "prettier": "2.0.5", 29 | "ts-loader": "^7.0.1", 30 | "typescript": "^3.8.3", 31 | "webpack": "^4.43.0", 32 | "webpack-cli": "^3.3.11", 33 | "webpack-dev-server": "^3.10.3", 34 | "webpack-node-externals": "^1.7.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /readme/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geckosio/phaser3-multiplayer-example-electron/d02ce63e32dec65b88c083e5085414cd611967b7/readme/screenshot.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "noImplicitAny": false, 7 | "esModuleInterop": true, 8 | "sourceMap": true, 9 | "strictPropertyInitialization": false, 10 | "lib": ["dom", "es5", "esnext", "ScriptHost"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /www/src/app.ts: -------------------------------------------------------------------------------- 1 | import { Game as PhaserGame } from 'phaser' 2 | import MainScene from './mainScene' 3 | 4 | new PhaserGame({ 5 | width: 800, 6 | height: 600, 7 | scale: { 8 | mode: Phaser.Scale.FIT, 9 | autoCenter: Phaser.Scale.CENTER_BOTH, 10 | }, 11 | scene: [MainScene], 12 | }) 13 | -------------------------------------------------------------------------------- /www/src/assets/dude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geckosio/phaser3-multiplayer-example-electron/d02ce63e32dec65b88c083e5085414cd611967b7/www/src/assets/dude.png -------------------------------------------------------------------------------- /www/src/assets/platform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geckosio/phaser3-multiplayer-example-electron/d02ce63e32dec65b88c083e5085414cd611967b7/www/src/assets/platform.png -------------------------------------------------------------------------------- /www/src/assets/sky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geckosio/phaser3-multiplayer-example-electron/d02ce63e32dec65b88c083e5085414cd611967b7/www/src/assets/sky.png -------------------------------------------------------------------------------- /www/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /www/src/mainScene.ts: -------------------------------------------------------------------------------- 1 | import geckos, { ClientChannel } from '@geckos.io/client' 2 | import { Scene, Types } from 'phaser' 3 | 4 | class MainScene extends Scene { 5 | dudes: Map = new Map() 6 | channel: ClientChannel 7 | cursors: Types.Input.Keyboard.CursorKeys 8 | 9 | constructor() { 10 | super('MainScene') 11 | } 12 | 13 | geckos() { 14 | const channel = geckos({ port: 3000 }) 15 | 16 | channel.onConnect((error) => { 17 | if (error) { 18 | console.error(error.message) 19 | return 20 | } else { 21 | console.log('You are connected') 22 | } 23 | 24 | channel.onDisconnect(() => { 25 | console.log('You got disconnected') 26 | }) 27 | 28 | channel.on('level', (data: any) => { 29 | data.forEach((el) => { 30 | const element = this.add.image(el.x, el.y, el.key) 31 | element.setScale(el.scale) 32 | }) 33 | }) 34 | 35 | channel.on('update', (data) => { 36 | // @ts-ignore 37 | data.forEach((d) => { 38 | const dude = this.dudes.get(d.id) 39 | if (!dude) { 40 | console.log('add new dude') 41 | this.dudes.set(d.id, this.add.sprite(d.x, d.y, 'dude')) 42 | } else { 43 | dude.x = d.x 44 | dude.y = d.y 45 | if (dude?.anims?.currentAnim?.key !== d.animation) 46 | dude.anims.play(d.animation, true) 47 | } 48 | }) 49 | }) 50 | 51 | channel.on('destroy', (data: any) => { 52 | const { id } = data 53 | const dude = this.dudes.get(id) 54 | if (dude) { 55 | console.log('Destroy player ', id) 56 | dude.destroy() 57 | this.dudes.delete(id) 58 | } 59 | }) 60 | }) 61 | 62 | return channel 63 | } 64 | 65 | preload() { 66 | this.load.image('sky', 'assets/sky.png') 67 | this.load.image('ground', 'assets/platform.png') 68 | this.load.spritesheet('dude', 'assets/dude.png', { 69 | frameWidth: 32, 70 | frameHeight: 48, 71 | }) 72 | } 73 | 74 | create() { 75 | this.anims.create({ 76 | key: 'left', 77 | frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }), 78 | frameRate: 10, 79 | repeat: -1, 80 | }) 81 | 82 | this.anims.create({ 83 | key: 'right', 84 | frames: this.anims.generateFrameNumbers('dude', { start: 5, end: 8 }), 85 | frameRate: 10, 86 | repeat: -1, 87 | }) 88 | 89 | this.add.image(400, 300, 'sky') 90 | 91 | this.channel = this.geckos() 92 | 93 | this.cursors = this.input.keyboard.createCursorKeys() 94 | 95 | this.add.text( 96 | 20, 97 | 20, 98 | 'This what the player sees.\nMove arrow keys to move the player.', 99 | { fontSize: 20 } 100 | ) 101 | } 102 | 103 | update() { 104 | if (this.channel) { 105 | if (this.cursors?.left?.isDown) { 106 | this.channel.emit('move', 'left') 107 | } else if (this.cursors?.right?.isDown) { 108 | this.channel.emit('move', 'right') 109 | } 110 | 111 | if (this.cursors?.up?.isDown) { 112 | this.channel.emit('move', 'jump') 113 | } 114 | } 115 | } 116 | } 117 | 118 | export default MainScene 119 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /www/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const CopyPlugin = require('copy-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | entry: ['./www/src/app.ts'], 8 | output: { 9 | publicPath: '', 10 | path: path.resolve(__dirname, './dist'), 11 | filename: '[name].bundle.js', 12 | chunkFilename: '[name].chunk.js', 13 | }, 14 | devServer: { 15 | open: true, 16 | // host: '0.0.0.0', 17 | }, 18 | resolve: { 19 | extensions: ['.ts', '.tsx', '.js'], 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | loader: 'ts-loader', 26 | }, 27 | ], 28 | }, 29 | plugins: [ 30 | new CopyPlugin([{ from: 'www/src/assets', to: 'assets' }]), 31 | new HtmlWebpackPlugin({ 32 | template: 'www/src/index.html', 33 | }), 34 | ], 35 | } 36 | --------------------------------------------------------------------------------