0

I’m currently working on a game built using Noa.js and Babylon.js. In the existing entity.js file from the project template, the player is created using a simple box mesh.

I’m trying to replace this box with a .glb model to serve as the main player character. I’ve successfully loaded the GLB model into the scene, but it spawns separately and doesn’t inherit the behavior or properties of the existing player (like movement, controls, or collision handling).

What’s the correct way to replace the current box mesh used for the player entity with a GLB model, while ensuring it functions as the main player (movement, collision detection, and controls) within the Noa.js engine?

I’ll attach my current implementation for reference in the comments — would really appreciate if someone could review it and help me identify where I’m going wrong.

Thanks in advance!

Here is my code:

import * as BABYLON from '@babylonjs/core';
import '@babylonjs/loaders';

import { noa } from './engine';
import { setMeshShadows } from './shadows';
import { blockIDs } from './registration';
import blastImg from './assets/blast.png';

let sheepMesh = null;
let gameActive = true;
let explosionSound = new Audio('./assets/explosion.mp3');

const npcSheepEntities = [];

/**
 * Convert 3D mesh position to 2D screen coordinates.
 */
function toScreenPosition(mesh, scene) {
  if (!mesh || !scene || !scene.activeCamera) {
    return { x: window.innerWidth / 2, y: window.innerHeight / 2 };
  }

  const worldMatrix = mesh.getWorldMatrix();
  const transformMatrix = scene.getTransformMatrix();
  const camera = scene.activeCamera;

  const engine = scene.getEngine();
  const viewport = camera.viewport.toGlobal(engine.getRenderWidth(), engine.getRenderHeight());

  const projected = BABYLON.Vector3.Project(
    mesh.position,
    worldMatrix,
    transformMatrix,
    viewport
  );

  return { x: projected.x, y: projected.y };
}

/**
 * Display a blast effect at given screen position.
 */
function showBlastEffect(screenX = window.innerWidth / 2, screenY = window.innerHeight / 2) {
  return new Promise((resolve) => {
    const blastElement = document.createElement('img');
    blastElement.src = blastImg;
    blastElement.style.position = 'fixed';
    blastElement.style.top = `${screenY}px`;
    blastElement.style.left = `${screenX}px`;
    blastElement.style.transform = 'translate(-50%, -50%)';
    blastElement.style.pointerEvents = 'none';
    blastElement.style.zIndex = '9999';
    blastElement.style.width = '300px';
    blastElement.style.height = '300px';
    blastElement.style.opacity = '1';
    blastElement.style.transition = 'opacity 1s ease-out';

    document.body.appendChild(blastElement);

    setTimeout(() => { blastElement.style.opacity = '0'; }, 300);
    setTimeout(() => {
      document.body.removeChild(blastElement);
      resolve();
    }, 1100);
  });
}

/**
 * Load a GLB model and attach it to a given entity.
 * Mesh follows entity position in game loop.
 */
async function loadAndAttachGLBToEntity(entityId, modelPath, modelFilename, options = {}) {
  const scene = noa.rendering.getScene();
  const dat = noa.entities.getPositionData(entityId);
  const { width: w, height: h, position: pos } = dat;

  const {
    scaleMultiplier = 1,
    yOffset = 0
  } = options;

  return new Promise((resolve, reject) => {
    BABYLON.SceneLoader.ImportMesh(
      null,
      modelPath,
      modelFilename,
      scene,
      (meshes) => {
        const visualMesh = meshes.find(m => m.getTotalVertices && m.getTotalVertices() > 0) || meshes[0];
        if (!visualMesh) return reject(new Error('No visible mesh found in GLB model.'));

        visualMesh.position.set(pos[0], pos[1] + h / 2 + yOffset, pos[2]);
        visualMesh.scaling = new BABYLON.Vector3(w, h, w).scale(scaleMultiplier);
        visualMesh.material = noa.rendering.makeStandardMaterial();
        visualMesh.visibility = 1;

        noa.entities.addComponent(entityId, noa.entities.names.mesh, {
          mesh: visualMesh,
          offset: [0, h / 2 + yOffset, 0],
        });

        setMeshShadows(visualMesh, true);

        if (entityId === noa.playerEntity) {
          sheepMesh = visualMesh;
        }

        noa.on('beforeRender', () => {
          if (!gameActive) return;
          const data = noa.entities.getPositionData(entityId);
          if (!data) return;
          visualMesh.position.set(
            data.position[0],
            data.position[1] + yOffset,
            data.position[2]
          );
          if (entityId === noa.playerEntity) {
            checkFenceCollision(data.position[0], data.position[1], data.position[2]);
          }
        });

        resolve(visualMesh);
      },
      null,
      (scene, message, exception) => {
        console.error('Error loading GLB model:', message, exception);
        reject(exception);
      }
    );
  });
}

