Haxelib review: libnoise

Libnoise

This is a haxe port of libnoise, the coherent noise library. The port is almost complete, only the gradient and noise2D utilities are missing.

Every now and then, I try out a new library and find something cool. libnoise is one such library, so it’s time for a Haxelib review!

In this post, I’ll explain what libnoise offers, how good it is at its job, and where it has room for improvement. Plus whatever else I think of along the way.

As its description states, libnoise is a tool for generating coherent noise. To keep things simple, I’m going to pretend “coherent noise” means “grayscale images.” But if you want to dig deeper, Rick Sidwell has an excellent rundown.

Let’s begin with some coherent noise.

This cloud-like image is what’s called Perlin noise, and it’s just one of many patterns libnoise can make.

Generators

Each of libnoise’s generators creates a distinct pattern. Four of these generators are dead simple.

  • Const a solid color. It can be any shade, though this demo keeps it simple.
  • Cylinder and Sphere create repeating gradients centered on the top-left corner. Because the demo is 2D, they barely resemble their namesakes, so you just have to imagine a bunch of 3D spheres nested inside one another, or a bunch of nested cylinders centered on the left edge.
  • Checker creates a checkerboard pattern on the pixel level. One pixel is white, the next is black, the next is white, and so on.

The other four involve randomness.

  • Perlin generates (what else?) Perlin noise. Not going to go into depth on how it works; see Rick Sidwell’s post for that. This example uses 8 octaves (fractal layers), meaning there’s extra detail but it takes longer to draw.
  • Billow creates what I’d describe as wandering lines, but under the hood it’s very similar to Perlin. If you compare the source code side-by-side, you’ll notice the algorithm is identical except for a single Math.abs() call on line 35.
  • RidgedMultifractal also creates wandering lines. I guess you could also call them “ridges”? That’s probably how it got that name. If you look at the source code, you’ll see that it’s also pretty similar to Perlin, though with a few more differences. In the end, it comes out looking like the inverse of Billow.
  • Voronoi creates a Voronoi diagram. This is a way of partitioning space based on a set of “seed” points, where every pixel is assigned to the closest seed… You know what? I can’t fit a full explanation here, so go read the article for the details.

3D patterns

I hinted at this already, but libnoise is built to generate 3D patterns. You can set the Z coordinate to 0 – as I did – and just make 2D images, but each of these 2D images is a cross-section of the full pattern. That’s why libnoise calls the classes Cylinder and Sphere rather than Line and Circle: the full 3D pattern really is cylindrical/spherical.

Voronoi, Perlin, Billow, and RidgedMultifractal all subtly change because of this. Each pixel in these patterns is influenced by points outside the 2D plane, meaning they appear different than if we were using a 2D algorithm to generate them. (Because the 2D algorithm would only calculate points in the 2D plane.) But it’s very hard to tell just by looking at a single image of them.

Here’s a way to see the difference. If we took different cross-sections of a cylinder, we could see its lines start to curve.

If we kept going until 90°, it would look fully circular, just like Sphere.

Operators

libnoise’s generators make the basic images, but its operators are where things get interesting. They modify those base images in all kinds of ways. Inverting, combining, you name it.

To get a good sense of how an operator works, it helps to see the input and output side-by-side. The downside is, it requires splitting up the canvas and cropping each input and output. To view more of a pattern, hover over it and click the “expand” button in the bottom corner.

Unary operators

The simplest operators are those that only take a single image as input, like the Rotate operator shown above. Most unary operators either perform simple arithmetic or move the pattern somehow.

The above three perform arithmetic on the brightness of each pixel. libnoise represents brightness using a value between -1 (black) and 1 (white).

  • Abs takes the absolute value of the brightness, so anything below 0 (gray) becomes positive. After this, no part of the pattern will be darker than gray.
  • Clamp sets a minimum and maximum value. This demo uses -0.5 (dark gray) as the minimum and 0.5 (light gray) as the maximum, but other programs could adjust further. This cuts out the shadows and highlights but leaves the midtones untouched.
  • Invert does exactly what it says on the tin.


Rotate, Scale, and Translate do exactly what their names imply. Though they can rotate, scale, and translate in any direction, this demo only includes two options each.

