Real-Time Data Transfer Using Vue and Socket.IO [Part 3 of 3] | A²I² Artificial Intelligence at Deakin Skip to content About Research Publications Themes Grants Awards Industry Projects Careers Study with us News Blog Contact . Home About Research Themes Grants Awards Publications Industry Projects Study with us News Blog Contact Home/Blog Real-Time Data Transfer Using Vue and Socket.IO [Part 3 of 3] Blog / Rob Hyndman / May 31, 2021 In the first post we covered setting up a simple server and client which communicate with each other using websockets. The second post focused on the creation of a chat room. This third and final post in the series will build upon the techniques that we covered in the previous posts to create a simple multiplayer game “Fill Frenzy!”. Add Real-Time Game The second of the main components we will cover will be the game. The game will allow multiple users to interact with the game board, with the ultimate goal being to fill in every tile on the game board while the server clears one of the tiles on a set interval. This step will follow much the same pattern as the chat component, but as it will also be lengthy and touch multiple files, I will break it down into manageable sections. We will start by implementing the game server. Create server/game.js and add the following code. let socketio = undefined;
let allTilesActive = false;
let deactivationTime = 1000;
let activeTiles = {}; Firstly, we create a variable socketio to use as a reference to our main Socket.IO object, and variables to keep track of the state of the tiles in the game. const difficultySettings = {
easy: {
name: 'Easy',
gridSize: 5,
tileSize: 128
},
medium: {
name: 'Medium',
gridSize: 10,
tileSize: 64
},
hard: {
name: 'Hard',
gridSize: 20,
tileSize: 32
}
};
let gameDifficulty = difficultySettings.easy; Secondly, we add some difficulty settings so that we can make the game challenging when we want, but also give us a nice easy mode to test our code with. const initialise = (io) => {
socketio = io;
setInterval(() => {
deactivateTile();
}, deactivationTime);
} Next we create an initialise function which we can use to set our socketio reference. We also start a repeating timer to call the deactivateTile function on a set interval. const run = (socket) => {
// new socket connected, send active tiles and game difficulty
socket.emit('activeTiles', activeTiles);
socket.emit('setDifficultyLevels', difficultySettings);
socket.emit('gameDifficulty', gameDifficulty);
if (allTilesActive) {
// game is already completed, notify new connection
socket.emit('gameCompleted');
}
handleActivateTile(socket);
handleAllTilesActive(socket);
handleResetGame(socket);
} The run function will handle the main functionality for the game for each individual client socket which is passed to the function as an argument. The function will be called only once per socket when a new connection is made, so we will take advantage of this to send the current state of the app to the socket by emitting the event activeTiles which passes the corresponding information. const handleActivateTile = (socket) => {
// listen for 'activateTile' events
socket.on('activateTile', (tile) => {
// attach a tile id
const key = `${tile.x},${tile.y}`;
tile.id = key;
// add the tile to the active tile collection
if (!activeTiles.hasOwnProperty(key)) {
activeTiles[key] = tile;
// send the activated tile to all connected sockets
socketio.emit('activateTile', tile);
}
});
} The handleActivateTile function listens for the activateTile event, and will respond appropriately. We receive a tile from the client, add an ID and save it to our activeTiles collection, and then update all clients with the new information. const handleAllTilesActive = (socket) => {
// listen for 'allTilesActive' events
socket.on('allTilesActive', (tileCount) => {
if (allTilesActive) {
return;
}
// count the number of active tiles
const totalTiles = Object.keys(activeTiles).length;
// compare the count of active tiles to the tile count received
if (totalTiles === tileCount) {
// complete the game
socketio.emit('gameCompleted');
allTilesActive = true;
}
});
} The handleAllTilesActive function listens for the allTilesActive event, and will respond appropriately. We receive a count of the maximum number of tiles from the client, and then compare it to the number of tiles that we have tracked as active on the server. If the counts match we then send the gameCompleted event to all clients. const handleResetGame = (socket) => {
// listen for 'resetGame' events
socket.on('resetGame', (difficulty) => {
resetGame(difficulty);
});
} The handleResetGame function listens for the resetGame event, and will also take a difficulty setting to use for the next game. const randomTile = () => {
// find existing keys
let keys = Object.keys(activeTiles);
// get a random key
const randomKey = keys[(keys.length * Math.random()) << 0];
// return the random tile
return activeTiles[randomKey];
} The randomTile function picks a random tile to return from the activeTiles collection. const deactivateTile = () => {
if (allTilesActive) {
return;
}
// select a random tile
let tile = randomTile();
if (tile) {
// deactivate the tile
socketio.emit('deactivateTile', tile.id);
delete activeTiles[tile.id];
}
} The deactivateTile function is called on a set interval, and will select a random tile from the activeTiles collection to deactivate. It sends the deactivateTile event to all clients and specifies which tile id has been deactivated. const resetGame = (difficulty) => {
// reset all tiles
allTilesActive = false;
activeTiles = {};
// set game difficulty
gameDifficulty = difficultySettings[difficulty.toLowerCase()];
socketio.emit('gameDifficulty', gameDifficulty);
socketio.emit('resetGame');
}
// export these functions for external use
module.exports = { initialise, run }; The resetGame function resets the game state back to default settings, and sends the resetGame event to all clients. Update server/index.js to include our new functionality. const express = require('express');
const http = require('http').Server(express);
const socketio = require('socket.io')(http, { pingTimeout: 60000 });
const chat = require('./chat');
const game = require('./game');
const port = 3030;
chat.initialise(socketio);
game.initialise(socketio);
socketio.on('connection', (socket) => {
// new socket connected
chat.run(socket);
game.run(socket);
});
http.listen(port, () => {
console.log('Server started on port', port);
}); After including the game.js file we created, we pass the socketio instance to the initialise function. Then, when the connection event triggers we pass the new socket connection through to the run function. Next we will implement the game client. Create src/store/game.js and add the following code. import Vue from 'vue';
export default {
strict: false,
namespaced: true,
state: () => ({
tiles: {}
}),
getters: {
GET_TILES: (state) => {
return state.tiles;
}
},
mutations: {
SET_TILES(state, tiles) {
state.tiles = tiles;
},
ADD_TILE(state, tile) {
Vue.set(state.tiles, tile.id, tile);
},
REMOVE_TILE(state, tileID) {
Vue.delete(state.tiles, tileID);
}
},
actions: {
RECEIVE_TILES({ commit }, tiles) {
commit('SET_TILES', tiles);
},
ACTIVATE_TILE({ commit }, tile) {
commit('ADD_TILE', tile);
},
DEACTIVATE_TILE({ commit }, tileID) {
commit('REMOVE_TILE', tileID);
}
}
}; The game store is similar to the chat store, with two main differences. There is only one set of data contained within the store, and we have added functionality to remove data. Update src/store/index.js with the following code. import Vue from 'vue';
import Vuex from 'vuex';
import * as socketio from '../plugins/socketio';
import chat from './chat';
import game from './game';
Vue.use(Vuex);
export default new Vuex.Store({
strict: false,
actions: {
SEND_EVENT({}, event) {
socketio.sendEvent(event);
}
},
modules: {
chat,
game
}
}); Loading our new file into the store as a module allows us to maintain good object separation and the usage of namespaces to access particular parts of the store. Create src/components/Game.vue and add the following code.
Move the mouse to fill in the whole game board
It's not impossible, but friends make it a lot easier!
First we add some instructions to the game.
Congratulations!
Next we build the game board, of which the bulk is taken care of using the v-for directives to loop over both the gameDifficulty.gridsize for x and y. The function getTileClass, as well as the computed property tileStyle are being used as tidy ways of setting styling within the template.
Reset
{{ percentCompleted }}% completed
The last part of the template adds a difficulty setting dropdown and a reset button if the game isCompleted. We also add a handy little tracker that presents the percentage of game completion. The usage of the v-if and v-else directives means that only one of these components is rendered at a time. Similar to the computed property, the watch property is also something a little different. It allows us to keep an eye on a variable (in this case the computed value of percentCompleted) and allows us to write some code that runs when the watched value changes. When our watched value changes, we see if the newValue equals 100, and if so we send the allTilesActive event to the server asking if it agrees that we’ve finished the game. We send the total number of game tiles along for the server to perform calculations with. The Game component makes use of the