Scale Property Not Syncing in target-template with Networked A-Frame

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!

1 Like

I’d be happy to take a look. Can you share your project with the support workspace?

Is this the right way to do it?

Yes, I’ll take a look.

Hi! After taking a look it seems like sending a custom message to change the scale might be what you want to do rather than adjusting the scale on the client only.

1 Like

This topic was automatically closed 4 days after the last reply. New replies are no longer allowed.