Monday 9 February 2015

TS2015 - Scenario Scripting in LUA Part 7 - Interfacing with Loco Controllers

In this article I wanted to give you the tools to really make the scenario script interface much more with the player.  You can cause them problems, provide assistance,  or simply make decisions based on what is happening.

What are controllers?


Before we look at how to get and set controllers, it's probably worth reviewing exactly what it is that they are.

Within the definition of a locomotive there are a set of controllers that represent values values managed by that locomotive.  Most values reported within the cab such as speed, ammeter and so forth are available as controllers, as are most inputs such as the position of the regulator, the state of the headlights and so forth.

Each locomotive is likely to have different controllers, though there are some common sets that you can predict between types of locomotives.  For example, just about everything has a "Regulator" control and a "Reverser" control.  Steam engines will have boiler pressure, where as diesel electrics will have the ammeter.

One way to quickly see what controllers are on a specific locomotive is to run the game with a special command line parameter

-ShowControlStateDialog

(see Part 2 - Logging - to see how to add these if you're not already familiar with command line parameters)

You will also need to be in Windowed mode to be able to see this box, so change to windowed mode if you are not already.

With this enabled, fire up a scenario or quick drive using the locomotive you'll have in your scenario and a new box will appear that shows every controller and its current value, changing in real time.  This can give fantastic insight in to what is happening in the locomotive as you drive it.  Operate some controls and watch how various values change as you start driving.


Reviewing the controllers in the locomotive featured in your scenario as the player loco will tell you what you can and can't do with that loco.  Many controllers can be read and written to, some such as the speed, boiler pressure etc can only be read from.

Virtual Controllers

One slight wrinkle is that there are also Virtual versions of controllers, so you might have a VirtualRegulator in a loco and you'll notice that operating the regulator actually moves this and not the "Regulator" control. 

Virtual Controllers really are just normal controllers in most aspects, the only difference is that the core game will usually act on their presence by making them supercede the controller they are overriding, so VirtualHorn will override the Horn.  There are some other variations such as "VirtualBrake" overrides the "TrainBrakeController" but for the most part the pattern works.

Be aware of Virtual controls and if you see any, check to see which ones are being used and then make sure you work out if these are the ones you should be really interacting with.

How to get values


Now that we know what we're interfacing with, we can start to look at retrieving values.  I'm not going to look at what you would do with that information yet, that comes in a little while, for now, let's just see a short example of how to retrieve a controller value and what it means:

regulator = SysCall("PlayerEngine:GetControlValue", "Regulator", 0)

This command will retrieve a controller called "Regulator" and store it in a local variable called "regulator".  Note that the case of the controller you want to retrieve must match the way it is expressed in the Control State Dialog.  The last digit "0" on the end will always be a zero.

How to set values


Setting control values looks very similar.

SysCall("PlayerEngine:SetControlValue", "Regulator", 0, 1)

In this case note that the "0" is still present and will again always be a "0".  In this case, i'm setting the value of the regulator to 1, which sets it to its maximum on most locomotives.  You should review the behaviour of the controllers via the Control State Dialog in order to understand what the minimums and maximums are, in order to be able to correctly understand what values you're reading and writing.

Uses


Now we come to the "why?".  We have the tools to read and write controller values, but what can we actually do with this knowledge?

I'm going to show four examples, but i'm sure there are more and I'll leave it to you to be innovative and surprise us all!

Triggering when a controller reaches a value


In this example, let's say I want to trigger some kind of pop-up message when the player moves their regulator above 50%.   Let's jump in to some code now and explain afterwards - again, i'll include some of the surrounding contextual code as well so you can see how the example fits in the wider sceheme of things:

function OnEvent(event)
  _G["OnEvent" .. event]()
end

function TestCondition(condition)
  _G["TestCondition" .. condition]()
end

function OnEventStart()
  SysCall ( "ScenarioManager:BeginConditionCheck", "CheckRegulator" );
end

function TestConditionCheckRegulator()
  regulator = SysCall("PlayerEngine:GetControlValue", "Regulator", 0)  
  if (regulator > 0.5) then
    SysCall ( "ScenarioManager:ShowInfoMessageExt", "Regulator too high!", "regulatortoohigh.html", 10, MSG_TOP + MSG_TOP, MSG_SMALL, TRUE );
    return CONDITION_SUCCEEDED
  end
  return CONDITION_NOT_YET_MET;
end

So our first scenario instruction was a trigger that fired "Start" as the event.  This causes OnEvent("Start"), we then call OnEventStart() and this begins checking a condition called CheckRegulator.

The game now starts very regularly running TestCondition("CheckRegulator"), which in turn calls TestConditionCheckRegulator().

TestConditionCheckRegulator() will retrieve the value of the regulator and if it exceeds 0.5 it will pop up a message.  Having returned "condition succeded" this means it will not then perform this check any more, so you only get one message.

You could use this example perhaps in some form of tutorial where you say "now move the regulator to around 50% to get the train moving", and then once the regulator has been moved, the pop-up message could provide the next set of instructions for the player.

Limiting a controller to a particular range


Perhaps you want to make it so that the player can only move a control within a particular range, simulating some mechanical fault for example.  You could use a similar technique as above, combined with a "set" instruction:

function OnEvent(event)
  _G["OnEvent" .. event]()
end

function TestCondition(condition)
  _G["TestCondition" .. condition]()
end

function OnEventRegulatorFault()
  SysCall ( "ScenarioManager:BeginConditionCheck", "ManageRegulatorFault" );
end

function OnEventStopRegulatorFault()
  SysCall ( "ScenarioManager:EndConditionCheck", "ManageRegulatorFault" );
end

function TestConditionManagerRegulatorFault()
  regulator = SysCall("PlayerEngine:GetControlValue", "Regulator", 0)  
  if (regulator > 0.7) then
    SysCall("PlayerEngine:SetControlValue", "Regulator", 0, 0.7)  
    return CONDITION_NOT_YET_MET
  elseif (regulator < 0.2) then
    SysCall("PlayerEngine:SetControlValue", "Regulator", 0, 0.2)  
    return CONDITION_NOT_YET_MET
  end
  
  return CONDITION_NOT_YET_MET;
end
 
This is a slightly more complex example.  Essentially i've allowed for an event that can be triggered called "RegulatorFault", and another event called "StopRegulatorFault".  So at some point in our scenario instructions we add the trigger to cause "RegulatorFault" and then perhaps later on we have another instruction which causes "StopRegulatorFault".

When we start the regulator fault, a condition begins being checked and all this will do is get the value of the regulator and if it is above 70% it will force it back to 70%, and if it is below 20% it will force it up to 20%.  Now if you try and move the regulator outside of this 20-70% range you'll find you can't!

Once you get to the "StopRegulatorFault" event, this will use the "EndConditionCheck" to cause the condition to stop being evaluated and the regulator will return to full behaviour again.  You could link this with some kind of pop-up perhaps explaining that the fault has now been rectified.

You could imagine that this kind of fault could cause some degree of stress in the wrong situation, so it's also important to carefully work out where to place this kind of thing.  Never put the player in a position that they can fail for reasons outside of their control, it might be realistic, but it's not even remotely fun!

Note also that in all cases i'm returning "CONDITION_NOT_YET_MET" - this is because I want the condition to continually fire over and over, if I were to return "CONDITION_SUCCEEDED" it would only fire once.

Forcing the train to stop


In this small code snippet, we're going to see if the locomotive is exceeding 30mph and if it is, we're going to reset the reverser, the regulator and apply full brakes - basically the same as the emergency brake does.

MPH = 2.23693629

function OnEvent(event)
  _G["OnEvent" .. event]()
end

function TestCondition(condition)
  _G["TestCondition" .. condition]()
end

function OnEventSpeedCheck()
  SysCall ( "ScenarioManager:BeginConditionCheck", "SpeedCheck" );
end

function TestConditionSpeedCheck()
  speed = math.abs(SysCall("PlayerEngine:GetSpeed")) * MPH;
  if (speed > 30) then
    SysCall("PlayerEngine:SetControlValue", "Regulator", 0, 0)  
    SysCall("PlayerEngine:SetControlValue", "Reverser", 0, 0)  
    SysCall("PlayerEngine:SetControlValue", "TrainBrakeController", 0, 1)  
    return CONDITION_SUCCEEDED;
  end
  return CONDITION_NOT_YET_MET;
end

In the key part of this code, the TestConditionSpeedCheck function, you can see that we get the speed, convert it to Miles Per Hour (remember that it's in meters per second by default) and then if it's greater than 30 we set the regulator and reverser to 0 and the train brake to 1, which will cause the train to start rapidly slowing down.

That's great, but it would be good to lock the player out from being able to now make changes until the train has stopped and perhaps been suitably chastised.  You could do this by firing another condition that constantly resets these controllers until the speed is back to zero however there is a neater way of doing it that i'll be covering in a future tutorial that involves actually locking the player out from using any controls at all!

Driving the train or operating controls for the player


For the last example, you could actually operate some controls for the player and either fully drive or part drive the locomotive for them.

Let's take the example of a steam locomotive and have the script take on the function of operating the reverser / cut-off, so the player will only need to operate the regulator while the script will do the reverser for them.  This kind of thing can look very cool to players as they see part of the loco essentially seeming to drive itself and you could apply storyline to it like the fireman is operating some of the controls for you, for example.

MPH = 2.23693629

function OnEvent(event)
  _G["OnEvent" .. event]()
end

function TestCondition(condition)
  _G["TestCondition" .. condition]()
end

function OnEventManageCutoff()
  SysCall ( "ScenarioManager:BeginConditionCheck", "ManageCutoff" );
end

function TestConditionManageCutoff()
  speed = math.abs(SysCall("PlayerEngine:GetSpeed")) * MPH;
  if (speed > 50) then
    SysCall("PlayerEngine:SetControlValue", "Reverser", 0, 0.25)  
  elseif (speed > 40) then
    SysCall("PlayerEngine:SetControlValue", "Reverser", 0, 0.35)  
  elseif (speed > 30) then
    SysCall("PlayerEngine:SetControlValue", "Reverser", 0, 0.45)  
  elseif (speed > 20) then
    SysCall("PlayerEngine:SetControlValue", "Reverser", 0, 0.55)  
  elseif (speed > 10) then
    SysCall("PlayerEngine:SetControlValue", "Reverser", 0, 0.65)  
  elseif (speed > 5) then
    SysCall("PlayerEngine:SetControlValue", "Reverser", 0, 0.70)  
  else
    SysCall("PlayerEngine:SetControlValue", "Reverser", 0, 0.75)  
  end
  
  return CONDITION_NOT_YET_MET;
end

I'll start out by saying that I've probably not got great values for the reverser here, but the point is still valid. 

So we're going to fire an event off in the scenario instructions, probably at the start, called ManageCutoff.  This will result in a condition being checked called ManageCutoff by the event handler OnEventManageCutoff.

When the condition runs it will check the current speed of the locomotive and then decide what value to set the reverser too, so as I speed the locomotive up, so the reverser will move to a lower and lower value and as I slow the loco down so the reverser will move its way up again.  With some tuning for a particular loco this can make it significantly easier to drive for new users.

There are a couple of interesting points about this script

Notice how the if statements are checking in reverse order.  This is because if I had checked for ">5" first, then every speed would have matched that one and the script wouldn't have worked.  If you don't get it, work it out on paper or even give it a try and you'll see what I mean.

It's important to realise that while the functions are called "Conditions" they are useful for far more than simply checking to see if a condition is true, just as in this case, you can use it to run processing in parallel with the actions the player is undertaking.

Visual Changes


One last example without code, because it's usually highly dependant on specific locomotives, to get you thinking is that on some locomotives you'll notice visual things can be affected by scripts.  These can include head boards, destination boards, lamp configuration and so forth.  You can of course so all this too via script if they're able to be affected by controller changes.  For example, you could have a steam loco set up so that it dynamically and automatically changes its lamp headcode configuration at various points in a scenario.

Last Words


One last word on the subject of using Conditions since we've used them a lot here - be careful that they are running all the time over and over again until they are stopped, therefore the more you do, the more you have running and the more intensive they are, the more of an impact they are going to have on frame rate.  So use them, but use them carefully.

This has been a fairly long post but I hope it has given a lot more examples about how you can handle reading and writing controllers.  I've generally used the regulator a lot in my examples but you could do anything - you could check for current speed and boiler pressure and use that to decide to start loading on coal for example, or simply put up a message saying "your boiler pressure is low, you need to open the damper and start the blower, and make sure you have plenty of coal in the fire!".

1 comment:

  1. Could you please help me with the PantographControl?

    ReplyDelete