Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use sockets to send character state updates #122

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ module.exports = {
Guilds: 'writable',
Items: 'writable',
Quests: 'writable',
Streamy: 'readable',
canAccessQuest: 'writable',
remote: 'writable',
_: 'readable',
Expand Down
2 changes: 2 additions & 0 deletions app/.meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ accounts-github@1.5.0
accounts-google@1.4.0
accounts-facebook@1.3.3
accounts-passwordless@2.1.3
yuukan:streamy
yuukan:streamy-rooms
2 changes: 2 additions & 0 deletions app/.meteor/versions
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,5 @@ underscore@1.0.10
url@1.3.2
webapp@1.13.1
webapp-hashing@1.1.0
yuukan:streamy@1.4.2
yuukan:streamy-rooms@1.2.5
7 changes: 7 additions & 0 deletions core/client/lemverse.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import audioManager from './audio-manager';
import { setReaction } from './helpers';
import initSentryClient from './sentry';

// Override Meteor._debug to filter custom messages
Meteor._debug = (function (superMeteorDebug) {
return function (error, info) {
if (!info?.msg) superMeteorDebug(error, info);
};
}(Meteor._debug));

initSentryClient();

scopes = {
Expand Down
7 changes: 6 additions & 1 deletion core/client/level-manager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Phaser from 'phaser';

import networkManager from './network-manager';

const defaultMapConfig = { width: 100, height: 100, tileWidth: 48, tileHeight: 48 };
const defaultLayerCount = 9;
const defaultLayerDepth = { 6: 10000, 7: 10001, 8: 10002 };
Expand Down Expand Up @@ -114,7 +116,7 @@ levelManager = {
loadingScene.setText('');
loadingScene.show(() => {
// Phaser sends the sleep event on the next frame which causes the client to overwrite the spawn position set by the server
userManager.onSleep();
networkManager.onSleep();

this.scene.scene.sleep();
Meteor.call('teleportUserInLevel', levelId, (error, levelName) => {
Expand Down Expand Up @@ -165,6 +167,9 @@ levelManager = {
onLevelLoaded() {
this.scene.scene.wake();

// listen room messages
networkManager.joinRoom(Meteor.user().profile.levelId);

// simulate a first frame update to avoid weirds visual effects with characters animation and direction
this.scene.update(0, 0);
setTimeout(() => game.scene.keys.LoadingScene.hide(() => this.scene.enableKeyboard(true)), 0);
Expand Down
115 changes: 115 additions & 0 deletions core/client/network-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
const characterInterpolationInterval = 200;

const networkEvents = Object.freeze({
characterState: 'character:state',
});

const networkManager = {
currentRoom: undefined,
userStreamyId: undefined,
throttledSendCharacterState: undefined,

init() {
this.userStreamyId = Streamy.id();
this.throttledSendCharacterState = throttle(this._sendCharacterNewState.bind(this), characterInterpolationInterval, { leading: false });
Streamy.on(networkEvents.characterState, this._onCharacterStateReceived.bind(this));
},

onSleep() {
this.throttledSendCharacterState?.cancel();
},

update() {
this.interpolateCharacterPositions();
},

joinRoom(roomId) {
if (this.currentRoom) Streamy.leave(this.currentRoom.identifier);

Streamy.join(roomId);
this.currentRoom = Streamy.rooms(roomId);
this.currentRoom.identifier = roomId;
},

/**
* Basic interpolation:
* - No rubber banding
* - No extrapolation
*
* Later we could add multiple states to predict positions, ….
* But since we work in tcp and on a simple simulation it doesn't seem necessary to me right now.
*/
interpolateCharacterPositions() {
const now = Date.now();
const controlledCharacter = userManager.getControlledCharacter();

Object.values(userManager.characters).forEach(character => {
if (character === controlledCharacter) return;

if (!character.lwTargetDate) {
character.setAnimationPaused(true);
return;
}

character.playAnimation('run', character.direction);

if (character.lwTargetDate <= now) {
character.x = character.lwTargetX;
character.y = character.lwTargetY;
character.setDepthFromPosition();
delete character.lwTargetDate;
return;
}

const elapsedTime = (now - character.lwOriginDate) / (character.lwTargetDate - character.lwOriginDate);
character.x = character.lwOriginX + (character.lwTargetX - character.lwOriginX) * elapsedTime;
character.y = character.lwOriginY + (character.lwTargetY - character.lwOriginY) * elapsedTime;
character.setDepthFromPosition();
});
},

_onCharacterStateReceived(state) {
if (state.__from === this.userStreamyId) return;

const character = userManager.getCharacter(state.userId);
if (!character) return;

character.direction = state.direction;
character.lwOriginX = character.x;
character.lwOriginY = character.y;
character.lwOriginDate = Date.now();
character.lwTargetX = state.x;
character.lwTargetY = state.y;
character.lwTargetDate = character.lwOriginDate + characterInterpolationInterval;
},

sendCharacterNewState(state) {
this.throttledSendCharacterState(state);
},

_sendCharacterNewState(state) {
if (!state) return;

this.currentRoom.emit(networkEvents.characterState, {
x: state.x,
y: state.y,
direction: state.direction,
userId: state.getData('userId'),
});
},

saveCharacterState(state) {
if (!state) return;

// No need to check that the userId really belongs to the user, Meteor does the check during the update
Meteor.users.update(state.getData('userId'), {
$set: {
'profile.x': state.x,
'profile.y': state.y,
'profile.direction': state.direction,
},
});
},
};

export default networkManager;
6 changes: 5 additions & 1 deletion core/client/scenes/scene-world.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import nipplejs from 'nipplejs';
import Phaser from 'phaser';

import networkManager from '../network-manager';

import { clamp } from '../helpers';

const fixedUpdateInterval = 200;
Expand Down Expand Up @@ -110,6 +112,7 @@ WorldScene = new Phaser.Class({

entityManager.init(this);
levelManager.init(this);
networkManager.init(this);
userManager.init(this);
zoneManager.init(this);
},
Expand Down Expand Up @@ -152,6 +155,7 @@ WorldScene = new Phaser.Class({
update() {
levelManager.update();
userManager.update();
networkManager.update();
},

postUpdate(time, delta) {
Expand Down Expand Up @@ -194,7 +198,7 @@ WorldScene = new Phaser.Class({
},

sleep() {
userManager.onSleep();
networkManager.onSleep();
},

shutdown() {
Expand Down
65 changes: 8 additions & 57 deletions core/client/user-manager.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Phaser from 'phaser';
import Character from './components/character';
import audioManager from './audio-manager';
import networkManager from './network-manager';
import { guestSkin, textDirectionToVector, vectorToTextDirection } from './helpers';

const userInterpolationInterval = 200;
const defaultUserMediaColorError = '0xd21404';
const characterPopInOffset = { x: 0, y: -90 };
const characterAnimations = Object.freeze({
Expand All @@ -18,18 +18,6 @@ const messageReceived = {
style: 'tooltip with-arrow fade-in',
};

savePlayer = player => {
Meteor.users.update(Meteor.userId(), {
$set: {
'profile.x': player.x,
'profile.y': player.y,
'profile.direction': player.direction,
},
});
};

const throttledSavePlayer = throttle(savePlayer, userInterpolationInterval, { leading: false });

userManager = {
inputVector: undefined,
characters: {},
Expand All @@ -48,15 +36,10 @@ userManager = {
},

destroy() {
this.onSleep();
this.characters = {};
this.controlledCharacter = undefined;
},

onSleep() {
throttledSavePlayer.cancel();
},

onDocumentAdded(user) {
if (this.characters[user._id]) return null;

Expand Down Expand Up @@ -126,16 +109,9 @@ userManager = {
const character = this.characters[user._id];
if (!character) return;

const { x, y, direction, reaction, shareAudio, guest, userMediaError, name, baseline, nameColor } = user.profile;
const { x, y, reaction, shareAudio, guest, userMediaError, name, baseline, nameColor } = user.profile;

// update character instance
character.direction = direction;
character.lwOriginX = character.x;
character.lwOriginY = character.y;
character.lwOriginDate = Date.now();
character.lwTargetX = user.profile.x;
character.lwTargetY = user.profile.y;
character.lwTargetDate = character.lwOriginDate + userInterpolationInterval;
character.showMutedStateIndicator(!guest && !shareAudio);

// is account transformed from guest to user?
Expand Down Expand Up @@ -235,40 +211,12 @@ userManager = {
}
},

interpolateCharacterPositions() {
const now = Date.now();
Object.values(this.characters).forEach(player => {
if (player === this.controlledCharacter) return;

if (!player.lwTargetDate) {
player.setAnimationPaused(true);
return;
}

player.playAnimation(characterAnimations.run, player.direction);

if (player.lwTargetDate <= now) {
player.x = player.lwTargetX;
player.y = player.lwTargetY;
player.setDepthFromPosition();
delete player.lwTargetDate;
return;
}

const elapsedTime = ((now - player.lwOriginDate) / (player.lwTargetDate - player.lwOriginDate));
player.x = player.lwOriginX + (player.lwTargetX - player.lwOriginX) * elapsedTime;
player.y = player.lwOriginY + (player.lwTargetY - player.lwOriginY) * elapsedTime;
player.setDepthFromPosition();
});
},

update() {
if (this.checkZones) {
zoneManager.checkDistances(this.controlledCharacter);
this.checkZones = false;
}

this.interpolateCharacterPositions();
this.controlledCharacter?.updateStep();
},

Expand Down Expand Up @@ -314,9 +262,12 @@ userManager = {
if (direction) this.controlledCharacter.playAnimation(characterAnimations.run, direction);
} else this.controlledCharacter.setAnimationPaused(true);

if (moving || this.controlledCharacter.wasMoving) {
// saves the position in base only when the user stops moving
if (!moving && this.controlledCharacter.wasMoving) {
networkManager.saveCharacterState(this.controlledCharacter);
} else if (moving || this.controlledCharacter.wasMoving) {
this.scene.physics.world.update(time, delta);
throttledSavePlayer(this.controlledCharacter);
networkManager.sendCharacterNewState(this.controlledCharacter);
}

if (!peer.hasActiveStreams()) peer.enableSensor(!(this.controlledCharacter.running && moving));
Expand All @@ -327,7 +278,7 @@ userManager = {
teleportMainUser(x, y) {
this.controlledCharacter.x = x;
this.controlledCharacter.y = y;
savePlayer(this.controlledCharacter);
networkManager.saveCharacterState(this.controlledCharacter);
},

interact() {
Expand Down