Almost-automatic batching for TDrawableImage, Tiled rendering incredibly faster (like 30x), ability to load Tiled into TCastleScene, new isometric game demos and the plan for 2D games based on maps

isometric_game_3d designed in editor
isometric_game_3d with shadows
Rendering Tiled map using TCastleScene
Rendering Tiled map using TCastleScene with 3D rotation
isometric_game using scenes and viewport
Rendering big Tiled map (400x400) in map_viewer example
Editing big map (400x400) in Tiled

OK, this post title is probably too long 🙂 But I really want to announce a few related things, and talk about related plans. Bear with me, and read on if you’re developing 2D games using Castle Game Engine with something like a map underneath.

  1. I’m happy to announce a very powerful and easy to use optimization for TDrawableImage rendering: almost automatic batching. The way it works is really simple: if your rendering code is mostly just a sequence of calls to TDrawableImage.Draw, then you can surround it with TDrawableImage.BatchingBegin and TDrawableImage.BatchingEnd calls (these are class methods) to achieve a significant speedup.

    Underneath, batching for TDrawableImage means that rendering many times the same image gets grouped into a single OpenGL(ES) draw call, and so it’s much faster. It works because GPU has great performance if you tell it to render million quads with a single draw call, while it’s quite inefficient if you use million draw calls to render one quad each.

    E.g. this code actually does just 2 draw calls now, not 200:

    TDrawableImage.BatchingBegin;
    for I := 0 to 99 do
      MyDrawableImage1.Draw(100, 10 * I);
    for I := 0 to 99 do
      MyDrawableImage2.Draw(100, 10 * I);
    TDrawableImage.BatchingEnd;
    

    This should be a great tool for people doing lots of image rendering.

  2. Using the above batching, I have greatly optimized our 2D Tiled rendering control TCastleTiledMapControl, that now internally always activates this batching.

    As Tiled rendering uses TDrawableImage to render each tile, it benefits greatly from the new batching.

    Moreover, as another optimization, TCastleTiledMapControl no longer renders tiles that fall outside of its render rectangle. This rendering was useless, and it could cost significant time.

    I have also added a test map, examples/tiled/map_viewer/data/maps/desert_big.tmx, with 400×400 tiles, to demonstrate this optimization. Before optimization, it had 2.7 FPS on my system (yeah, less than 3 FPS…), clearly demonstrating that previous rendering was unoptimal. After adding batching, FPS increased to 24, and then avoiding rendering useless tiles gets us to nice 61 FPS. The FPS stay reasonable even at significant zoom out.

    You’re welcome to test it all yourself, just build and run examples/tiled/map_viewer/ yourself.

  3. In parallel to improving our simple 2D drawing (TDrawableImage, TCastleTiledMapControl) we also pursue another way to render 2D things in Castle Game Engine: render your whole 2D world as a number of objects (like TCastleScene and TCastleImageTransform) in a viewport.

    The approach to put things in scenes and viewports is in general much more feature-rich. Scenes can have physics, they can easily render sprite sheet animations, they can be designed visually in CGE editor (see e.g. Platformer demo done in editor this way), they also work in 3D (so a lot of work and optimizations benefits both 3D and 2D games).

    This means that in general, we advise you to use scenes and viewports for 2D games. Our new project template “2D Game” shows this approach.

    That said, the approach of “scenes and viewports” is admittedly not as efficient as (much simpler internally) rendering using TDrawableImage, TCastleTiledMapControl. This is especially visible if you try to design a big map for 2D game. Both memory (because of our X3D nodes memory usage) and performance suffers (because our DynamicBatching cannot yet handle some cases, and it doesn’t batch across scenes, so it’s worse than TDrawableImage.BatchingBegin and TDrawableImage.BatchingEnd in case of maps).

    There’s no single best solution (yet) to this “dualism”. We work on optimizing our scenes and viewports, and until it is perfect — I hope you can enjoy 2 methods of rendering 2D stuff. They are documented on How to render 2D games manual page.

  4. Pursuing the approach to render things using scenes, with many thanks to Matthias (from Free Pascal meets SDL) you can now load Tiled maps to TCastleScene. Simply point TCastleScene.URL to a Tiled map file (.tmx).

    This allows to render map inside a viewport, mix it with other viewport things (like scenes with sprite sheets) and use viewport camera (also at design-time) to pan and zoom the map. And you can even rotate map, even in 3D 🙂

    The loading is not yet efficient, and doesn’t account for some map types (like isometric or hex). So TCastleTiledMapControl remains our recommended approach to render Tiled maps for now — but at some point in the future using TCastleScene should be fully efficient and functional and then it will be advised as more feature-packed.

  5. I have been playing with creating isometric games (without Tiled help) too.

    The result are new demos examples/isometric_game and examples/isometric_game_3d.

    They use TCastleViewport with items (like TCastleImageTransform and TCastleScene with sprite sheets) to express the map and objects on it.

    They show how it’s a feature-rich approach — e.g. isometric_game_3d has cool shadows 🙂

    It is possible that in the future we could have a built-in tiled editor in CGE. I still haven’t decided when is the right time to pursue this — for now, relying on Tiled is great, but a built-in tiled editor remains an interesting option.

  6. Finally, the optimizations necessary for great performance when you use lots of small scenes and images to render maps are really in progress 🙂

    I’m now working on branch shadow-volumes-new that, despite the name, includes a few useful renderer refactors. In particular it changes the way our shape renderer needs to be initialized / finalized, opening the door for cross-scene batching.

Thanks for reading! This was a complicated post, as admittedly what we do may be a bit confusing — we do pursue (and even document) 2 approaches for 2D maps, and we improve them both. But we also have a preference which one should “win” in the future (viewport and scenes!) but it will take time to get there with full performance. I hope what we do is clearer now and that you will enjoy the journey 🙂

As always, please ask on our Discord or forum if anything is unclear.

And we appreciate supporting us on Patreon. We also opened the option to donate using Ethereum if crypto is your thing 🙂

1 Like