├── .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 | [](/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 |
--------------------------------------------------------------------------------