Sunday, 15 February 2015

Apologies for the Delay

Article 10 (Dynamic Weather) is in progress - however Blogger just corrupted all the images in it and I need to finish up and test some bits to make sure it's all right, it's just taking longer than the other articles because there's a lot more to it :)

Hopefully early this week I'll get it finished and I can then carry on - audio and video are significantly shorter topics :)

Wednesday, 11 February 2015

TS2015 - Scenario Scripting in LUA Part 9 - Ending the Scenario Early

Sometimes when a scenario is being played you want to be able to reflect sufficiently bad playing by ending the scenario early.  The game can already do this in some circumstances, for example leaving the firebox door open when you go in to a tunnel or running out of water in the boiler can both terminate the scenario early.

Wouldn't it be great if you could build some of this gameplay in to your scenarios, coming up with unique situations that fit what is going on?

Let's take a previous example - where you're driving along with wagons that have a lower speed limit than the maximum line speed, so you've scripted something that will watch the speed and then stop the train if you do it too much.  It works by first warning you, then by stopping the train - but what's the next level of escalation?  In reality you would be relieved of your duties pending a review and disciplinary action, and the closest thing to that in the game is just simply to end the scenario early and essentially make the player start again and pay more attention next time.

You can end a scenario as a failure

SysCall ( "ScenarioManager:TriggerScenarioFailure", "Scenario failed! Please try again." );

Or you can end it as a success:

SysCall ( "ScenarioManager:TriggerScenarioComplete", "Scenario completed!" );

It's easy to come up with reasons to end a scenario with a failure, though I would certainly caution against using this too much, use it where you really do think the player has done so badly that it is inappropriate to allow them to finish the scenario.  Think of it as an "ultimate penalty" and you absolutely should have a multi-level approach to this including warnings, don't just bang them out, that makes for a frustrating experience and they're unlikely to come back for another go.

However, ending a scenario early as a success? What on earth is going on there!

There are fewer uses for it, but there certainly are some potential uses.  One example is that you could set up a career scenario which has you run along a line and you mark out 5 stops each worth 200 points.  During the initial journey for these first four stops, you have some background script checking the performance of the player, perhaps you track how many seconds are spent overspeeding, or if they over-drive the engines by pushing the ammeter in to the red and track how many seconds they do that.  When the player then arrives at the fourth stop, you then make a decision.  Has the player done well?  If they have not, then end the scenario early with a success.  They have now technically finished the scenario but they cannot possibly access a gold medal.  If they have done well, then don't end it, let them complete the journey to the final stop and receive the additional points.  In this way, you're actually limiting the possible score (and medal) that the player can earn based on wider driving attributes and skills than simply their ability to not speed and not be late, but they're still actually able to get a medal/tick even if they're not doing very well.

Those are some simple ideas, I leave it to you to come up with some more interesting gameplay concepts and i'm looking forward to seeing what you can come up with!





Tuesday, 10 February 2015

TS2015 - Scenario Scripting in LUA Part 8 - Locking and Unlocking Controls

We've covered some interesting examples in previous articles where you can take control of the player train, perhaps to bring it to stop if the player breaks some speed limit too much.

One of the weaknesses in those examples is that currently they are one-shot, once the script has changed the controls to stop the train, it sits back and relaxes, so the player can just as easily jump in and reset them to keep going again if they want to.

Some times we want to be a little more insistant.  There are a couple of ways of doing it and some are more appropriate for different situations than others.

You could use all that you've learned so far to simply keep continually resetting a control to some stored value, that way no matter what the player does with it, it will be forced back to the same place again.  This has the effect of preventing its movement, but doesn't look great in the UI because the control appears to move and then keep springing back.

Another approach is to ask the game to lock out the controls, the LUA script command to do this is:

SysCall ( "ScenarioManager:LockControls");

You can (and must, at some point!) then unlock them using this control:

SysCall ( "ScenarioManager:UnlockControls");

When you issue the "LockControls" command it will remove any HUD and prevent all keyboard inputs, it will be impossible to move any controls at all!

There is a downside to this... once you have locked out the controls, the player cannot pause the game or exit it.  So if you use it, then please make sure you use it sparingly and appropriately, and always remember to unlock it again!

