DEV Community

Cover image for Code Migration with ECS in JavaScript (from C++)
Slobi
Slobi

Posted on

Code Migration with ECS in JavaScript (from C++)

There are instances when the need arises to import concepts from strongly typed and statically typed languages into the dynamic JavaScript. Rather than reinventing the wheel, I often turn to online solutions. Copying and adapting these solutions to match the target language is standard practice in my coding ventures.

Once upon a time

I encountered a scenario where I needed to incorporate an Entity-Component-System (ECS) architecture into my JavaScript project.

A Search for Better ECS Solution

My quest began with a disappointment in existing ECS JavaScript solutions that relied on passing strings to denote component types. This approach seemed sub-optimal, guiding me to explore alternatives.
I firmly believe that a solution that relies on strings during runtime is, in essence, a terrible one. A marginal improvement involves usage of enums, but the pinnacle is achieved when the component type inherently determines its belonging.
You may be stunned, but I am deeply into JavaScript types and inheritance chain, so it was imperative to work with ECS system in that way.

Despite exhaustive searches, I couldn't find a solution tailored to JavaScript that aligned with my principles. While TypeScript may offer some alternatives, my projects were firmly rooted in JavaScript, and I was reluctant to leave them behind.

Adopting C++ Wisdom for JavaScript Challenges

My solution emerged from an unexpected source—a C++ implementation of ECS. Taking the core of this C++ ECS implementation, I painstakingly rewrote it to suit JavaScript's dynamic nature. The transition was not without its challenges, especially when encountering issues with my existing code based on prototypical inheritance in JavaScript. However, this journey proved to be a valuable learning experience. The result of this endeavor is documented for future read My Vector library story.

Evolving Data Structures for Performance Gains

The initial data structures for the EntityManager in my JavaScript ECS library resembled the following:

function EntityManager() {
    this._generation = {}
    this._free_indices = []
    this._entities = {}
    this._components = {}
    this.__entities_with_type = {}
}
Enter fullscreen mode Exit fullscreen mode

Recognizing a bottleneck in complex games, I decided to exchange plain objects for more efficient Maps. The transformation yielded a considerable performance boost:

function EntityManager() {
    this._generation = new Map()
    this._free_indices = []
    this._entities = new Map()
    this._components = new Map()
    this.__entities_with_type = new Map()
}
Enter fullscreen mode Exit fullscreen mode

I was happy and that could be the end of this story.
Over time I used this library more frequently and it has become a base for my work flow.

Revisiting: A Code Renaissance

Recently, a wave of dissatisfaction swept over me. Even though my ECS library worked as intended certain features felt misplaced in the transition from C++ to JavaScript. Despite the initial hesitation, considering the library's extensive usage in 10+ projects, I decided to revisit the code-base. Armed with a robust test suite, I refactored the Entity structure:

function Entity(id) {
    this.id = id
}
Entity.prototype.index = function () {
    return this.id & ENTITY_INDEX_MASK
}
Entity.prototype.generation = function () {
    return (this.id >> ENTITY_INDEX_BITS) & ENTITY_GENERATION_MASK
}
Enter fullscreen mode Exit fullscreen mode

become:

function Entity() {}
Enter fullscreen mode Exit fullscreen mode

The migration involved utilizing memory addresses as keys for Maps, replacing arrays with Sets, and simplifying the overall code-base. The EntityManager transformed into a leaner, more efficient form:

function EntityManager() {
    this._entities = new Set()
    this._components = new Map()
}
Enter fullscreen mode Exit fullscreen mode

A Leaner, Meaner ECS Library

This streamlined approach yielded remarkable results. The library lost 40% of its code-base, resulting in enhanced performance and a simpler workflow. The conclusion drawn from this experience is clear: migration is acceptable, using a solution as-is is fine, but when the time comes, ensure that your interface is comprehensively covered in tests. This approach allows you to recreate and tailor your solution to the evolving environment.

May our code continue to adapt and thrive.

The path from C++ ECS wisdom to a refined JavaScript implementation that has been both challenging and rewarding. Embracing change, optimizing performance, and simplifying workflows are vital aspects of a code-base's evolution.

Happy coding!

Links:
lib
tests

Top comments (0)