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