Update 12: Asynchronous rendering & asset loading, UI skinning


One of the exciting things I've worked on over the last few weeks was improving on the very bare UI fragments I had implemented into the game in the PoC phase (using scene2d). I had initially started building 2D UI components out of sheer necessity but this time I wanted to take this aspect to the next level by skinning different components in order to not just make them functional but hopefully appealing as well.

Skinning the UI

So when it comes to the main menu, I decided to ditch the basic logo we came up with a few months ago in favour of a more representative image of what our post-apocalyptic world is supposed to feel like eventually. I then went on to start skinning the scene2d components I'm using for the menu (buttons, windows, etc) and picking colors that more or less fit that same visual theme. This is what I ended up rolling with for now


Once you click on play, you get a component that's more like a form:

Some of you may be interested in knowing that I used Skin Composer, a tool which is part of the Libgdx ecosystem in order to test out my skin textures and 9patches early and package them as a TextureAtlas & JSON skin file in the game:

This is only the first iteration of UI skins and while there's still a lot of work to be done I believe it'll help take the player a bit closer to the feeling of actually playing a legit game.

I also started playing around with in-game UI overlays, in particular for spell bars and currently selected skills as well as positioning vitals gauges. The style does not fit with the new UI skin yet (I haven't come around to skinning these components just yet), but it's also starting to give an idea of the vibe and the type of setup I'm looking for for the in-game overlay (see the bottom part of the following screenshot):


I also briefly experimented with representation of cooldowns as demonstrated in this brief video:

... but I decided to walk back the entire idea of cooldowns in the game. I want the experience to be much closer to the way it was in Diablo 2 where the only systems or factors serving as throttles for casting spells were A) Mana availability and B) Attack or Casting speed. I don't want the gameplay to be based on constantly reproducing & optimizing the same sequence of spells based on the way cooldowns line up. I don't want cooldowns to be a factor in the way people choose and pick their skills. I want combat to be as responsive, fluid and engaging as possible. I consider cooldowns to be largely contrary to that goal.

Asynchronous rendering

I spent a substantial amount of time working on an issue which had started appearing back when I enabled full-screen mode by default on the client. When the game launches in full screen mode and a player tabs out (for instance, to take a break and use their browser for a moment), the client gets disconnected. It's a bit of an annoyance as it's not that unusual for a player to want to pause the game and I wanted to fix that. But that proved to be a bit more of a challenge than I initially thought.

I pretty much already knew the origin of the problem going into implementing the change I'm about to describe. It was that world processing was tightly coupled to rendering, and that when a full screen LWJGL application gets tabbed out or minimized, the rendering thread gets paused as a result. And as a consequence of that, the world also gets paused and so updates sent by the server are not processed anymore. Eventually the server notices that the client is not behaving properly and shuts down the corresponding connection and logs the player out. That's all intentional and done by design but what I hadn't really accounted for initially was that the rendering thread could legitimately get paused (as explained up above, in the event of tabbing out of the game).

When I first picked up libgdx and artemis, coupling the world updates and the renderer did seem like the easiest thing to do at the time: you simply place the call to world.process() (i.e the method call which incrementally moves the world forward in time) inside the render() method of the game app. With that comes the major benefit that the entities being rendered have all necessarily been updated just before rendering would take place. In other words, there's always something to draw, and everything is drawn in its "freshest" possible state.

In order to make sure the world would continue updating even when the rendering thread is paused, however, I needed to implement asynchronous rendering. This means that world updates take place in a dedicated thread and rendering is now the only thing that takes place within the app.render() (i.e the main LWJGL thread). However, the difficult thing about this approach is that, depending on when the world produces an update, there may or may not be available data ready to be fed to the renderer, which can result in subtle but very annoying visual stutter in the app. There were different ways in which this "stutter" or "jerky behaviour" would manifest itself. Here's an illustration of that in this slowed-down video where, on top of having micro-stutter, the camera position would also occasionally desync from the character's, making things even worse:

So long story short, I ended up rolling with a concurrent queue for passing world updates to the renderer and buffering world updates, making sure that the world is always "ahead" of the renderer so that there's always fresh data to retrieve from the queue and hence fresh entity states to draw. That's summarized by the following diagram:

In this approach, rendering and world updating take place in distinct threads but these threads are still more or less loosely synchronized. In order to make sure that the application stays responsive and input events don't feel delayed, I slow down the world processing thread if and when the renderer starts to lag behind it by 2 updates or more (i.e the size of the queue >= 2). To smooth things out even further, I may add an extra bit of logic called "linear interpolation" at some later point in order to let the rendering thread run completely loose and as often as it can. That way the game would support rendering frequencies higher than 60Hz which are nice for low-latency monitors. In an interpolated approach, the speed and frequency at which the world updates are processed are not so important anymore and buffering can be removed because interpolation allows to bridge the incremental gaps between two world updates by creating intermediate, artificial values for things like entity positions. But I'll revisit this topic if and when I implement this technique.

Those interested in finding out more about my initial struggle with async rendering can check out the following reddit thread, in which I documented most of my findings: https://www.reddit.com/r/libgdx/comments/n33b3o/response_time_spikes_in_lwjglapp...

Asynchronous asset loading

I also decided to go ahead and optimize the loading speed of the game by implementing concurrent, asynchronous asset loading and separating the assets into two groups: the ones that are necessary to display the main screen and menus as soon as the game launches, and the ones that are necessary for use in the actual game's world. I still need to move around some chunks of code that still run a bit prematurely and represent some additional potential in terms of time that can be saved, but I've already cut down the loading speed by a substantial amount. Before implementing this optimization, that amount probably sat somewhere between 5 and 10 seconds but more importantly, in practice, it will pretty much remain capped at the new value regardless of how many additional assets make their way into the game, whereas before, each additional sound effect or texture would increase the loading time.

This is what the experience of launching the game now feels like (and once again, a few seconds could still be and will still be shaved off in one of the upcoming passes):


The remaining time that can be saved mostly comes from initiating a connection with the server right off the bat (+ some resulting ping pong between client & server), which can easily be moved to a later stage.

I also took a moment to rework the way the main screen music intro's and make the build up a bit more compelling right off the bat, but I might still replace this whole theme entirely with a more chill / low-key one in the future though, we'll see.

That's it for this update!

Leave a comment

Log in with itch.io to leave a comment.