TL;DR: day of week demand and distance demand tiers, instancing of passengers in origin stations, Voronoi station areas, fares, satisfaction metrics, accounting, mods
Day of week demand and distance demand tiers
As I prepare for the EA launch, it is time to close the major gameplay loop of NIMBY Rails. Most of the initial development was focused on a system levels POV, specially around the tracks and trains simulation. While that makes it a very valid sandbox, it also needs a more classical gameplay loop to make use of those systems, and to give them an extra dimension beyond the design of rail networks.
This task started last month with the crazy individual passenger simulation, and I continued with a review and overhaul of the demand system. The first task was to take the existing station demand generator, based on the time of day, and make it more complex by also taking into account the day of the week, and by dividing passengers into 3 tiers based on distance: locals, regionals and long distance.
This allows the game to consider different demand factors depending on the weekday, time, and how far a passenger wants to travel. This last point is important to make sure a station is capable of generating both a balanced and varied variety of passenger destinations, rather than always trying to the same nearby stations because they “win” more often in the random walker I’m still using for calculating random destination (aside: a random walker is at least as efficient, and often faster, than a full blown A* pathfinder for this task, as counter intuitive it may sound at first).
For now the demand curves are hand made and fixed, and I’ve tried to add interesting things. For example Sunday afternoons see a demand rush for long distance passengers, with the intention to simulate weekenders going back from a holiday. I will see in the future if some procedural generation could help here. But fear not about infinite sources of passengers with no effort, because this is just the first link of the gameplay loop chain.
Soft instancing of passengers in origin stations
In the September post I mentioned how I gave up on instancing individual passengers in stations to wait for the train, instead generating them when they board the train (passengers waiting to transfer trains were always preserved fully instanced). This was bugging me a lot, since having real local passengers waiting in stations would be a very natural way of balancing demand and avoiding “stampede” effects when generating them for a train. Basically the code was too eager to fill a newly arrived train, so I would need to add another layer of demand modelling on top of the existing one. This just looked awful.
So I instead implemented a “soft instancing” for local waiting passengers:
These passengers are stored as just a destination ID + counter, so the problem with exploding storage requirements was solved with this compact representation. It means these passengers do not have the full range of data normal instanced passengers have. Waiting time would be very interesting to have, but it had to be sacrificed. But it means the normal demand formula works just fine, and that trains can come and find just the 5 passengers that want to board it out of any amount waiting in the station, without any tricks.
Voronoi station areas
At this point demand was a direct function of weekday and hour, modulated by the population density in the influence area of the station (aside: the density map is completely fake and based on street intersections; I have post-EA plans for looking into a real raster layer for pop density). But, since the earliest times of the station implementation, this has mean any close together stations have overlapping influence areas, and share the populations and have the same density factor, with no detriment due to splitting the demand. There is a historical reason: original I wanted to modulate demand in a “Sim City” fashion, down to individual buildings, so this splitting of demand would be completely natural as a side effect of that system. But since buildings are out (for now!), the idea doesn’t work anymore.
Instead I’ve changed station influence areas to be a combination of radius and a Voronoi diagram:
Now close together stations are actually eating into each other influence areas. The code goes the extra mile and does subpixel-correct sampling of the pop density texture while doing a rasterization of a triangulation of each station area. I may have gone a bit overboard on this one :)
This completed the first link of the loop, the station demand system.
The next link is to tie passengers with the economy of the game, by adding fares to lines. Fares are very simple to manage: you can add a base price, which will be always paid when a passenger boards a train currently running that line. And a per-kilometer price, which will be paid when a passenger leaves a train running the line. Passengers pay these item incrementally, as they board and leave trains, as many times as needed in their journey. Passengers lists now always show a fare column, to see the fare the passenger has paid so far.
While playing a game is possible for the player to make changes to lines and trains that strand passengers in a station or just leave them running in a train that will never link to their destination. This is handled by the game by refunding these passengers their full fare and paying them a compensation.
Continuing with the gameplay loop, fares transform demand into cash, as long as the player builds tracks and trains to satisfy that demand. But with just these systems alone it would still mean an infinite hose of in-game cash. So it needs something else, the last link in the gameplay loop.
Passengers will continuously rate their experience riding your trains in two axis: time and money. From these ratings a general satisfaction rating will be calculated. And this satisfaction rating will be used to modulate the station demand (for the origin, destination and any connection stations in the passenger journey), so stations with low satisfaction will see their demand greatly reduced, and thus their potential income. And stations with good ratings will always operate at max demand, or even higher if you can manage to service enough trains and platforms. Satisfaction modulating the station demand is what closes the gameplay loop.
The time axis of satisfaction is calculated by comparing the travel time of the trip with an idealized travel time, which depends on the distance (longer trips are expected to be completed at a higher average speed). The cash axis is based on an ideal price per KM, also depending on the distance, and also considers the ideal speed as a factor. Both of these functions are based on magic numbers I will need to tweak as I test the game and later in the EA, for sure. For now they are quite hard to play.
The takeaway of this system is that the player has control over both factors. They can build a better, faster train network, with more capacity and faster trains. They can also lower the line(s) fare. And one thing can compensate the other. You can ask for more money on a high speed line since the short travel time will compensate the satisfaction hit from the high price. Or a slow line can still have a good rating if it’s cheap enough.
What is a management sim without a cash balance screen? Fleshing out these systems made cash a front and center feature, so I added itemized accounting:
The plumbing for this was already done, it just needed an interface. I will also soon implement historical records, and display for other data that is being accounted for, like passenger statistics and satisfaction. Actually station satisfaction is implemented as the average of the historical record for the past 30 days, so it is not only for show.
I definitely want to launch the Early Access release with Steamworks mod support, so it was time to start the mod system. I’ve implemented a local mod system, which will be the basis for the Steamworks one, and it will also allow mod authors to develop and test their mod before uploading it to Steam. In order to test it, I made the default train sets a local mod, with the same rules and structure third party mods will have to follow. Now, when creating a new game, the player will be able to enable any installed local mods:
I also started the modding guide in Steam. For now only train mods are supported. The other kind of mod I 100% want to support before EA is localization mods. Track mods are also a very obvious one but are a bit more complex. I will still try implement them before EA but it may be discarded for later. And further out would be general game rules mods, so for example the various formulas and magic numbers I mentioned earlier would be made moddable.