Does Havok actually work on a server?

Hello again all,

I’ve been trying for quite a long time to make Havok work on the server the same way it does on the client, and it just doesn’t work. I’ve made quite a beautiful game and the bottleneck has for the last year just been getting Havok to work on the server.

Are we really sure this actually works on a headless server? There are no real examples of the server running and providing authoritative havok physics. Nearly every question on this forum doesn’t end in a solution.

I love this game engine, and i’ve tried incredibly hard for a long time. It doesn’t seem usable for my purposes.

Can someone point to me an example of someone loading a glb file on a headless server, applying physics bodies to the meshes, creating the character controller on the server, and providing authoritative physics. Or literally any individual component of the above requests.

2 Likes

@Cedric is out for a few days, but he’s probably the best person to answer this.

you have actually two questions here:

  • Can havok run on a headless server: yes. There is nothing in it that prevents it. As long as your server support WASM I see no reason why it won’t work
  • Loading a glb and basically having Babylon running on a server is a totally different game as we depends on WebGL which is not supported by headless server. To some extend you can use our NullEngine that works well with headless server but a lot of features are missing (everything rendering basically :))

To move the needle, we will need to know what is your architecture, what are the errors, etc…

2 Likes

Apologies, my choice in wording is poor. I have havok physics “running” on the server. The issue is that running physics on the server is pointless if the actual environment cannot be used for physics bodies.

Unfortunately I have no errors to report, only that I can’t get it to work. The examples from my original post would be most helpful for me, but I can try to get some relevant portions of code from my game and post it here later today.

What does “running” entail? On the headless server: Can you set up a scene, enable physics and spawn a ground mesh and sphere mesh? Can you then apply a physics impulse to the sphere? Can you then obtain e.g. the sphere.position vector? Can you send these position data to a client? Can a client respond to that message back to the server?

As for examples, maybe this can be helpful:

2 Likes

You’ve pointed out a critical flaw in my original question, I was off base in saying much of what I said. I can set up a scene, enable physics and spawn a ground mesh and sphere mesh. I can send the position data to a client and have the client utilize that and send a response.

Actually much of what you said really helped me. Im creating a fake ground on the server at the y position of the client’s glb file. Both client and server are running from a shared physics file to make sure everything runs the same on both.

My only true issue is that I cannot load a glb on the server (the environment terrain) and apply physics bodies to it.

Here is the example with NullEngine and physics bodies applied, GLB as well - https://playground.babylonjs.com/#NM90WI#3

3 Likes

It sure does work in my game, I run nullengine in the server with havok wasm using bun. I also have a dynamic system to create physics objects, though I just basically use box collider for now.

I have a small wrapper class in my game to communicate between ecs and babylon:

import {
  HavokPlugin,
  InstancedMesh,
  Mesh,
  PhysicsAggregate,
  PhysicsAggregateParameters,
  PhysicsShapeBox,
  PhysicsShapeCapsule,
  PhysicsShapeType,
  Scene,
  TransformNode,
  Vector3,
} from '@babylonjs/core'
import HavokPhysics from '@babylonjs/havok'

export class PhysicsManager {
  physicsObjects: Map<number, PhysicsAggregate>
  groundAggregates: PhysicsAggregate[] = []
  scene: Scene | undefined

  constructor() {
    this.physicsObjects = new Map<number, PhysicsAggregate>()
  }

  async setup(scene: Scene, wasmBinary?: ArrayBuffer) {
    try {
      const havokInstance = await HavokPhysics(wasmBinary ? { wasmBinary } : undefined)
      this.scene = scene
      scene.enablePhysics(new Vector3(0, -9.81, 0), new HavokPlugin(true, havokInstance))
      scene.getPhysicsEngine()?.setTimeStep(1 / 30)
      console.log('physics-manager: physics are enabled', scene.physicsEnabled)
      console.log('physics-manager: physics havok instance available', !!havokInstance)
    } catch (error) {
      console.error('physics-manager: Failed to load Havok Physics:', error)
      throw error
    }
  }

  addPhysicsObject(
    id: number,
    node: TransformNode,
    shape: PhysicsShapeCapsule | PhysicsShapeBox,
    options?: PhysicsAggregateParameters
  ) {
    const physicsAggregate = new PhysicsAggregate(node, shape, options)
    this.physicsObjects.set(id, physicsAggregate)
    return physicsAggregate
  }

  addImpulse(id: number, force: Vector3) {
    const physicsObject = this.getPhysicsObject(id)
    if (physicsObject) {
      const impulseOrigin = physicsObject.transformNode.position
      physicsObject.body.applyImpulse(force, impulseOrigin)
    }
  }