Turbulence moves pixels in random directions. “Low” and “high” refer to the maximum distance pixels are allowed to move. However, there’s a bit more to it than that, if we look closely. Fortunately, libnoise allows applying an operator to an operator, so we can Scale the Turbulence.

While it normally looks grainy, if we zoom in enough times, the turbulence starts to look surprisingly smooth. It turns out that it doesn’t move pixels completely at random. There’s an underlying pattern, and that pattern is called… Perlin noise.

Yes indeed, the Turbulence class generates Perlin noise to figure out where it should move each pixel. Perlin noise is a type of gradient noise, and gradient noise always looks smooth when you zoom in. The trick is that the Turbulence class does not zoom in by default, creating the illusion that it isn’t smooth.

Binary operators

libnoise’s two-input operators all perform arithmetic. As a reminder, libnoise represents brightness using a value between -1 (black) and 1 (white).

  • Add takes the sum of two patterns. If both inputs are bright, the output will be even brighter. If both are dark, the output will be even darker.
  • Subtract takes the difference. It’s the same as if you inverted the second pattern before adding.
  • Multiply takes the product of the numbers. Since we’re multiplying numbers less than 1, they always tend towards 0, and we end up with a lot of gray.
  • Average looks like Add but grayer, because that’s exactly what it is: (a + b) / 2. Strangely libnoise doesn’t include this operator, so I implemented it myself for demo purposes.
  • Min takes the darker value at each point.
  • Max takes the lighter value at each point.

Ternary operators

Finally, libnoise has two three-input operators, and they’re both very similar. Their output is always a combination of pattern 1 and pattern 2, and they use pattern 3 to decide what combination.

  • Select is all-or-nothing. If pattern 3 is darker than a certain value, it selects pattern 1 and shows that. If pattern 3 is above that threshold, it selects pattern 2.
  • Blend interpolates between the first two patterns, using pattern 3 to decide how much of each to blend. If pattern 3 is dark, it’ll use more of pattern 1. If pattern 3 is light, it’ll use more of pattern 2.
  • Select also has a fallOff value that makes it do a little of both. If a number is close to the threshold value, it blends just like Blend. If the number is farther away, it selects like normal.

Hopefully now you have a good idea of what each generator and operator does, so it’s time for some more interesting combinations. But first, it’s time to take the training wheels off:

The UI is simple to explain, if unwieldy. Hover over a section of canvas to see a dropdown. Pick an option from the dropdown to fill that part of the canvas. Scroll all the way to the bottom of the dropdown if you want to split the canvas up (or put it back together).

With that out of the way, here are some interesting patterns I’ve come across. Try them out, and be sure to try switching up the generators. You can always revert your changes by clicking the button again.

What patterns can you come up with? If you make something you want to share, you can copy it with ctrl+C, and others can paste it in with ctrl+V. Feel free to post it in the comments, but make sure to insert four spaces in front of your code. If you don’t, WordPress could mess up your formatting.

For those who want to dig even deeper, check out the demo’s source code or libnoise’s source code.

Oh right, the review

I was supposed to be reviewing this library, not just showing off a bunch of cool patterns. …Though on the other hand, showing the cool patterns gives you an idea of what the library is good at. Isn’t that half the point of a review?

Well, perhaps I should do a traditional review too. I’d describe libnoise as functional and well-designed, but lacking in documentation and not very beginner-friendly.

Let’s look at some code. Here’s just about the simplest way you could implement the “minimum of two gradients” sample. (As a reminder, that’s Min, Billow, and Cylinder.)

//Step 1: define the pattern.
var billow:Billow = new Billow(0.01, 2, 0.5, seed, HIGH);
var cylinder:Cylinder = new Cylinder(0.01);
var min:Min = new Min(billow, cylinder);

//Step 2: create something to draw onto.
var bitmapData:BitmapData = new BitmapData(512, 512, false);

//Step 3: iterate through every pixel.
for(x in 0...bitmapData.width) {
    for(y in 0...bitmapData.height) {
        //Step 3a: Get the pixel value, a number between -1 and 1. Use a z coordinate of 0.
        var value:Float = min.getValue(x, y, 0);
        
        //Step 3b: Convert to the range [0, 255].
        var brightness:Int = Std.int(128 + value * 128);
        if(brightness < 0) {
            brightness = 0;
        } else if(brightness >= 256) {
            brightness = 255;
        }
        
        // Step 3c: Convert to a color.
        var color:Int = brightness << 16 | brightness << 8 | brightness;
        
        //Step 3d: Save the pixel color.
        bitmapData.setPixel32(x, y, color);
    }
}

