Hacker News new | past | comments | ask | show | jobs | submit login
Game performance optimization – A practical example from Industry Idle (ruoyusun.com)
98 points by insraq on Jan 30, 2022 | hide | past | favorite | 34 comments



Lots of cool stuff here. But on the last point about managing renders, for my money any browser game is in a state of original sin if its logic and render loops are tightly coupled.

The Right Thing To Do is to run logic and rendering at separate cadences, with rendering driven exclusively by requestAnimationFrame. Then when the browser is minimized, RAF stops firing and rendering is skipped entirely (on top of the other benefits of decoupling logic and rendering).


Hi, the game is already doing this. As mentioned in the article, logic tick is running at a much lower frequency than rendering loop (30/60FPS). When playing in the browser and is minimized, the game will simply pause - rendering and logic. Browsers will throttle timers which will cause inaccurate logic tick as well. Most idle gamers actually do not like this behavior [1] - people prefer the game to run in the background as well.

The "background mode" mentioned in the article is about the Steam version, which runs in Electron and I have to set `backgroundThrottle: false` flag. In this case, Electron will not throttle timers and I can safely disable rendering while leave the logic running.

[1] https://www.reddit.com/r/incremental_games/comments/seid8w/c...


Thanks for the extra details!

I think idle games are a special case here, in that variable timesteps aren't necessarily a liability for them. In an ideal world, one would like all the systems in an idle game to be able to quickly simulate arbitrary amounts of time (for offline progression etc), and if the whole game is made that way then in principle one is immune to throttling. But I think games actually built that way are pretty few and far between.


Are the logic and the rendering loop in separate threads?

