Tips for Route Development
Back to Index Page
If you know how to code CSV routes for openBVE, then you can find some of the techniques that I used on my Chashinai Railway project here. Maybe you find something you didn't know and can make use of in one of your next routes. This information is intended for advanced developers only, not novices.
  1. Time of Day Selection
  2. Lighting and Tunnels
  3. Tunnels and Cab Brightness
  4. Randomization and Probability
  5. Conditional code via $Include
  6. Conditional code via $Chr/$Sub

Time of Day Selection

One of the challenges for me was to create a single route file that could be used for sunrise, daytime, sunset and nighttime runs. The basic building block is of course an appropriate setting for lighting.
With Route
.AmbientLight 160; 160; 160
If you wanted to use another time of day, you would have to change those lighting values manually. But there is a more clever way to do this. In Chashinai Railway, I am using a single $Sub variable that defines which time of day to use.
$Sub(99) = 1
; 0 = sunrise
; 1 = day
; 2 = sunset
; 3 = night
Then, I am defining a table of variables that holds the lighting conditions for each time of day.
$Sub(00) = 160, $Sub(01) = 130, $Sub(02) = 110, ; sunrise (0)
$Sub(10) = 160, $Sub(11) = 160, $Sub(12) = 160, ; day (1)
$Sub(20) = 140, $Sub(21) = 110, $Sub(22) = 100, ; sunset (2)
$Sub(30) = 070, $Sub(31) = 070, $Sub(32) = 070, ; night (3)
Here, variables 00, 01 and 02 (the leading 0 is only for clarification) correspond to RGB values for sunrise, the variables 10, 11, 12 to day, and so on. Once this is defined, you can select the right lighting condition depending on $Sub(99) that holds the time of day.
In order to do that, visualize that $Sub(99)0 is the index of the variable which stores the red value, $Sub(99)1 is the index to the green value, and $Sub(99)2 is to the blue value. Now we only need to query that variable.
With Route
.AmbientLight $Sub($Sub(99)0); $Sub($Sub(99)1); $Sub($Sub(99)2)
Using this approach, you only need to change the $Sub(99) variable, which you best store at the top of the file. By just changing that one variable, you can get different lighting conditions with ease. In Chashinai Railway, this variable is even randomized, meaning one single route file for any time of day.
Of course you can use the same technique for diffuse light, for the light direction or for the run interval.
Sunrise Day Sunset Night

Lighting and Tunnels

One problem I have encountered in tunnels is that the tunnel walls, signals, transponders, and everything inside a tunnel, will by default also reflect the lighting conditions. A red tunnel wall on sunset might look appropriate in the tunnel entrance section, but deep in the tunnel, you will want to get consistent lighting conditions that are unaffected by the sunlight. In Chashinai Railway, every single tunnel object, without exception, is unaffected by the sunlight and will thus look exactly the same under all conditions.
The trick is to disable reflective lighting and use emissive lighting instead. Consider the following object code:
CreateMeshBuilder
AddVertex, -0.4, -0.05, -0.2
AddVertex, -0.4, -0.05, 0.2
AddVertex, 0.2, -0.05, 0.2
AddVertex, 0.2, -0.05, -0.2
AddFace, 0, 1, 2, 3
The above object will reflect sunlight and will be almost dark at nighttime. On the other hand, the following object has a consistent look, suitable for a tunnel object:
CreateMeshBuilder
AddVertex, -0.4, -0.05, -0.2
AddVertex, -0.4, -0.05, 0.2
AddVertex, 0.2, -0.05, 0.2
AddVertex, 0.2, -0.05, -0.2
AddFace, 0, 1, 2, 3
SetColor, 0, 0, 0
SetEmissiveColor, 255, 255, 255
It is important to disable reflective lighting (SetColor) entirely so that the object does not reflect any sunlight. You can set any desired value for the emissive color.
If you create all your tunnel objects this way, and don't make use of any object that reflects sunlight, you can easily create a route at any time of day, and still, tunnels will look consistent.
Day Night