//Step 4: display and/or save the bitmap.
addChild(new Bitmap(bitmapData));

libnoise makes steps 1 and 3a easy enough, but you have to fill in all the other steps yourself. That’s fine – maybe even desirable – for advanced users. However, a new user who just wants to try it out isn’t going to appreciate the extra work. The new user would like to be able to do something like this:

//Step 1: define the pattern.
var billow:Billow = new Billow(0.01, 2, 0.5, seed, HIGH);
var cylinder:Cylinder = new Cylinder(0.01);
var min:Min = new Min(billow, cylinder);

//Step 2: make the drawing.
var noise2D:Noise2D = new Noise2D(512, 512, min);
var bitmapData:BitmapData = noise2D.getBitmapData(GradientPresets.grayscale);

//Step 3: display and/or save the bitmap.
addChild(new Bitmap(bitmapData));

Room for improvement

Here’s what I’d work on if I ever began using libnoise seriously.

  • Finish the port. The GradientPresets and Noise2D classes I mentioned would make it much easier to add color and export images. They existed in the original version(s) of libnoise, but didn’t survive the port.
    • The author explained that they didn’t want to tie libnoise to outside libraries (like OpenFL), but I’d say it’s more than worth it. Besides, conditional compilation makes it easy to support OpenFL without depending on it.
  • More generators. If you ignore the fluff, libnoise only implements two noise algorithms: Perlin and Voronoi. There are plenty more algorithms out there, including Simplex, an improvement on Perlin noise whose patent recently expired, and Worley noise, resembling a textured Voronoi diagram.
    • libnoise already implements value noise under the hood, but doesn’t make it available to the user. It’d be very easy to add.
    • And there’s no need to limit the library to coherent noise. Why not pure static?
  • More options for existing operators. Turbulence, for instance, can’t be scaled without applying an Scale operator to the whole image. But what if you want the zoomed-in turbulence effect without zooming in on the underlying pattern?
  • More operators. I made a custom Average operator for the demo, but that should be in the base library. Besides that, I’d like to see operators to blur, lighten, or darken the image.
  • Better performance, if possible.
  • Better documentation and code style. libnoise is a port of a port, and each time it was ported, most of the comments were lost or replaced. (Nor were all of the surviving comments accurate, due to overzealous copy-pasting.)

That’s all that comes to mind, which is probably a good sign. libnoise is entirely usable as-is, even if there’s work left to do.

Comparing libnoise to other libraries

I’m aware of four different noise libraries in Haxe. libnoise, MAN-Haxe, noisehx, and hxNoise. (Interesting coincidence: all four are from 2014-2016 and haven’t been updated since.)

Let’s look at what each library brings to the table.

  • noisehx offers Perlin noise, and that’s it. It’s also the only library to offer both 2D and 3D Perlin noise, meaning you can save processing time if you only need a 2D image.
  • hxNoise offers diamond-square noise as well, which basically functions as a faster but lower-quality version of Perlin noise. Its Perlin noise is 3D but its diamond-square noise is 2D.
  • MAN-Haxe doesn’t offer Perlin noise at all, though it offers two other types of noise that resemble it. It also offers Worley noise and a couple maze-generation algorithms. That last one is why it’s called “Mazes And Noises.” Oh, and all of this is 2D.

I’d call MAN-Haxe the most grounded of the libraries. It’s built for one specific purpose: to generate maps for HaxeFlixel games. It just incidentally happens to do images too. If your goal is to generate 2D rectangular maps for a 2D game and you happen to be using HaxeFlixel, then MAN-Haxe is right for you.

None of these alternative libraries offer operators, which means libnoise can generate a greater variety of images. That said, it isn’t like libnoise’s operators are particularly complicated. If you wanted to invert hxNoise’s diamond-square noise, you could do that yourself.

…I do believe that wraps it up. For convenience, here’s a shortcut back up to the demo. Now go forth and make some noise!

Haxe has too many ECS frameworks

