Automatic tick progression | 0.1.6


Summary

Turns/ticks in the program can now progress automatically. Previously the user needed to spam the "next turn" button in order for the game to "play". The tick progression can be run at different speeds, chosen by a slider. And can be paused/resumed by hitting the "space" key. There is still an option to run the ticks manually, mostly useful for debugging.

This update doesn't introduce any new mechanics or systems in the game, it just automates what is already there. So this devlog will be a bit more technical in nature, describing the method I use for tick automation.

Implementation

There is now a new GUI element in the top right. The slider can be used to select the desired tick duration, in seconds. If you select 3, all systems involved in the tick progression will be updated every third second. 

I use Unity's Coroutines to process a tick. I check things like: is the previous tick done, is the game not paused, should we do a tick as soon as possible (manual tick progression) or should we wait until the desired tick duration as elapsed. If the necessary conditions are met, I start the coroutine.


On an abstract level the tick scheduling can be visualized by the image below. The white center line symbolized the progression of time, from left to right. The coloured bars are blocks of time. During a single frame, there are 2 things that take up time. The frame overhead, which I define as everything (graphics, ui, etc.) that takes up time but is not part of the tick processing, and the green is tick processing. In the tick processing we do all the calculations needed to update the systems/simulations.

Inside the coroutine each system has it's own while loop (except for the transportation and industry systems), there are more systems but all did not fit on screen :). In each iteration of these loops a subset of the tiles are simulated.  The methods used for simualtion, example: Ecosystem.SimulateForSubsetOfTiles(), returns true if there are more tiles to process, and false if that system is done. The content of this coroutine makes up the green part in the image above. Further down I'll talk about that is _IsOutOfTime() method is, and how it "ensures" 60 fps.

In order to not get lag spikes (fps < 60), we need to keep the red block + green block under 1/60 s for each frame. The red block is overhead we have no control over, but we can modify the size of the green block in various ways. 


After each subset calculation in the coroutine the IsOutOfTime() is used to determine if we can process another subset or not. This method return false if we can continue, and true if we need to break and continue in the next frame.

The method contains an overstep variable which compensates if we were to take too much time in one frame. So that on average we keep the correct pace.


Another part of optimization is choosing the correct subset size. If we choose a small subset, we will need to do a lot of passes in the while loops, to process all tiles. This creates a lot of overhead and can make low "desired tick duration" values unreachable, but it makes it much easier to avoid lag spikes (fps < 60). If we choose too large subsets on the other hand lag spikes can become a problem, but we avoid a lot of overhead.


Choosing the right subset size is not trivial, since different hardware will process the same subset size at different speeds. The image below shows a possible scenario where the same subset size can possibly create problems on a low end CPU, but not on a high end one. At the moment subset sizes are hardcoded for what works on my machine (pretty high end). But in the future I plan to add an option for the user to personally select this. 


Here are 2 images of CPU usage on my machine.  The first is for "desired tick duration" = 5, the second is for "desired tick duration" = 1. For gameplay purposes I think a tick duration of around 10 is practical. Using a value of 1, is like playing on super speed. Maybe useful in some scenarios, but not often I would think.


A thing to not is that not all systems are updated in the tick processing. Erosion, weather (temperature, precipitation) are not included. They simulated/calculated at start up and then remain static. 

Extra thoughts

My implementation is not the only way of doing this kind of automatic update. It's probably not close to optimal, im learning a lot whilst doing these kinds of things.

An alternative implementation could be to use a queue of "processes". That is sort of what I do with my while loops, but it could probably be done more general. Say that you create a large queue at the start of the coroutine, containing all subsets for systems to simulate. You then go though that queue one process at a time in one single loop. This would make it easier to add new systems, but under the hood it should do the same stuff that I do, just in a more general way. That would require the creation of a "process" class that holds some reference/pointer to the system to simulate, and the subset size to use. 

Another thing to consider is: do every system need to be updated at the same frequency? Does the ecosystem for example really need to be updated each tick? Maybe it's sufficient that it updates every 10'th tick. Other systems like resource transportation needs to be updated every tick of course, but maybe not the industries? There are probably reductions like these that can be made to reduce CPU load, but keep the same general gameplay experience. 

There are pretty large systems like AI decisions which have not been implemented yet, that will require large amounts of computations. The implementation I've described in this update might need to be modified when I come to that stage. 

TODO

Im about 1 week behind in the schedule. I've started tinkering on the population dynamics, but far from done. The current aim is that by mid April the game will be more playable, in the sense that industries will require resources to construct (cost). The aim is that global resources like "gold"/"currency" can be generated by some buildings and used to construct new stuff. My goal is to implement systems in as general a way as possible. Most things related to population, resources, industries and roads should be changeable using .xml template files. I'll implement the industry construction costs in the same way. It shouldn't be hardcoded that industries are built with "gold"/"currency". It should be possible to pay the cost in any kind of resource physical (wood, stone, steel) or global ("money"/"currency"/"authority"), determined by the .xml templates. 

Im having questions about my design decisions in general. Things like: Is it too much to have industries on each tile? Should I make industries operate more like in Anno, with a central building supported by surrounding "harvesters"? Is it good to represent resources as fields? Or should I represent them as discrete objects, more like in Transport tycoon? The decisions which are best for simulation/research, isn't necessarily the best for gameplay. 


Files

Orbis multiplex 0.1.6.1.zip 35 MB
Mar 23, 2022

Get Orbis Multiplex

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.