/**
 * Collision detection between player and fence blocks.
 */
function checkFenceCollision(x, y, z) {
  if (!gameActive) return;
  const blockBelow = noa.getBlock(x, y, z);
  const blockAtFeet = noa.getBlock(x, y + 0.5, z);
  if (blockBelow === blockIDs.fence || blockAtFeet === blockIDs.fence) {
    endGame();
  }
}

/**
 * Update sheep counter in UI.
 */
function updateSheepCount() {
  const sheepCountElement = document.querySelector('#sheep-counter .counter-value');
  if (sheepCountElement) {
    sheepCountElement.textContent = npcSheepEntities.length.toString();
  }
}

/**
 * End the game — hide sheep mesh, play sound and show blast.
 */
async function endGame() {
  if (!gameActive) return;
  gameActive = false;

  const scene = noa.rendering.getScene();
  let screenPos = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
  if (sheepMesh) {
    screenPos = toScreenPosition(sheepMesh, scene);
    sheepMesh.setEnabled(false);
  }

  explosionSound.play();
  await showBlastEffect(screenPos.x, screenPos.y);

  document.getElementById('end-game-screen').style.display = 'flex';
  document.getElementById('game-ui').style.display = 'none';
}

/**
 * Spawn a single NPC sheep entity.
 */
async function spawnNPCSheep(startX, startY, startZ) {
  const eid = noa.entities.add([startX, startY, startZ], 0.5, 1.2);
  npcSheepEntities.push(eid);

  await loadAndAttachGLBToEntity(eid, '/castle/', 'sheep.glb', {
    scaleMultiplier: 0.4,
    yOffset: 0,
  });

  updateSheepCount();

  // Give sheep circular wandering movement
  const centerX = startX + (Math.random() - 0.5) * 100;
  const centerZ = startZ + (Math.random() - 0.5) * 100;
  const radius = 5 + Math.random() * 15;
  const angularSpeed = 0.0005 + Math.random() * 0.001;
  const angleOffset = Math.random() * 2 * Math.PI;

  noa.on('beforeRender', () => {
    if (!gameActive) return;
    const time = performance.now();
    const angle = angleOffset + angularSpeed * time;
    const x = centerX + Math.cos(angle) * radius;
    const z = centerZ + Math.sin(angle) * radius;
    const currentY = noa.entities.getPosition(eid)[1];
    noa.entities.setPosition(eid, [x, currentY, z]);
  });
}

/**
 * Spawn a herd of NPC sheeps around player.
 */
async function spawnNPCSheepHerd(centerX, centerY, centerZ) {
  for (let i = 0; i < 5; i++) {
    const randX = centerX + (Math.random() - 0.5) * 160;
    const randZ = centerZ + (Math.random() - 0.5) * 160;
    await spawnNPCSheep(randX, centerY, randZ);
  }
}

/**
 * Initialize player entity and NPC herd after game tick.
 */
noa.once('tick', async () => {
  try {
    const eid = noa.playerEntity;
    if (!eid) throw new Error('Player entity not found');

    // Replace box mesh with GLB model for player
    await loadAndAttachGLBToEntity(eid, '/castle/', 'sheep.glb', {
      scaleMultiplier: 0.4,
      yOffset: 0
    });

    // Adjust camera follow position for player sheep
    const sheepHeight = noa.ents.getPositionData(eid).height;
    const eyeOffset = 0.8 * sheepHeight;

    noa.ents.removeComponent(noa.camera.cameraTarget, 'followsEntity');
    noa.ents.addComponent(noa.camera.cameraTarget, 'followsEntity', {
      entity: eid,
      offset: [0, eyeOffset, 0],
    });

    // Spawn NPC herd
    const pos = noa.entities.getPosition(eid);
    await spawnNPCSheepHerd(pos[0], pos[1], pos[2]);

    console.log('Player sheep loaded and NPC herd spawned.');
  } catch (error) {
    console.error('Failed to initialize game:', error);
  }
});

https://github.com/fenomas/noa-examples

I clone the code from here

1 Answer 1

1

Short version, create a parent and reparent the glb to the same parent as the box mesh. Now, if you're doing physics calculations (which it appears you're doing) you might need to twiddle with bounding boxes and whatnot.

Sign up to request clarification or add additional context in comments.

1 Comment

Hello sir, can you please tell me if i need to made the mesh solid can i do that? I made ai related sheep's here can i make meshes solid. right now it didn't get solid and moves like ghost each other

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.