Iâve been working on my game for the past 3 or 4 years or so and have rewritten the engine multiple times from scratch due to unforseen performance overhead when running everything in a single (main) thread.
From my experience, running the entire renderer (threejs) in a worker will give you a pretty big performance increase if youâre having a lot of things happening in the background and on the UI-side of things. Remember that your browser does alot of DOM-related tasks on the main thread too whenever a user clicks something or an elementâs CSS is updated.
Having threejs in a dedicated worker allows it to keep rendering images while your browser can perform its tasks unhindered on the main thread as well, giving your users a much smoother experience (no microstutters). Youâll have to use requestAnimationFrame
from a worker though, which is relatively new.
As for things like pathfinding, Iâve moved this to a separate thread as well because my level geometry is made up of voxels, so its pretty small. You donât want to ping-pong your entire scene graph over to other threads, thats too much overhead due to serialization. So basically just sending the mutated state of the level to the worker whenever it changes (which isnât very often), then I simply query for a path using âfromâ and âtoâ vectors from the pathfinding thread.
It takes a lot of work to set-up âinter thread communicationâ properly and effectively, and it can create a lot of overhead if done improperly, but it can most certainly give more benefits than doing everything in a single thread. My engine (or rather framework) uses dependency injection to have âsmall modulesâ which can communicate with each other over different threads. Everything is written in Typescript, which makes development so much easier and more comprehensible.
// This runs in a worker context.
@Thread // Mark this class as a "spawnable worker" that can be created using the ThreadFactory.
class PathfinderThread
{
@Delegate // mark the method is a delegator for threading.
public async getPath(from: Vector3, to: Vector3): Promise<Vector3[]>
{
const route = [];
// Route building logic here.
return route;
}
}
// This runs in another thread. In this case, the renderer thread where Threejs lives.
@Injectable
export class Renderer
{
@Inject(ThreadFactory) private threadFactory: ThreadFactory;
private pathFinder: ThreadDelegate<PathFinder>;
constructor()
{
(async() => {
this.pathFinder = await this.threadFactory.spawn(PathFinder);
// Fetch the route. The "request" here is the [[ThreadDelegate]] that knows about methods
// marked with the @Delegate annotation and communicates with them as regular javascript functions.
// Data (de-)serialization happens automatically based on received types.
const myRoute = await this.pathFinder.request.getRoute(
new Vector3(0, 0, 0),
new Vector3(10, 5, 23)
);
console.log(myRoute); // Will show an empty array :p
})();
}
}
tl;dr: Yes, web workers are benificial, but only if done properly and used for the right reasons. The only thing I personally found most benificial is moving the renderer (threejs) to a worker. The other things like AI-logic or game-logic completely depends on what your workers need to know and if the communication between threads is worth the overhead.