UNPKG

networked-aframe

Version:

A web framework for building multi-user virtual reality experiences.

620 lines (516 loc) 18.8 kB
/* global AFRAME, NAF, THREE */ var deepEqual = require('../DeepEquals'); var InterpolationBuffer = require('buffered-interpolation'); // InterpolationBuffer.MODE_LERP is not exported, it's undefined var MODE_LERP = 0; var DEG2RAD = THREE.MathUtils.DEG2RAD; var OBJECT3D_COMPONENTS = ['position', 'rotation', 'scale']; // Expose InterpolationBuffer on NAF global NAF.InterpolationBuffer = InterpolationBuffer; function defaultRequiresUpdate() { let cachedData = null; return (newData) => { if (cachedData === null || !deepEqual(cachedData, newData)) { cachedData = AFRAME.utils.clone(newData); return true; } return false; }; } function isValidVector3(v) { return !!( v.isVector3 && !isNaN(v.x) && !isNaN(v.y) && !isNaN(v.z) && v.x !== null && v.y !== null && v.z !== null ); } function isValidQuaternion(q) { return !!( q.isQuaternion && !isNaN(q.x) && !isNaN(q.y) && !isNaN(q.z) && !isNaN(q.w) && q.x !== null && q.y !== null && q.z !== null && q.w !== null ); } var throttle = (function () { var previousLogTime = 0; return function throttle(f, milliseconds) { var now = Date.now(); if (now - previousLogTime > milliseconds) { previousLogTime = now; f(); } }; })(); function warnOnInvalidNetworkUpdate() { NAF.log.warn(`Received invalid network update.`); } function clearObject(obj) { for (const key in obj) { delete obj[key]; } return obj; } AFRAME.registerSystem("networked", { init() { // An array of "networked" component instances. this.components = []; this.nextSyncTime = 0; }, register(component) { this.components.push(component); }, deregister(component) { const idx = this.components.indexOf(component); if (idx > -1) { this.components.splice(idx, 1); } }, tick: (function() { // "d" is an array of entity datas per entity in this.components. const data = { d: [] }; return function() { if (!NAF.connection.adapter) return; if (this.el.clock.elapsedTime < this.nextSyncTime) return; // Reset the "d" array data.d.length = 0; for (let i = 0, l = this.components.length; i < l; i++) { const c = this.components[i]; if (!c.isMine()) continue; if (!c.el.parentElement) { NAF.log.error("entity registered with system despite being removed"); //TODO: Find out why tick is still being called return; } const syncData = this.components[i].syncDirty(); if (!syncData) continue; data.d.push(syncData); } if (data.d.length > 0) { NAF.connection.broadcastData('um', data); } this.updateNextSyncTime(); }; })(), updateNextSyncTime() { this.nextSyncTime = this.el.clock.elapsedTime + 1 / NAF.options.updateRate; } }); AFRAME.registerComponent('networked', { schema: { template: {default: ''}, attachTemplateToLocal: { default: true }, persistent: { default: false }, networkId: {default: ''}, owner: {default: ''}, creator: {default: ''} }, init: function() { this.OWNERSHIP_GAINED = 'ownership-gained'; this.OWNERSHIP_CHANGED = 'ownership-changed'; this.OWNERSHIP_LOST = 'ownership-lost'; this.onOwnershipGainedEvent = { el: this.el }; this.onOwnershipChangedEvent = { el: this.el }; this.onOwnershipLostEvent = { el: this.el }; this.conversionEuler = new THREE.Euler(); this.conversionEuler.order = "YXZ"; this.bufferInfos = []; this.bufferPosition = new THREE.Vector3(); this.bufferQuaternion = new THREE.Quaternion(); this.bufferScale = new THREE.Vector3(); var wasCreatedByNetwork = !!this.el.firstUpdateData; this.onConnected = this.onConnected.bind(this); this.syncData = {}; this.componentsData = {}; this.componentSchemas = NAF.schemas.getComponents(this.data.template); this.cachedElements = new Array(this.componentSchemas.length); this.networkUpdatePredicates = this.componentSchemas.map(x => (x.requiresNetworkUpdate && x.requiresNetworkUpdate()) || defaultRequiresUpdate()); // Fill cachedElements array with null elements this.invalidateCachedElements(); this.initNetworkParent(); let networkId; if (this.data.networkId === '') { networkId = NAF.utils.createNetworkId() this.el.setAttribute(this.name, {networkId}); } else { networkId = this.data.networkId; } if (!this.el.id) { this.el.setAttribute('id', 'naf-' + networkId); } if (wasCreatedByNetwork) { this.firstUpdate(); } else { if (this.data.attachTemplateToLocal) { this.attachTemplateToLocal(); } this.registerEntity(this.data.networkId); } this.lastOwnerTime = -1; if (NAF.clientId) { this.onConnected(); } else { document.body.addEventListener('connected', this.onConnected, false); } document.body.dispatchEvent(this.entityCreatedEvent()); this.el.dispatchEvent(new CustomEvent('instantiated', {detail: {el: this.el}})); this.el.sceneEl.systems.networked.register(this); }, attachTemplateToLocal: function() { const template = NAF.schemas.getCachedTemplate(this.data.template); const elAttrs = template.attributes; // Merge root element attributes with this entity for (let attrIdx = 0; attrIdx < elAttrs.length; attrIdx++) { this.el.setAttribute(elAttrs[attrIdx].name, elAttrs[attrIdx].value); } // Append all child elements while (template.firstElementChild) { this.el.appendChild(template.firstElementChild); } }, takeOwnership: function() { const owner = this.data.owner; const lastOwnerTime = this.lastOwnerTime; const now = NAF.connection.getServerTime(); if (owner && !this.isMine() && lastOwnerTime < now) { this.lastOwnerTime = now; this.removeLerp(); this.el.setAttribute('networked', { owner: NAF.clientId }); this.syncAll(); this.onOwnershipGainedEvent.oldOwner = owner; this.el.emit(this.OWNERSHIP_GAINED, this.onOwnershipGainedEvent); this.onOwnershipChangedEvent.oldOwner = owner; this.onOwnershipChangedEvent.newOwner = NAF.clientId; this.el.emit(this.OWNERSHIP_CHANGED, this.onOwnershipChangedEvent); return true; } return false; }, initNetworkParent: function() { var parentEl = this.el.parentElement; if (parentEl['components'] && parentEl.components['networked']) { this.parent = parentEl; } else { this.parent = null; } }, registerEntity: function(networkId) { NAF.entities.registerEntity(networkId, this.el); }, applyPersistentFirstSync: function() { const { networkId } = this.data; const persistentFirstSync = NAF.entities.getPersistentFirstSync(networkId); if (persistentFirstSync) { this.networkUpdate(persistentFirstSync); NAF.entities.forgetPersistentFirstSync(networkId); } }, firstUpdate: function() { var entityData = this.el.firstUpdateData; this.networkUpdate(entityData); this.el.firstUpdateData = undefined; }, onConnected: function() { if (this.data.owner === '') { this.lastOwnerTime = NAF.connection.getServerTime(); this.el.setAttribute(this.name, { owner: NAF.clientId, creator: NAF.clientId }); setTimeout(() => { //a-primitives attach their components on the next frame; wait for components to be attached before calling syncAll if (!this.el.parentNode){ NAF.log.warn("Networked element was removed before ever getting the chance to syncAll"); return; } this.syncAll(undefined, true); }, 0); } document.body.removeEventListener('connected', this.onConnected, false); }, isMine: function() { return this.data.owner === NAF.clientId; }, createdByMe: function() { return this.data.creator === NAF.clientId; }, tick: function(time, dt) { if (!this.isMine() && NAF.options.useLerp) { for (var i = 0; i < this.bufferInfos.length; i++) { var bufferInfo = this.bufferInfos[i]; var buffer = bufferInfo.buffer; var object3D = bufferInfo.object3D; var componentNames = bufferInfo.componentNames; buffer.update(dt); if (componentNames.includes("position")) { const position = buffer.getPosition(); if (isValidVector3(position)) { object3D.position.copy(position); } else { throttle(warnOnInvalidNetworkUpdate, 5000); } } if (componentNames.includes("rotation")) { const quaternion = buffer.getQuaternion(); if (isValidQuaternion(quaternion)) { object3D.quaternion.copy(quaternion); } else { throttle(warnOnInvalidNetworkUpdate, 5000); } } if (componentNames.includes("scale")) { const scale = buffer.getScale(); if (isValidVector3(scale)) { object3D.scale.copy(scale); } else { throttle(warnOnInvalidNetworkUpdate, 5000); } } } } }, /* Sending updates */ syncAll: function(targetClientId, isFirstSync) { if (!this.canSync()) { return; } var components = this.gatherComponentsData(true); var syncData = this.createSyncData(components, isFirstSync); if (targetClientId) { NAF.connection.sendDataGuaranteed(targetClientId, 'u', syncData); } else { NAF.connection.broadcastDataGuaranteed('u', syncData); } }, syncDirty: function() { if (!this.canSync()) { return; } var components = this.gatherComponentsData(false); if (components === null) { return; } return this.createSyncData(components); }, getCachedElement(componentSchemaIndex) { var cachedElement = this.cachedElements[componentSchemaIndex]; if (cachedElement) { return cachedElement; } var componentSchema = this.componentSchemas[componentSchemaIndex]; if (componentSchema.selector) { return this.cachedElements[componentSchemaIndex] = this.el.querySelector(componentSchema.selector); } else { return this.cachedElements[componentSchemaIndex] = this.el; } }, invalidateCachedElements() { for (var i = 0; i < this.cachedElements.length; i++) { this.cachedElements[i] = null; } }, gatherComponentsData: function(fullSync) { var componentsData = null; for (var i = 0; i < this.componentSchemas.length; i++) { var componentSchema = this.componentSchemas[i]; var componentElement = this.getCachedElement(i); if (!componentElement) { if (fullSync) { componentsData = componentsData || clearObject(this.componentsData); componentsData[i] = null; } continue; } var componentName = componentSchema.component ? componentSchema.component : componentSchema; var componentData = componentElement.getAttribute(componentName); if (componentData === null) { if (fullSync) { componentsData = componentsData || clearObject(this.componentsData); componentsData[i] = null; } continue; } var syncedComponentData = componentSchema.property ? componentData[componentSchema.property] : componentData; // Use networkUpdatePredicate to check if the component needs to be updated. // Call networkUpdatePredicate first so that it can update any cached values in the event of a fullSync. if (this.networkUpdatePredicates[i](syncedComponentData) || fullSync) { componentsData = componentsData || clearObject(this.componentsData); componentsData[i] = syncedComponentData; } } return componentsData; }, createSyncData: function(components, isFirstSync) { var { syncData, data } = this; syncData.networkId = data.networkId; syncData.owner = data.owner; syncData.creator = data.creator; syncData.lastOwnerTime = this.lastOwnerTime; syncData.template = data.template; syncData.persistent = data.persistent; syncData.parent = this.getParentId(); syncData.components = components; syncData.isFirstSync = !!isFirstSync; return syncData; }, canSync: function() { // This client will send a sync if: // // - The client is the owner // - The client is the creator, and the owner is not in the room. // // The reason for the latter case is so the object will still be // properly instantiated if the owner leaves. (Since the object lifetime // is tied to the creator.) if (this.data.owner && this.isMine()) return true; if (!this.createdByMe()) return false; const clients = NAF.connection.getConnectedClients(); for (let clientId in clients) { if (clientId === this.data.owner) return false; } return true; }, getParentId: function() { this.initNetworkParent(); // TODO fix calling this each network tick if (!this.parent) { return null; } var netComp = this.parent.getAttribute('networked'); return netComp.networkId; }, /* Receiving updates */ networkUpdate: function(entityData) { // We received a network update but the component is not initialized yet, // merge this.el.firstUpdateData with latest received entityData and return. // The component initialization will call this.firstUpdate() that will call networkUpdate(this.el.firstUpdateData) // https://github.com/networked-aframe/networked-aframe/issues/476 if (this.componentSchemas === undefined) { if (this.el.firstUpdateData && this.el.firstUpdateData !== entityData) { this.el.firstUpdateData.components = {...this.el.firstUpdateData.components, ...entityData.components}; } return; } // Avoid updating components if the entity data received did not come from the current owner. if (entityData.lastOwnerTime < this.lastOwnerTime || (this.lastOwnerTime === entityData.lastOwnerTime && this.data.owner > entityData.owner)) { return; } if (this.data.owner !== entityData.owner) { var wasMine = this.isMine(); this.lastOwnerTime = entityData.lastOwnerTime; const oldOwner = this.data.owner; const newOwner = entityData.owner; this.el.setAttribute('networked', { owner: entityData.owner }); if (wasMine) { this.onOwnershipLostEvent.newOwner = newOwner; this.el.emit(this.OWNERSHIP_LOST, this.onOwnershipLostEvent); } this.onOwnershipChangedEvent.oldOwner = oldOwner; this.onOwnershipChangedEvent.newOwner = newOwner; this.el.emit(this.OWNERSHIP_CHANGED, this.onOwnershipChangedEvent); } if (this.data.persistent !== entityData.persistent) { this.el.setAttribute('networked', { persistent: entityData.persistent }); } this.updateNetworkedComponents(entityData.components); }, updateNetworkedComponents: function(components) { for (var componentIndex = 0, l = this.componentSchemas.length; componentIndex < l; componentIndex++) { var componentData = components[componentIndex]; var componentSchema = this.componentSchemas[componentIndex]; var componentElement = this.getCachedElement(componentIndex); if (componentElement === null || componentData === null || componentData === undefined ) { continue; } if (componentSchema.component) { if (componentSchema.property) { this.updateNetworkedComponent(componentElement, componentSchema.component, componentSchema.property, componentData); } else { this.updateNetworkedComponent(componentElement, componentSchema.component, componentData); } } else { this.updateNetworkedComponent(componentElement, componentSchema, componentData); } } }, updateNetworkedComponent: function (el, componentName, data, value) { if(!NAF.options.useLerp || !OBJECT3D_COMPONENTS.includes(componentName)) { if (value === undefined) { el.setAttribute(componentName, data); } else { el.setAttribute(componentName, data, value); } return; } let bufferInfo; for (let i = 0, l = this.bufferInfos.length; i < l; i++) { const info = this.bufferInfos[i]; if (info.object3D === el.object3D) { bufferInfo = info; break; } } if (!bufferInfo) { bufferInfo = { buffer: new InterpolationBuffer(MODE_LERP, 0.1), object3D: el.object3D, componentNames: [componentName] }; this.bufferInfos.push(bufferInfo); } else { var componentNames = bufferInfo.componentNames; if (!componentNames.includes(componentName)) { componentNames.push(componentName); } } var buffer = bufferInfo.buffer; switch(componentName) { case 'position': buffer.setPosition(this.bufferPosition.set(data.x, data.y, data.z)); return; case 'rotation': this.conversionEuler.set(DEG2RAD * data.x, DEG2RAD * data.y, DEG2RAD * data.z); buffer.setQuaternion(this.bufferQuaternion.setFromEuler(this.conversionEuler)); return; case 'scale': buffer.setScale(this.bufferScale.set(data.x, data.y, data.z)); return; } NAF.log.error('Could not set value in interpolation buffer.', el, componentName, data, bufferInfo); }, removeLerp: function() { this.bufferInfos = []; }, remove: function () { document.body.removeEventListener('connected', this.onConnected, false); if (this.isMine() && NAF.connection.isConnected()) { var syncData = { networkId: this.data.networkId }; if (NAF.entities.hasEntity(this.data.networkId)) { NAF.connection.broadcastDataGuaranteed('r', syncData); } else { // The entity may already have been removed if the creator (different of the current owner) left the room. // Don't log an error in this case. if (!(this.data.creator && NAF.connection.activeDataChannels[this.data.creator] === false)) { NAF.log.error("Removing networked entity that is not in entities array."); } } } NAF.entities.forgetEntity(this.data.networkId); document.body.dispatchEvent(this.entityRemovedEvent(this.data.networkId)); this.el.sceneEl.systems.networked.deregister(this); }, entityCreatedEvent() { return new CustomEvent('entityCreated', {detail: {el: this.el}}); }, entityRemovedEvent(networkId) { return new CustomEvent('entityRemoved', {detail: {networkId: networkId}}); } });