Ever since I learned about it, I’ve wanted to use the entity component system (ECS) pattern to make games. Used properly, the pattern leads to clean, effective, and in my opinion cool-looking code. So when I was getting started my game engine, I looked for an ECS library that to build off of. And I found plenty.

The Haxe community is prolific and enthusiastic, releasing all kinds of libraries completely for free. That’s great, but it’s also a bit of a problem. Instead of working together to build a few high-quality libraries, everyone decided to reinvent the wheel.

xkcd: Standards

It did occur to me that I was preparing to reinvent the wheel, but no one had built a game engine capable of what I wanted, so I went ahead with it. Eventually I realized that’s probably what all the other developers were thinking too. Maybe there’s a reason for the chaos.

Let’s take a look at (eleven of) the available frameworks. What distinguishes each one?

Or if you want to see the one I settled on, skip to Echoes.

Ash

Let’s start at the beginning. Ash was one of the first ECS frameworks for Haxe, ported from an ActionScript 3 library of the same name. Makes sense: Haxe was originally based on AS3, and most of the early developers came from there.

Richard Lord, who developed the AS3 version, also wrote some useful blog posts on what an ECS architecture is and why you might want to use it.

Objectively, Ash is a well-designed engine. However, it’s held back by having started in ActionScript. Good design decisions there (such as using linked list nodes for performance) became unnecessary in Haxe, but the port still kept them in an effort to change as little as possible. This means it takes a bunch of typing to do anything.

//You have to define a "Node" to indicate which components you're looking for; in this case Position and Motion.
class MovementNode extends Node<MovementNode>
{
    public var position:Position;
    public var motion:Motion;
}
//Then systems use this node to find matching entities.
private function updateNode(node:MovementNode, time:Float):Void
{
    var position:Position = node.position;
    var motion:Motion = node.motion;

    position = node.position;
    motion = node.motion;
    position.position.x += motion.velocity.x * time;
    position.position.y += motion.velocity.y * time;
    //...
}

It honestly isn’t that bad for one example, but extra typing adds up.

ECX

ECX seems to be focused on performance, though I can’t confirm or debunk this.

As far as usability goes, it’s one step better than Ash. You can define a collection of entities (called a “Family” instead of a “Node”) in a single line of code, right next to the function that uses it. Much better organized.

class MovementSystem extends System {
    //Define a family.
    var _entities:Family<Transform, Renderable>;
    override function update() {
        //Iterate through all entities in the family.
        for(entity in _entities) {
            trace(entity.transform);
            trace(entity.renderable);
        }
    }
}

Eskimo

Eskimo is the programmer’s third attempt at a framework, and it shows in the features available. You can have entirely separate groups of entities, as if they existed in different worlds, so they’ll never accidentally interact. It can notify you when any entity gains or loses components (and you can choose which components you want to be notified of). Like in ECX, you can create a collection of components (here called a View rather than a Family) in a single line of code:

var viewab = new View([ComponentA, ComponentB], entities);
for (entity in viewb.entities) {
    trace('Entity id: ${entity.id}');
    trace(entity.get(ComponentB).int);
}

The framework has plenty of flaws, but its most notable feature is the complete lack of macros. Macros are a powerful feature in Haxe that allow you to run code at compile time, which makes programming easier and may save time when the game is running.

Lacking macros (as well as the “different worlds” thing I mentioned) slows Eskimo down, and makes it so you have to type out more code. Not as much code as in Ash, but it’s still inconvenient.

Honestly, though, I’m just impressed. Building an ECS framework without macros is an achievement, even though the framework suffers for it. Every single one of the other frameworks on this list uses macros, for syntax sugar if nothing else. Even Ash uses a few macros, despite coming from AS3 (which has no macros).

edge

edge (all lowercase) brings an amazing new piece of syntax sugar:

class UpdateMovement implements ISystem {
    function update(pos:Position, vel:Velocity) {
        pos.x += vel.vx,
        pos.y += vel.vy;
    }
}

You no longer have to create a View yourself, or iterate through that view, or type out entity.get(Position) every time you want to access the Position component. Instead, just define an update function with the components you want. edge will automatically give you each entity’s position and velocity. You don’t even have to call entity.get(Position) or anything; that’s already done. This saves a lot of typing when you have a lot of systems to write.

