Multiplayer Game

Neil HaddleyMay 9, 2023

Creating a new firebase project

I created a firebase project using https://console.firebase.google.com

I deployed Drew Conley's Multiplayer Game to Firebase.

https://haddley-multiplayer.web.app/

https://haddley-multiplayer.web.app/

Firebase project settings

Firebase project settings

firebase initfirebase deploy

firebase initfirebase deploy

Responding to updated state

The state of the games is maintained in a Firebase Realtime Database.

The allPlayersRef.on("XXX", (snapshot) => {...}) functions are called when a player joins the game, leaves the game or moves.

The allCoinsRef.on("value", (snapshot) => {...}) functions are called when coins are created or collected.

Realtime Database

Realtime Database

ChatGPT's commentary

This code appears to be a JavaScript implementation of a game that involves players moving characters on a map and collecting coins. Let's go through the code and explain its different parts:

1. The mapData object defines the boundaries of the game map (minX, maxX, minY, maxY) and the coordinates of blocked spaces on the map (blockedSpaces).

2. The playerColors array contains different colors that can be assigned to players in the game.

3. The randomFromArray function takes an array as input and returns a random element from that array.

4. The getKeyString function takes x and y coordinates and returns a string in the format "x:y".

5. The createName function generates a random name for a player by combining a random prefix and a random animal name.

6. The isSolid function checks if a given coordinate (x, y) represents a solid space on the map. It returns true if the space is blocked or if the coordinates are outside the map boundaries.

7. The getRandomSafeSpot function returns a random coordinate from a predefined set of safe spots on the map where players can initially spawn.

8. The code then defines an immediately-invoked function expression (IIFE) that encapsulates the game logic. Inside this function, various variables are declared and initialized:

playerId and playerRef represent the unique ID and reference for the current player.

players is an object that stores information about all the players in the game.

playerElements is an object that stores references to DOM elements representing each player's character.

coins is an object that stores information about the coins present on the map.

coinElements is an object that stores references to DOM elements representing each coin.

9. The code selects relevant DOM elements using document.querySelector.

10. The placeCoin function is responsible for randomly placing a coin on the map. It selects a safe spot using getRandomSafeSpot, creates a reference to the coin in the Firebase database, and schedules a recursive call to placeCoin after a random timeout.

11. The attemptGrabCoin function is called when a player moves to a new coordinate. It checks if there is a coin at the given coordinates and, if so, removes the coin from the database and increments the player's coin count.

12. The handleArrowPress function is called when a player presses an arrow key to move their character. It calculates the new coordinates based on the current position and the arrow key pressed. If the new coordinates represent a valid move (not blocked by a solid space), it updates the player's position, direction, and saves the changes to the database. It also calls attemptGrabCoin to check if the player grabbed a coin at the new position.

13. The initGame function initializes the game by adding event listeners for arrow key presses, setting up database listeners for player and coin updates, and creating DOM elements for players and coins.

14. The code sets up Firebase authentication using anonymous sign-in (firebase.auth().signInAnonymously()) and handles the authentication state change using firebase.auth().onAuthStateChanged. When a user is authenticated, the game initializes by generating a random name for the player, setting up the player's initial position, and adding the player to the database. It also sets up a disconnection handler to remove the player's data from the database when they disconnect.

15. Finally, the IIFE is invoked immediately, starting the execution of the game code.

In summary, this JavaScript code sets up a multiplayer game where players can control characters on a map. The game uses Firebase for real-time data synchronization. Players can move their characters using arrow keys, collect coins, and see the positions and coin counts of other players. The code initializes the game, handles player authentication, updates player and coin data in the database, and manages the rendering of player and coin elements in the game interface.

Security Rules

TEXT
1{
2  "rules": {
3    ".read": "auth != null",
4    ".write": false,
5    "players": {
6      "$uid": {
7        ".write": "auth != null && auth.uid == $uid"
8      }
9    },
10    "coins": {
11        ".write": "auth != null"
12    }
13  }
14}

index.html