  getPhysicsObject(id: number) {
    return this.physicsObjects.get(id)
  }

  removePhysicsObject(id: number) {
    const physicsAggregate = this.getPhysicsObject(id)
    if (!physicsAggregate) {
      throw new Error('Physics object not found')
    }
    physicsAggregate.dispose()
  }

  createGroundAggregates(meshes: (Mesh | InstancedMesh)[]) {
    if (this.groundAggregates.length > 0) {
      this.groundAggregates.forEach((groundAggregate) => groundAggregate.dispose())
    }
    this.groundAggregates = meshes.map(
      (mesh) =>
        new PhysicsAggregate(
          mesh,
          PhysicsShapeType.MESH,
          { mass: 0, friction: 0.2, restitution: 0.3 },
          this.scene
        )
    )
    return this.groundAggregates
  }
}

I create a physics object like:

world.physicsManager.addPhysicsObject(
  entity,
  meshPackage.mesh,
  new PhysicsShapeBox(
    new Vector3(0, meshPackage.meshTopPoint.position.y / 2, 0),
    new Quaternion(0, 0, 0, 1),
    meshPackage.meshExtent,
    world.scene
  ),
  { mass: 1 }
)
7 Likes

Let me tell you that I love your art direction!

3 Likes

Appreciate everyone’s feedback here. Still having issues with loading the environment glb on the nullengine server side.

This is the error im getting:

BJS - [19:09:31]: Babylon.js v8.3.1 - Null engine
[HavokPhysics] Initialized with external plugin: HavokPlugin
Failed to load environment GLB: ReferenceError: XMLHttpRequest is not defined

I’ve tried many different forum posts but they either end without solution or the solution doesn’t work for me. Exposing the glb and making it accessibly over http still results in the same error.

I believe I faced this issue before and resolved it with:

import XMLHttpRequest from 'xhr2';

global.XMLHttpRequest = XMLHttpRequest;

Sources:

1 Like

Can confirm I use this still in my current project :slight_smile:

Unfortunately with typescript I have ignored the types as well for now:

// @ts-ignore
import xhr2 from 'xhr2'

// @ts-ignore
global.XMLHttpRequest = xhr2.XMLHttpRequest

Then I’m loading models from my frontend server (for now), so the models are the same that the client uses.

EDIT1: I’ve been thinking about trying to load models via file system at some point, but we will see. I think there was a problem in the past with it, like the babylon loaders not being able to do that or there was some other hassle. Not sure if this is a good idea in general, kind of depends on if you have a small set of models, since bundling them all into the game server would be a slight overkill and make the game server physically bigger.

EDIT2: There are also some other pitfalls in the server side model handling. Make sure that

  • Materials/textures: Model materials are removed/stripped, since they are not used. Might be that babylon still does some processing with them
  • Animations: Do you need the animations server side? If not, make sure they are stopped if you have them in your glb files.
  • Do you actually need the models? What is it that you need from the model in server side? Meaning that if it is just for collisions/physics, just a box might represent the model well enough. Or if you want to make it dynamic, you can load the glb and figure out the box dimensions yourself. This might be good for actor like meshes/entities. Of course for the environment you might need to have the actual mesh. BUT also for these, I have a pattern/possibility to have a separate simplified collision mesh with the actual mesh. Depends on how complex your mesh is basically if you need this.
4 Likes

That worked.. Thank you everyone.

import { LoadAssetContainerAsync } from "@babylonjs/core/Loading/sceneLoader.js";

import { HavokPhysics } from "@shared/Physics/HavokPhysics";
import path from "path";
import fs from "fs/promises";
import "@babylonjs/loaders/glTF"

// @ts-ignore
import xhr2 from 'xhr2'
// @ts-ignore
global.XMLHttpRequest = xhr2.XMLHttpRequest

import { Mesh } from "@babylonjs/core/Meshes/mesh";

    async _loadEnvironmentAndPhysics(): Promise<void> {
        const glbPath = path.resolve(
            process.cwd(),
            "public/models/environment/physics/tutorial_island.glb"
        );

        const nodeBuf = await fs.readFile(glbPath);

        const container = await LoadAssetContainerAsync(nodeBuf, this.scene, {
            name: "tutorial_island.glb",
            pluginExtension: ".glb"
        });

        container.addAllToScene();
        this.scene.createDefaultCameraOrLight(true, true, true);

        this.scene.meshes.forEach(mesh => {
            mesh.computeWorldMatrix(true);
            mesh.isPickable = true;
            mesh.doNotSyncBoundingInfo = true;
            mesh.freezeWorldMatrix();
            mesh.checkCollisions = false;

            mesh.metadata = {
                type: "environment",
            };

            const collider = HavokPhysics.instance.createEnvironmentCollider(
                mesh as Mesh,
                false,
                true
            );

            if (collider) {
                console.log(`[GameRoom] Added physics collider to mesh: ${mesh.name}`);
            } else {
                console.warn(`[GameRoom] Failed to create collider for mesh: ${mesh.name}`);
            }
        });
    }
