NIMBY Rails devblog 2021-02

First month of Early Access

NIMBY Rails was released as Early Access a month ago with a very good launch! Players built amazing, huge networks with thousands of KM of tracks and hundreds (and sometimes thousands) of trains. And modders put on a lot work and created more than 300 train mods and translations in just over a month. I was expecting just a few train management game fans to take notice but nothing like the impact it had. The hype has died down now, but it will allow me to work on the game fulltime for one more year without any problems. Thank you very much to everybody who bought NIMBY Rails!

During this month players have peeked and poked at everything in the game, and a myriad of bugs and problems surfaced. I worked nonstop to fix most of them, until the actual limitations of the design started to surface. Some of these are already scheduled to be worked on in the future, and some others are either urgent enough or basic enough to warrant a development effort even in this initial 1.1 series of builds. The full changelog for 1.1 has all the details, but here’s some of the major work already done for 1.1 during February:

  • Rewritten the state sync code for multiplayer, to make use of a full sliding window approach. This makes the sync code capable of recognizing various sync errors like going ahead or too behind the server, and apply measures to fix them.

  • Implemented a new version of the train modding schema. This new version allows the modder to offer custom compositions and it’s no longer restricted to the rigid single head-car(s)-tail structure.

  • Made the initial track validation a secondary task during normal gameplay, which reduces the time required to start playing after loading a game, both for SP and MP.

  • Rewritten the line and track rendering code to be able to cache its output. For some users, and depending on their CPU, GPU, and build complexity, rendering tracks and lines sometimes became a bottleneck, which is not acceptable. This now has been largely fixed.

  • Allowed to enable mods in already started games. The ground work for this is the first-class support for mods data as a game object. This also made me support better dynamic loading of textures for mods, with the game supporting up to 4096 train textures now.

  • Rebalance pax expectations to be a lot more generous on the fares. This was required since the capital expenses of construction and train purchases are high, and having realistic fares would mean the ROI would take years of in-game time. Even with a faster AI, it would never be fast enough for the required time frames. This is what I call “tycoon game balancing” and I’m not 100% happy with it. At some point the game will offer rules mods so it will be possible to go back to the older balance if desired.

  • Dynamic traction for trains, with more realistic acceleration.

  • The leg time calculator now sims a train using the same formulas and code as the actual train AI, making it much more precise. Additionally some bugs with the train timer speed limiter were fixed and it was made visible to the player.

  • A limited form of dynamic path decision was added to pax AI, but it needs more work.

  • Max speed mode, to run the sim as fast as possible without any map or game rendering.

The new pax global path finder

Unlike past blog posts, this time I want to explain a feature not yet in the game, but which will be implemented (or better said, experimented) very soon. Pax, the inhabitants of NIMBY Rails, make decisions at two levels: the global and the local. The local level is the “now”, and it relates to the decision of taking a train in front of them or waiting for a future one. This local level is informed by the global level. At the global level, pax can ask a mysterious hive mind that knows about all the lines and train stops in the game about what is the best path from A to B, and maybe adding some constraints to it. They then use that information to make decisions at the local level. The local level has a lot of work to do too, but it’s not the subject of this feature of this point. The goal now is to replace this global path finder with a better one.

The global path finder works by building a map of your lines network, at a level above the tracks. It only cares about lines and stations. The current v1 map is a direct, straightforward representation:

This structure is called a graph and it just a set of nodes linked with edges. In this v1 map, a node represents a station, and edges represent the legs of lines linking stations. There is only one node for each station and only one edge for every leg of every line. Nothing more, nothing less. And while this is a faithful representation of your network, it is in fact not suitable for finding an optimal route inside it.

When a pax travels, that travel has a cost. For now this cost is purely time. But there’s a variety of actions and states a pax may find itself into. It can be just spawned. It can be riding a train. It can be stopped at a station inside a train. It can be waiting inside a station for a train. Some of these states are similar but they have a different time cost. And it is not possible to find a route in the v1 graph that properly distinguishes between passing by a station and stopping at it to wait for another train. For this reason a new map is required, one that can represent the actions of pax, not just the shape of your network. This is the new v2 pax path finder map:

It’s a lot more complex than v1 because what pax want do optimally is a lot more complex than what v1 allowed them to. There are now 2 kinds of nodes and 3 kinds of edges:

  • station wait-at nodes: The action of waiting at a station
  • A > B pass-by nodes: The action of arriving to station B inside a train that departed from A
  • transfer edges (thick arrows): these edges always link wait-at nodes to pass-by nodes. They represent the decision to take a certain train after waiting at a station
  • skip station edges (medium arrows): these edges always link pass-by to other pass-by nodes. They represent the decision to stay inside a train which arrived to a station
  • leave train edges (thin black arrows): these edges always link pass-by nodes to wait-at nodes. They represent the decision to leave a train which has arrived to a station

The design of this new network graph now allows to assign different cost to different decisions. Its goal is to represent what would a pax do and what decisions it makes at every step, not what the network looks like. And then assign different costs to these decisions. “leave train” decisions are free, since it takes no time to do so. “skip station” has some cost, it involves the stop min-wait and the leg travel time. And transfer decisions are the costliest of them all, since it involves the min-wait, the leg travel time and as a new feature in v2, it will also take into account the interval time.

The dynamic components of train timing will still be missing in this new system, but unlike the earlier one, it will be possible to take them into account in the future. And in shorter term, I hope this will contribute to make pax make a lot more sensible routing decisions, since it will be able to properly take into account the cost of transfers.