Asset Management and Repaints - Hex Devlog #3 (Patreon)
Content
One of the major roadblocks for Hex is, believe it or not, performance. You probably don’t think about VNs having performance issues, but I was getting like 5 FPS on Chrome as recently as two months ago. In addition to more traditional performance problems, there’s also the issue of loading assets from over the Internet on the web version of the game. So there are two core problems to solve:
- Load assets in a way that doesn’t interrupt the gameplay experience, particularly in the web version.
- Avoid triggering repaints, which is where the browser removes and redraws an entire image, and Chrome/webkit LOVES to repaint things dozens of times in a single animation frame if you let it.
Problem 1 is the easier one to solve, and some of the solutions are just roided up versions of things Succubus Stories was already doing. For one, while the game has and supports 4k assets, these will only be used by default if your screen resolution is higher than a certain raw pixel count and you are not on the web version, though users can turn the 4k assets on or off in the game settings if they want to. The entire interface also scales to match the screen resolution.
While all the game assets have to be packaged with the game, bloating the file size, on the web version, only the files the game actually needs are unzipped and downloaded, saving a lot of bandwidth and speeding up the load times, in most cases doubling them (since the full HD assets are less than half the size of the 4k versions). This setting exists in Succubus Stories, so I had a good base to work off of. While there are improvements in Hex’s version of the engine, a lot of the leg work and logic was already done.
Hex doesn’t just grab a few images at a time, though. In Succubus Stories you had a portrait in the corner with layers for a few different levels of dirtiness and for outfits and expressions, then maybe one CG. A lot of the layers used for portraits in Succubus Stories were actually always loaded from the start and then hidden, so there was even less downloading on a moment-to-moment basis than it would first appear. In Hex, there are many additional layers just for the protagonist, plus NPCs with three or more layers apiece, plus CGs which can have layers, plus backgrounds, which can also have layers. The amount of bandwidth a single scene in Hex uses is many times higher than anything in Succubus Stories.
This leads to additional performance costs and download times. When I first started working on the game, scenes themselves were choppy and slow. There is a limit to how many connections to a single domain a browser will allow; this is why many sites use CDNs to deliver media-rich web pages. However, utilizing CDNs would compromise my ability to distribute the game easily, so I didn’t want to do that. You can think of this limitation the same way you might think of how a CPU works. There are only so many threads you can have operating at the same time before you just need to wait for one to finish what it's doing. So if you’re downloading a background and three layers of Iris, there may only be “room” for one layer of Meila until another “thread” is freed. This means that even on a very fast Internet connection, for an moment, you’ll see this:
This sort of error only lasts for a split second before a few “threads” are freed up, then her dress and face will quickly blink into existence. This doesn’t happen to the same extent when the files are being loaded from the web browser’s cache, so these sorts of errors are worst the first time a given file is used in a play session.
The length of time this visual error takes to resolve is based on how fast your Internet connection is. On the fastest connections, it’s only a split second, as I said. So it’s still a problem there, but the issue is compounded on slower connections. Testing on a simulated 3G connection shows this kind of bug can last for up to tens of seconds before the assets finally load in. So what’s the solution? Well, it’s load screens, unfortunately.
To avoid these sorts of errors, Hex preloads assets before a scene starts. Not all assets are preloaded, and not everything it preloaded all the time. For example, encounter CGs aren’t preloaded because having them slowly loading in doesn’t look that bad to me. On the other hand, characters starting nude and having their clothes suddenly snap on or their faces suddenly materialize is distracting. So before a scene starts, I can provide a list of characters who will be present, plus any other arbitrary files I want, and the game engine will create a series of “loaders” for each image. The game pulls a bit of a trick to make the browser think it needs to show the user the images immediately to start them loading, then waits for the loaders to finish before it allows players to start the scene. Once a loader finishes its work, the image assets should be cached for future use. There can still be some “snapping” when lots of images are changed at once, but the issue is far less severe.
I will admit that I had misgivings about using loading screens at first. I still do to some extent. Most web games cache everything up front, which I opted not to do because I find waiting for long start-up loads annoying. Succubus Stories uses such a system, though only critical assets are preloaded unless you tell the game to load everything up front. I think getting the player into the game and making them wait for shorter periods of time more often is a better approach, so that’s what I’ve done here.
Anyway, now let’s talk about repaints! The worst thing that can happen when you have 15 layers of 4k images on the screen is that the browser decides to repaint all of them 57 times in a single frame. Firefox does not ever do this (which is why it's the best), but Chrome and every other major browser (Edge, Opera, and even to some extent Safari are all Chrome skins deep down) triggers a repaint if you look at it funny.
There are a few reasons for this, and while I joke around about it, there are positives to the approach Chrome and other webkit-based browsers have taken. But a big negative is that things like scrolling can trigger many repaints of certain images, usually background images, that are totally unnecessary. The impact to performance, even on powerful machines, can be severe, especially because the entire engine for Hex is based around making “piles” as I call them which are special background images for elements that allow me to quickly generate and alter sprites in layers that can never be separated. While at first I thought I was a genius for this solution to layered sprites, I discovered that the reason other web games don’t do this is because Chrome is gonna repaint your dozens of layers of backgrounds a million times every time anything at all changes.
But I am a genius. I found a really horrific, awful way to solve the performance issue in Chrome by directly injecting a stylesheet into the game at runtime, then removing it and re-adding it to strategically trigger my own manual repaints. My solution here basically removes the images and re-adds them on the next animation frame to trigger a single repaint instead of potentially triggering dozens or hundreds in a single frame. This stops the browser from repainting the images repeatedly in exchange for repainting them a single time manually, but still doing it before the next frame. I am dead inside.
This is not a great solution and I would probably do it differently if I were to start development over, but it does work, and even in insane stress-tests it hold up totally fine. I solved the problem in the end, and now the game performs perfectly fine in Chrome and its inbred cousins. I think performance and smoothness are still superior in Firefox, but that's something I find with Succubus Stories as well, so I don't think it's an indication of any major issues.
Repaints aren’t only a thing in Chrome; there are other things that can trigger repaints, and I've had to think about this a lot more than I ever expected to. I'm used to Succubus Stories where I can really do whatever I want in terms of removing and replacing elements just to change some text here and there, but with the number of assets rendered on screen in Hex, I have to be careful and make sure I'm only changing the bare minimum when I need to update the screen. Anything that can cause a reflow anywhere on the page can trigger a repaint, or multiple repaints. It's critical to the game's performance and stability that I trigger as few repaints and reflows as I possibly can, and this has led me to writing extremely efficient code, much more efficient than I'm used to writing.
Overall, I think I've come up with a pretty good system here, and it runs pretty well. Some of my solutions are clever, others are kind of cursed, but I think I have managed to make a web-based visual novel engine that doesn't make huge tradeoffs (other than loading screens, I guess).
On the next (and penultimate) Hex devlog we'll discuss the combat system! Then, in the final devlog, I want to actually talk about the story and setting a bit. After that, I probably won't release more of these devlogs and instead I plan to focus on continuing my "Before Hex" series of short prequel stories and doing some other new things instead, so stay tuned for that stuff!