A short post today, more tomorrow!




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!".

Sunday, 8 February 2015

TS2015 - Scenario Scripting in LUA Part 6 - Speed Checking

In addition to providing various visual interactions such as pop-ups and cinematics, you can also use the LUA scripting logic to provide for additional gameplay features as well.

One such feature you can quickly use to great effect is to use the scripting to monitor the players speed and then react to it in some way.

There are a couple of uses for this, one is to see whether the player has got the train moving - and if so perhaps congratulate them and give them some next instructions.

Another is to see if the player has stopped, in which case you might want to remind them how to get moving again.  This is particularly useful if you're aiming your scenario at newcomers to the game!

The final one and perhaps the most useful generally is to check for going too fast according to some additional rules (beyond speed limits and so forth).  For example, while the speed limit on a particular stretch of line might be 70mph, if the train you're hauling has wagons with a maximum 40mph speed limit (perhaps they're sensitive or dangerous cargo, or simply old and not suitable for higher speeds for example) then it would be good to be able to factor this additional rule in to the scenario, rather than simply advising the user at the start and then just assuming they adhere to a guideline they probably forgot 10 minutes in to the scenario.

How do you get the speed?


This is the easy bit, getting the speed can be done with a simple single command:

speed = math.abs(SysCall("PlayerEngine:GetSpeed"))

Here, we call a command PlayerEngine:GetSpeed in the game core and then run it by a math function called "abs" - what this will do is ensure that the result is always positive.  That way no matter if you're going forwards or backwards, the speed returned is always a positive number.  The result is then stored in a variable called "speed".

Note that the units returned by GetSpeed are in meters per second, NOT, miles or kilometers per hour.

What I like to do is add some more constants to the section of definitions at the top of the script to help with these calculations:

MPH = 2.23693629
KMH = 3.6

gSpeedUnits = MPH
I can now adjust the above GetSpeed example as follows:

speed = math.abs(SysCall("PlayerEngine:GetSpeed")) * gSpeedUnits

Now "speed" will contain the current speed of the loco in miles per hour.  It might at first seem odd to have "gSpeedUnits" - why not just use the constant "MPH" ?  You could do that - but if you wanted to re-use components of your script in a future script and that needed to be in KMH then you would need to do a careful search and replace to swap "MPH" to "KMH" - by using "gSpeedUnits" everywhere you can simply change the assignment to gSpeedUnits at the top of the script and the rest will simply fall in to place.

Where does it go in the script?


You will generally want to check speed as part of a condition so that you can do it regularly.  You could check to see if you're at a particular speed as part of a one-off check in an event if you wish but there aren't many cases where you really want to do that.

Let's set up a small example to check if the player has started moving yet, triggered as part of the first trigger instruction in a scenario:

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

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

function OnEventStartMovingCheck()
  SysCall ( "ScenarioManager:BeginConditionCheck", "AreWeMovingYet" );
end

function TestConditionAreWeMovingYet()
  speed = math.abs(SysCall("PlayerEngine:GetSpeed")) * gSpeedUnits;
  if (speed > 1) then
    SysCall ( "ScenarioManager:ShowInfoMessageExt", "You're moving!", "moving.html", 10, MSG_TOP + MSG_TOP, MSG_SMALL, TRUE );
    return CONDITION_SUCCEEDED;
  end
  return CONDITION_NOT_YET_MET;
end

Ok, lots to see there, but most of it you've already seen in previous articles, i've just included it to provide context.

So in our scenario set-up in the timetable view of the game, we've added a trigger instruction that fires an event "StartMovingCheck".  This results in a call on OnEvent with event set to "StartMovingCheck". That in turn calls OnEventStartMovingCheck.

OnEventStartMovingCheck will begin a condition test called "AreWeMovingYet".  As of that moment, the game will begin firing calls to TestCondition with "condition" set to "AreWeMovingYet" and it in turn will call TestConditionAreWeMovingYet.

Ok, so we made it to the actual important bit of this example finally!

TestConditionAreWeMovingYet, which is being called very regularly now over and over, will obtain the speed of the player.  If the speed is more than 1mph then it will open an HTML message box that loads "moving.html" and might give the player some feedback now they're moving and perhaps more instructions.  The return of "condition succeeded" means it will now no longer perform this check, so if the player stops and starts moving again it will NOT do this again.

