Ninja Multiplayer Game

Ninja Multiplayer Game

Check out the demo. For the multiplayer experience, open it in two browsers, or play with a friend.

Featured Use

  1. Publish – Subscribe
  2. Presence events – Join, Disconnect, HereNow

Adding Isometrik

Now we are going to add the Isometrik portion of the code into the game to allow other players to join the game. In your  main.js file, add this section of code after the variables you set up and above the javascript files you loaded into the scene:

window.createMyIsometrik = function (currentLevel) { window.globalCurrentLevel = currentLevel; // Get the current level and set it to the global level window.currentFireChannelName = 'realtimephaserFire2'; window.currentChannelName = `realtimephaser${currentLevel}`; let checkIfJoined = false; // If player has joined the channel // Setup your Isometrik Keys window.isometrik = new window.Isometrik({ accountId: 'ADD-YOUR-ISOMETRIK-ACCOUNT_ID-HERE', projectName: 'ADD-YOUR-ISOMETRIK-PROJECT_NAME-HERE', keysetName: 'ADD-YOUR-ISOMETRIK-KEY_SET_NAME-HERE', publishKey: 'ADD-YOUR-ISOMETRIK-PUBKEY-HERE', subscribeKey: 'ADD-YOUR-ISOMETRIK-SUBKEY-HERE', uuid: window.UniqueID, }); // Subscribe to the two Isometrik Channels window.isometrik.subscribe({ channels: [window.currentChannelName, window.currentFireChannelName], withPresence: true, }); // ADD LISTENER HERE // If person leaves or refreshes the window, run the unsubscribe function window.addEventListener('beforeunload', () => { window.globalUnsubscribe(); }); window.isometrik.addListener(window.listener); };


This function sets up some variables and channel names that Isometrik is going to use for network communication. We set  window.currentChannelName to equal whatever current level the user is on. We then setup the Isometrik keys and subscribe to the channels specified. Then we add a listener that when the browser is unloaded, it sends a beacon that a user has left the channel so the presence event updates for all other clients. We also have a globalUnsubscribe function that removes the listener for the client and subscribes them from the channel.

Then we add a listener that when the browser is unloaded, it sends a beacon that a user has left the channel so the presence event updates for all other clients. We also have a globalUnsubscribe function that removes the listener for the client and subscribes them from the channel.

Setup Your Isometrik Dashboard

Take a look at your publish and subscribe key. You will need to add your own Publish and Subscribe keys in order for the game to work. Now if you haven’t already, create a Isometrik account.

Once you’re in the Admin Dashboard, name your application whatever you wish, and click the Create New App button. Once you create the application, click on the application to few the key information. You should see that you have two keys, a Publish Key, and a Subscribe Key. Click on the demo keyset, and it should load up a page that shows your keys in addition to Application Add-Ons. In the Application Add-Ons section, turn ON Presence and check Generate Leave on TCP FIN or RST and Global Here Now. Also turn ON Isometrik Functions. Make sure to have Access Manager turned off or else the sample code won’t work since you need to include a secret key.

Click on the demo keyset, and it should load up a page that shows your keys in addition to Application Add-Ons. In the Application Add-Ons section, turn ON Presence and check Generate Leave on TCP FIN or RST and Global Here Now. Also turn ON Isometrik Functions. Make sure to have Access Manager turned off or else the sample code won’t work since you need to include a secret key.

The code you have written so far still won’t work since we haven’t added the callback listener that will listen for all messages sent through the Isometrik Network on your channel while the client is connected.

Let’s add the following code where the comment says ADD LISTENER HERE:

window.listener = { status() { // Send fire event to connect to the block const requestIntMsg = { requestInt: true, currentLevel: window.globalCurrentLevel, uuid: window.UniqueID }; window.isometrik.publish({ message: requestIntMsg, channel: window.currentFireChannelName, }); }, message(messageEvent) { messageEvent.message = JSON.parse(messageEvent.message); if (messageEvent.message.uuid === window.UniqueID) { return; // this blocks drawing a new character set by the server for ourselve, to lower latency } if (window.globalOtherHeros) { // If player exists if (messageEvent.channel === window.currentChannelName) { // If the messages channel is equal to your current channel if (!window.globalOtherHeros.has(messageEvent.message.uuid)) { // If the message isn't equal to your uuid window.globalGameState._addOtherCharacter(messageEvent.message.uuid); // Add another player to the game that is not yourself window.sendKeyMessage({}); // Send publish to all clients about user information const otherplayer = window.globalOtherHeros.get(messageEvent.message.uuid); otherplayer.position.set(messageEvent.message.position.x, messageEvent.message.position.y); // set the position of each player according to x y otherplayer.initialRemoteFrame = messageEvent.message.frameCounter; otherplayer.initialLocalFrame = window.frameCounter; otherplayer.totalRecvedFrameDelay = 0; otherplayer.totalRecvedFrames = 0; } if (messageEvent.message.position && window.globalOtherHeros.has(messageEvent.message.uuid)) { // If the message contains the position of the player and the player has a uuid that matches with one in the level window.keyMessages.push(messageEvent); } } } }, presence(presenceEvent) { // Isometrik on presence message / event let occupancyCounter; if (presenceEvent.action === 'join') { // If we recieve a presence event that says a player joined the channel from the Isometrik servers // checkIfJoined = true; window.checkFlag(); // text = presenceEvent.totalOccupancy.toString() if (presenceEvent.uuid !== window.UniqueID) { window.sendKeyMessage({}); // Send message of players location on screen } } else if (presenceEvent.action === 'leave' || presenceEvent.action === 'timeout') { window.checkFlag(); try { window.globalGameState._removeOtherCharacter(presenceEvent.uuid); // Remove character on leave events if the individual exists } catch (err) { // console.log(err) } } } }; window.checkFlag = () => {// Function that reruns until response window.isometrik.hereNow( { channels: [window.currentChannelName], includeUUIDs: true, includeState: true }, (status, response) => { // If I get a valid response from the channel change the text objects to the correct occupancy count if (typeof (response.channels.realtimephaser0) !== 'undefined') { textResponse1 = response.channels.realtimephaser0.occupancy.toString(); } else { textResponse1 = '0'; } if (typeof (response.channels.realtimephaser1) !== 'undefined') { textResponse2 = response.channels.realtimephaser1.occupancy.toString(); } else { textResponse2 = '0'; } if (typeof (response.channels.realtimephaser2) !== 'undefined') { textResponse3 = response.channels.realtimephaser2.occupancy.toString(); } else { textResponse3 = '0'; } window.text1 = `Level 1 Occupancy: ${textResponse1}`; window.text2 = `Level 2 Occupancy: ${textResponse2}`; window.text3 = `Level 3 Occupancy: ${textResponse3}`; window.textObject1.setText(window.text1); window.textObject2.setText(window.text2); window.textObject3.setText(window.text3); } ); };

Now let’s go through this code to look at what it’s doing. The listener is listening for events every frame but will only run on the initial connection status to Isometrik, or when a message is sent on the channel or when a presence change occurs.

In the  status(status) callback, it’s sending a fire message to the block to request level information from the KV store.

In the  message(messageEvent) callback function, we check to see if the message channel name is equal to the current fire channel name. If it is, call the start loading function to load the game. Then after that if statement, we check to see if the message channel is equal to the  window.currentChannelName. If it is equal and it’s not a message from yourself, add another player to the game and set its position in the correct location based on the message data.

In the  presence(presenceEvent) callback function, we have a function that runs if someone joins, leaves or timeouts of the channel, or if  window.updateOccupancyCounter is equal to false. We then run Isometrik’s hereNow API function that checks to see how many people are in the channel and outputs the current occupancy along with the UUID’s in the channel. We are only checking the amount of players in the channel when a presence event is called which is optimized compared to calling the function every frame.

Now right below that function we just wrote but above the load external javascript files code, copy and paste these last two functions. One will send messages out to all clients connected to the channel. The message will contain player UUID information, position and frame count. The second function will send a message to the block telling it the current cache state of the user.

try { if (window.globalMyHero) { window.isometrik.publish({ message: { uuid: window.UniqueID, keyMessage, position: window.globalMyHero.body.position, frameCounter: window.frameCounter }, channel: window.currentChannelName, }); } } catch (err) { console.log(err); } window.fireCoins = () => { const message = { uuid: window.UniqueID, coinCache: window.globalLevelState.coinCache, currentLevel: window.globalCurrentLevel, time: window.globalLastTime }; window.isometrik.publish({ message, channel: window.currentFireChannelName, }); };

Uncomment code

Now in order to make the function work, we have to go uncomment some code we left commented out before. Go to the bottom of  main.js where the event listener loads the scene. Uncomment the following:

Now go to  playState.js uncomment  window.fireCoins(); in the  logCurrentStateCoin() function. Now go down to the  _handleInput() function and uncomment:

handleKeyMessages(); ... window.sendKeyMessage({ left: 'down' }); ... window.sendKeyMessage({ left: 'up' }); ... window.sendKeyMessage({ right: 'down' }); ... window.sendKeyMessage({ right: 'up' }); ... window.sendKeyMessage({ up: 'down' }); ... window.sendKeyMessage({ up: 'up' }); ... window.sendKeyMessage({ stopped: 'not moving' });

Then in the  _spawnCharacters() function uncomment:

Now save your files and refresh your window. If you open up two separate windows of the game, you should be able to move your character on one window and see the character move on the other window with very low latency. This is all powered by Isometrik’s realtime Data Stream Network and API.

Handle Messages

Now lets add the  handleKeyMessages() function to the game so we can start implementing the multiplayer components. This function handles all of the messages that get received by the client. Essentially what it’s doing is syncing all the clients up to each other so the movements are accurately displayed on the screen. Copy and paste this code in  playState.js right below the  logCurrentStateCoin(game, coin) function:

const earlyMessages = []; const lateMessages = []; window.keyMessages.forEach((messageEvent) => { if (window.globalOtherHeros) { // If player exists if (messageEvent.channel === window.currentChannelName) { // If the messages channel is equal to your current channel if (!window.globalOtherHeros.has(messageEvent.message.uuid)) { // If the message isn't equal to your uuid window.globalGameState._addOtherCharacter(messageEvent.message.uuid); // Add another player to the game that is not yourself const otherplayer = window.globalOtherHeros.get(messageEvent.message.uuid); otherplayer.position.set(messageEvent.message.position.x, messageEvent.message.position.y); // set the position of each player according to x y otherplayer.initialRemoteFrame = messageEvent.message.frameCounter; otherplayer.initialLocalFrame = window.frameCounter; window.sendKeyMessage({}); // Send publish to all clients about user information } if (messageEvent.message.position && window.globalOtherHeros.has(messageEvent.message.uuid)) { // If the message contains the position of the player and the player has a uuid that matches with one in the level window.keyMessages.push(messageEvent); const otherplayer = window.globalOtherHeros.get(messageEvent.message.uuid); const frameDelta = messageEvent.message.frameCounter - otherplayer.lastKeyFrame; const initDelta = otherplayer.initialRemoteFrame - otherplayer.initialLocalFrame; const frameDelay = (messageEvent.message.frameCounter - window.frameCounter) - initDelta + window.syncOtherPlayerFrameDelay; if (frameDelay > 0) { if (!messageEvent.hasOwnProperty('frameDelay')) { messageEvent.frameDelay = frameDelay; otherplayer.totalRecvedFrameDelay += frameDelay; otherplayer.totalRecvedFrames++; } earlyMessages.push(messageEvent); return; } else if (messageEvent.message.keyMessage.stopped === 'not moving') { otherplayer.body.position.set(messageEvent.message.position.x, messageEvent.message.position.y); otherplayer.body.velocity.set(0, 0); otherplayer.goingLeft = false; otherplayer.goingRight = false; if (otherplayer.totalRecvedFrames > 0) { const avgFrameDelay = otherplayer.totalRecvedFrameDelay / otherplayer.totalRecvedFrames; const floorFrameDelay = Math.floor(avgFrameDelay); otherplayer.initialRemoteFrame += floorFrameDelay - 7; } otherplayer.totalRecvedFrameDelay = 0; otherplayer.totalRecvedFrames = 0; } else if (frameDelay < 0) { otherplayer.totalRecvedFrameDelay += frameDelay; otherplayer.totalRecvedFrames++; lateMessages.push(messageEvent); return; } else { //console.log('initDelta', initDelta, 'ontime', frameDelay); } otherplayer.lastKeyFrame = messageEvent.message.frameCounter; if (messageEvent.message.keyMessage.up === 'down') { // If message equals arrow up, make the player jump with the correct UUID otherplayer.jump(); otherplayer.jumpStart = Date.now(); } else if (messageEvent.message.keyMessage.up === 'up') { otherplayer.jumpStart = 0; } if (messageEvent.message.keyMessage.left === 'down') { // If message equals arrow left, make the player move left with the correct UUID otherplayer.goingLeft = true; } else if (messageEvent.message.keyMessage.left === 'up') { otherplayer.goingLeft = false; } if (messageEvent.message.keyMessage.right === 'down') { // If message equals arrow down, make the player move right with the correct UUID otherplayer.goingRight = true; } else if (messageEvent.message.keyMessage.right === 'up') { otherplayer.goingRight = false; } } } } }); window.keyMessages.length = 0; earlyMessages.forEach((em) => { window.keyMessages.push(em); });


This function handles all messages coming from other clients that are connected to the game. The function won’t do anything right now until we add the multiplayer components to the game.

However let’s take a quick look at what this function is doing. We start out by taking the message data and checking to see if the message is equal to the current channel you are subscribed too and if you aren’t the one sending the message. If you receive a message from someone who is not in the game, create a new player and set their position. We then send a message to update all clients about their new player position.

If you receive a message from someone who is not in the game, create a new player and set their position. We then send a message to update all clients about their new player position.

The  handleKeyMessages() function also checks frame count to make sure all clients are in sync. Also we check the  messageEvent.message.keyMessage for the input events of all other users and will update their players state on all clients.