Showing posts with label scripting. Show all posts
Showing posts with label scripting. Show all posts

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!




TS2015 - Scenario Scripting in LUA Part 4 - Recorded Messages

Following on from the series begun earlier, this post talks about how you can allow your users to recall pop-up messages easily.

If you're providing any kind of instruction to the player then you should definitely use the Recorded Message capability of the scripting system to handle that pop-up message box so that the player can bring it back on-screen to remind themselves what they need to do.

Let's write a quick function to simplify showing these messages, this will allow us to use a more simple command later when we want to actually show them, as well as enforce a degree of consistency on our code later on.

function DisplayRecordedMessage( messageName )
   SysCall("RegisterRecordedMessage", "StartDisplay" .. messageName, "StopDisplay" .. messageName, 1);
end

The way that the Recorded Message capability works is that you tell it a function name to execute when it shows message and another function to execute when it hides the message.  In the case of simply showing a simple or HTML message you generally won't have anything in the "stop" function.  Where things get potentially complex and powerful is when you realise you can put any code in those functions, so you can trigger cameras, audio playback and so forth.  As an example, if you use the "Start" function to trigger a cinematic camera then the "Stop" function probably should put it back to the cab view.

Let's define those two functions now for a simple HTML pop-up:

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

function StopDisplayIntroText()
end


So, why did we call the functions "StartDisplayIntroText" and "StopDisplayIntroText" ?  Let's actually write some code to call "DisplayRecordedMessage" and that will complete the picture.

function OnEventStart()
  DisplayRecordedMessage("IntroText")
end

Again, this is done using our previously described mechanism of having each event in its own function.

You can see that when we call DisplayRecordedMessage we are passing in the parameter "IntroText", the DisplayRecordedMessage function will then find the two functions that begin "StartDisplay" and "StopDisplay" that have this in them - so StartDisplayIntroText and StopDisplayIntroText, and register them with the game to show a recorded message.

When you call DisplayRecordedMessage the game will immediately call the StartDisplay function and when the player closes the message box, the game will call the StopDisplay function.

That's it for todays post - tomorrow I will talk about Cinematic Cameras as well as how you can call up the standard cameras from script such as the Cab Camera.

Tuesday, 3 February 2015

TS2015 - Scenario Scripting in LUA Part 1 - Getting Started

Last night on Twitch I did another tutorial about scenario creation, this time it covered the much more complex topic of using LUA scripting to add a lot of other cool things to your scenarios.

I thought i'd start this blog to allow me to put other commentary on the topic as well as provide access to various script snippets to help you reproduce the techniques in your own work.

I'm not going to go in to great step by step detail, that was covered in the show and available on YouTube.

The video from the show is here: https://www.youtube.com/watch?v=1RcO_og8q7M

Once you've got your script created and have a scary blank window in your editor of choice (I use Notepad++), the first thing you're going to want to do is drop some definitions in to make the code easier to read as we go.  Here they are - just copy and pasted to the top of your script file:

-- true/false defn
FALSE = 0
TRUE = 1

-- condition return values
CONDITION_NOT_YET_MET = 0
CONDITION_SUCCEEDED = 1
CONDITION_FAILED = 2

-- Message types
MT_INFO = 0   -- large centre screen pop up
MT_ALERT = 1  -- top right alert message

MSG_TOP = 1
MSG_VCENTRE = 2
MSG_BOTTOM = 4
MSG_LEFT = 8
MSG_CENTRE = 16
MSG_RIGHT = 32

MSG_SMALL = 0
MSG_REG = 1
MSG_LRG = 2

Next, you'll want to put in the standard function definitions that the game will call:

function OnEvent(event)
end

function TestCondition(condition)
end

Anyone who codes a lot knows the golden rule about not putting too much code in any one single function.  Unfortunately these two functions are going to inherently get very big with lots of "if this then that else if this then the other etc etc".  Fortunately LUA comes to the rescue with a neat capability that I didn't cover in the show. Modify the above two functions to read as follows:

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

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

What this does is mean that every event or condition can now be placed in to its own function!

So where we might have had this before:

function OnEvent(event)
   if (event == "IntroText") then
      -- do some stuff
   end
end

Now we've got this:

function OnEventIntroText()
   -- do some stuff
end

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

Now any time you want to add a new event you just need to add a new function.

The same applies with conditions; using the snippet above for the modified TestCondition, the conditions will now go to their own function such as this:

function TestConditionOverspeed()
   return CONDITION_NOT_YET_MET;
end

That's the foundations of your script in place, in future articles i'll start covering specific features.

I'd love to hear your feedback on this article, not just if you like it but if there's anything you think could be improved!