You can see that it would be easy to modify this example to check if the player is stopped, however in that case I would strongly recommend checking for speed<0.1 rather than speed == 0 simply because of rounding and physics behaviours in the game.

This has been a simple over view on how to check the speed, you should be able now to see how you can apply it to various different cases and provide some additional gameplay behaviours.

To give you some examples, there are some scenarios on Western Lines of Scotland where you are hauling tanker wagons and these have reduced speed limits.  The storyline of the scenario is that you're riding with a fireman and guard who are very cautious of accidents.  Even though the line speed is high, the train is in some cases only rated for 30mph maximum speed.  As you drive along, the speed is constantly being checked and if you ever exceed 30mph you get a warning to slow down, after two or three warnings then the brakes will be applied to bring the train to a stop.  If you then continue to cause this to happen more times then eventually the scenario is actually terminated early, resulting in a fail.  Of course you need to tune how flexible the scripts are - if they bang you out with a scenario failure for one error then that really does qualify as a super hard (possibly super frustrating!) scenario and you should set things up to match the level you're trying to achieve.






Saturday, 7 February 2015

TS2015 - Scenario Scripting in LUA Part 5 - Cinematic Cameras

In todays article, I'm going to cover one of the most exciting features you can access through LUA scripting in your scenarios, the use of Cinematic Cameras.  These cameras allow you to create specific sequences that might commonly be used to set a scene at the start of a scenario, or perhaps even during a scenario they can be used to indicate some thing that's going on in the world around the player.

In addition to the Cinematic Cameras, I'll also then cover how you can access the other fixed cameras such as forcing cab view or track side view for example.

If you have not yet watched the YouTube video from the TSL show segment that covered this then I would recommend you do this first because it's always easier to see things visually.

Click here to watch the episode

There are a couple of steps to using the cinematic cameras:

1. Place and set up the cameras to create the sequence itself
2. Start the camera sequence from LUA script

Setting up the Camera


Most of how you place the camera is far better explained visually in the video but here's a route outline of what you need to do.


First, you need to place a Cinematic Camera asset, which you can find on the middle-left flyout under the "miscellaneous" group, which is the bag in the bottom right of the little group of icons.


First, you need to place a Cinematic Camera asset, which you can find on the middle-left flyout under the "miscellaneous" group, which is the bag in the bottom right of the little group of icons.







Double click on the camera asset to get access to its properties on the top right fly-out.

You can use one of the arrow icons (third one along) to move the camera so that it is pointing to exactly what you are currently looking at, this is by far the easiest way to position the camera.

You can change the field of view by changing the value in the fifth field down (it defaults to 65) to get a narrower angle by lowering it, or a more wide angle by raising it.

Click the "+" icon (second icon) to add a new key frame and then use the left/right yellow and white arrows on the top right to move between the key frames.  It tells you below the arrows which frame you're on, e.g. 3/4 means third out of four.  You can use the "bin" icon (first one) to delete the current key frame.

Once you've positioned all the cameras, go back and set times using the sixth field down - so for key frame 1, the time means how long to spend on that key frame before advancing to the next.

Once you've finished setting up all the key frames for your cinematic sequence, you can test it out right away by clicking on the "play" icon at the bottom.  This will run through the sequence and you can use the clock on the bottom right of the fly-out to watch as the sequence runs through it's timings to make sure everything works out as you planned it.

Once you start reviewing the sequence with "play" it is important to make sure that you click "stop" to end it as many of the fields are not accessible while it is playing.  You can use the rest of the controls along the bottom such as rewind and pause the same way you would expect to.

Once you're happy with the sequence, put a name in to the bigger text field (with the cube next to it) - this is the name that we'll use to start the sequence off.

Starting the Sequence


At this point, we're finished with the cinematic camera editor and can now return to the scenario editor and write a little LUA code to activate it.

In this example, let's assume it's an opening camera sequence.  I named the sequence "coolopening" in the cinematic camera name box described above, and in the scenario i've added a trigger box which triggers an event "Start".

Let's see the code that now makes the magic happen.

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