If so, how do you ensure that when the rendering loop reads the shared state, it sees something consistent (i.e. something that hasn't been updated by the logic loop in between the time when the rendering loop started accessing it, and the time when the rendering loop finished accessing it)?


No. Multithread JavaScript (WebWorker) is a bit hard to work with. So currently they are all running in the main thread, just "at separate cadences"


I'd go further: for many games the right thing to do is pause execution entirely if the window is minimised or the user switches to another tab or application. Obviously not possible for online multiplayer games, but a good practice for single player.

Rendering can be but isn't always the most expensive portion of a game's execution, but if you have a lot feeding into that in terms of object updates and game logic you can find yourself chewing a surprising amount of CPU time, and therefore power, which isn't great if the host device is running on batteries.


Please have this setting be configurable if you do this. I hate when games pause when I alt-tab to another window on another screen while I'm waiting for something to finish in a game.


For many games, idle games are a special case though, I don't think anyone who plays an idle game would want the logic loop to pause. They typically take a long time to play so minimizing and checking back every now and then is a very common thing.


Industry Idle is cool - didn't realize you're on HN!

One of the most frustrating things about using a web stack is issues like this where you've got an embarrassingly parallel problem and you can't use threads easily.

Even in Unity non ECS, which is fairly constrained, high performance dot processing would be fairly straight forward.

I'm working on an idle web game using ClojureScript, and it's very much the other end of the spectrum. The game design is event driven and state small out of necessity.


Whilst I imagine the final line "I cannot get a discrete GPU at a reasonable price" is more a frustration, never underestimate the importance of working with restraints. If you develop against a terrible GPU you're forced to fix issues like this quickly.

Although I'll of course agree a better computer helps the iterative dev cycle.


It also helps with user empathy! I work on the web rather than on games, but I used to have my small team walk over to Best Buy (American electronics store) and load our site on the worst laptops we could find to actually see what performance is like for worst-case users.

You forget what “real people” experiences are like when you’re on your $2000 beefy rig.


Huh? I am surprised that you choose to use a "setTimeout"-like callback there.

Everyone on the internet is an expert (/s), so here is my intuition: If a mine produces $stuff at a rate of x $stuff/s, the factory it is sending to will have a supply of x $stuff/s coming from that mine; the factory's total supply will be the sum of all these x from the various mines that are sending to that factory. So what I would do is a setTimeout("increase supply of stuff for target factory by x every 2.5s", 2.5) to link a mine to a factory (and "decrease..." for unlink). This simulates that the first resources take some time to arrive.

Are the deliveries continuous and not bursty? Then the cheap way is then to increase the resources every second (or whatever your ticklength is) for the appropriate fraction by iterating over the factories - which are probably much fewer than the resources in flight. Players might notice this.

Are the deliveries bursty? Keep an array of pairs (burst_amount, burst_time, burst_stride) and when iterating over the factories to add resources, add those for which "burst_time modulo tick_time == burst_stride". This requires some discretisation, but would the players even notice...? Just make sure that if you draw resources on the screen that they arrive roughly in sync with the actual resource update in the factory.

Bonus: This could go in a thread of it's own that only takes care of these updates. But I am not sure if that can be done in your language like I would do it in C++.

Curious why you've chosen to use massive amounts of timeouts with the respective overhead.


> Are the deliveries continuous and not bursty

They are not "bursty", but they can fluctuate. Power fluctuation, fuel shortage or upstream supply chain issue could cause a building to skip a production cycle - and impact all its downstream. In fact that is a core challenge in the game. So the core game logic has to be simulated, not calculated.

Doing multithread in JavaScript (via WebWorker) is kind of painful.


With the suggested method you could skip a production cycle by unlinking a producer from a consumer. Due to the setTimeout the fluctuations propagate through the factory just as they would with resource nodes flying around. Of course the details are a bit more complicated (you don't want to miss resources or produce too much due to the abstraction), and I see why you went the easier route ;-)

Yeah, multithreading is often a pain. OTOH the gains can be enormous.


Because that was closest to the idea of what's conceptually happening in the game world, and it was the quickest to implement - he mentions it in the article


> To optimize this, instead of scheduling a function call, we simply add the information to a Map - the arrival time, resource, amount, and target building. Then in the tickDots method, we loop through the map and if a resource has arrived, we remove it from the Map and add the resource to the target building. This reduces the frame time from barely under 16.7ms to a comfortable 11ms.

What you have implemented here is a priority queue, with O(1) insertion and O(n) deletion. It is easy to get down priority queues to O(log n) deletion without sacrificing insertion speed noticeably much. So unless you have some good reason to iterate all resources every iteration, changing to a better priority queue implementation will most probably yield you even more of a benefit.


You are right. A heap/priority queue would be good. However the resources needs to be iterated and ticked every frame - to update the rendering data (transform) and perform culling (because the viewport might change - a player might move or zoom the camera).


Thanks for the write-up. Always cool to see how different people solve these kind of problems.

I don't know much about cocos2d but since the dots are purely visual, do they need to be game objects? Couldn't you keep their transform in a separate data structure which you can update independently and pass to a shader (or similar) for rendering? I'm thinking something similar to how particles in VFX are usually done.


Thanks for the comment. It is indeed something I've been considering. In fact, lots of the optimization done here is to move the logic away from the game objects , which should enable the flow you've described.

I ended up not doing it in this iteration because:

- cocos2d's custom shader support is kind of poor, especially documentation is pretty much zero

- the dot is not "purely" visual. For example you can trace and highlight a resource's movement. This is not a blocker per se, but requires more work


Missing one important thing: if your object goes in a straight line at a fixed speed with a known start and end position, instead of going through the game engine tweens you can build an equivalent CSS animation between the start and stop position and it'll be blazingly fast.


Browser games like this are normally drawing pixels into a canvas, not creating DOM objects for each game entity.


I know, that's not a limit tho. you get the canvas origin from the engine and offset the animation. you keep a pool of div on the side, and when you need one depending on your culling result you attach the sprite image as a background, tweak the css animation start,end and attacch it to the overlay. you keep a relative offseted parent on the div, so that as the canvas scroll, you can adjust it wherever you go. overflow hidden takes care of the visibility if they go out of the canvas area.

hybrid canvas html game are a joy to work with, there's no reason to limit oneself to canvases as long as you can export and import events to the external world. I've built some, and having for example the ux in html sending control events to the canvas cut the development time a lot. all these 2d libs have painful ux api, with dreadful limitation, while in html you can just slap whatever stile and it'll always be crisp.


Sounds massively complex compared to just a stateless rendering pipeline clearing the canvas and drawing the sprites with drawImage on every RAF.


I do hybrid canvas/DOM gamedev myself, and it's fine for overlaying a few pieces of UI and styled text and so on. But when you have 30,000 of something like in TFA, there is no world where it makes sense to draw them via the DOM.


Why should it be faster? It's doing the same work. In modern browsers with their heavily optimized and JITed JavaScript runtimes, an animation in JavaScript will often perform better than the equivalent CSS. (And that goes double if you're using WebGL and can just hand it off to the graphics hardware).


I'd like a citation for the claim that JavaScript animations are often faster than CSS animations. If it is true, it would be a very interesting read.


It was absolutely untrue five years ago (I'm sorry, no data - this is from experience). Perhaps JS interpreters have come a long way since then.


> if you're using WebGL

iphone's chrome entered the chat


I’ve done some hobby game-developing in the browser and I always end up frustrated by the poor and inconsistent performance.

Using Unity to compile to the browser seems to yield much better performance out of the box


I think a well written GPU renderer (in webGL for example, if running in the browser) ought to be able to render 30,000 dots at 60fps, even on an intel GPU.


have you considered moving the simulation to worker(s), and providing the game state to the render function through a SharedArrayBuffer?


is the profiler a home-grown one? or publicly available?



Hero




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: