Writing world scripts with Lua

From Serious Sam Wiki
< Lua
Jump to: navigation, search



Editing world scripts in Serious Editor

Creating script entities in world editor

Just drop a script entity into the world (represented by an orange box) and double click it to open his script in the script editor. In script editor you can easily edit Lua source code.

Editing Lua scripts with built in script editor

Indenting code

Each line is automatically indented to the same level as the previous line of code, so you need only to indent the first line in the block (using tab key) and the rest of the lines will follow. You can easily indent entire blocks of code by first selecting them and by pressing tab to indent the selected code by one level or by pressing shift+tab to unindent the selected code by one level.

Auto complete

You can use the auto complete functionality with any variable whose data type is known at the time of editing. Examples of such variables are entity variables (variables created by dragging and dropping entities from the world) and variables for which data type is hinted. Variable type hints are Lua comments which contain variable name and a valid data type, separated by semicolon, for example:

 -- puppet : CLeggedPuppetEntity
local puppet = SomeFunctionWhichReturnsAPuppet()
puppet:PlayA -- here you can use the auto complete

Auto complete functionality is activated by pressing ctrl+space (more about auto complete functionality can be found in chapter on Entity variables).

Locating entities corresponding to script variables

Entities which correspond to script variables can be located in world using keyboard shortcuts. These are the keyboard commands:

  * Locate (=F9=) - centers the view on the entity (or entities) and highlights it using a "crosshair"
  * Select (=F10=) - selects the entity (or entities) and highlights it using a "crosshair"

And these are the ones available using only the mouse and modifer keys:

  * =Ctrl+Alt+LMB= - highlight the entity (or entities) using a "crosshair"
  * =Ctrl+Alt+LMBx2= - selects the entity (or entities)
  * =Ctrl+Alt+Shift+LMBx2= - adds entity (or entities) to the current world selection
  * =Ctrl+Alt+RMB= - focus the view on the entity
  * =Ctrl+Alt+RMBx2= - focus and align the view on the entity

Finding syntax errors

Syntax errors are detected as you type. If you have a syntax error in your code, line which contains the first syntax error will be underlined with a red line and error description will be shown in the status bar at the bottom of the script editor. Note a couple of things about syntax errors: while you are typing code in script editor, you will have syntax errors most of the time (because the code will not be complete) so you have to worry about them only when you are done typing some block or a function; when one line is reported to have a syntax error it may be the case that there is nothing wrong with that line because the error is caused by code in the previous line of code. Also note that script source which has no syntax errors in not guaranteed to run properly since errors can always occur in run-time (while the script is executing).

Syntax highlighting

While you type in the script editor, syntax will be highlighted according to meaning of words you type. Different types of words and characters will be colored differently. This feature can give you good hints that the things you are writing are correct or incorrect.

Running and debugging world scripts

Scripts are run automatically at world start. In case of any runtime errors, an error message will be printed out after which the stack trace will follow. From stack trace you can find out which function from which world script (or file) called other function and so on (so you can determine the cause of error). Script entities and files in error and in stack trace are identified in square brackets. Let's take a look at an example stack traceback:

Lua error: D:\work\main\Sources\ScriptLua\LuaScheduler.lua:656: Invalid handler filter (1) passed to RunHandled! (Did you forget to use On or OnEvery?)
Lua stack traceback:
    [C]: in function assert
    D:\work\main\Sources\ScriptLua\LuaScheduler.lua:656: in function RunHandled
    [Script entity id = 255 (ErrorScript)]:14: in main chunk

From the above example stack trace we can read the following: 1. Error was reported in file D:\work\main\Sources\ScriptLua\LuaScheduler.lua, on line 656 and the error message is "Invalid handler filter (1) passed to RunHandled! (Did you forget to use On or OnEvery?)" 1. Error was reported using a C function 'assert' (note the [C] in) 1. assert function was called from file D:\work\main\Sources\ScriptLua\LuaScheduler.lua on line 656 1. Function 'RunHandled' was called from script entity with an id = 255 (named 'ErrorScript') on line 14 of main chunk. The previous stack traceback is very useful since the bottom of the call stack describes the real cause of the error - script entity with id = 255 on line 14 - and the error message explains us what is wrong - we forgot to use ''On'' or ''OnEvery'' in ''RunHandled''.

Constructs used in world scripts

World scripts written in Lua are valid Lua scripts, but with some additions. Croteam.Here we will cover functions and variables which are specially supported by Serious Engine for use in world scripts. We will describe how to wait on delays, events, how to handle specific events, how to access entity variables, how to hint types on variables, etc.

Variables in world scripts

There are a couple classes of variables which can be used in world scripts. You can use local variables anywhere in scripts, you can use entity variables, you can use the special variable thisScript, and you can create and access member variables in special table worldGlobals. We will describe all the variable types in detail. Note that classic Lua global variables are not allowed in world scripts.

Local variables

When we say local variables, we refere to Lua variables which are declared using the local keyword. For example:

-- declaration of the local variable 'puppet'
local puppet

There are a couple uses for local variables in Lua scripts. With local variables, you can even make variables which are global for the entire scope of the world script (if you declare them at the beginning of the world script in main chunk), in that case they are not so local after all. For example

-- beginning of the world script
local puppet

local function PlayHello()
  puppet:PlayAnim("Hello") -- here we are using the variable puppet declared at the begining of the script
end

local function Die()
  puppet:DropDead() -- here we are using the variable puppet declared at the begining of the script
end

local function MyPuppet()
  local puppet = {}  -- here we have created a variable puppet local to this function
  print(puppet) -- here we are using the variable puppet local to this function
end

As we can see from the above example, local variables in world scripts are just normal Lua local variables. Also note that in previous example we have also created local functions, that is local variables which hold functions. If you try to create a non local variable (also aplies to functions since function declarations are nothing more than variables to which the function is assigned) you will get an error in run-time. For example:

-- trying to write to a global variable
puppet = GetPuppet() -- will produce a run-time error: "Script tried to write to new global variable..."

function Func()  -- will produce a run-time error: "Script tried to write to new global variable..."
  return 1
end

When using local variables, you should remember to make their scope as narrow as possible (if you need the variable only in one block, declare it in that block, just when you need it). Also remebmer to use one variable for one purpose - do not reuse the same variable for different things. This generally leads to better code which contains less errors.


Entity variables

Entity variables can be created by writing a variable name in the script editor onto which are dragged and dropped (holding ctrl+alt+shift) the entities from world. Entity variables expose member functions (methods) and events. You can call member functions on entity variables and you can wait for events on entity variables. When you call member functions on entity variables, you must use the colon (:) operator (entityVariable:MemberFunction()). When you index events you must use the dot (.) operator (entityVariable.SomeEvent). For example:

-- calling SpawnSimple method on variable spawner (which is a spawner entity)
spawner:SpawnSimple() -- note the colon (:) operator!

-- waiting for event "SpawneeAvailable" on variable spawner
Wait(Event(spawner.SpawneeAvailable)) -- note the dot (.) operator!

Note that you can use auto complete functionality on entity variables by pressing ctrl+space after you have written the variable and the colon (you will get methods) or dot operator (you will get events).

You can also add new member variables to entity variables (like on any other table objects). The dot (.) operator is used for member variable access. When you add new members (which can hold anything, even functions) to entity variables, these members are attached to the entity which the entity variable holds. These new members can also be accessed from all other scripts of the same world, even if they have the entity variable by another name, as long as that entity variable references the same entity.

For example, we have a spawner entity (let's say his id is 242) and script one has an entity variable which references that spawner (variable name civilianSpawner). Script two also references that spawner, but under a different name (for illustration purposes only - this is definitely not a good practice), let's say spawner01. When script one creates a new member variable, the other script can access it (and of course any other scripts which reference the same entity). This new member exists as long as that entity exists in the world (in this case entity with id 242), providing that no one removes the member (by setting the member to nil).

-- script one:
-- adding a new boolean member "alreadySpawned" with value "true" on variable civilianSpawner
civilianSpawner.alreadySpawned = true

-- script two:
-- accessing the new boolean member
print(spawner01.alreadySpawned) -- will print 'true' in case that script one executed the code before this statement

Note: when adding new member variables, you should watch out that there are no members or events with the same name. If you follow the naming conventions for variable names, you should not get into trouble because variables should start with a lowercase letter and methods and events start with uppercase letters.

Groups of entity variables

You can drag and drop an entire group of entities into a variable in script editor and create a group entity variable. Group of entities variable is in fact a Lua array which contains all of the variables. You can iterate through the array or index them the same way as any other Lua array. For example:

-- let staticModels be a group variable containing 3 static models

-- print the number of contained variables
print(#staticModels) -- should print 3

-- make the first static model disappear
staticModels[1]:Dissapear()

-- make the third static model appear
staticModels[3]:Appear()

-- make all contained static models appear
for i, staticModel in ipairs(staticModels) do
  staticModel:Appear()
end


-- try to access the fourth static model (which doesn't exist)
staticModels[4]:Appear() -- will result in a run-time error: "Trying to index a nil value..."

It is a good idea to name the variable containing group of entities in plural (like the above variable staticModels). This way it is clear that the variable contains more than one value.


The thisScript variable

Variable thisScript is a special script variable that holds the script entity in which the script is executing. It is in fact an entity variable, but it exists implicitly (no need to drag and drop it like other entity variables). Main use for thisScript variable is to create functions which can be called from other script entities. For example:

-- script entity 1
-- we will create a function which can be called from other scripts
function thisScript:Func()
  print("you called Func in script entity 1")
end


-- script entity 2
-- scriptEntity1 is an entity variable corresponding to script 1
scriptEntity1:Func()

Other use for thisScript variable could be for sending events and waiting on them.

The worldGlobals variable

Variable worldGlobals contains a table of variables which are global to the all script entities in the world. Inside it you should place only the variables which are used from more than one script entity (and those variables can contain any Lua type: integers, booleans, entity variables, functions, strings, etc.). For example:

-- script entity 1
-- create a new boolean variable in worldGlobals and set it to true
worldGlobals.scriptEntity1Ready = true


-- script entity 2
-- see if script entity 1 is ready
if worldGlobals.scriptEntity1Ready then
  print("script entity 1 is ready")
end

Wait and WaitForever functions

Wait is a special function which blocks the execution of the code which called it until some condition is fulfilled. By condition, we mean an event or a timed delay. Waiting on events and timed delays will be explained in the following chapter.

WaitForever function will block the code which called it 'forever'. By forever, we mean until the world is stopped. Main use of WaitForever is in RunHandled construct.

Events

Events are very important in world scripts, using events, you can block execution of a function until that event is signaled and you can execute special handler functions in response to some event (more about that in the chapter about RunHandled construct). A simplest event can be identified only by a string which is the event name. There can also be events which are bound to some object. Events can be signaled from game entities, but can also be signaled using the SignalEvent function. There are predefined events which are signaled from game entities (with them we use the Event function) and you can create as many events as you like, just by giving them new names (we use CustomEvent function with them). It is also possible to associate some value with the event and obtain that value when the event is caught by waiting code. We will cover the use of events through a couple of examples:

Example 1: How to wait on an event using Wait function. Note the different syntax used for custom events and predefined events.

-- wait until custom event named "Event1" is signaled
Wait(CustomEvent("Event1"))

-- wait until predefined event "DesiredPositionReached" on puppet object is signaled
Wait(Event(puppet.DesiredPositionReached))

-- wait until custom event "Event1" on puppet object is signaled
Wait(CustomEvent(puppet, "Event1"))

Example 2: How to signal custom events. (Note: you can signal the predefined events on objects the same way, but you should probably avoid that.)

-- signal event "Event1"
SignalEvent("Event1")

-- signal event "Event1" on puppet object
SignalEvent(puppet, "Event1")

-- [1] signal the event "Event1", giving the event payload (a table)
SignalEvent("Event1", {everythingOk = true, sentFrom = thisScript, currentDay = "Monday"})

-- [2] signal the event "Event1" on object puppet, giving the event payload (a number)
SignalEvent(puppet, "Event1", 42)

Note that waiting for an event "Event1" on some object is different than waiting for an event "Event1" on some other object which is also different than waiting on an event "Event1" with no object specified: CustomEvent("Event1") ~= CustomEvent(puppet1, "Event1") ~= CustomEvent(puppet2, "Event1")

Example 3: How to wait on events and obtain the payload. Note that when there is no special payload, the payload is simply a boolean value true.

-- wait for predefined event "SpawneeAvailable" on object spawner (of type CSpawnerEntity) 
-- and obtain the associated object (of type CSpawneeAvailableScriptEvent)
local spawneeAvailableEventObject = Wait(Event(spawner.SpawneeAvailable))
local spawnedPuppet = spawneeAvailableEventObject:GetSpawnedEntity() -- here we have obtained the spawned puppet

-- wait for custom event "Event1" and obtain the associated value (a table)
-- (we will assume that the event was signaled using the SignalEvent below [1] from previous example)
local eventObject = Wait(CustomEvent("Event1"))
print("Everything OK: ", eventObject.everythingOk, " Sent from: ", eventObject.sentFrom, " Current day: ", eventObject.currentDay)

-- wait for custom event "Event1" on object puppet and obtain the associated value (a number)
-- (we will assume that the event was signaled using the SignalEvent below [2] from previous example)
local eventNumber = Wait(CustomEvent(puppet, "Event1"))
print(eventNumber) -- should print 42

Timed delays

Timed delays are created using a Delay function inside the Wait function or in RunHandled construct. Timed delays are like events in the sense that they get signaled, but differ from them in the fact that they are signaled automatically when the specified delay time passes. Delay function takes a single positive number as input parameter and that number specifies the delay time in seconds. For example:

-- block execution for 3 seconds
Wait(Delay(3))

-- block execution for 0.1 seconds
Wait(Delay(0.1))

Composite filters

From now on we will refer to anything that can be waited upon as "filter"; events and delays are both referred to as filters. Sometimes we need to wait for multiple filters at the same time. For that purpose we use the composite filters All, Any and Times which are described below.

All

When we want to wait until all of the filters in some set are signaled, we use the All filter function. All can contain two or more filters (each of these filters can also be a composite filter). We will describe this concept using the following examples:

-- wait until both "Event1" and "Event2" are signaled
Wait(All(CustomEvent("Event1"), CustomEvent("Event2")))

-- wait until event "Event1" is signaled and 3 seconds have passed
Wait(All(CustomEvent("Event1"), Delay(3)))

Any

When we want to wait until at least one of the filters in some set is signaled, we use the Any filter function. Any can contain two or more filters (each of these filters can also be a composite filter). Again, we will describe the concept of Any filter on a few examples:

-- wait until "Event1" is signaled, or "Event2" is signaled, or 3 seconds have passed
Wait(Any(CustomEvent("Event1"), CustomEvent("Event2"), Delay(3)))

-- wait until "SpawneeAvailable" on object spawner is signaled, or "Event1" is signaled, or 3 seconds have passed
Wait(Any(Event(spawner.SpawneeAvailable), CustomEvent("Event2"), Delay(3)))

Times

Times is not a composite filter like All and Any, but we consider him composite since it can contain one other filter. It makes sense to use only event filters in Times filter. Times filter function accepts two parameters, a number greater than 1 and a valid event filter. Number parameter specifies how many times must the event filter be signaled before the entire Times filter is signaled. Example:

-- block execution until "Event1" is signaled two times
Wait(Times(2, CustomEvent("Event1"))

-- block execution until "OneSpawned" event on object spawner is signaled 10 times
Wait(Times(10, Event(spawner.OneSpawned))

Events on group variables

There is a special way for waiting on events on group variables (group of entities in a single variable). In order to be able to wait on an event on a group variables, all objects in that group must share a common type (for example: all entities in group must be puppets, or all entities must be static models). Let's illustrate this concept on a few examples.

Example 1: Group variable
spawners
contains three spawners. We want to wait until all puppets on all spawners are killed
-- wait until all puppets of all spawners are killed
Wait(All(Events(spawners.AllKilled)))

Note the difference in syntax - we are using the function __Events__ instead of _Event_ because we are dealing with a group variable _spawners_. The more complicated (but also valid) way to specify the same condition is this (this is given only to illustrate things - you shouldn't be using it):

-- wait until all puppets of all spawners are killed
Wait(All(Event(spawners[1].AllKilled), Event(spawners[2].AllKilled), Event(spawners[3].AllKilled)))

Note that these two ways of waiting on events are logically equivalent, but the first one is easier to write and less error prone, so you should be using the first way of waiting on events on a group of variable.

Example 2: Group variable
spawners
contains three spawners. We want to wait until all puppets on any (at least one) spawner in the group are killed.
-- wait until all puppets on at least one spawner are killed
Wait(Any(Events(spawners.AllKilled)))

Again, this is equivalent to more complicated way of specifying the wait condition:

-- wait until all puppets on at least one spawner are killed
Wait(Any(Event(spawners[1].AllKilled), Event(spawners[2].AllKilled), Event(spawners[3].AllKilled)))


Writing complex composite filter

Croteam.Here we will illustrate on a couple of examples how to write arbitrary complex composite filters.

Example 1: Wait until "Event1" is signaled, and "Event2" is signaled, and "Event3" is signaled or "Event4" is signaled 3 times or 20 seconds have passed.

Wait(All(CustomEvent("Event1"), CustomEvent("Event2"), 
            Any(CustomEvent("Event3"), Times(3, "Event4"), Delay(20))))

This complex composite filter will be signaled when both "Event1" and "Event2" are signaled and one of: "Event3" or "Event4" are signaled or 20 seconds have passed.

Handling events and the RunHandled construct

Until now we have only considered blocking execution of a single thread of code. There are more complex problems which require more complex approach to event handling. We address this using the RunHandled construct. For example, we would like to execute some function when one event occurs and the other when the other event occurs. This is not possible using just Wait in combination with events and delays. RunHandled construct makes this possible.

General form of RunHandled construct is the following:

RunHandled(
  function() -- handled function
    -- this is the body of the handled function
  end, -- note the comma operator(,) after end!
  On(<filter1>), -- On + filter: executed once when filter is signaled (note the comma operator again)
    function() -- 
      -- handler function for filter1
    end, -- note the comma operator!
  OnEvery(<filter2>), -- OnEvery + filter: executed every time when filter is signaled (note the comma operator!)
    function()
      -- handler function for filter2
    end,
  ...  
)

Let's explain this a bit. RunHandled is a function which takes the handled function as the first parameter and one or more pairs of filters and handler functions. Handled function is the 'main' function of the RunHandled construct, while the code of handled function is running, we can handle the events specified in handler filters. Handlers are specified using handler filters and handler functions which are executed when the handler filter is signaled. There are two types of handler filters: On and OnEvery. Both On and OnEvery accept other filters like event, delay and composite filters. On is used when we want the handler function to be executed exactly once, when the contained filter is signaled. OnEvery is used when we wan to execute the handler function each time the contained filter is executed.

Let's give a few examples:

Example 1: Handled function will just wait forever, and we will handle every "SpawneeAvailable" event on spawner. Once, after 2 seconds we will print some message. We will print another message every 5 seconds.

RunHandled(
  function()      -- handled function
    WaitForever()
  end,
  OnEvery(Event(spawner.SpawneeAvailable)), -- handler 1
    function()
      print("One spawnee became available")
    end,
  On(Delay(2)), -- handler 2
    function()
      print("Delay 2")
    end,
  OnEvery(Delay(5)), -- handler 3
    function()
      print("Delay 5")
    end,
)

Example 2: Handled function will finish when "EndChapter" event is signaled on object chapter1. We will handle every "SpawneeAvailable" event on object spawner. We will handle once when both custom events "Event1" and "Event2" are signaled. We will handle every time when one of the events "Event3" and "Event4" is signaled. We will handle every 5th signaling of "Event5".

RunHandled(
  function()
    print("Handled started")
    Wait(CustomEvent(chapter1, "EndChapter"))
  end,
  OnEvery(Event(spawner.SpawneeAvailable)),
    function()
      print("One spawnee became available")
    end,
  On(All(CustomEvent("Event1"), CustomEvent("Event2"))),
    function()
      print("Both 'Event1' and 'Event2' were signaled")
    end,
  OnEvery(Any(CustomEvent("Event3"), CustomEvent("Event4"), Delay(10))),
    function()
      print("Either 'Event3' or 'Event4' was signaled, or 10 seconds have passed")
    end,
  OnEvery(Times(5, CustomEvent("Event5"))),
    function()
      print("'Event5' got signaled five times")
    end
)

Obtaining event payload in handler functions

It is possible to obtain event payload value in handler functions, by accepting a parameter in the handler function. This parameter receives the same value as when using the Wait function (see earlier chapter on Events). Let's illustrate this on an example:

RunHandled(
  function()
    WaitForever()
  end,
  OnEvery(Event(spawner.SpawneeAvailable)),
    function(eventPayload)   -- receive event payload into the variable eventPayload
      local spawnedPuppet = eventPayload:GetSpawnedEntity() -- retrieve the spawned puppet from spawner
      spawnedPuppet:PlayAnim("Idle")
    end
)

Obtaining event payload with composite filters

With the obtained payload, it is possible to read which event has been fulfilled. Payload of an Any composite filter has a table member called any, from which we can read event status(if payload.any[1] ~= nil then). If it has any value, or in other words if it's not nil, it means that this event has been fulfilled.

...
On(Any(Event(detector1.Activated), Event(detector2.Activated))), 							
    function(payload)  -- obtaining payload
      if payload.any[1] ~= nil then -- checking status of first event we provided to Any function
        print("First Detector Has Been Activated")
      end
      if payload.any[2] ~= nil then -- checking status of second event we provided to Any function
        print("Second Detector Has Been Activated")
      end
    end
...

payload.any[1] holds the same value as payload for single On(Event())


In another example we did a similar thing and tried to see which event in both Any composite filters have been fulfilled first. By writing if payload.all[1].any[1] then we are checking the first event in All composite filter and first event in Any composite filter.

...
On(All(
    Any(Event(detector1.Activated), Event(detector2.Activated)),   -- = payload.all[1]
    Any(Event(detector3.Activated), Event(detector4.Activated)))), -- = payload.all[2]
  function (payload) -- payload now has a table of _All_ composite filters 
    if payload.all[1].any[1] then 
      print("detector1 was activated")
    elseif payload.all[1].any[2] then
      print("detector2 was activated")
    end
    
    if payload.all[2].any[1] then
      print("detector3 was activated")
    elseif payload.all[2].any[2] then
      print("detector4 was activated")
    end
  end
...
Finding the signaled payload in Any composite filter

It is often interesting to find the event payload which caused Any composite filter to become signaled. For this purpose you can use the signaled member of any. For example:

...
OnEvery(Any(Events(spw_kleer_Circle.SpawneeAvailable))),
    function(eventPayload)
      -- note how we need to write eventPayload.any.signaled since eventPayload is for Any composite filter
      local kleer_Circle_Spawnee = eventPayload.any.signaled:GetSpawnedEntity()
      kleer_Circle_Group[i] = kleer_Circle_Spawnee
      i=i+1
    end

...

You can also find out index of a signal sender inside the group variable like this:

...
    local iSignalSenderIndex = eventPayload.any.signaledIndex
...