function OnEventStart()
  SysCall ( "CameraManager:ActivateCamera", "coolopening", 0 );
end

So we start out with our standard "OnEvent" function which passes it on to a separate function to handle the specific event - in this case, OnEventStart, because the first event we triggered was called "Start".

Inside that event, we then call the ActivateCamera function in the game and ask it to run "coolopening"; the name of our cinematic camera sequence.

That's really all there is to getting it working, but there are some extra bits that are worth knowing.

Delaying the next instruction


If you want to run a camera sequence and then do something else (which you most likely will!) then make sure that the trigger instruction has a timed offset.  This is because the command to run a camera sequence actually finishes immediately and returns control back to the game to run the next instruction in the scenario, while at the same time it runs the camera in parallel.  If you don't put a delay on the next instruction it can result in the camera sequence being interrupted.


The example above shows that in the first field of the trigger instruction, i've set up a 15 second delay - which means that from the moment this instruction would normally execute, it will hold off for 15 seconds.  If our first instruction was to fire a camera sequence that lasted 15 seconds, this would mean that the instruction above waits for 15 seconds and then fires another trigger to continue the script on to the next step.  You will simply need to add up the total time of your cinematic sequence (including the time on the last key frame) and then put that in to the delay box.

Chaining multiple camera sequences together


If you want to chain two camera sequences together, e.g. to do a pass through of a platform and then another pass over the player train then you simply set them up as normal and make sure that the trigger instruction which causes the second has a suitable delay so that it will not trigger before the first one has finished running.

Accessing Internal Cameras


There are a number of internal cameras defined that you can refer using the ActivateCamera call. Note that these names must be entered precisely, including the correct capitalisation.

CabCameraSwitches to the cab camera (like pressing 1 normally)
ExternalCameraSwitch to view "2"
TrackSideCameraSwitch to view "4"
CarriageCameraSwitch to view "5"
CouplingCameraSwitch to view "6"
YardCameraSwitch to view "7"
HeadOutCameraSwitch to view "Shift-2"
FreeCameraSwitch to view "8"

Helper Functions

All good coders know the importance of keeping your code clean, and one of the important ways of doing this is to spread your code in to different functions that look after different aspects.  One key area you can do this, and also greatly simplify reading of your code later, is to create some short helper functions that wrap the SysCall's, something like this:

function ShowCamera(camera)
  SysCall ( "CameraManager:ActivateCamera", "coolopening", 0 )
end

function ShowCabCamera()
  ShowCamera("CabCamera")
end

function ShowTracksideCamera()
  ShowCamera("TrackSideCamera")
end

It might seem redundant to have "ShowCabCamera" simply call ShowCamera when it could simply call the SysCall itself, this is a style choice you can make.  Personally I like to keep duplication to a minimum and the cost of a function call in these situations is minimal, so the code remains nicely readable and there's only one place that actually calls the game.

Having built yourself a nice library of helper functions (and this is a process you can apply to everything we talk about in these posts) your actual main code becomes quite a bit more readable:

function OnEventStart()
  ShowCamera("coolopening")
end

Recorded Messages and Cinematic Cameras


It is perfectly possible, and some times quite desirable, to link playing a cinematic camera with a recorded message.  It's important to remember to reset the camera back to the cab when you do this, as shown in the following example where I want to have a cinematic highlight a road traffic accident that the player is driving past.  I would have set up the scene using scenario-specific assets, then put the camera on it to fly around it a bit.  I could then call it like this:

function StartDisplayRTA()
  ShowCamera("RTACamera")
end

function StopDisplayRTA()
  ShowCabCamera()
end

function OnEventShowRTA()
  DisplayRecordedmessage("RTA")
end

Here you can see i'm continuing to use the new library of functions i've built up previously and the code is immediately far more readable than a bunch of SysCall's all over the place.  In this case the cinematic will play as normal and when it finishes it will return to the cab.  If the player then clicks on the "previous message" button on the HUD then it will simply re-play the cinematic and when finished go back to the cab.

As mentioned in an earlier article, you really can use the Recorded Message function for all kinds of things, not just messages.




Friday, 6 February 2015

TS2015 - Scenario Scripting in LUA Part 3 - HTML Pop-Up Messages

