├── .github └── FUNDING.yml ├── .gitignore ├── public ├── images │ └── char01.png ├── index.html └── js │ └── game.js ├── go.mod ├── README.md ├── go.sum ├── LICENSE └── server.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: dpogorzelski 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | server 2 | phaser_multiplayer_demo 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /public/images/char01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpogorzelski/phaser_multiplayer_demo/HEAD/public/images/char01.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dpogorzelski/phaser_multiplayer_demo 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gorilla/mux v1.8.0 7 | github.com/gorilla/websocket v1.4.2 8 | github.com/pborman/uuid v1.2.1 9 | ) 10 | 11 | require github.com/google/uuid v1.0.0 // indirect 12 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Phaser Multiplayer 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is this 2 | A basic demonstration of how to build a multi-player game with [Phaser](https://phaser.io/) 3 | 4 | # How to use 5 | 6 | ``` 7 | git clone https://github.com/dpogorzelski/phaser_multiplayer_demo.git 8 | cd phaser_multiplayer_demo 9 | go get 10 | go run server.go 11 | ``` 12 | Open your browser at [http://127.0.0.1:3000](http://127.0.0.1:3000) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= 2 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 4 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 5 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 6 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 7 | github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= 8 | github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dawid Pogorzelski 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 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "sync" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/gorilla/websocket" 10 | "github.com/pborman/uuid" 11 | ) 12 | 13 | type player struct { 14 | Y int // Y position of the player 15 | X int // X position 16 | ID string // a unique id to identify the player by the frontend 17 | Online bool 18 | socket *websocket.Conn // websocket connection of the player 19 | } 20 | 21 | var players = make(map[string]*player) 22 | var mutex sync.RWMutex 23 | 24 | func remoteHandler(res http.ResponseWriter, req *http.Request) { 25 | var err error 26 | 27 | //when someone requires a ws connection we create a new player and store a 28 | // pointer to the connection inside player.Socket 29 | ws, err := websocket.Upgrade(res, req, nil, 1024, 1024) 30 | if _, ok := err.(websocket.HandshakeError); ok { 31 | http.Error(res, "not a websocket handshake", 400) 32 | return 33 | } else if err != nil { 34 | log.Println(err) 35 | return 36 | } 37 | 38 | log.Printf("got websocket conn from %v\n", ws.RemoteAddr()) 39 | id := uuid.New() 40 | player := new(player) 41 | player.socket = ws 42 | player.ID = id 43 | player.Online = true 44 | 45 | mutex.Lock() 46 | players[id] = player 47 | mutex.Unlock() 48 | 49 | mutex.RLock() 50 | for k := range players { 51 | if k != player.ID { 52 | if err = player.socket.WriteJSON(players[k]); err != nil { 53 | log.Println(err) 54 | } 55 | } 56 | } 57 | mutex.RUnlock() 58 | 59 | go func() { 60 | for { 61 | if err = player.socket.ReadJSON(&player); err != nil { 62 | log.Println("player Disconnected waiting", err) 63 | disconnectHandler(player) 64 | return 65 | } 66 | 67 | mutex.RLock() 68 | for k := range players { 69 | if k != player.ID { 70 | if err = players[k].socket.WriteJSON(player); err != nil { 71 | log.Println(err) 72 | } 73 | } 74 | } 75 | mutex.RUnlock() 76 | } 77 | }() 78 | } 79 | 80 | func disconnectHandler(p *player) { 81 | p.Online = false 82 | mutex.RLock() 83 | for k := range players { 84 | if k != p.ID { 85 | if err := players[k].socket.WriteJSON(p); err != nil { 86 | log.Println(err) 87 | } 88 | } 89 | } 90 | mutex.RUnlock() 91 | 92 | mutex.Lock() 93 | delete(players, p.ID) 94 | mutex.Unlock() 95 | log.Println("number of players still connected ...", len(players)) 96 | } 97 | 98 | func main() { 99 | r := mux.NewRouter() 100 | r.HandleFunc("/ws", remoteHandler) 101 | 102 | r.PathPrefix("/").Handler(http.FileServer(http.Dir("./public/"))) 103 | http.ListenAndServe(":3000", r) 104 | } 105 | -------------------------------------------------------------------------------- /public/js/game.js: -------------------------------------------------------------------------------- 1 | window.onload = function () { 2 | var game = new Phaser.Game(1024, 768, Phaser.AUTO, null, { 3 | preload: preload, 4 | create: create, 5 | update: update, 6 | }); 7 | 8 | var player, //our player 9 | players = {}, //this will hold the list of players 10 | sock, //this will be player's ws connection 11 | style = { 12 | font: "12px Arial", 13 | fill: "#ffffff", 14 | }, //styling players labels a bit 15 | ip = "127.0.0.1"; //ip of our Go server 16 | 17 | function preload() { 18 | game.load.spritesheet("char", "images/char01.png", 32, 48); 19 | } 20 | 21 | function create() { 22 | game.stage.backgroundColor = "#2d2d2d"; 23 | 24 | player = game.add.sprite(0, 0, "char"); 25 | 26 | player.animations.add("down", [0, 1, 2], 10); 27 | player.animations.add("left", [12, 13, 14], 10); 28 | player.animations.add("right", [24, 25, 26], 10); 29 | player.animations.add("up", [36, 37, 38], 10); 30 | 31 | player.body.collideWorldBounds = true; 32 | 33 | //create a new ws connection and send our position to others 34 | sock = new WebSocket("ws://" + ip + ":3000/ws"); 35 | sock.onopen = function () { 36 | var pos = JSON.stringify({ 37 | x: player.x, 38 | y: player.y, 39 | }); 40 | sock.send(pos); 41 | }; 42 | 43 | // when we receive a message we spawn, destroy or update a player's 44 | // position depending on the message's content 45 | sock.onmessage = function (message) { 46 | var p = JSON.parse(message.data); 47 | if (!(p.ID in players)) { 48 | players[p.ID] = spawn(p); 49 | uPosition(p); 50 | } 51 | 52 | if (p.Online === false) { 53 | players[p.ID].label.destroy(); 54 | players[p.ID].destroy(); 55 | } else { 56 | uPosition(p); 57 | } 58 | }; 59 | } 60 | 61 | function update() { 62 | if (game.input.keyboard.isDown(Phaser.Keyboard.LEFT)) { 63 | player.animations.play("left"); 64 | player.x -= 3; 65 | var pos = JSON.stringify({ 66 | x: player.x, 67 | }); 68 | sock.send(pos); 69 | } else if (game.input.keyboard.isDown(Phaser.Keyboard.RIGHT)) { 70 | player.animations.play("right"); 71 | player.x += 3; 72 | var pos = JSON.stringify({ 73 | x: player.x, 74 | }); 75 | sock.send(pos); 76 | } else if (game.input.keyboard.isDown(Phaser.Keyboard.UP)) { 77 | player.animations.play("up"); 78 | player.y -= 3; 79 | var pos = JSON.stringify({ 80 | y: player.y, 81 | }); 82 | sock.send(pos); 83 | } else if (game.input.keyboard.isDown(Phaser.Keyboard.DOWN)) { 84 | player.animations.play("down"); 85 | player.y += 3; 86 | var pos = JSON.stringify({ 87 | y: player.y, 88 | }); 89 | sock.send(pos); 90 | } 91 | } 92 | 93 | function spawn(p) { 94 | var label = p.ID.match(/(^\w*)-/i)[1]; 95 | var p = game.add.sprite(p.X, p.Y, "char"); 96 | p.animations.add("down", [0, 1, 2], 10); 97 | p.animations.add("left", [12, 13, 14], 10); 98 | p.animations.add("right", [24, 25, 26], 10); 99 | p.animations.add("up", [36, 37, 38], 10); 100 | p.label = game.add.text(p.X, p.Y - 10, label, style); 101 | return p; 102 | } 103 | 104 | function uPosition(p) { 105 | if (players[p.ID].x > p.X) { 106 | players[p.ID].animations.play("left"); 107 | } else if (players[p.ID].x < p.X) { 108 | players[p.ID].animations.play("right"); 109 | } else if (players[p.ID].y > p.Y) { 110 | players[p.ID].animations.play("up"); 111 | } else { 112 | players[p.ID].animations.play("down"); 113 | } 114 | players[p.ID].x = players[p.ID].label.x = p.X; 115 | players[p.ID].y = p.Y; 116 | players[p.ID].label.y = p.Y - 10; 117 | } 118 | }; 119 | --------------------------------------------------------------------------------