4 Likes

Super cool, so you are loading from the file system? :star_struck: Might have to retry that as well…

Also just a heads up also, when adding physics to your game server, you are also introducing another layer of processing in it. Kind of depends where you will host your servers, but it might introduce some additional costs, well, because of additional processing :slight_smile: I had this realization in fly.io server tiers as well.

This is my init code for havok in node.js. I don’t remember the details, but it looks like I had to use a polyfill for the fetch function

    let engine = new NullEngine();
    scene = new Scene(engine);
    scene.useRightHandedSystem = true;

    //add support for file:// urls for havok
    let nodeFetchFunction = fetch;
    (fetch as any) = function (resource, options?) {
        if (typeof resource === 'string' && resource.startsWith('file://')) {
            return new Promise((resolve, reject) => {
                let filename = url.fileURLToPath(resource);
                fs.readFile(filename, undefined, (err, data) => {
                    if (err) reject(err);
                    else {
                        let r = new Response(data);
                        if (filename.endsWith('.wasm'))
                            r.headers.set('Content-Type', 'application/wasm');
                        resolve(r);
                    }
                });
            });
        } else
            nodeFetchFunction(resource, options);
    }

    //init havok physics
    HavokPhysics().then((havok) => {

        havokPlugin = new HavokPlugin(false, havok); //note: parameter _useDeltaForWorldStep does not work...
        scene.enablePhysics(new Vector3(0, -30, 0), havokPlugin);
    }

For loading GLBs from files, I have this code below to add support for local files to xhr2.
But it looks like SceneLoader.LoadAssetContainer is now deprecated and LoadAssetContainerAsync will work just fine from a buffer, as shown in a previous post

import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader.js";
import XMLHttpRequest from "xhr2";

//code from https://github.com/pwnall/node-xhr2/issues/10
class NetworkError extends Error {
    constructor(text) {
        super(text);
    }

};

const URL_BASE = './';
class LocalFilesXMLHttpRequest extends XMLHttpRequest {
    _method: any;
    upload: any;
    _finalizeHeaders: any;
    _request: any;
    _dispatchProgress: any;
    _sync: any;
    _url: any;
    _response: any;
    status: any;
    statusText: string | undefined;
    _totalBytes: any;
    _lengthComputable: any;
    _setReadyState: any;
    _onHttpResponseData: any;
    _onHttpResponseEnd: any;

    _sendRelative(data) {
        const fullUrl = new URL(URL_BASE, super._url.href).href;
        super._url = super._parseUrl(fullUrl);
        this._sendFile(data);
    }
    _sendFile(data) {
        if (this._method !== 'GET') {
            throw new NetworkError("The file protocol only supports GET");
        }
        if (data && (this._method === 'GET' || this._method === 'HEAD')) {
            console.warn(`Discarding entity body for ${this._method} requests`);
            data = null;
        } else {
            // Send Content-Length: 0
            data = data || '';
        }
        this.upload._setData(data);
        this._finalizeHeaders();
        this._request = null;
        this._dispatchProgress('loadstart');
        const defer = this._sync ? (f) => f() : process.nextTick;
        defer(() => {
            let status = 200;
            try {
                data = fs.readFileSync(url.fileURLToPath('file://' + this._url.pathname));
            } catch (error) {
                console.error(error);
                data = Buffer.from(`${error}`);
                status = 404;
            }
            this._response = null;
            this.status = status;
            this.statusText = http.STATUS_CODES[this.status];
            this._totalBytes = data.length;
            this._lengthComputable = true;
            this._setReadyState(XMLHttpRequest.HEADERS_RECEIVED);
            this._onHttpResponseData(null, data);
            this._onHttpResponseEnd(null);
        });
    }
}

(global.XMLHttpRequest as any) = LocalFilesXMLHttpRequest;

...

SceneLoader.LoadAssetContainer(url.pathToFileURL(serverParameters.mapDirectory).toString(), filename, scene, function (container) {
    ....
}

All above for Babylon.js 7.13.0

One thing is that I cant get draco compression to work on the server, but thats not a major issue currently. especially because one could make a blender script that exports a simplified glb from the mesh information specifically for the server to use.

would be cool to create custom file type that stores the havok collision information and a loader to read and parse the information, instead of loading a full blown glb server side.

1 Like