Devlog #008

Posted by Kenneth Ellersdorfer

Hello Capitalists!

It's been some time since the last devlog, but I've been busy with technical overhauls and also designing an entirely new train system.

Trains

So, I've finally managed to find a new system for the trains, that meets the following goals:

  • Easy to use, difficult increases with the desired grade of efficiency
  • Possible to implement very efficiently
  • User-control for network traffic flow

The core idea of the new train system revolves around automatic load balancing and congestion avoidance.
Signals still exist, but they only control which directions trains are allowed to pass; they serve no other function.

So, the new system works by separating the train network into several subsections joined together by intersections. Along with this, the new train behavior rules are as follows:

  • Whenever a train approaches an intersection, it will check if there's a path available that is faster than its current path - and if so, it will take the faster path
  • The pathfinder now takes into account "congestion" when finding paths. Congestion is defined by the percentage of tiles in a subsection being currently occupied by a train (it's a bit more complex, but this is the general concept)
  • If trains collide, they will slow down significantly, penalizing poorly planned or signaled track layouts

In addition, the train logistics changed so that you need to define which train station is able to ship goods to which train station, this allows fine-grained control over train dispatching within the logistics system.

I'm not fully done with the implementation, there's still room for improvement like congestion prediction (how congested will a subsection be at the time the path searching train arrives?) and I'm pretty sure there are more things I just haven't thought of yet, but I will leave the functionality on what I've outlined here for the next playtest. At this point, the main remaining task is optimization. Currently, it is still quite choppy, but I'm sure it is possible to make this implementation scalable and very high performance in the future.

Here's a video I've recorded after after finishing the train implementation (very unoptimized):

Also, another thing, I've also finally overhauled the rail visuals and replaced the old programmer art with something more suitable:

Old

New

Quite the improvement, eh? 😅

Vehicle optimization

It's getting quite technical now. I've refactored the vehicle behavior into a new system alongside the new train system implementation.
The idea is to update vehicles with the same behavior in groups. Previously, each vehicle was its own object, containing its own components (such as movement logic), which were also separate objects - you get the idea.

This approach was suboptimal, not only in terms of memory layout but also in terms of code architecture. There was no central place to store data such as grid occupancy or other variables shared across a group of vehicles.

I've now fully refactored the vehicle code into two systems:

  • A high-level vehicle, which remains an object for easily passing around a vehicle's identity and accessing its behavior API
  • A low-level vehicle behavior system, which is processed in chunks and accessed via the vehicle's identity

The low-level vehicle behaviors implement an interface (or a base class, where appropriate) that exposes access to the underlying data, an update method, and additional API functions. Once a vehicle is spawned (i.e., dispatched and moving on the map), its behavior data is created (stored in structs) and assigned to a free slot within a chunk (containing up to 32 vehicles).

This behavior data encapsulates everything related to the active vehicle, such as its current job, path, world transform, movement speed, and more. Updates are performed in a linear loop, which is easily parallelizable. Further optimization is possible by switching from an array-of-structs layout to a struct-of-arrays layout, which is not yet implemented.

In summary, this architecture groups related data more effectively, enables many future optimizations, and reduces overall code complexity. I’m quite happy with how this turned out. 😀

User Interface

So, I’ve rewritten the game’s UI several times by now. Initially, it used Unity’s UI Toolkit in the very early stages.

Later, when I started building my own engine, I switched to a custom retained-mode GUI that I wrote myself (very basic - I only spent about two weeks on it).

However, I quickly reached a point where it became too constraining to work with, so I ported the entire UI to Dear ImGui. That worked great - Dear ImGui is excellent for quickly building something that's "good enough" for end-user-facing UIs. That said, it's easy to tell it's designed more for developer tools and applications than for games.

So I started asking myself: what should I do next?

I didn't want to move back to a retained-mode system, since it would be difficult to implement efficiently with hot-reloading or real-time editing would be a nightmare. At the same time, I didn't want to invest too much time into building yet another UI system from scratch.

I looked for alternatives, but didn't find much. The only promising option was PanGui, but it's still in development with no public release yet, and I generally prefer to avoid adding too many third-party dependencies. So, I decided to go ahead and implement a fully fledged immediate-mode UI system myself.

It took me about two months to get it to an acceptable state, and another six weeks to port the entire UI over. So far, it's working quite well. There's still a lot of convenience functionality to add, but the core system is solid.

It's also written entirely in managed C#, and it's fast enough to run in real time.

Here's how it looked before (with Dear ImGui):

And this with the new ui system:

So far, I'm quite happy with the aesthetics. There's still plenty of room for improvement, but I think it's a solid result for the relatively small amount of time invested.
From here on, I'll continue improving the visuals and usability incrementally. I don't expect to switch UI systems again - I'm pretty tired of rewriting UI code at this point

Land Ownership

Another interesting problem I ran into was how to handle land ownership. The current system allows the player to purchase land at any time. When placing buildings, the displayed cost already includes the price of the land - I originally did this to make the system easier to work with.

However, this approach turned out to be a mistake. It's frustrating to use, lacks transparency, and I've even had to build automation around it just to hide the underlying complexity.

Because of this, I've decided to completely rework the system.

To make land ownership more intuitive, meaningful, and less tedious, I plan to assign every tile on the map to a city, effectively creating spheres of influence. Each sphere of influence will contain a set of resources, and in order to build within it, the player will need to purchase a permit (and potentially construct a building to establish a 'local management presence').

This change has several gameplay implications:

  • Choosing where to settle becomes a meaningful long-term strategic decision
  • Micromanagement is significantly reduced
  • The system can be expanded later - for example, cities could control taxation within their sphere of influence or even refuse to grant permits altogether

There are still some open questions, and I'm not yet sure whether I'll implement the system exactly as described. However, I believe this is the best direction to take.

I'll share an update once I start implementing it. It's currently planned for the next testing phase.

That's it for this devlog.

Thanks for reading!