edge also provides most of the other features I’ve mentioned so far. Like in Eskimo, you can separate entities into different “worlds” (called Engines), and you can receive notifications when entities gain or lose components. You can access Views if needed/preferred, and it only takes a line of code to set up. Its “World” and “Phase” classes are a great way to organize systems, and the guiding principles are pretty much exactly how I think the ECS pattern should work.

Have I gushed enough about this framework yet? Because it’s pretty great. Just one small problem.

A system’s update function must be named update. A single class can only have one function with a given name. Therefore, each system can only have one update function. If you want to update two different groups of entities, you need two entire systems. So the syntax sugar doesn’t actually save that much typing, because you have to type out an entire new class declaration for each function.

Eventually, the creator abandoned edge to work on edge 2. This addresses the “one function per system” problem, though sadly in its current state it loses all the convenience edge offered. (And the lack of documentation makes me think it was abandoned midway.)

Baldrick

Baldrick is notable because it was created specifically in response to edge. Let’s look at the creator’s complaints, to see what others care about.

• It requires `thx.core`, which pulls a lot of code I don’t need

That’s totally fair. Unnecessary dependencies are annoying.

• It hasn’t been updated in a long time, and has been superceded by the author by edge2

It’s always concerning when you see a library hasn’t been updated in a while. This could mean it’s complete, but usually it means it’s abandoned, and who knows if any bugs will be fixed. I don’t consider this a deal-breaker myself, nor do I think edge 2 supersedes it (yet).

• Does a little bit too much behind-the-scenes with macros (ex: auto-creating views based on update function parameters)

Oh come on, macros are great! And the “auto-creating views” feature is edge’s best innovation.

• Always fully processes macros, even if display is defined (when using completion), slowing down completion

I never even thought about this, but now that they mention it, I have to agree. It’s a small but significant oversight.

• Isn’t under my control so isn’t easily extendable for my personal uses

It’s… open source. You can make a copy that you control, and (optionally) submit your changes back to the main project. If the original project isn’t abandoned, they’ll usually accept your contributions. (And if it is abandoned, then you can just tell people to use your fork.)

• Components and resources are stored in an `IntMap` (rather than a `StringMap` like in edge)

This is actually describing what Baldrick does, but it still mentions something edge does wrong. StringMap isn’t terrible, but Baldrick’s IntMap makes a lot more sense.

Anyway, Baldrick looks well-built, and it’s building on a solid foundation, but unfortunately it’s (quite intentionally) missing the syntax sugar that I liked so much.

var movingEntities:View<{pos:Position, vel:Velocity}> = new View();

public function process():Void {
    for(entity in positionEntities) {
        entity.data.pos.x += entity.data.vel.x;
        entity.data.pos.y += entity.data.vel.y;
    }
}

That seems like more typing than needed – entity.data.pos.x? Compare that to edge, which only requires you to type pos.x. I suppose it could be worse, but that doesn’t mean I’d want to use it.

Oh, and as far as I can tell, there’s no way to get delta time. That’s inconvenient.

exp-ecs

Short for “experimental entity component system,” exp-ecs is inspired by Ash but makes better use of Haxe. It does rely on several tink libraries (comparable to edge’s dependency on thx.core). The code looks pretty familiar by now, albeit cleaner than average:

@:nodes var nodes:Node<Position, Velocity>;

override function update(dt:Float) {
    for(node in nodes) {
        node.position.x += node.velocity.x * dt;
        node.position.y += node.velocity.y * dt;
    }
}

Not bad, even if it isn’t edge.

Under the hood, it looks like component tracking is slower than needed. tink_core’s signals are neat and all, but the way they’re used here means every time a component is added, the entity will be checked against every node in existence.

Ok, I just realized how bad that explanation probably was, so please enjoy this dramatization of real events instead, featuring workers at a hypothetical entity factory:

Worker A: Ok B, we just added a Position component. Since each node needs to know which entities have which components, we need to notify them.

Worker B: On it! Here’s a node for entities with Hitboxes; does it need to be notified?

Worker A: Nope, the entity doesn’t have a Hitbox.

Worker B: Ok, here’s a node that looks for Acceleration and Velocity; does it need to be notified?

Worker A: No, the entity doesn’t have Acceleration. (It has a Velocity, but that isn’t enough.)

Worker B: Next is a node that looks for Velocity and Position; does it need to be notified?