Tunnels and Cab Brightness

If the lighting conditions are low (e.g. night), even if you use Track.Brightness(255), the cab will look darker. This is good for outdoor scenarios, but not good for tunnels. Unfortunately, there is no way of completely avoiding this problem, but there are ways of cheating to some degree.
I have observed that with the lighting conditions described in the above Time of Day Selection section, Track.Brightness(180) on a nighttime route will lead to a cab that looks just as bright as Track.Brightness(36) on a daytime route. The actual values are something you need to figure out for your route by trial and error.
This means that for a tunnel, I could use Track.Brightness(36) on daytime, and Track.Brightness(180) on nighttime, while I can use Track.Brightness(255) for outdoors at any time. A transition from outdoors to tunnel looks like the following for nighttime:
With Track
100, .Brightness 255
105, .Brightness 180
And it looks like the following for daytime:
With Track
100, .Brightness 255
105, .Brightness 36
By defining variables and using the $Sub(99) which holds the time of day, the above two codes can be joined into the following code that unifies them:
$Sub(0) = 080, ; sunrise
$Sub(1) = 036, ; day
$Sub(2) = 110, ; sunset
$Sub(3) = 180, ; night
$Sub(95) = $Sub($Sub(99)), ; holds tunnel brightness

With Track
100, .Brightness 255
105, .Brightness $Sub(95)
This way, there won't be brightness differences during different times of day in tunnels in the cab. However, you will need a bit of trial and error to figure out the exact values needed under any given lighting condition. Basically, you cannot make the cab brighter in tunnels than the maximum brightness you can achieve at nighttime for a Track.Brightness(255) command.

Randomization and Probability

You can easily select randomly between integers by using the $Rnd command.
$Rnd(0;3)
In the above example, a random integer between 0 and 3 is selected, with the same probability for each number. On some occasions, you might want different probabilities.
For most control, define all possibilities with variables and then randomly select a variable.
$Sub(0) = 0
$Sub(1) = 0
$Sub(2) = 0
$Sub(3) = 1
$Sub(4) = 1
$Sub(5) = 1
$Sub(6) = 1
$Sub(7) = 2
$Sub(8) = 2
$Sub(9) = 3
$Sub($Rnd(0;9))
In the above example, value 0 has a probability of 3/10, 1 has 4/10, 2 has 2/10 and 3 has 1/10. The final query of $Sub gives the intended result which you could store or use somewhere.
For power-of-two probabilities, e.g. 1/16, 1/32, 1/64, and so on, you can do much shorter than this. If you want to get the number 0 with varying probabilities, use any of the following lines:
$Rnd(0;1) ; 1/2
$Rnd(0;$Rnd(0;1)) ; 1/4
$Rnd(0;$Rnd(0;$Rnd(0;1))) ; 1/8
$Rnd(0;$Rnd(0;$Rnd(0;$Rnd(0;1)))) ; 1/16
If you want to get the number 1 with varying probabilities, use any of the following lines:
$Rnd(0;1) ; 1/2
$Rnd($Rnd(0;1);1) ; 1/4
$Rnd($Rnd($Rnd(0;1);1);1) ; 1/8
$Rnd($Rnd($Rnd($Rnd(0;1);1);1);1) ; 1/16

Conditional code via $Include

Sometimes, you might want to insert whole sections of code based on a condition - usually a random value. In Chashinai Railway for example, the Minaminaka Line starts randomly from one of two sidings, and likewise, the Misaki Line starts randomly from one of four sidings.
The $Include directive easily allows to choose randomly between multiple files:
$Include(siding1.include; 1; siding2.include; 1)
The numbers behind the file names are weights. If the weights are the same for all files, you will get equal an probability for either file. If you want unequal probabilities, just specify the weights accordingly:
$Include(siding1.include; 2; siding2.include; 3)
This means that there is a 2/5 chance that siding1.include will be chosen, and a 3/5 chance that siding2.include will be chosen.

Conditional code via $Chr/$Sub