TEXT
1<html>
2
3<head>
4  <title>Multiplayer Demo</title>
5  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6  <link rel="preconnect" href="https://fonts.gstatic.com">
7  <link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;700&display=swap" rel="stylesheet">
8  <link rel="stylesheet" href="/styles.css" type="text/css">
9</head>
10
11<body>
12
13  <div class="game-container">
14  </div>
15  <div class="player-info">
16    <div>
17      <label for="player-name">Your Name</label>
18      <input id="player-name" maxlength="10" type="text" />
19    </div>
20    <div>
21      <button id="player-color">Change Color</button>
22    </div>
23  </div>
24
25  <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js"></script>
26  <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-auth.js"></script>
27  <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-database.js"></script>
28  <script>
29    // NOTE - INCLUDE YOUR FIREBASE CONFIG HERE FOR ANYTHING TO WORK:
30    const firebaseConfig = {
31      apiKey: "AIzaSyBfoImSImcCkM7TD-Z3X-LxWs4OEbGOaUQ",
32      authDomain: "haddley-multiplayer.firebaseapp.com",
33      projectId: "haddley-multiplayer",
34      storageBucket: "haddley-multiplayer.appspot.com",
35      messagingSenderId: "119673300748",
36      appId: "1:119673300748:web:8cb5b0ba8dbbae0834cb29"
37    };
38    firebase.initializeApp(firebaseConfig);
39  </script>
40
41
42  <script src="/KeyPressListener.js"></script>
43  <script src="/app.js"></script>
44</body>
45
46</html>

app.js