Continuing this series of articles about how to make use of LUA scripting in scenarios, this time we're going to take a look at how you can pop up a message box that contains HTML, including images and formatting.

If you haven't read the previous two articles or seen the YouTube video recording from the live Twitch session then I would strongly recommend doing so first.  The aim of these articles is really to dig a bit deeper and be a bit more detailed, so it might miss out some steps (all of which should be covered in the video).

You can find the video here: https://www.youtube.com/watch?v=1RcO_og8q7M

Where are HTML files placed?

You cannot place the HTML files directly in the Scenario folder, it is important to note that they are localised (meaning you can display different files for different languages, such as having a German HTML file shown to a German user, or an English one to an English user).

If your scenario is located in:

c:\program files (x86)\steam\SteamApps\common\RailWorks\Content\Routes\6700a000-afeb-4129-8b36-3a88d83ed073\Scenarios\502f3106-14cc-4626-8421-700ee44610a7

Then your English HTML files must be in an "En" folder beneath that, i.e.

c:\program files (x86)\steam\SteamApps\common\RailWorks\Content\Routes\6700a000-afeb-4129-8b36-3a88d83ed073\Scenarios\502f3106-14cc-4626-8421-700ee44610a7\En

Your German scenario files will be in a "De" folder beneath that, i.e.:

c:\program files (x86)\steam\SteamApps\common\RailWorks\Content\Routes\6700a000-afeb-4129-8b36-3a88d83ed073\Scenarios\502f3106-14cc-4626-8421-700ee44610a7\De

and so forth.

If you want to include any images then they should be in the base scenario folder (i.e. one folder above where the HTML files are).

For most cases, images should be 128x128 in size, if you want something bigger then note that they need to be multiples of 128, e.g. 256x256 or 128x256.  If you go above 128x128 then you must always pop up LARGE message boxes (more on that in a moment).

It's important to note that the HTML files shown in in-game message boxes cannot be very complex and only a minimal amount of HTML is supported.

Here's an example, simple, HTML file to get us started:

<html>
   <body bgcolor="#ff0000">
      <font color="#ffff00" face="Arial" size="3">
         <table>
            <tr>
               <td><img src="../professor.png" width="128" height="128"></td>
               <td>
                  <p><b>Welcome to this scenario</b></p>
                  <p>Can you <i>finish</i>  it in one piece??</p>
               </td>
            </tr>
         </table>
      </font>
   </body>
</html>
In this file, we've used a table to create two columns, in the left hand column is an image (which must be placed in the main scenario folder) and the right hand column has some example text with basic formatting options.

Save that file as "introtext.html" in the En folder.

Scripting

The next step is to update the script so that it will call up our HTML message box.

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

function OnEventIntroduction()
  SysCall ( "ScenarioManager:ShowInfoMessageExt", "Title of the box", "introtext.html", 0, MSG_VCENTRE + MSG_CENTRE, MSG_REG, TRUE );
end

I've included a copy of the "OnEvent" function just to remind you how we get to the "OnEventIntroduction" function.

So you can see that to open a message box, we use ScenarioManager:ShowInfoMessageExt and pass in a bunch of parameters.

Let's just dig in to each one of those, starting from after the name of the function.

The title of the message box
The HTML file that you want to display (the En or De etc bit will be added automatically)
MSG_VCENTER means center it vertically, this could also be MSG_TOP or MSG_BOTTOM.
MSG_CENTER means center it horizontally, this could also be MSG_LEFT or MSG_RIGHT.
MSG_REG means make a middle-sized regular box, this could also be MSG_SMALL or MSG_LRG.
Finally, the "TRUE" at the end means that while this message box is displayed, the game should be paused.  If this is FALSE then the game will continue even though the message box is still displayed.

If you want to use an image of greater size then 128x128 then you will need to use a MSG_LRG if you want to avoid an unsightly scrollbar appearing.

That's really all there is to it, in the next article, i'll talk about how to make it so that these pop-up messages can be recalled by the player any time they want to re-read through the instructions you're giving them as they go.

Don't forget to follow the blog so that you are kept immediately up to date with new posts and if you have any feedback I'd love to hear it in the comments section!