Why is Goodluck the way it is?
Why is it called Goodluck?
The GL in OpenGL stands for „graphics library”. It’s commonly used as a prefix in OpenGL API and constant names, like glDrawArrays()
and GL_ARRAY_BUFFER
. I’ve always read it as „good luck”, however, like in „good luck and have fun”, GLHF.
Why does Goodluck use the Entity-Components-System architecture?
ECS is a data-driven architecture developed to make game development easier. It favors composition over inheritance. Rather than build complex hierarchies of base classes and subclasses, which unavoidably ends with subclasses inheriting too much logic from superclasses, in ECS you compose bits of logic together in form of components enabled on a single entity.
ECS makes it easy to tweak the component composition for an entity even very late into the development cycle, without forcing a complete re-think of the inheratice hierachy. It's great for prototyping, shifting schedules, tight deadlines, and a lot of re-scoping. In other words, it's great for game jams (but not only!).
We were intrigued by ECS ever since we started our gamedev adventure. When we found a simple C implementation of the pattern on the gamedev.net forums, we thought it could actually work great in JavaScript, too. That's how Goodluck was born.
All of that doesn’t mean that inheritance and OOP are inherently bad, of course. In fact, Goodluck uses inheritance where it makes sense, too. And for an example of how to build composable APIs through inheritance (and with the help of the scene graph), see Godot Engine.
Why does Goodluck use bitflags?
Goodluck uses bitflags and bitmasks to store the component composition of entities. Bitflags are fast and simple to work with. They also work great with for loops that Goodluck uses to iterate over all entities in the world inside systems (see below). Bitwise operations are very fast, despite the fact that under the hood, the JS engine casts the 64-bit float to a 32-bit integer for the purpose of the bitwise operation, and then upgrades it back to a float.
Some of the most performant and advanced ECS implementations use so called archetypes or other solutions instead. They usually require more code, but are able to achieve greater iteration speeds. That doesn’t mean Goodluck’s approach is slow. In fact, Goodluck’s ECS is still one of the fastest among popular JavaScript implementations.
With bitflags, enabling and disabling components on an entity is very fast. To add a component, bitwise-OR it with the entity’s signature:
world.Signature[entity] |= Has.Children;
To disable a component on an entity, bitwise-AND its negation with the entity's signature:
world.Signature[entity] &= ~Has.Children;
Checking whether whether the entity’s signature has a specific component enabled requires a bitwise AND.
if (world.Signature[entity] & Has.Children) …
Checking whether the entity’s signature has a specific composition of components enabled also needs only a single AND, but it also requires that the result of the AND be compared to the mask that was checked for.
const QUERY = Has.Transform | Has.Shake; if ((world.Signature[entity] & QUERY) == QUERY) …
Why does Goodluck use for
loops everywhere?
For iterating over all entities that satisfy a particular component composition, Goodluck uses regular for loops. These loops tend to be very fast. It is usually OK to have many systems that iterate over the entire world once or even a few times every frame.
const QUERY = Has.Transform | Has.Shake; export function sys_shake(game: Game, delta: number) { for (let ent = 0; ent < game.World.Signature.length; i++) { if ((game.World.Signature[ent] & QUERY) == QUERY) { // … } } }
Iterating over even tens of thousands of signatures takes a fraction of a millisecond on modern computers and phones. Some more advanced ECS implementations optimize the iteration and data access even further. For example, it’s common to map sparse arrays of component data to dense arrays of so-called archetypes, and iterate over contiguous data. This can improve the iteration speed at the cost of code complexity and speed of other common operations, like toggling a component mask.
Another more advanced technique is to store component data in typed arrays, one for each data field. This can improve the performance of iteration by an order of magnitude, again at the cost of code and API complexity. We find that Goodluck's approach is fast enough for most use-cases. Goodluck's ECS is rarely the bottleneck. Consequently, we decided to prioritze lean code and the simple API.
for
loops are so fast that it's usually a better idea to iterate over the entire world to find a particular entity every time you need it, than to do it once, cache it and then keep the cache up-to-date. Iterating over live world data gives the guarantee that the data you work with is fresh and up-to-date. This can help avoid many bugs. Goodluck makes a few notable exceptions to this rule, however, e.g. in sys_camera
, which updates the game.Cameras
array, used in many other systems, as well as in sys_collide
, whose time complexity is quadratic. In both of these examples, however, the data collected from the world doesn't persist across frames.
Why is component data stored in arrays?
In ECS, the data related to a particular concern is stored in one large array, indexed by entity. This improves data locality, which in turn improves performance. Think of it as a column-oriented database. This pattern is sometimes also called a structure of arrays, as opposed to the array of structures (i.e., a list of game objects).
Some ECS implementations optimize the indexing by mapping large sparse arrays to smaller, tightly-packed dense arrays. Goodluck doesn't, because it would require more code. In Goodluck, the implementation is the API. The per-entity component data is stored in large arrays, stored themselves on the World class. Each of these arrays has the length equal to the total number of entities in the world. This is why it's common to see the following pattern in Goodluck's systems:
// Access the current entity's transform and lifespan. let entity_transform = world.Transform[entity]; let entity_lifespan = world.Lifespan[entity];
Why math functions take an out param?
The math library in common/
is based on the excellent glMatrix, which we ported to TypeScript. glMatrix uses the pattern of all vector functions taking as the first parameter the reference to the out vector, into which the function's result is written. This helps avoid short-lived allocations of new vectors in the memory.
Goodluck continues with this pattern, also for performance reasons. It's not so much because creating small arrays is slow (it's not), but rather because they cause intermittent GC pauses, which can lead to skipped frames. This is why it's a common pattern in Goodluck's systems to use "static" vectors, defined outside the function body, which hold values temporarily. This works because JavaScript is single-threaded. Just be careful to not pass such vectors to functions that take ownership of the arguments, like transform()
!
// Store the target world position in this vector to avoid allocating new // vectors for each processed entity. const target_position: Vec3 = [0, 0, 0]; function update(game: Game, entity: Entity) { // Follower must be a top-level transform for this to work. let transform = game.World.Transform[entity]; let follow = game.World.Follow[entity]; let target_transform = game.World.Transform[follow.Target]; get_translation(target_position, target_transform.World); lerp(transform.Translation, transform.Translation, target_position, follow.Stiffness); game.World.Signature[entity] |= Has.Dirty; }
In general, we found that predictable performance is better than very good performance sometimes. Smooth animation require stable performance.
Why are components defined as interfaces rather than classes?
Interfaces are a TypeScript feature. They describe the properties of component data for the purpose of type-checking. On build time they "evaporate" which helps keep the bundle size small, although the actual impact is likely negligible. Today, there's little incentive to switch to classes, so we continue with interfaces.
We also wanted to make it more clear that component data should by just data, which is aligned with the ECS architecture. Component objects are data dictionaries, and should not contain any logic (methods).
Why are components added using mixin functions?
Component mixins are functions defined by convention in component implementation files, which return thunks called by instantiate()
when a new entity is created.
// The shake mixin. export function shake(radius: number) { // Returns a thunk with further logic. The thunk can modify the Game // instance, and has knowledge of the newly created entity. return (game: Game, entity: Entity) => { // Enable the Shake bitflag on the entity's signature. game.World.Signature[entity] |= Has.Shake; // Store the Shake data in the corresponding World array. game.World.Shake[entity] = { Radius: radius, }; }; }
Component mixins is where Goodluck allows itself to use an abstraction to improve developer experience. Unlike classes with constructors, mixin functions allow mutable access to Game
and World
instances, which makes it easy to store the entity's data in the corresponding component array, as well as modify the entity's signature.
Why does Goodluck use free functions rather than methods?
A lot of logic in Goodluck is coded as free functions (a.k.a. non-member functions) that take the object to process as the first parameter. We use this approach, rather than exposing the same functionality as methods of the object, to allow the build pipeline to effectively tree-shake the codebase and remove unused logic from the final bundle.
Why is Goodluck a repository template?
Goodluck is designed for extreme hackability. When you generate a new repository from it, you get all the code, for you to modify as you please, for the sole purpose of shipping a game. You don't need to worry about upgrading to newer versions of Goodluck, because bootstrapping is a one-way process. Goodluck projects are optimized for a relatively short development cycle and a single release date — at the game jam's deadline.
Why does Goodluck use symlinks?
The upstream Goodluck repository, the one that you can use as a template to generate a new repository for your project, uses symlinks to share the core component and system files across multiple examples.
Symlinks are the least bad solution that I found to a problem that is only relevant in the upstream Goodluck repository: with 15 different examples, both in 2D and 3D, the total number of components is greater than 32. Because of how JavaScript performs bitwise operations, that’s more than Goodluck can support. At the same time, none of the examples has more than twenty-something components, and the real-life experience leads me to believe that 32 is an acceptable limit for the kind and scope of games Goodluck was developed for.
If components and systems were files that are imported into your codebase rather than symlinks, they would need to be generic over concrete values of component bitmasks, specific to your codebase. This would mean more code and likely more complex APIs.
Instead, symlinks allow Goodluck to stay lean and pretend that sys_transform
in example A uses example A’s value of the Has.Transfom
mask.
Once you bootstrap a new project by running ./bootstrap.sh NewProject2D
or ./bootstrap.sh NewProject3D
, all symlinks are replaced with their targets, and you never have to worry about them again.