TEXT
1const mapData = {
2  minX: 1,
3  maxX: 14,
4  minY: 4,
5  maxY: 12,
6  blockedSpaces: {
7    "7x4": true,
8    "1x11": true,
9    "12x10": true,
10    "4x7": true,
11    "5x7": true,
12    "6x7": true,
13    "8x6": true,
14    "9x6": true,
15    "10x6": true,
16    "7x9": true,
17    "8x9": true,
18    "9x9": true,
19  },
20};
21
22// Options for Player Colors... these are in the same order as our sprite sheet
23const playerColors = ["blue", "red", "orange", "yellow", "green", "purple"];
24
25//Misc Helpers
26function randomFromArray(array) {
27  return array[Math.floor(Math.random() * array.length)];
28}
29function getKeyString(x, y) {
30  return `${x}x${y}`;
31}
32
33function createName() {
34  const prefix = randomFromArray([
35    "COOL",
36    "SUPER",
37    "HIP",
38    "SMUG",
39    "COOL",
40    "SILKY",
41    "GOOD",
42    "SAFE",
43    "DEAR",
44    "DAMP",
45    "WARM",
46    "RICH",
47    "LONG",
48    "DARK",
49    "SOFT",
50    "BUFF",
51    "DOPE",
52  ]);
53  const animal = randomFromArray([
54    "BEAR",
55    "DOG",
56    "CAT",
57    "FOX",
58    "LAMB",
59    "LION",
60    "BOAR",
61    "GOAT",
62    "VOLE",
63    "SEAL",
64    "PUMA",
65    "MULE",
66    "BULL",
67    "BIRD",
68    "BUG",
69  ]);
70  return `${prefix} ${animal}`;
71}
72
73function isSolid(x,y) {
74
75  const blockedNextSpace = mapData.blockedSpaces[getKeyString(x, y)];
76  return (
77    blockedNextSpace ||
78    x >= mapData.maxX ||
79    x < mapData.minX ||
80    y >= mapData.maxY ||
81    y < mapData.minY
82  )
83}
84
85function getRandomSafeSpot() {
86  //We don't look things up by key here, so just return an x/y
87  return randomFromArray([
88    { x: 1, y: 4 },
89    { x: 2, y: 4 },
90    { x: 1, y: 5 },
91    { x: 2, y: 6 },
92    { x: 2, y: 8 },
93    { x: 2, y: 9 },
94    { x: 4, y: 8 },
95    { x: 5, y: 5 },
96    { x: 5, y: 8 },
97    { x: 5, y: 10 },
98    { x: 5, y: 11 },
99    { x: 11, y: 7 },
100    { x: 12, y: 7 },
101    { x: 13, y: 7 },
102    { x: 13, y: 6 },
103    { x: 13, y: 8 },
104    { x: 7, y: 6 },
105    { x: 7, y: 7 },
106    { x: 7, y: 8 },
107    { x: 8, y: 8 },
108    { x: 10, y: 8 },
109    { x: 8, y: 8 },
110    { x: 11, y: 4 },
111  ]);
112}
113
114
115(function () {
116
117  let playerId;
118  let playerRef;
119  let players = {};
120  let playerElements = {};
121  let coins = {};
122  let coinElements = {};
123
124  const gameContainer = document.querySelector(".game-container");
125  const playerNameInput = document.querySelector("#player-name");
126  const playerColorButton = document.querySelector("#player-color");
127
128
129  function placeCoin() {
130    const { x, y } = getRandomSafeSpot();
131    const coinRef = firebase.database().ref(`coins/${getKeyString(x, y)}`);
132    coinRef.set({
133      x,
134      y,
135    })
136
137    const coinTimeouts = [2000, 3000, 4000, 5000];
138    setTimeout(() => {
139      placeCoin();
140    }, randomFromArray(coinTimeouts));
141  }
142
143  function attemptGrabCoin(x, y) {
144    const key = getKeyString(x, y);
145    if (coins[key]) {
146      // Remove this key from data, then uptick Player's coin count
147      firebase.database().ref(`coins/${key}`).remove();
148      playerRef.update({
149        coins: players[playerId].coins + 1,
150      })
151    }
152  }
153
154
155  function handleArrowPress(xChange=0, yChange=0) {
156    const newX = players[playerId].x + xChange;
157    const newY = players[playerId].y + yChange;
158    if (!isSolid(newX, newY)) {
159      //move to the next space
160      players[playerId].x = newX;
161      players[playerId].y = newY;
162      if (xChange === 1) {
163        players[playerId].direction = "right";
164      }
165      if (xChange === -1) {
166        players[playerId].direction = "left";
167      }
168      playerRef.set(players[playerId]);
169      attemptGrabCoin(newX, newY);
170    }
171  }
172
173  function initGame() {
174
175    new KeyPressListener("ArrowUp", () => handleArrowPress(0, -1))
176    new KeyPressListener("ArrowDown", () => handleArrowPress(0, 1))
177    new KeyPressListener("ArrowLeft", () => handleArrowPress(-1, 0))
178    new KeyPressListener("ArrowRight", () => handleArrowPress(1, 0))
179
180    const allPlayersRef = firebase.database().ref(`players`);
181    const allCoinsRef = firebase.database().ref(`coins`);
182
183    allPlayersRef.on("value", (snapshot) => {
184      //Fires whenever a change occurs
185      players = snapshot.val() || {};
186      Object.keys(players).forEach((key) => {
187        const characterState = players[key];
188        let el = playerElements[key];
189        // Now update the DOM
190        el.querySelector(".Character_name").innerText = characterState.name;
191        el.querySelector(".Character_coins").innerText = characterState.coins;
192        el.setAttribute("data-color", characterState.color);
193        el.setAttribute("data-direction", characterState.direction);
194        const left = 16 * characterState.x + "px";
195        const top = 16 * characterState.y - 4 + "px";
196        el.style.transform = `translate3d(${left}, ${top}, 0)`;
197      })
198    })
199    allPlayersRef.on("child_added", (snapshot) => {
200      //Fires whenever a new node is added the tree
201      const addedPlayer = snapshot.val();
202      const characterElement = document.createElement("div");
203      characterElement.classList.add("Character", "grid-cell");
204      if (addedPlayer.id === playerId) {
205        characterElement.classList.add("you");
206      }
207      characterElement.innerHTML = (`
208        <div class="Character_shadow grid-cell"></div>
209        <div class="Character_sprite grid-cell"></div>
210        <div class="Character_name-container">
211          <span class="Character_name"></span>
212          <span class="Character_coins">0</span>
213        </div>
214        <div class="Character_you-arrow"></div>
215      `);
216      playerElements[addedPlayer.id] = characterElement;
217
218      //Fill in some initial state
219      characterElement.querySelector(".Character_name").innerText = addedPlayer.name;
220      characterElement.querySelector(".Character_coins").innerText = addedPlayer.coins;
221      characterElement.setAttribute("data-color", addedPlayer.color);
222      characterElement.setAttribute("data-direction", addedPlayer.direction);
223      const left = 16 * addedPlayer.x + "px";
224      const top = 16 * addedPlayer.y - 4 + "px";
225      characterElement.style.transform = `translate3d(${left}, ${top}, 0)`;
226      gameContainer.appendChild(characterElement);
227    })
228
229
230    //Remove character DOM element after they leave
231    allPlayersRef.on("child_removed", (snapshot) => {
232      const removedKey = snapshot.val().id;
233      gameContainer.removeChild(playerElements[removedKey]);
234      delete playerElements[removedKey];
235    })
236
237
238    //New - not in the video!
239    //This block will remove coins from local state when Firebase `coins` value updates
240    allCoinsRef.on("value", (snapshot) => {
241      coins = snapshot.val() || {};
242    });
243    //
244
245    allCoinsRef.on("child_added", (snapshot) => {
246      const coin = snapshot.val();
247      const key = getKeyString(coin.x, coin.y);
248      coins[key] = true;
249
250      // Create the DOM Element
251      const coinElement = document.createElement("div");
252      coinElement.classList.add("Coin", "grid-cell");
253      coinElement.innerHTML = `
254        <div class="Coin_shadow grid-cell"></div>
255        <div class="Coin_sprite grid-cell"></div>
256      `;
257
258      // Position the Element
259      const left = 16 * coin.x + "px";
260      const top = 16 * coin.y - 4 + "px";
261      coinElement.style.transform = `translate3d(${left}, ${top}, 0)`;
262
263      // Keep a reference for removal later and add to DOM
264      coinElements[key] = coinElement;
265      gameContainer.appendChild(coinElement);
266    })
267    allCoinsRef.on("child_removed", (snapshot) => {
268      const {x,y} = snapshot.val();
269      const keyToRemove = getKeyString(x,y);
270      gameContainer.removeChild( coinElements[keyToRemove] );
271      delete coinElements[keyToRemove];
272    })
273
274
275    //Updates player name with text input
276    playerNameInput.addEventListener("change", (e) => {
277      const newName = e.target.value || createName();
278      playerNameInput.value = newName;
279      playerRef.update({
280        name: newName
281      })
282    })
283
284    //Update player color on button click
285    playerColorButton.addEventListener("click", () => {
286      const mySkinIndex = playerColors.indexOf(players[playerId].color);
287      const nextColor = playerColors[mySkinIndex + 1] || playerColors[0];
288      playerRef.update({
289        color: nextColor
290      })
291    })
292
293    //Place my first coin
294    placeCoin();
295
296  }
297
298  firebase.auth().onAuthStateChanged((user) => {
299    console.log(user)
300    if (user) {
301      //You're logged in!
302      playerId = user.uid;
303      playerRef = firebase.database().ref(`players/${playerId}`);
304
305      const name = createName();
306      playerNameInput.value = name;
307
308      const {x, y} = getRandomSafeSpot();
309
310
311      playerRef.set({
312        id: playerId,
313        name,
314        direction: "right",
315        color: randomFromArray(playerColors),
316        x,
317        y,
318        coins: 0,
319      })
320
321      //Remove me from Firebase when I diconnect
322      playerRef.onDisconnect().remove();
323
324      //Begin the game now that we are signed in
325      initGame();
326    } else {
327      //You're logged out.
328    }
329  })
330
331  firebase.auth().signInAnonymously().catch((error) => {
332    var errorCode = error.code;
333    var errorMessage = error.message;
334    // ...
335    console.log(errorCode, errorMessage);
336  });
337
338
339})();