Unstable core features
Hi all, love the work you are all doing here, but recently ran into an issue with some core features, such as setting position and velocity data, leading to a WASM unreachable error.
This instability was almost enough to make me swap physics libaries, so I hope it gets addressed.
Here's the situation: This is on a Velocity based Kinematic body. I need to run 1-15 "TICKS" as fast as possible, each completing a round trip of user input -> update bodies with new forces -> run physics simulation -> report body's physics state back to the simulation. However, doing so at a high frequency will lead to a crash (not guarenteed, and more frequent the higher the tick rate)
if (p != null)
body.setTranslation(new Vector3(p[0], p[1], p[2]))
if (q != null)
body.setRotation(new Quaternion(q[0] || 0, q[1] || 0, q[2] || 0, q[3] || 1))
if (v != null)
body.setLinvel(new Vector3(v[0], v[1], v[2]))
if (aV != null)
body.setAngvel(new Vector3(aV[0], aV[1], aV[2]))
for (const cID in components) {
const { Controller, body, rigidBodyCollider, grounded } = components[cID]
if (Controller && rigidBodyCollider) {
const vel = body.linvel()
if (vel.x > 0 || vel.x < 0)
vel.x /= body.mass()
if (vel.y > 0 || vel.y < 0)
vel.y /= body.mass()
if (vel.z > 0 || vel.z < 0)
vel.z /= body.mass()
if (grounded)
vel.y = 0
else
vel.y -= 0.25
Controller.computeColliderMovement(
rigidBodyCollider, // The collider we would like to move.
vel, // The movement we would like to apply if there wasn’t any obstacle.
)
const movement = Controller.computedMovement()
if (movement.x > 0 || movement.x < 0)
movement.x /= physics.timestep
if (movement.y > 0 || movement.y < 0)
movement.y /= physics.timestep
if (movement.z > 0 || movement.z < 0)
movement.z /= physics.timestep
body.setLinvel(movement)
components[cID].grounded = Controller.computedGrounded()
}
}
Relevant code in case it helps. All values are of the right type.
Tried downgrading to 0.11.1, but still got the issue.
Edit: After a lot of attempts to diagonse this, I still get the issue, but I can't recreate a state where it fails consistently. The snapshot I load when the error occurs work if loaded manually, which makes me think there is some sort of race condition internally with the library.
This issue is a deal breaker for me and sadly I can't use this library until it's addressed.
Got this reply on the discord, but not sure it works since I've switched off the library
Hello.
I encountered this problem once. It was caused by some references of Rapier body objects that were kept in an array after the bodies were removed from the physics world. I think it messed up the lifecycle somehow.
I fixed it by only keeping their "handle" and not a reference to the full object.
I hope it can help.
I'm having this issue when dynamically adding/removing rigid bodies while the render loop is running. I have made sure to queue actions which add/remove from the physics world until after the step function is completed.
No matter what I do i run into this issue, I have made sure there is no async logic, and made sure to only store references to the rigid body / collider handles and not the direct objects.
Unfortunately this library is currently too unstable to rely on.
I've tried to narrow it down, but it seems to be something to do with dynamic RigidBodies and colliders:
// Player.ts
export class Player {
private id: string;
private rigidBodyHandle: RAPIER.RigidBodyHandle | null;
private colliderHandle: RAPIER.ColliderHandle | null;
constructor(world: RAPIER.World, position: Position = { x: 0, y: 10, z: 0 }) {
this.id = uuidv4();
const rigidBodyDesc: RAPIER.RigidBodyDesc = RAPIER.RigidBodyDesc.dynamic() // <-- changing to fixed - error doesn't occur
.setTranslation(position.x, position.y, position.z);
const rigidBody = world.createRigidBody(rigidBodyDesc);
this.rigidBodyHandle = rigidBody.handle;
const colliderDesc = RAPIER.ColliderDesc.capsule(0.5, 0.5); // <-- commenting out - error doesn't occur
const collider = world.createCollider(colliderDesc, rigidBody); // <-- commenting out - error doesn't occur
this.colliderHandle = collider.handle; // <-- commenting out - error doesn't occur
}
getId(): string {
return this.id;
}
removePlayer(world: RAPIER.World): void {
if (!world) return;
if (this.colliderHandle !== null) {
const collider = world.getCollider(this.colliderHandle);
if (collider) world.removeCollider(collider, true);
this.colliderHandle = null;
}
if (this.rigidBodyHandle !== null) {
const rigidBody = world.getRigidBody(this.rigidBodyHandle);
if (rigidBody) world.removeRigidBody(rigidBody);
this.rigidBodyHandle = null;
}
}
}
I'm adding/removing players via the World.ts class I've defined:
// World.ts
import * as RAPIER from '@dimforge/rapier3d-compat';
import { Position } from '../../shared/types';
import { Player } from './Player';
export class World {
private players: Map<string, Player> = new Map();
private world: RAPIER.World;
constructor() {
const gravity: Position = { x: 0, y: -9.81, z: 0 };
this.world = new RAPIER.World(gravity);
// Create ground
const groundCollider = RAPIER.ColliderDesc.cuboid(50.0, 0.1, 50.0)
.setFriction(0.8)
.setRestitution(0)
.setFrictionCombineRule(RAPIER.CoefficientCombineRule.Max);
this.world.createCollider(groundCollider);
}
addNewPlayer(): Player {
const player = new Player(this.world);
this.players.set(player.getId(), player);
return player;
}
removePlayer(id: string): void {
const player = this.players.get(id);
if (!player) return;
this.players.delete(id);
player.removePlayer(this.world);
}
step(): void {
try {
this.world.step();
} catch (error) {
console.log('error', error);
}
}
}
World.ts is being called from index.ts as such:
// index.ts
import { WebSocketServer, Server, WebSocket } from 'ws';
import * as RAPIER from '@dimforge/rapier3d-compat';
import { World } from './World';
const wss = new WebSocketServer({ port: 3001 });
await RAPIER.init();
const world = new World();
const connections: Map<string, WebSocket> = new Map();
type Command = () => void;
const pendingCommands: Command[] = [];
wss.on('connection', (ws: WebSocket) => {
let playerId: string | undefined;
// Attempt to queue and delay actions until after step function
pendingCommands.push(() => {
const player = world.addNewPlayer();
playerId = player.getId();
connections.set(playerId, ws);
console.log(`> Player connected: ${playerId}`);
ws.on('close', () => {
pendingCommands.push(() => {
if (!playerId) return;
console.log(`> Player disconnected: ${playerId}`);
world.removePlayer(playerId);
connections.delete(playerId);
});
});
});
});
const TICK_RATE = 20; // 20 updates per second
const TICK_INTERVAL = 1000 / TICK_RATE;
let lastTime = performance.now();
function gameLoop() {
const now = performance.now();
const delta = now - lastTime;
if (delta >= TICK_INTERVAL) {
lastTime = now;
world.step();
// Process any pending commands
while (pendingCommands.length > 0) {
const command = pendingCommands.shift();
command?.();
}
}
setImmediate(gameLoop);
}
gameLoop();
Error:
asm://wasm/0062c87e:1
RuntimeError: unreachable
at wasm://wasm/0062c87e:wasm-function[1033]:0x154305
at wasm://wasm/0062c87e:wasm-function[1680]:0x17345b
at wasm://wasm/0062c87e:wasm-function[1458]:0x16ea48
at wasm://wasm/0062c87e:wasm-function[99]:0x976b8
at wasm://wasm/0062c87e:wasm-function[904]:0x148d34
at wasm://wasm/0062c87e:wasm-function[26]:0x174e3
at wasm://wasm/0062c87e:wasm-function[184]:0xc5ed1
at step (/Users/.../Documents/Projects/.../server/node_modules/@dimforge/rapier3d-compat/rapier_wasm3d.js:3750:14)
at step (/Users/.../Documents/Projects/.../server/node_modules/@dimforge/gen3d/gen3d/pipeline/physics_pipeline.ts:69:22)
at step (/Users/.../Documents/Projects/.../server/node_modules/@dimforge/gen3d/gen3d/pipeline/world.ts:269:30)