I have developed a technique for conditional code before the $Include directive was available. This is a more dirty approach than using $Include, but it allows to inline the code instead of referencing external files.
The basic idea is with two or more blocks of code, you just disable all the commands in the sections you don't want. Manually, you would do this:
With Track
1000, .Rail 1; 4
1025, .Rail 1; 2
; 1000, ; .Rail 1; -4
; 1025, ; .Rail 1; -2
1050, .RailEnd 1; 0
Or, if you want the other block of code to apply, you would do this:
With Track
; 1000, ; .Rail 1; 4
; 1025, ; .Rail 1; 2
1000, .Rail 1; -4
1025, .Rail 1; -2
1050, .RailEnd 1; 0
In order to automate this, we define variables that store either a space (if the commands should not be disabled), or a semicolon (if the commands should be disabled). For two blocks, we need two such variables. We can use $Chr to create characters, where a space is 32 and a semicolon is 59:
With Track
; start casing
$Sub(101) = $Chr(32), ; first case (32 = enabled)
$Sub(102) = $Chr(59), ; second case (59 = disabled)
; case 1
$Sub(101) 1000, $Sub(101) .Rail 1; 4
$Sub(101) 1025, $Sub(101) .Rail 1; 2
; case 2
$Sub(102) 1000, $Sub(102) .Rail 1; -4
$Sub(102) 1025, $Sub(102) .Rail 1; -2
; end casing
1050, .RailEnd 1; 0
In the above code, you would still have to change the 32 and 59 manually, so you need to do a bit more in order to automate this:
With Track
; start casing
$Sub(109) = $Rnd(1;2), ; the condition
$Sub(111) = $Chr(32), $Sub(112) = $Chr(59)
$Sub(121) = $Chr(59), $Sub(122) = $Chr(32)
$Sub(101) = $Sub(1$Sub(109)1)
$Sub(102) = $Sub(1$Sub(109)2)
; case 1
$Sub(101) 1000, $Sub(101) .Rail 1; 4
$Sub(101) 1025, $Sub(101) .Rail 1; 2
; case 2
$Sub(102) 1000, $Sub(102) .Rail 1; -4
$Sub(102) 1025, $Sub(102) .Rail 1; -2
; end casing
1050, .RailEnd 1; 0
In this code, we first define the condition, $Sub(109), a random number. Then a table, $Sub(1xy), defines all possible candidates for each case. With 2 cases, you need a table with 4 entries, with 3 cases, it's 9 entries, and with 4 cases, it's 16 entries. Then, the correct character for each case, $Sub(10x), is selected from the table based on the condition. The table is easy to expand - just make sure that the diagonal has 32s, and all other values are 59s. The following is an example of the table for 4 cases:
With Track
; start casing
$Sub(109) = $Rnd(1;4), ; the condition
$Sub(111) = $Chr(32), $Sub(112) = $Chr(59), $Sub(113) = $Chr(59), $Sub(114) = $Chr(59)
$Sub(121) = $Chr(59), $Sub(122) = $Chr(32), $Sub(123) = $Chr(59), $Sub(124) = $Chr(59)
$Sub(131) = $Chr(59), $Sub(132) = $Chr(59), $Sub(133) = $Chr(32), $Sub(134) = $Chr(59)
$Sub(141) = $Chr(59), $Sub(142) = $Chr(59), $Sub(143) = $Chr(59), $Sub(144) = $Chr(32)
$Sub(101) = $Sub(1$Sub(109)1)
$Sub(102) = $Sub(1$Sub(109)2)
$Sub(103) = $Sub(1$Sub(109)3)
$Sub(104) = $Sub(1$Sub(109)4)
The table logic is inserted and adapted quickly. However, you still need working code for each section. I first created the code in different route files, checked with Route Viewer, and as soon as everything was correct, I assembled the code of the different files into one file, and prepended the correct $Sub before each command. This is the important thing to do: A $Sub command must appear before each command, not just before each line. It might seem a bit of work and is harder to maintain, but can be really worth the effort.
Case one Case two