2D Engines benchmark 2021 - including Construct 3 results

0 favourites
From the Asset Store
Casino? money? who knows? but the target is the same!
  • Hi all,

    I ran a benchmark across various 2D engines to check rendering/processing performance - no Unity or Unreal sorry. If you're familiar with the bunnymark benchmark, it's similar - spawn a lot of instances, give them each a random direction/velocity, and wrap their positions to the screen. The result shown is the number of instances the engines could handle before FPS dropped below 30FPS. Tested on Windows 10 PC with Ryzen 3600, 16GM RAM, Radeon RX 570. Construct 3 was tested with Edge (Chromium) but the other engines were all exported to native applications.

    Gamemaker Studio 2

    Gamemaker has several export options - VM (C++ engine but running interpreted GML code) and YYC (everything is compiled into c++) - and for both of these there's a 32-bit option and a new 64-bit option. Results:

    VM 32-bit: 37,000

    YYC 32-bit: 39,00

    VM 64-bit: 48,000

    YYC 64-bit: 67,000

    YYC 64-bit Delta Time*: 55,000

    *Note that by default GameMaker used fixed step timing (no delta time), so to make a fairer comparison I ran another test incorporating delta time.

    AppGameKit Studio

    AppGameKit Tier 1 uses a C++ engine to interpret AGK script - this seems to be the option used by most developers. There is AppGameKit Tier 2 which is basically using their API to help write the game in C++ but I didn't test this. Results:

    AGK Tier 1*: 21,000 - 36,000

    *Note the range of values - it dropped to 30FPS at 21,000 but was able to hold 30FPS up to 36,000 - I suspect this is related to how they handle frame timing or report FPS.

    GDevelop

    The only other JavaScript based engine on the list. Was curious given it's 'similarity' to Construct. Results:

    G-Develop: 15,000

    Godot

    Godot uses a C++ engine to interpret GDScript - again this seems to be the option used by most developers. There is the option to write in C# or C++ (with a lower level API) but I didn't test this.

    Godot GDScript* - 21,000

    *Note that this was a debug build because I had issues trying to compile a release build, so performance is lower than it should be, but based on past experience it is slower than GameMaker VM, so my guess would be 25-35,000 in a release build.

    Defold

    Defold uses a C++ engine with LuaJIT scripting (generally considered the fastest interpreted language). Results:

    Defold*: 16,000-25,000

    *Note this another case where the engine drops to but then holds to 30FPS.

    Raylib

    Raylib is a pure C engine, very light weight and very low level, more framework than engine, similar to SDL, SFML etc. Also worth noting that this benchmark was programmed somewhat differently, it doesn't use instances, but a large array of positions, velocities etc., and then uses those array values to draw the texture - it is very performant in terms of memory access/CPU caching etc. - it would be more similar to how Construct 3 handles particles VS sprites.

    Results:

    Raylib: 230,000

    Raylib with rotation*: 150,000

    *Note that there are different rendering commands depending if the sprite is rotated. As you can see having to apply a rotation transform has a significant performance impact.

    NOW WHAT YOU HAVE BEEN WAITING FOR!

    Construct 3

    The Construct 3 benchmark was programmed in multiples ways; (a) using the inbuilt bullet and wrap behaviours (b) using events instead of behaviours (c) using a conditional expression instead of multiple IF events. Results:

    C3 behaviours: 72,000

    C3 events: 60,000

    C3 conditional expression*: 85,000

    C3 conditional expression with rotation*: 58,000

    *Construct 3 seems to have an optimization which allows it to render non-rotated sprites more efficiently.

    Final Thoughts

    Hats off to the Construct team. These results had me triple checking my Construct code to make sure I wasn't cheating.

    Apart from Raylib, which was expected to easily win and isn't really a fair comparison, Construct 3 shows better performance than the other engines. Construct even manages to beat the fully compiled C++ native export of GameMaker, which was really surprising actually.

    There is some overhead to events (about 20% in this case), however I feel like I could have optimized the events version a lot more if ELSE conditions in Construct worked like in other languages, i.e. filter a smaller and smaller subset of instances to subsequent conditions.

    The fact there seem to be inbuilt optimizations for rendering non-rotated sprites etc. is nice. And the boon is that Construct performance was better while at the same time offering more inbuilt functionality than most of these engines.

    Not to end on a downer, but keep in mind benchmarks are benchmarks, and don't reflect a whole game. I know there are certain things like tilemaps and collisions which are faster in other engines. But on the whole, a very impressive showing by Construct.

  • Thanks simstratic,

    1) I'm curious how such a pronounced difference can be possible between C3 and gamemaker if, as you state, gamemaker YYC export is compiling to entirely C++. Is this a case where JIT compiling can outperform C++? My guess is it probably has to due with the difficulties of replicating comparisons between different engines.

    2) Can you go into more detail about the different ways you set up Construct 3s experiments? (Behaviors vs Events vs conditional expressions vs conditional expression with rotation) There are some significant differences between these results that could be relevant to those of us trying to squeeze out every bit of performance possible.

    Thanks,

    1Step

  • The only problem I see is that the browser just doesn't offer a viable monetization method. At least not in any way that would rely on being performant.

    Windows Wrapper would most likely return equal results since it was done using Edge, but the wrapper is not a great option... yet.

  • 1) I think part of it is because the GML scripting language is not typed, so even when it's compiled to C++ code, it probably has to do a lot of inefficient type checking/casting. And if I remember correctly, certain types of variable lookup can also be slow, which is why GameMaker people will often copy them into a local variable first. So it's not as fast as a C++ engine could be without a scripting language.

    2)

    a) Behaviours was simply assigning the Bullet and Wrap behaviours to the object. Wrap was set to wrap to viewport. In the create object event, the Bullet actions were used to set a random direction and velocity to the Bullet behaviour for that instance.

    b) For the events, I deleted the behaviours from the object and I added two instance variables - sx and sy (speed x and speed y). Then in the event sheet - on create object I set sx and sy to random values. Each step I use actions to apply movement - i.e. set sprite x = x + sx*dt, set sprite y = y + sy*dt. Then there's a series of IF events to handle the wrapping:

    if sprite x < 0 set sprite x to 800 (800 being viewport width)

    if sprite x > 800 set sprite x to 0

    if sprite y < 0 set sprite y to 450 (450 being viewport width)

    if sprite y > 450 set sprite y to 0

    c) Avoids all the if statements by putting it into a single conditional expression when applying movement to the object, i.e. each step:

    action - set sprite x = x < 0 ? 800 : x > 800 ? 0 : x + sx*dt

    action - set sprite y = y < 0 ? 450 : y > 450 ? 0 : y + sy*dt

    These expressions allow you to 'short circuit' the evaluation, i.e. if the first part (x<0) is true then the rest of the checks will be skipped. This is what I wished the ELSE event would do...

    Hope that makes sense...

  • The only problem I see is that the browser just doesn't offer a viable monetization method. At least not in any way that would rely on being performant.

    Windows Wrapper would most likely return equal results since it was done using Edge, but the wrapper is not a great option... yet.

    Yes, I am more interested in desktop publishing, not browser or mobile, so the new Windows wrapper definitely perked my interest, but for me it needs some Steam integration first (at least achievements) to be a viable option.

  • c) Avoids all the if statements by putting it into a single conditional expression when applying movement to the object, i.e. each step:

    action - set sprite x = x < 0 ? 800 : x > 800 ? 0 : x + sx*dt

    action - set sprite y = y < 0 ? 450 : y > 450 ? 0 : y + sy*dt

    These expressions allow you to 'short circuit' the evaluation, i.e. if the first part (x<0) is true then the rest of the checks will be skipped. This is what I wished the ELSE event would do...

    Hope that makes sense...

    Wow, 25k difference by switching to conditional expressions?

    I thought the else condition operates the same way, but with the downside of still having event block overhead unlike the conditional expression. This is interesting, thanks again for these experiments.

  • I thought the else condition operates the same way, but with the downside of still having event block overhead unlike the conditional expression. This is interesting, thanks again for these experiments.

    I could be wrong, somebody correct me if I am, but I think the ELSE condition still checks every instance (not only those that failed the prior condition).

    To give credit, it was eleanorjmorel who showed me how much faster conditional expressions can be.

  • Yeah, we talked about conditional expressions here: construct.net/en/forum/construct-3/general-discussion-7/personal-performance-160459

    This test just really showcases how big of a difference this can be.

    As for ELSE, per the manual it is as you state. I use ELSE quite religiously, so this is good to know.

  • Try Construct 3

    Develop games in your browser. Powerful, performant & highly capable.

    Try Now Construct 3 users don't see these ads
  • As for ELSE, per the manual it is as you state. I use ELSE quite religiously, so this is good to know.

    I used to use it quite a bit, but they changed negated, or inverted to work with just about everything.

  • Why not include C3 test project at least? I think bunnymark test usually does not use wrap, they jump up and fall down.

  • Why not include C3 test project at least? I think bunnymark test usually does not use wrap, they jump up and fall down.

    SnipG yes the traditional bunnymark bounced off the screen edges instead of wrapping. The reason why I used wrapping is because Construct and GameMaker both have an inbuilt wrap function/behaviour, so I wanted to test the performance of the inbuilt functions (engine) vs manually coded wrapping (scripting).

    I cleaned up the C3 project and uploaded, there is a separate layout for each approach, so preview the appropriate layout (not the whole project). Link:

    drive.google.com/file/d/1TpfYWE-WsrfsGgfDEVDzQUWep7X216fB/view

    Credit - The sprite wabbit_alpha.png is taken from the Raylib repository:

    github.com/raysan5/raylib/tree/master/examples/textures/resources

  • Not like it matters. But you can replace conditional expression with:

    (((Self.X)%800)+800)%800+Self.sx*dt

    (((Self.Y)%450)+450)%450+Self.sy*dt

    To wrap even more bunnies in bunnywrap test.

  • We've worked hard to make Construct performance shine, and it's nice to see that reflected in a microbenchmark. Modern JavaScript JITs are also extremely sophisticated and it's cool to see that they're competitive even with compilers.

    However I'd caution against taking any performance advice from tests like these. As noted often you can get significantly different results by making obscure changes to events. However these changes usually only affect microbenchmarks that are hammering one code path super hard. Real games generally don't do that, so porting microbenchmark-level changes to real games will probably have no effect. So don't look at results like these and think "Wow, I should always use conditional expressions instead of conditions". Most likely you'll just needlessly obscure your events to no performance gain. As ever, with real games the only meaningful approach is to make measurements, and target optimisations at the things you've actually measured are slow, and only keep optimisations that measurably improve things.

  • Even using Set position, instead set X and Y. Nets like +10k objects in this test. So even simple changes give more performance.

  • A thing to be aware of when doing micro-benchmarks is what you are testing.

    In case of "bunnymark", it is "how many moving objects with little to no logic can I have", which, assuming that each respective engine has the general competency of using GPU for rendering and batching same-textured objects together using vertex buffers, effectively translates to "how little overhead there is to an object that has a sprite and nothing else".

    The results are consistent with expectations:

    • GameMaker historically has a certain amount of "luggage" attached to instances, which has them check for velocity, check if they have timers to tick, tick animation frames, etc. Consequently, this also means that storing your buns in an array of structs or a ds_grid will yield much better results.
    • Godot has a different kind of luggage - although objects don't have velocity and timers to calculate by default, they do have support for child objects and consequently calculate transformation matrices.
    • Construct has little luggage - unless an object actively presents itself on the scene, it won't even have position variables. Although some of this advantage would fade as you add more complex logic to the objects, this makes it a good fit for this kind of comparisons, which had been the case for quite a while.
    • You bring your own luggage for RayLib - with no entity system to boot, you likely wrote the exact amount of code to stuff your bun-structs into a vector and move them around, or maybe opted to use separate arrays per-property for better caching, or even used par_unseq to allow bun movements to be processed in parallel on multiple CPU cores, or even implemented GPU instancing... rest assured, the result unbeatable because the overhead comes solely from your own oversight.

    In the end, what Ashley said - use the profiler to figure out pain points, and especially be wary of incorporating "cool tricks" that appear to perform better on synthetic tests.

Jump to:
Active Users
There are 1 visitors browsing this topic (0 users and 1 guests)