Worker A: Yes! The entity has both Velocity and Position.

Worker B: Here’s a node that needs both Position and Appearance; does it need to be notified?

Worker A: No, this is an invisible entity, lacking an Appearance. (It has a Position, but that isn’t enough.)

Worker B: Ok, next is a node for entities with Names; does it need to be notified?

Worker A: It would, but it already knows the entity’s Name. No change here.

Worker B: Next, we have…

This process continues for a while, and most of it is totally unnecessary. We just added a Position component, so why are we wasting time checking dozens or hundreds of nodes that don’t care about Position? None of them will have changed. Sadly, exp-ecs just doesn’t have any way to keep track. It probably doesn’t matter for most games, but in big enough projects it could add up.

(Please note that exp-ecs isn’t the only framework with this issue, it’s just the one I checked to be sure. I suspect the majority do the same thing.)

On the plus side, I have to compliment the code structure. There’s no ECS framework in existence whose code can be understood at a glance, but in my opinion exp-ecs comes close. (Oh, and the coding style seems to perfectly match my own, a coincidence that’s never happened before. There was always at least one small difference. So that’s neat.)

Cog

Cog is derived from exp-ecs, and calls itself a “Bring Your Own Entity” framework. You’re supposed to integrate the Components class into your own Entity class (and you can call your class whatever you like), and now your class acts like an entity. I don’t buy it. Essentially their Components class is the Entity class, they’re just trying to hide it.

As far as functionality, it unsurprisingly looks a lot like exp-ecs:

@:nodes var movers:Node<position, velocity>;
override public function step(dt:Float) {
    super.step(dt);
    for (node in movers) {
        node.position.x += node.velocity.x * dt;
        node.position.y += node.velocity.y * dt;
    }
}

I was pleasantly surprised to note that it has component events (the notifications I talked about for Eskimo and edge). If Cog had existed when I started building Runaway, I would have seriously considered using it. In the end I’d probably have rejected it for lack of syntax sugar, but only barely.

Awe

Awe is a pseudo-port of Artemis, an ECS framework written in Java. I’m not going to dig deep into it, because this is the example code:

var world = World.build({
    systems: [new InputSystem(), new MovementSystem(), new RenderSystem(), new GravitySystem()],
    components: [Input, Position, Velocity, Acceleration, Gravity, Follow],
    expectedEntityCount: ...
});
var playerArchetype = Archetype.build(Input, Position, Velocity, Acceleration, Gravity);
var player = world.createEntityFromArchetype(playerArchetype);

Java has a reputation for being verbose, and this certainly lives up to that. I can look past long method names, but I can’t abide by having to list out every component in advance, nor having to count entities in advance, nor having to define each entity’s components when you create that entity. What if the situation changes and you need new components? Just create a whole new entity I guess? This engine simply isn’t for programmers like me.

That said, the README hints at something excellent that I haven’t seen elsewhere…

@Packed This is a component that can be represented by bytes, thus doesn’t have any fields whose type is not primitive.

…efficient data storage. With all the restrictions imposed above, I bet it takes up amazingly little memory. Sadly this all comes at the cost of flexibility. It reminds me of a particle system, packing data tightly, operating on a set number of particles, and defining the limits of the particles’ capabilities in advance.

OSIS

OSIS combines entities, components, systems, and network support. The networking is optional, but imposes a limitation of 64 component types that applies no matter what. (I’ve definitely already exceeded that.) I don’t have the time or expertise to discuss the can of worms that is networking, so I’ll leave it aside.

Also notable is the claim that the library “avoids magic.” That means nothing happens automatically, and all the syntax sugar is gone:

var entitySet:EntitySet;

public override function init()
    entitySet = em.getEntitySet([CPosition, CMonster]);

public override function loop()
{
    entitySet.applyChanges();

    for(entity in entitySet.entities)
    {
        var pos = entity.get(CPosition);
        pos.x += 0.1;
    }
}

I have to admit this is surprisingly concise, and the source code seems well-written. The framework also includes less-common features like component events and entity worlds (this time called “EntityManagers”).

I still like my syntax sugar, I need more than 64 components, and I don’t need networking, so this isn’t the library for me.

GASM

According to lib.haxe.org, GASM is the most popular haxe library with the “ecs” tag. However, I am an ECS purist, and as its README states:

