Hello everyone,
Iβm working on a multiplayer experience using Networked A-Frame and encountering an issue where the scale
attribute of the target-template
isnβt syncing correctly across clients. Below is my complete code for reference. I would appreciate any insights or suggestions to resolve this problem.
//body
<!-- body.html is optional; elements will be added to your html body after app.js is loaded. -->
<a-scene
networked-scene="adapter: sharedar; connectOnLoad: false"
lobby-handler
lobby-pages="lobbyPage.minPlayers: 2; lobbyPage.maxPlayers: 2;"
xrextras-loading
xrextras-runtime-error
xrextras-gesture-detector>
<a-assets>
<a-asset-item id="snowball-model" src="assets/snowball.glb"></a-asset-item>
<a-asset-item id="stone-model" src="assets/stone.glb"></a-asset-item>
<a-asset-item id="stone-model2" src="assets/stone2.glb"></a-asset-item>
<!-- Avatar Template -->
<template id="avatar-template">
<a-cone scale="0.001 0.001 0.001"></a-cone>
</template>
<template id="target-template">
<a-entity
geometry="primitive: box"
position="0 0.5 -1"
material="
color: #AD50FF; shader: flat;
src: https://cdn.8thwall.com/web/assets/cube-texture.png"
></a-entity>
</template>
<!-- White Sphere Template -->
<template id="white-sphere-template">
<a-entity >
<a-gltf-model
src="#snowball-model"
scale="0.1 0.1 0.1"
shadow="cast: true; receive: false;">
</a-gltf-model>
</a-entity>
</template>
<!-- Black Sphere Template with stone.glb -->
<template id="black-sphere-template">
<a-entity >
<a-gltf-model
src="#stone-model"
scale="0.005 0.005 0.005"
shadow="cast: true; receive: false;">
</a-gltf-model>
</a-entity>
</template>
</a-assets>
<a-camera
id="camera"
position="0 2 2"
raycaster="objects: .cantap"
cursor="fuse: false; rayOrigin: mouse;">
</a-camera>
<!-- Spawner without specifying a template -->
<a-entity
networked="template: #avatar-template"
xrextras-attach="target: camera"
spawner>
</a-entity>
<a-box
id="ground"
class="cantap"
scale="1000 2 1000"
position="0 -0.99 0"
material="shader: shadow; transparent: true; opacity: 0.4"
shadow>
</a-box>
<a-entity
id="sphere"
networked="template: #target-template; networkId: sphere; persistent: true; owner: scene; attachTemplateToLocal: true"
>
</a-entity>
</a-scene>
//app.js
// app.js is the main entry point for your 8th Wall app. Code here will execute after head.html
// is loaded, and before body.html is loaded.
import {lobbyHandlerComponent} from './lobby-handler'
AFRAME.registerComponent('lobby-handler', lobbyHandlerComponent)
AFRAME.registerComponent('spawner', {
init() {
const ground = document.getElementById('ground');
const camera = document.getElementById('camera');
const sceneEl = this.el.sceneEl;
const target = document.getElementById('sphere');
// Function to determine player number
const getPlayerNumber = () => {
const connectedClients = NAF.connection.getConnectedClients();
const clientIds = Object.keys(connectedClients);
clientIds.push(NAF.clientId);
clientIds.sort(); // Ensure consistent ordering
const playerNumber = clientIds.indexOf(NAF.clientId) + 1;
return playerNumber;
};
ground.addEventListener('click', (event) => {
// Determine the player number
const playerNumber = getPlayerNumber();
// Select the sphere template based on player number
let sphereTemplate = '#white-sphere-template'; // Default to white sphere
if (playerNumber === 2) {
sphereTemplate = '#black-sphere-template';
}
// Create a new entity for the sphere
const newElement = document.createElement('a-entity');
// Get the camera position
const cameraPos = new THREE.Vector3();
camera.object3D.getWorldPosition(cameraPos);
newElement.setAttribute('position', cameraPos);
// Set the networked attribute using the selected sphere template
newElement.setAttribute('networked', {
template: sphereTemplate,
attachTemplateToLocal: true,
});
// Append the new element to the scene
sceneEl.appendChild(newElement);
// Get the target sphere position
const targetPos = new THREE.Vector3();
target.object3D.getWorldPosition(targetPos);
// Calculate the direction vector from camera to target sphere
const direction = new THREE.Vector3();
direction.subVectors(targetPos, cameraPos).normalize();
// Calculate the target position (current target sphere position)
const finalPos = targetPos.clone();
// Animate the sphere to move towards the target position
newElement.setAttribute('animation', {
property: 'position',
to: `${finalPos.x} ${finalPos.y} ${finalPos.z}`,
dur: 1000,
easing: 'linear',
});
// Add the check-collision component
newElement.setAttribute('check-collision', '');
// Optionally, remove the sphere after a certain time to prevent accumulation
setTimeout(() => {
if (newElement.parentNode) {
newElement.parentNode.removeChild(newElement);
}
}, 1000); // Adjust the duration as needed
});
},
});
AFRAME.registerComponent('check-collision', {
init: function () {
this.target = document.getElementById('sphere');
this.sphere = this.el;
this.lifespan = 5000; // Sphere lifespan in milliseconds
this.startTime = Date.now();
},
tick: function () {
const now = Date.now();
// Remove the sphere after its lifespan expires
if (now - this.startTime > this.lifespan) {
if (this.sphere.parentNode) {
this.sphere.parentNode.removeChild(this.sphere);
}
return;
}
const spherePos = new THREE.Vector3();
const targetPos = new THREE.Vector3();
this.sphere.object3D.getWorldPosition(spherePos);
this.target.object3D.getWorldPosition(targetPos);
const distance = spherePos.distanceTo(targetPos);
// Collision detection (adjust the threshold as needed)
if (distance < 0.5) {
// Get the current scale of the target sphere
const targetScale = this.target.getAttribute('scale') || { x: 1, y: 1, z: 1 };
// Determine if the sphere is a snowball or stone
const template = this.sphere.getAttribute('networked').template;
if (template === '#white-sphere-template') {
// Snowball - increase size
this.target.setAttribute('scale', {
x: targetScale.x + 0.1,
y: targetScale.y + 0.1,
z: targetScale.z + 0.1,
});
} else if (template === '#black-sphere-template') {
// Stone - decrease size
const newScale = {
x: Math.max(0.1, targetScale.x - 0.1),
y: Math.max(0.1, targetScale.y - 0.1),
z: Math.max(0.1, targetScale.z - 0.1),
};
this.target.setAttribute('scale', newScale);
}
NAF.utils.takeOwnership(this.el)
// Remove the sphere after collision
if (this.sphere.parentNode) {
this.sphere.parentNode.removeChild(this.sphere);
}
}
},
});
// Define NAF Schemas for Networked A-Frame
const addNafSchemas = () => {
NAF.schemas.getComponentsOriginal = NAF.schemas.getComponents
NAF.schemas.getComponents = (template) => {
if (!NAF.schemas.hasTemplate('#avatar-template')) {
NAF.schemas.add({
template: '#avatar-template',
components: [
'position',
'rotation',
],
})
}
if (!NAF.schemas.hasTemplate('#target-template')) {
NAF.schemas.add({
template: '#target-template',
components: [
'position',
'rotation',
'scale',
],
})
}
const components = NAF.schemas.getComponentsOriginal(template)
return components
}
}
// Wait on DOM ready
setTimeout(() => {
addNafSchemas()
})
Issue:
The scale
attribute defined in the target-template
doesnβt seem to synchronize across different clients. Changes to the scale occur locally but arenβt reflected for other connected players.
What Iβve Tried:
- Verified that the
scale
component is included in the NAF schema. - Ensured that ownership is correctly transferred using
NAF.utils.takeOwnership
. - Checked for any console errors related to networking or component synchronization.
Any advice or solutions would be greatly appreciated!
Thank you!