Note that ECS purists will not consider this a proper ECS framework, since components contain the logic instead of systems. If you are writing a complex RPG or MMO, proper ECS might be worth looking in to, but for more typical small scale web or mobile projects I think having logic in components is preferable.

Listen, if it doesn’t have systems, then don’t call it “ECS.” Call it “EC” or something.

It seems to be a well-built library, better-supported than almost anything else on this list. However, I’m not interested in entities and components without systems, so I chose to keep looking.

Ok, so what did I go with?

Echoes

Echoes’ original creator described it as a practice project, created to “learn the power of macros.” Inspired by several others on the list, it ticked almost every single one of my boxes.

It has syntax sugar like edge’s (minus the “one function per system” restriction), no thx or tink dependencies, yes component events, convenient system organization, and a boatload of flexibility. Despite deepcake’s (the creator’s) modesty, this framework has a lot to it. It received 400+ commits even before I arrived, and is now over 500. (Not a guarantee of quality, but it certainly doesn’t hurt.)

Echoes’ performance

I haven’t seriously tested Echoes’ speed, but deepcake (the original dev) made speed a priority, and I can tell that it does several things right. It uses IntMap to store components, it keeps track of which views care about which components (meaning it’s the first one I’m sure doesn’t suffer from the problem I dramatized in the exp-ecs section), and it does not let you separate entities into “worlds.” It’s a shame that it lacks that last feature, but on the other hand I haven’t needed worlds yet, and they do incur a performance hit.

Echoes’ flexible components

Let’s talk about how components work. In every other framework I’ve discussed thus far, a component must be a class, and it must extend or implement either “Component” or “IComponent,” respectively. There’s a very specific reason for these restrictions, but they still get in the way.

For instance, say you wanted to work with an existing library, such as—oh, I don’t know—Away3D. Suppose that Away3D had a neat little Mesh class, representing a 3D model that can be rendered onscreen. Suppose you wanted an entity to have a Mesh component. Well, Mesh already extends another class and cannot extend Component. It can implement IComponent, but that’s inconvenient, and you’d have to edit Mesh.hx. (Which falls squarely in the category of “edits you shouldn’t have to make.”) Your best bet is to create your own MeshComponent class that wraps Mesh, and that’s just a lot of extra typing.

In Echoes, almost any Haxe type can be a component. That Mesh? Valid complement, no extending or implementing necessary. An abstract type? Yep, it just works. That anonymous structure? Well, not directly, but you can wrap it in an abstract type. Or if wrapping it in an abstract is too much work, make a typedef for it. (Note: typedefs don’t work in deepcake’s build, but they were the very first thing I added, specifically because wrapping things in abstracts is too much work.)

All this is accomplished through some slightly questionable macro magic. Echoes generates a lot of extra classes as a means of data storage. For instance, Position components would be stored in a class named ContainerOfPosition. Echoes does this both to get around the “extend or implement” restriction, and because it assumes that it’ll make lookups faster. This may well be true (as long as the compiler is half-decent), it’s just very unusual.

Echoes: conclusion

I settled on Echoes for the syntax sugar and the component events. At the time, the deciding factor was component events, and I hadn’t realized any other libraries offered those. So… whoops.

I don’t regret my choice, at all. The syntax sugar is great, abstract/typedef support is crucial, and the strange-seeming design decisions hold up better than I first thought.

Addendum: ecso

ecso (whose name is always written in monospace font) is a promising addition to the Haxe ECS ecosystem. Unlike all the other libraries, this one is a compiler plugin, meaning it modifies the behavior of Haxe itself, which theoretically makes it faster to compile.

It’s a well-thought-out library, offering most of the features I want to see in an ECS framework. The main thing it’s missing is component events. The dev initially expressed interest in adding them, so I figured I’d hold off on reviewing ecso until that happened. Sadly, the library hasn’t been updated in a while, so I decided to write a short review.

As it stands now, ecso is a solid option if you want a simpler alternative to Echoes and don’t care about component events. In particular, ecso tries to provide as much transparency as possible. Its API really is only four functions long, and those functions do nothing more or less than what their names suggest. It turns out that four functions really are enough, as long as you’re willing to track and schedule everything. For better or worse, there’s no inversion of control here.