Tomb Raider Forums  

Go Back   Tomb Raider Forums > Tomb Raider Level Editor and Modding > Tomb Raider Level Editor > Tutorials and Resources

Closed Thread
 
Thread Tools
Old 26-08-24, 16:55   #1
AkyV
Moderator
 
Joined: Dec 2011
Posts: 5,074
Default TEN - Script elements and operations (basics)

Made using Tomb Editor 1.7.1. pack (including Tomb Engine 1.4.)
Last update to TE/TEN: 1.7.2./1.5.


In the tutorial about the script files I started a sequence about introducing the basics of the so-called Lua scripting: i.e. Lua scripting techniques.
Lua scripting is the method how the script is made for TEN.

The second tutorial of this sequence is about the things which I found the most important script element for a TEN level builder: functions.

This current tutorial is the third part about scripting techniques in TEN. It is about the basic script elements and operations.

CONTENTS:

1. Placing script elements
2. Variables and variable values
3. Basic mathematical operations
4. Checking conditions
5. Logical operations

----------

Notes:
  • As the title says, it is “only” a Tomb Engine (TEN) tutorial. Many aspects you need to know for this tutorial are specific Tomb Editor (TE), TombIDE (TIDE), WadTool (WT) etc. features.
    So non-TEN features are only referred now, if it is necessary. And as much as I found it necessary. You need to also meet these features in other tutorials, for further details about them.
  • Some related TEN features are only mentioned, but not discussed in this tutorial, because they belong better to (an)other tutorial(s).
    Find another info source or be patient till those other tutorials are made.
  • Some options or commands can be executed in several ways. I may not introduce all of the ways in the tutorial. (Using default keys are presumed.)
  • Explainig basics about Lua scripting is not the best decision in a precise way when you, the reader, is a beginner TEN builder. That is why I try to interpret Lua scripting mostly with my own words - while I also try to remain authentical, naturally. (Which is not too hard, anyway, because when I am making this tutorial sequence then I am not a Lua expert myself...)
  • Perhaps you understand the Lua basics better in a form of another tutorial.
    Help dropdown menu of Scripting Studio also leads you to this linked tutorial, anyway.

Last edited by AkyV; 04-11-24 at 20:10.
AkyV is online now  
Old 26-08-24, 18:26   #2
AkyV
Moderator
 
Joined: Dec 2011
Posts: 5,074
Default

1. Placing script elements

Even the brand new, still empty level script files have six initial functions (so-called "level callbacks"), which represent different game phases:
  • OnLoad: a one moment long phase, it happens when a savegame has just been loaded.
    (Either loaded from the same level or another one.)
  • OnSave: a one moment long phase, it happens when a savegame has just been saved.
    You will realize this phase only when the savegame menu has just been closed, after the saving. (Or, if you started the saving from the inventory, then the inventory menu naturally also needs to be closed now.)
  • OnStart: a one moment long phase, it happens when the level starts from the beginning.
    It naturally also happens when the level restarts, i.e. when Lara comes back to a previous level. (Even if it is an alternative point of the restart at a LARA_START_POS.)
  • OnLoop: this phase has several moments, i.e. it includes all the moments of the effective playtime. That is why we call it loop, the loop of the game: because the phase is repeated again and again, in each moment once while the level is being played in the game.
    The effective playtime naturally doesn't include the time when the menus are open, causing the game is paused.
  • OnEnd: a one moment long phase. I mean, the game detects the end of the level (eg. in a FinishLevel trigger), then it starts loading, eg. another level. In the moment when the level end is detected, this phase happens.
  • OnUseItem: a one moment long phase, it happens when an item has been chosen in the inventory. (Even if that item doesn't do anything: eg. you chose a medipack when Lara is fully healthy.)
    You will realize this phase only when the inventory has just been closed.
  • Out of any callbacks, but in the level script files: a one moment long phase. It happens when the level starts from the beginning or when a savegame has just been loaded. (However, technically it happens one moment earlier than either OnStart or OnLoad.)
The script elements and operations naturally need to be placed in the phase where they will be done (see examples in that "functions" tutorial):
  • The one moment long phases need to be considered this way:

    • Variable definitions are mostly simple, like X = 6. These definitions will be easily done in this moment.
      On the other hand, definitions sometimes could be more complex. Eg. Y = A + 6 (when A could have different values during the playtime), or eg. "if Z variable value is the current animation index of Lara". In these cases Y and Z values could have obviously only the current A/animation index values of this moment.
    • Function definitions (like FunctionX = function(A, B, C)) are done easily in this moment.
    • Function calls (like FunctionX(3, 2, 4)) will happen in this moment. But if any function argument or any part of the function contents are not constant, then the function will run/will be checked only with the current argument values/current function contents values of this moment.
    • If the running function is called (i.e. started running) in this moment, then basically it runs only in this moment.
      However, if the function has some timer, then the function and the timer both start running in this moment. When the timer expires, the function stops running. (I say that only for in-build timers of preset functions. Your custom timers will be discussed in another tutorial.)
    • If the function is checked in this moment, then it will be naturally checked only for the value which the function has in this moment.
    • It is meaningless to call functions in the moment of OnEnd: because in the next moment the game loads you out of this level, so the function can't have time enough to do what it should do. (Probably the function should have at least one more moment before the loading starts, to be able to do anything.)
      It is also meaningless if you come back later to this level, to see what happened in that moment.
    • The only one moment of these phases actually can be more than one moments. I mean, eg. a moment when a savegame has been loaded can happen any time when you load a savegame during playing that level.
      If you don't want to use all of the moments of a phase, then you can narrow the amount of these calls/checks in some ways. - Eg. if it is a function called in the volume, then the volume has a "Call count" value. (Further narrowing techniques will be introduced in other tutorials.)

  • The several moments long Loop phase needs to be considered this way:

    • You can do that, but it is better not to type operations (variable or function definitions, function calls) here which do not need to call more than once in the level.
      Eg. in the X = 6 operations the loop defines in the first moment of the playtime (i.e. actually when the level starts) that the value of X is 6, it doesn't need to define it again and again, in the further moments of the game loop.
      Or, if the loop calls a function in the first moment of the playtime which ignites a specific flame, then naturally you don't need to try to ignite the flame again and again, in the further moments of the game loop. (However, it is not fully meaningless: I mean, if you try to antitrigger this flame, any time during the playtime, then you can't do it, because the loop re-ignites it immediately, so it looks a continuous flame in the game - i.e. it is an interesting technique to ignore some triggers.)
      However, naturally usually you don't want to use the loop to do these simple operations when the level starts. (I.e. OnStart is used for that.) Fortunately, there are methods to do these operations only in a specific moment of the playtime: if some condition is true (including if some timer reaches a specific moment of the playtime). - But those methods won't be discussed in this chapter (or in this tutorial).
      (That condition still could be true for a longer time, so the operation will be called unnecessarily more than once. - Later, in another tutorial, I will tell how you can do some "one shot" for these conditions.)
    • This game phase is the only real place to type continuous operations (continuous variable definitions, continuous function calls/checks) in the level script.
      A continuous operation here will run during the whole effective playtime - except if you set some conditions to start/stop it when you want.
      ("The only real place" means see above how continuous operations work in one moment long phases.)
    • A continuous variable definition typed here can define more than one values for the variable.
      For example, if Z variable value is the current animation index of Lara, then Z variable will always store the animation index which Lara just has in the current moment of the playtime.
    • I call a function continuous here, if at least one function argument and/or at least one part of the function contents are not constant. Now the function can have more than one results during the playtime, while the value of that argument/part is changing. (Either the function executes something or checks a value.)
      For example, if you have a function to print the value of an argument on the screen, but you change the argument value during the playtime, then always the current value of the argument will be printed.
    • The in-build timers of the preset functions will be frozen at the first moment of the timer if you run that function in OnLoop. So the function seems working "forever". The timer starts only when you stop the function with some condition.
    • Call count of a volume is not really useful to narrow the working of an OnLoop function. Eg. Call count = 1 definitely means "only in the moment when the level starts" - and, as I said, OnLoop usually doesn't used to control things at the level start.
      However, as I also said, there are conditions to narrow down when a function works in OnLoop.
Further informations to this:
  • Don't forget: the game always reads the script as the script lines are typed, from top to bottom. So always type in the upper position which you want to happen first (or which needs to happen first), and type below that, in the lower position, which you want to happen after that (or which needs to happen later). For example, you can run a function only after defining its contents.
  • Whatever you type in a callback, that will be fully read once at each moment of that callback.
    So, for example, if 89 script lines are typed in OnLoad callback, all the 89 lines will be read once, each time when you load a level.
    Or: if 626 script lines are typed in OnLoop callback, all the 626 lines will be read once, each moment of the level playtime.
  • If you type anything in a callback, that naturally will be detected/called only in that callback - except: see the global variables below.
  • To use the level script files out of the level callbacks:

    • Whatever you type here, that will be valid in all the level callbacks. So if you want to define a variable or a function, which is used in all (or, at least, more than one) callbacks of OnStart, OnEnd, OnSave, OnLoad, OnLoop, OnUseItem, then that definition should be typed here.
      (Which means that those definitions should be typed before the game reads those callbacks - i.e. typed above the callbacks.)
    • As I said in the function tutorial I linked above: if you call the function in a volume, then the best place for typing the definition of the function is here. - Probably below all the callbacks.
      (Remember: functions called in volumes don't need to be called in a level callback in the script - even if the function in the volume is placed in a game phase which has the same name as a script callback.
      You can read more about game phases in volumes here and here).

  • To OnEnd callback:

    • Be careful, because OnEnd actually is more than completing a level at a FinishLevel trigger. I mean when you leave the level in one of these ways, those will be all considered as level ends:

      • FinishLevel trigger (or a similar volume/scripted event) loads to another level.
      • Loading a savegame, saved previously in another level.
      • FinishLevel trigger (or a similar volume/scripted event) loads back to the title.
      • The player loads back to the title, from Pause menu.
      • Lara's death loads back to the title.

      If you don't want OnEnd to serve all of these situations, then you need to narrow down the callback - with a condition that checks EndReason constants. These are the constants for OnEnd situations: EXITTOTITLE, LEVELCOMPLETE, LOADGAME, DEATH, OTHER. (Obviously OTHER is something specific, which experts can use.)
      Name any argument for OnEnd (name it as you wish), and check its value. This value will be the value of EndReason - for example:

      Code:
      -- FILE: Levels\My_level.lua
      
      LevelFuncs.OnLoad = function() end
      LevelFuncs.OnSave = function() end
      LevelFuncs.OnStart = function() end
      LevelFuncs.OnLoop = function() end
      LevelFuncs.OnEnd = function(levelend)
      
      (....)
      
      if levelend == EndReason.LEVELCOMPLETE then
      (...)
      
      end
      This means the thing you type where we see (...) now will happen only if you leave the level with a FinishLevel trigger.
      You can type further checks in OnEnd (eg. "if levelend == EndReason.DEATH then"), to execute somehing else in other OnEnd situations. (But if you type something in OnEnd now, where an EndReason constant isn't checked - see where (....) is typed - that naturally can be executed in all the OnEnd situations. - So you can ignore now that the callback has an argument.)
    • As I said, there is no time to execute a function in OnEnd callback, because the level will be left immediately by loading.
      However, that doesn't mean that you cannot use functions in this situation. I mean, for example, if you want to run a function when Lara dies, but you want to see the function result before the loading starts, then you need to place conditions in OnLoop callback, to check if Lara is performing a specific frame of her dying animation. If she is, then the function starts - so you will have some moments to see the function result before the game loads you back into the title.
    • Probably your opinion is that OnEnd callback seems useless to a casual builder - even if they have skills enough to make TEN scripting.
      That is only partially true. I mean, you can set values in variables in the moment when OnEnd happens - so eg. that variable value will be transferred into the level whereto the FinishLevel trigger here will load you now.
      (In the next chapter we will discuss which variables are useable to be transferred into another level.)
----------

Notes:
  • You can't find OnUseItem callback in the examples of the tutorial, because those are made with a previous version of TE.
  • I mentioned custom timers above. A complex form of them is the same what we call "Organizers" in TRNG.
    "Casual" custom timers and "organizers" are also available in TEN - but we won't discuss them in this tutorial, as I said.
  • The OnLoop callback can have a so-called "delta time" argument which we don't discuss in this tutorial, only later.
  • Bad news that EndReason constants seem buggy.
    I mean, I told above that OnEnd is surely useful to transfer variable values which are set in OnEnd. So I tested EndReason constants to transfer a variable value, and my test results say that LOADGAME situations seemingly don't have an effect, and whichever way I choose to go back to the title, the variable value won't be transferred there.
    My suggestion is that till these bugs are fixed, use only "if argument is LEVELCOMPLETE" check, because that works. (Technically it is unnecessary, though. I mean, as you can see, for the time being the callback works only if you complete the level, so you don't need to narrow the callback down to that.)
  • As you can see, TEN script constants have to be used in a "constant_name.constant_value" way, for example: EndReason.LEVELCOMPLETE.
    However, TEN script constants are placed in the same master table/module structure like the classes or the functions. EndReason constant belongs to Logic module of TEN master table. As you already know, functions directly available in modules can be called simply by the function name, or also naming the class, or also naming the class and the master table.
    That is why it is also true for the constants. I.e. eg. this LEVELCOMPLETE constant value can be used in any of these ways:

    EndReason.LEVELCOMPLETE
    Logic.EndReason.LEVELCOMPLETE
    TEN.Logic.EndReason.LEVELCOMPLETE


  • In the case of volume (local) event sets they have enter (one moment), inside (more moments) and leave (one moment) trigger phases. Naturally functions called in them act like calling the functions in one moment/more moments long game phases.

Last edited by AkyV; 04-11-24 at 20:21.
AkyV is online now  
Old 02-09-24, 19:47   #3
AkyV
Moderator
 
Joined: Dec 2011
Posts: 5,074
Default

2. Variables and variable values

Variables are simple textual names to your eyes, they don't have any graphical form.
The purpose of the variables is to store some value. (I.e. the value is assigned to the textual variable name.)
You can use the value stored in the variable, anywhere in your game, if you simply refer to the textual variable name.

If you are already an advanced or expert TRNG builder, then you surely used some variables there previously. - Let's sum up the most important things about the TRNG variables:
  • You need triggers to set a value for a variable: one of the trigger parameter is the variable itself, and the other trigger parameter is that value.
  • All the variables initially exist. I.e. they are all there, even if you use only one or a few of them. Or even if you don't use them at all.
    So you can choose one of the existing variables if you need a variable, you can't create a custom variable. So if you are just using all the existing variables, then you can't use more variables - except if you first stop using one of these variables for something else.
  • Variable names are constant, you can't change them.
    The name refers to the general properties of the variable, like eg. "Local", "Delta" or "Short".
  • Variable values could be textual or numerical values. Some variables are able to handle only texts, while all the other variables are able to handle only numbers.
  • The numerical variable values could be either positive or negative, but always integers.
  • None of the variables have a preset task. Feel free to use a textual variable for any textual task or a numerical variable for any numerical task.
  • There are some special numerical variables, but the most of them belong to a variable group.
    There are six variable groups: Local Alfa, Local Beta, Local Delta, Global Alfa, Global Beta, Global Delta:

    • Each group has seven variables. Basically you can choose freely a variable in any group to use, but if you already chose a variable of a group, then perhaps you can't choose another variable of the same group, because the two variables may interfere with each other.
    • Local variables keep their values only in their levels. (I.e. each level has its own Local Alfa/Beta/Delta groups.) On the other hand, global variables keep their values in the whole game. (I.e. there is only one Global Alfa/Beta/Delta group in each game.)

  • The numerical variables have different sizes: "byte" variables can store only tiny numbers, "short" variable can store even bigger numbers, and "long" variables can store even really huge numbers.
  • You can set directly a value in a variable. (Eg. "The variable value is 6.")
    Or there are the so-called "memory zone fields". The game automatically stores different values in these fields - eg. the current amount of big medipacks in the inventory. These fields are also existing initially. You can easily identify any field, because each of them has a specific name, referring to its task.
    What you can do is to copy the current value of this field into a variable, to set the value of the variable.
  • You can do mathematical operations to change the current value of a variable.
    Or you can also check this value eg. if it is bigger than the current value of another variable.
  • There is a plugin which extends the amount of variables. You can use these "new variables" in a more flexible way, you don't need to follow the strict rules of the "casual" variables for this.
  • There are further variables in the engine code, but we don't need to care about them.
And now let's see that how the same things work in TEN:
  • Values stored in the variables can be set with trigger parameters. But this time these triggers are nodes in volumes. (Later, in another tutorial, we will discuss these nodes.)
    On the other hand, you can set variables directly in the script: first you type the variable name, then you type an equals sign, then you type the value of the variable, eg. X = 6.
  • There are no preset variables in TEN. So none of the variables are existing when you start building your level. I.e. if you need a variable, then first you need to create it.
    This creation is very easy: the variable will be created in the moment when you set its value. I.e. when you set a variable value in a node, or when you type in the script that X = 6, then in that moment that variable has been created. - I.e. feel free to have as many custom variables as you wish.
    If you don't need a custom variable any more, then you can delete it. (You can also use nodes for it. Or, for further methods, see later in the chapter how.)
  • Feel free to find out any name for a custom variable, when you create it. (I suggest names which tell nicely what that variable is for.) However, I recommend to examine here, that how developers named preset function arguments, and try to follow the same naming patterns for your variable names.
  • Variable values could be still textual or numerical values. (Or they can also have a special value: true or false.)
    A variable still can handle only one value type. The type (i.e. text, number, true/false) will be declared in the moment when you set the value for the variable. So eg. if you type 6 after "X =", then it is obvious that it is a variable to store a number.
  • The numerical variable values could be either positive or negative - but this time it is not integer everway, it can be a floating number. (You don't need to declare that a custom variable is integer or floating. So eg. when X variable value is 6, and you want a floating value for the variable, then just type in a new script row eg. that X = 9.12, to change X value to this.)
    According to my experiences, the amount of digits after the decimal point could be even high.
  • Feel free to define any task for your custom variables.
  • TEN variables can also be groupped - but these are custom groups, i.e. you can make groups only if you wish. (The method is just like at the custom function groups: declare the group name with a "group_name = {}" script line, so it can be followed by a "group_name.variable_name =" script line for a variable in this group.)
  • TEN variables will never interfere with each other.
  • TEN variables are also local or global - see later in the chapter that what those mean in this engine.
  • TEN variables don't need to declare their size and they have no size limits. So eg. a numerical variable value can be even very-very huge. (However, there are some limits in the amount of digits you use, if you want to print this value nicely on the screen. - But this limit is still very huge, so you don't need to worry about it.)
  • The value of a variable naturally could be also set in a direct way in TEN.
    On the other hand, we don't talk about memory zone fields in TEN. But there is something similar here. I mean, you could see in that "functions" tutorial that a variable value can be set as the result of a function.
  • Naturally in TEN you can also do mathematical operations on variables, or you can also check them here.
  • There is no "variable extension" in TEN. (You have so many possibilities, that you don't need an extension, anyway. Besides, TEN does not have any plugins.)
  • There are further variables even in the TEN engine code (also including as preset function contents), but we still don't need to care about them.
Let's see some examples to set (i.e. define) the value of "X" variable in TEN script:

X = 6

The variable value is a positive integer number.

X = -122

The variable value is a negative integer number.

X = 52.487

The variable value is a positive floating number. (Note that that is a decimal point, not a comma.)

X = Y

The value of Y variable will be copy/pasted to be the value of X variable.

X = X + 1

The variable value is bigger with 1 than the previous value of the same variable.

X = true

The variable value is a boolean (true or false).

X = OCBValue == 1

The variable value is "true", if the value of OCBValue variable is 1, or "false", if the value of OCBValue variable is not 1.

X = "Hello, Lara!"

The variable value is some text. (Note the quotation marks.) The text don't need to be repeated in any strings LUA file.

X = flame:GetOCB()

The variable value is the result of a function.

X = TEN.Objects.GetMoveableByName("flame_emitter2_117" )

The variable value is an object - as the result of a function, anyway.

X = nil

If a variable can't have a valid value, then the variable will be deleted automatically. - For example:

Code:
-- FILE: Levels\My_level.lua

LevelFuncs.OnLoad = function() end
LevelFuncs.OnSave = function() end
LevelFuncs.OnStart = function() end
LevelFuncs.OnLoop = function()
     objectSize = (...) 
     if objectSize < 600 then
          X = 8
     end

     A = X + 46
end
LevelFuncs.OnEnd = function() end
I marked with (...) that objectSize variable means anything now, it doesn't matter. However, objectSize is set inside OnLoop, so its value can be changed, even in each moment of the game loop.
"If" naturally will be checked in each moment of the loop. The "end" inside the loop belongs to "if", which means that all the contents between that "if" and that "end" are something which are checked by the "if". So if objectSize value is less than 600, any time during the loop, then 8 value will be set for X variable - while if it is not true, then won't be any value set for X variable, during the loop.
So, again, the game reads all the OnLoop script lines again and again, in each moment of the game loop. So A = X + 46 line will be also read again and again. If the game reads that line when objectSize is less than 600, then the line will use X=8 variable value, so A will be 8+46=54. However, if the game reads that line when objectSize is equal to or bigger than 600, then the line can't use any X variable value, because X value doesn't exist for this objectSize value, X variable is deleted automatically for this case. - I.e. this is seemingly a bad scripting, variable A value cannot be computed when objectSize >= 600.

"Nil" is not zero, nil means "nothing" - i.e. this is the value of a variable before the creation and after the deletion of the variable.
Forcing this value in a variable is the way to remove the variable manually.

(Note that neither "true", nor "nil" are set in quotation marks for the variable, because these values are technically not texts.)

Text or number

What if you want to print a number on the screen? Eg. the current X value (8 or nil) of the previous example.
Then you can't do it directly. I mean, printing functions can print only texts. So, if you want to print a number on the screen, then you need to interpret it as some text. It will be done for you with "tostring" function:
So eg. if X=8, then tostring will interpret 8 for a printing function as some text:

printX = DisplayString(tostring(X), 250, 250, Color(255,0,0))
ShowString(printX, 10)


But don't misunderstand: "8" or "nil" will be printed, i.e. 8 won't be printed as "eight".

Linking values

If the text you want to print on the screen has some non-constant contents, then you can use the "two dots operation" to link the text parts to each other. - For example (please, note the spaces inside the quotation marks, to also have spaces in the printed text):

A = 5
B = 7
C = 18
D = 100

printtext = DisplayString(tostring(A) .. ", " .. tostring(B) .. " and " .. tostring(C) .. " is less than " .. tostring(D) .. ", but bigger than 3.", 250, 250, Color(255,0,0))
ShowString(printtext, 10)


And now this is what will be printed on the screen:

5, 7 and 18 is less than 100, but bigger than 3.

Local or global variables

In TEN the fact that a variable is global, is not a guarantee that the global variable which you defined in Level A, will be also available in Level B.
I.e. when you say in TEN that a variable is local or global, that has basically a different meaning, compared to TRNG:
  • Local variables are available only in their blocks.
  • Global variables are available anywhere in the level.
I call a "block" now anything in the script, which is closed with an "end" sign. You have already met some blocks like that in TEN: an initial function (i.e. a callback), a "casual" function, an "if" procedure. - In the example below you can find all the three of them:

Code:
-- FILE: Levels\My_level.lua

function PrintA()
     A = 200
     printA = DisplayString(tostring(A), 250, 100, Color(255,0,0))
     ShowString(printA, 10)
end

function PrintAnyNumber(numberany)
     printAny = DisplayString(tostring(numberany), 250, 250, Color(255,0,0))
     ShowString(printAny, 10)
end

LevelFuncs.OnLoad = function() end
LevelFuncs.OnSave = function() end
LevelFuncs.OnStart = function()
     A = 100
     PrintA()

     if A == 100 then
          PrintAnyNumber(A)  
     end

     if A == 200 then
          A = 300
          PrintAnyNumber(A)  
     end     
end
LevelFuncs.OnLoop = function() end
LevelFuncs.OnEnd = function() end
This situation prints two values on the screen, when the level starts: one value in (250, 100) position, and another value in (250, 250) position.
The printed values are based in different variations of A variable:
  • A = 100 is defined in the OnStart callback block.
  • A = 200 is defined in the PrintA function block.
  • A = 300 is defined in the "if A == 200" block.
Values printed on the screen (mixed - i.e. some variables are local and some are global - setups are not examined now):
  • In the case of all A definitions are local: (250, 100) = 200, (250, 250) = 100.
  • In the case of all A definitions are global: (250, 100) = 200, (250, 250) = 300.
Why? Let's see the explanations - first the one for the local cases:
  1. When the level starts, the game first defines that A=100, for all the script lines of OnStart callback.
  2. Then PrintA function starts running. First it defines its own A value for this function block (200), which is why A=100 will be overwritten - but only for this block.
  3. Then the function prints A value in (250, 100) position, so 200 will be printed on the screen.
  4. Then an "if" checks if A=100. This "if" isn't in PrintA block, but it is in OnStart block, so value 100 for OnStart is the valid A value now.
  5. So the condition is true, PrintAnyNumber function will start running. The value of the "numberany" argument of the function is A variable value this time, that is why the function prints 100 on the (250, 250) position of the screen.
  6. Then an "if" checks if A=200. We are still have the A value of OnStart block, which is 100.
  7. So the condition is false, all the condition contents will be ignored. (Including that A=300 definition.)
And now let's see how it works for global variables:
  1. When the level starts, the game first defines that A=100, for the whole level.
  2. Then PrintA function starts running. First it gives another A value (200) for the whole level, which is why A=100 will be overwritten.
  3. Then the function prints A value in (250, 100) position, so 200 will be printed on the screen.
  4. Then an "if" checks if A=100. But the current A value is 200 for the level, so the contents of the condition will be ignored.
  5. Then an "if" checks if A=200. The value is still 200, so the condition is true - which is why a new A value (300) will be defined for the whole level.
  6. PrintAnyNumber function will start running. The value of the "numberany" argument of the function is A variable value this time, that is why the function prints 300 on the (250, 250) position of the screen.
But how will the game know that a variable is local or global? - Well:
  • A variable will be local if you label its definition as "local".
  • A variable will be global if you won't label its definition at all.
So the example above is only true in the case when all the variables are global.
Why? Because none of the variable definitions are labelled there.

So in the case when all the variables are local, all of their definitions should be labelled as "local":

Code:
-- FILE: Levels\My_level.lua

function PrintA()
     local A = 200
     printA = DisplayString(tostring(A), 250, 100, Color(255,0,0))
     ShowString(printA, 10)
end

function PrintAnyNumber(numberany)
     printAny = DisplayString(tostring(numberany), 250, 250, Color(255,0,0))
     ShowString(printAny, 10)
end

LevelFuncs.OnLoad = function() end
LevelFuncs.OnSave = function() end
LevelFuncs.OnStart = function()
     local A = 100
     PrintA()

     if A == 100 then
          PrintAnyNumber(A)  
     end

     if A == 200 then
          local A = 300
          PrintAnyNumber(A)  
     end     
end
LevelFuncs.OnLoop = function() end
LevelFuncs.OnEnd = function() end
Saved variable values

When you save a savegame, and then load it, then will the variable value be restored from it? I.e. will variables be saved in savegames?

Well, you don't need to worry about it, in the case of continuous variable definitions. I mean, see the example above when the value of Z variable in OnLoop callback is the current value of Lara's animation index. I.e. when you load a savegame, then you don't need to restore the value of this variable from the savegame, because Z variable will get the current index immediately from the current playtime.

But the situation is different with simple variable definitions like X = 8.
Let's suppose you define X = 8 global variable value in OnStart callback, i.e. when the level starts. Then you start playing. The variable keeps its value, because it is global. After that you save the game, and then load that savegame. - Bad surprise! The variable has been automatically deleted.
I.e. variable values usually won't be saved in savegames. (Either they are local or global, anyway.)

You can have some tricks to prevent that:
  • Type another X = 8 definition in OnLoad callback, to restore the value when the level has just been loaded.
    But it is really boring and tiresome.
    Besides, if you changed the variable value in the meantime, then X = 8 will restore an obsolete value.
  • Type the variable definitions always in OnLoop callback. So each moment of the playtime automatically sets this value again and again - including the moments just after a savegame having been loaded.
    But, as I said above, usually it is a bit odd to place simple variable definitions in the game loop.
    Besides, now you need to find out some tricky conditions, when you want to change this value, otherwise the loop keeps restoring 8 value for X variable.
The real solution is:
Use variables having "LevelVars" or "GameVars" tags in their name. - For example:

LevelVars.objectSize = 8
GameVars.FlipMapNumber = 16


LevelVars and GameVars variables are special global variables, because their value will be automatically saved when you save a savegame, and will be automatically restored when you load that savegame.

Variable values transferred to other levels

The values of local, "casual" global or LevelVars variables cannot be transferred to other levels.
That is why the difference between LevelVars and GameVars variables is important:
  • A LevelVars variable is valid only on its level.
  • A GameVars variable is valid on its level, plus it will be transferred to the next level, whereto a FinishLevel trigger (or a similar volume/scripted event) loads Lara.
So, for example, if you define a value for GameVars.X variable on Level A, and then Lara will be loaded to Level B, then feel free to do an operation with GameVars.X in Level B script. I.e. you don't need to define the value for GameVars.X variable in Level B script, because the value will be used which was defined in Level A script.

----------

Notes:
  • Function arguments are not variables. I mean, eg. in a FunctionX(object) syntax, the argument name "object" is important only to identify that argument for the function contents.
    But when you run this function eg. as FunctionX(A), that means that the value of "object" argument is the value of A variable.
  • If the variable value is an object (or anything else, which can be exactly identified, eg. a room), then that value is not a number or a text.
    So eg. if you want to print this X variable value on the screen:

    flame = TEN.Objects.GetMoveableByName("flame_emitter_1" )
    printflame = DisplayString(tostring(flame), 250, 250, Color(255,0,0))
    ShowString(printflame, 10)


    then that technically will look like some text - but it is clear that it is not a text which is useable by a level builder. Something similar like this:

    sol.sol::detail::unique_usertype<Moveable>: 0000013F47299778

    Instead, try to to print eg. the name ("flame_emitter_1") of the object:

    flame = TEN.Objects.GetMoveableByName("flame_emitter_1" )
    printflame = DisplayString(flame:GetName(), 250, 250, Color(255,0,0))
    ShowString(printflame, 10)


  • This condition naturally means "if X variable doesn't exist...":

    if X == nil then

  • As you can see, in TEN script you can use even non-TEN specific functions, like tostring.
    I.e. not all the preset functions are made by the developers, there are ones which are initial LUA functions.
    I will introduce further initial LUA functions in my TEN script tutorials. But feel free to browse the web to identify any initial LUA functions. (Naturally it is recommended only for the ones who are already advanced or expert in TEN scripting.)
    However, you don't need to follow any name hierarchy (as I told it for developer-made TEN preset functions), just use the initial TEN function syntax as it is introduced to you.
  • Tostring function can interpret characters as texts, even if the characters are not numbers. See eg. the "true" boolean.
    If you don't know the type you use for a printing, then use tostring everyway. I mean, eg. in the case of tostring(a) the function will print nicely even if the actual value of a variable is some text (i.e. when you don't need tostring), not a number.
  • If you want to print the current value of a TEN constant on the screen, then the constant will be printed with the numerical ID value which belongs to that constant. - For example, you print the current state of the fall in the level:

    printtext = DisplayString(tostring(GetCurrentLevel().weather), 250, 350, Color(255,0,0))

    Possible values are:

    • There is no falling. Constant name: WeatherType.None. Constant value: 0.
    • There is raining. Constant name: WeatherType.Rain. Constant value: 1.
    • There is snowing. Constant name: WeatherType.Snow. Constant value: 2.

    In my tutorials I won't care about TEN constant numerical values.
  • The linking feature can be used even instead of tostring function.
    Just indicate with empty quotation marks that you want to show some texts there, then stuff some contents between these empty quotation marks, adding (linking) eg. some number to them. - For example:

    printtext = DisplayString("" .. A .. ", " .. B .. " and " .. C .. " is less than " .. D .. ", but bigger than 3.", 250, 250, Color(255,0,0))

    As you can see, the quotation marks don't need to be empty, if you want to link the number to some texts.
    Besides, now you can see even something else - that in the example above with the many tostrings, only the first tostring is necessary (because that is not linked to any text), so you can simply type the variables in the other cases:

    printtext = DisplayString(tostring(A) .. ", " .. B .. " and " .. C .. " is less than " .. D .. ", but bigger than 3.", 250, 250, Color(255,0,0))

    Even if you type the variable value in the place of the variable:

    printtext = DisplayString(tostring(A) .. ", " .. 7 .. " and " .. 18 .. " is less than " .. 100 .. ", but bigger than 3.", 250, 250, Color(255,0,0))

    (According to my experiences, in some cases perhaps only tostring or only "" works nicely to start the first printable value. And sometimes you cannot avoid tostring for the second, third etc. printable values.)
  • Be careful with the complex using of variables, like the example above.
    I mean, if eg. A variable cannot be printed for some reason, then the whole text cannot be printed, even if everything is okay with B, C and D variables.
  • You can type variables even in a part of the level script, where there are no blocks: it is naturally the part of the script out of any callbacks.
    So if you type a variable there (but out of any custom block), then it could be either local or global, because if it is local, then its "block" means the whole level script. I.e. it works just like a global one now.
    (For technical reasons, I recommend to use local variables in these cases.)
  • Please notice that the label of local should be typed only when you set the first value or a new value for a local variable. But if you call it for an action or check it in a condition, then you shouldn't do it.
  • In the case of groupped local variables, the label of local should be typed at the group name, ignoring it at the variable definition:

    local object = {}
    object.objectSize = 1024


  • LevelVars and GameVars variables can also be groupped, the same way as the other variables - for example:

    LevelVars.object = {}
    GameVars.FlipMap = {}

    LevelVars.object.objectSize = 8
    GameVars.FlipMap.FlipMapNumber = 16


    (Like in the case of custom function group names, "Engine" group name of LevelVars/GameVars is reserved for the developers.)
  • LevelVars values will be deleted when you leave the level.
    So when you want to keep the value of a variable for the time when you come back to this level, then you should use GameVars for that variable.
  • Unfortunately, I discovered two important GameVars bugs:

    • It seems that if you once set a GameVars variable value in a level (either setting it there, or transferring it from another level), then you cannot transfer another value of the same variable to that level. (But you can still set another value for that variable in that level.)
      For example: Lara is on Level A, where GameVars.X = 8 is set. She is loaded to Level B, transferring GameVars.X = 8 value there. Later she is loaded back to Level A. GameVars.X here gets another value: 7. Now she goes back to Level B again - but X remains 8 there. However, while she is in Level B, GameVars.X can get another value there. Even 7, for example.
    • GameVars values cannot be transferred to the title. (Perhaps it is the same issue I told above at EndReason constants.)

    So I don't recommend to try to transfer GameVars values to levels whereto Lara is just going back, or to title.
  • And one more thing about GameVars - and also LevelVars. Not a bug, only some limitation!
    If the value of the LevelVars/GameVars variable is an identified thing (see above what that means: eg. an object), then that cannot work well with GameVars/LevelVars. For example: an action with that variable value (eg. to spawn a baddy) won't be executed. Or the variable works like it as if it were not be LevelVars/GameVars: i.e. it will loose its value after loading a savegame.
    Eg. this can be wrong:

    LevelVars.Flames = GetMoveableByName("flame_emitter_1")
    LevelVars.Flames:Enable()


    The solutions:

    • In these cases, don't use the variable as a LevelVars/GameVars.
    • Or store only the identifier (eg. the name) in the variable, identifying the thing by this variable value out of the variable:

      LevelVars.Flames = "flame_emitter_1"
      GetMoveableByName(LevelVars.Flames):Enable()

  • In the case when you want to use variables in other LUA files, not in script files, then seemingly you can do it. For example, in gameflow.lua you want to set the same value for a parameter of your levels, then set it as a variable. So, when you change the variable value, then it will change at all the levels, right away.
    You can declare it either local or global, because the block for this variable is seemingly is used as the whole LUA file. (Mostly that you won't use other blocks there, eg. for an "if".) - So level "blocks" of gameflow.lua are not blocks in this meaning.
    On the other hand, it is meaningless to use GameVars here, because GameVars variables will transfer values only between level script files. (And naturally it is also meaningless to use LevelVars variables here, because these LUA files cannot understand what saving/loading a savegame is.)

Last edited by AkyV; 19-11-24 at 20:49.
AkyV is online now  
Old 08-09-24, 10:40   #4
AkyV
Moderator
 
Joined: Dec 2011
Posts: 5,074
Default

3. Basic mathematical operations

Let's sum up all the basic mathematical operations you can use in the script. In our examples we have two variables, which are A = 6.5 and B = 4.2:
  • Addition:
    C variable value will be the sum of A and B, which is 10.7 this time:

    C = A + B

  • Subtraction:
    C variable value will be the difference between A and B, which is 2.3 this time:

    C = A - B

  • Multiplication:
    C variable value will be A multiplied by B, which is 27.3 this time:

    C = A * B

  • Float division:
    C variable value will be A divided by B, which is (approximately) 1.547619047619 this time:

    C = A / B

    (It would be a floating number even if the solution is an integer, like 8 / 4 = 2.0.)
  • Floor division:
    C variable value will be the integer part of A divided by B (in a floating form), which is 1.0 this time (i.e. 1.547619047619 → 1.0):

    C = A // B

    (It would be simple "1" if you divided integer numbers, like 6 // 4.)
  • Modulo:
    C variable value will be the remainder after A dividing by B, which is 2.3 this time:

    C = A % B

    It looks like a subtraction, but it is true only for this example. See another example, to understand it better: 100 % 7.26 = 5.62.
    Why? Because 100 / 7.26 = 13.77 (approximately). So 7.26 is 13 times in 100, without a remainder. 7.26 * 13 = 94.38. 100 - 94.38 = 5.62. So that remainder is 5.62 now.
  • Exponentiation:
    C variable value will be A to power of B, which is 2595.59 (approximately) this time:

    C = A ^ B

    Perhaps it is more understandable if B is an integer value, i.e. 4 this time.
    I.e. "power" means A will be multiplied B times by itself, so 6.5 ^ 4 is 6.5 * 6.5 * 6.5 * 6.5.

    The operation is naturally also useable to calculate roots, all you need to do is to use a reciprocal for the power. Eg. the cube (i.e. third) root of 729 is 9, which means 729 / 9 / 9 = 9. So 729 ^ (1 / 3) = 9. (The result will be considered as 9.0.) So A = 729, B = 1 / 3 now.
  • Unary minus:
    It simply turns the plus or minus state of a number. So if B = -A, then B is 7 if A is -7, or B is -7 if A is 7.
----------

Note:
Always use the round brackets in the proper way.
I mean, I hope it is clear for you that eg.

2 * (A + B)

or

2 * A + B

have different results.

Last edited by AkyV; 05-10-24 at 09:36.
AkyV is online now  
Old 08-09-24, 12:55   #5
AkyV
Moderator
 
Joined: Dec 2011
Posts: 5,074
Default

4. Checking conditions

You could already see that conditions are handled in "if" blocks:
  • First you set the line of "if" statement, where the condition is typed.
  • Then you type what the game should do when the condition is true.
  • Then you close the "if" block by an "end" line.
For example, this function ignites a specific fire when Lara's health is exactly 50 % (because her maximum HP is 1000) - and, thanking to the tabulation, you can see nicely that which "end" belongs to the function, and which one to the "if":

Code:
function LaraFlame()
	local LaraHealth = Lara:GetHP()
	local flame = TEN.Objects.GetMoveableByName("flame_emitter2_117")
	
	if LaraHealth == 500 then
		flame:Enable()
	end
end
But you can also check other relations, not only equality. All you need is to use different sign instead of ==. - Your possibilities:

==: equality
~=: inequality ("is not equal to")
<: less than
>: greater than
<=: less or equal
>=: greater or equal

However, a condition could be either true or false. As you can see above, we don't need to mark it if it is "true", because it is obvious ("if LaraHealth == 500 then"). However, you can mark it, if you want:

if LaraHealth == 500 == true then

But if you want to check if the condition is false, then you need to mark it everyway:

if LaraHealth == 500 == false then

Naturally you can embed condition checks into each other:

Code:
function LaraFlame()
	local LaraHealth = Lara:GetHP()
	local flame = TEN.Objects.GetMoveableByName("flame_emitter2_117")
	
	if LaraHealth > 500 == true then
		flame:Enable()
		if Lara:GetRoomNumber() == 10 == false then
			Lara:SetPoison(15)
		end
	end
end
Now the code says that that specific flame will be ignited if Lara's health is bigger than 50 %. Besides, if if Lara's health is bigger than 50 %, and Lara is surely not in room which has ID10 in TE, then she will be slightly poisoned.

"Else" and "elseif" statements

Naturally you can check a condition even with its opposite.
I mean eg. if you tell the game what to do when the condition is true, then perhaps you should also tell the game what to do when it is false:

Code:
if LaraHealth > 500 then
	flame:Enable()
end
if LaraHealth <= 500 then
        flame:Disable()
end
Or, instead of finding out what "opposite" means, probably it is easier if you simply check the original conditon with the opposite boolean (so eg. you check it now as "false" if the original one was "true"):

Code:
if LaraHealth > 500 then
	flame:Enable()
end
if LaraHealth > 500 == false then
        flame:Disable()
end
Or, more easier if you use an "else" statement instead of a second "if" (i.e. "else" means: the game checks the opposite of the condition here):

Code:
if LaraHealth > 500 then
	flame:Enable()
else
        flame:Disable()
end
However, the condition can be checked for an opposite boolean even if the other condition is not exactly its opposite, like:

Code:
if LaraHealth == 500 then
	flame:Enable()
end
if LaraHealth == 600 then
        Lara:SetPoison(15)
end
So now we said:
  • Check if the variable value is true (check if health is 500).
  • Then also check if the variable value is not true (check if health is not 500), but only for some specific other value of the variable (check if health is not 500, but only when it is 600).
In this case it could be useful if you use "elseif" instead of end+if, i.e. which makes the script typing easier:

Code:
if LaraHealth == 500 then
	flame:Enable()
elseif LaraHealth == 600 then
        Lara:SetPoison(15)
end
Besides, now if you also link an "else" to this, that means the opposite of all of them, i.e. "not 500, not 600, but anything else":

Code:
if LaraHealth == 500 then
	flame:Enable()
elseif LaraHealth == 600 then
        Lara:SetPoison(15)
else
        flame:Disable()
end
----------

Notes:
  • Perhaps it looks nicer (but not necessary) when you put in round brackets what you check with a condition, like:

    Code:
    if (LaraHealth == 500) == true then
         flame:Enable()
    end
  • As I said, "true" doesn't need to be typed everyway.
    And it is true even if you don't say exactly what is true.
    So eg. usually you check if a function is true in this way:

    if CheckTinyFlame() == true then

    But it also means the same:

    if CheckTinyFlame() then

  • Naturally checking "true" is the same as checking "not false", and checking "false" is the same as checking "not true":

    ~= false then

    ~= true then


  • So "else" and "elseif" are typed instead of an "end" (and an "if"), but that doesn't mean that now you united those two blocks. - I emphasize it now for two reasons:

    • Remember what I said above about the blocks of local variables.
    • I said in that "functions" tutorial that don't type any new line for a function, after the line of "return" statement. However, you can set even more return values for a function, if "if" blocks at the end of the function wants to define different outputs for the function, one return per each block, like:

      Code:
      if LaraHealth > 500 then
      	return true
      else
              return false
      end
      In this example you set the function as "true" when Lara's health is bigger than 50 %, otherwise you set it as "false". (So this time there is no "yes or no" question at "return", you directly state that it is yes or no. - But naturally you can set other return values even in these cases.)

  • I said in that "functions" tutorial that you can also check conditons at "return" values, like "return OCB == 1". Naturally you can also check all the other relations there (~=, <, >, <=, >=).

Last edited by AkyV; 09-09-24 at 18:47.
AkyV is online now  
Old 08-09-24, 13:08   #6
AkyV
Moderator
 
Joined: Dec 2011
Posts: 5,074
Default

5. Logical operations

You will use logical operators mostly to check conditions.

"And" operator in conditions

It could be really tiresome to check more than one conditions together:

Code:
if LaraHealth > 500 == true then
	if Lara:GetRoomNumber() == 10 == false then
                flame:Enable()
		Lara:SetPoison(15)
	end
end
It says that if Lara's health is bigger than 50 %, then check if Lara is in room ID10. If she is not, then ignite a specific flame, and make Lara slightly poisoned.
So you can modify the structure, using an "and" operator:

Code:
if LaraHealth > 500 == true and Lara:GetRoomNumber() == 10 == false then
        flame:Enable()
        Lara:SetPoison(15)
end
This checks those conditions at the same time: if Lara's health is bigger than 50 % and if Lara is not in room ID10, then ignite a specific flame, and make Lara slightly poisoned.

"Or" operator in conditions

Now it is enough if one of the conditions is passed, to execute the condition contents:

Code:
if LaraHealth > 500 == true or Lara:GetRoomNumber() == 10 == false then
        flame:Enable()
        Lara:SetPoison(15)
end
If Lara's health is bigger than 50 % or if Lara is not in room ID10, then ignite a specific flame, and make Lara slightly poisoned.

"Not" operator in conditions

It works the same way as a ~= relation (but only for true or false):

Code:
if LaraHealth > 500 == not false then
        flame:Enable()
        Lara:SetPoison(15)
end
If Lara's health is bigger than 50 %, then ignite a specific flame, and make Lara slightly poisoned.

The operators out of conditions

Now the operator needs to know that an argument:
  • is true (or has a value set), or
  • is false (or doesn't have a value set, i.e. nil).
And this is how it works:
  • "And" will always choose the argument on its left side, if that is false. Otherwise it will choose the argument on its right side. - For example:

    ShowString(DisplayString(tostring(A and B), 250, 100, Color(255,0,0)), 5)

    A variable value is 1, B variable value is 2. Now the game prints 2 on the screen.
    Or A variable value is still not defined, B variable value is 2. Now the game prints nil on the screen.
  • "Or" will always choose the argument on its left side, if that is true. Otherwise it will choose the argument on its right side. - For example:

    ShowString(DisplayString(tostring(A or B), 250, 100, Color(255,0,0)), 5)

    A variable value is 1, B variable value is 2. Now the game prints 1 on the screen.
    Or A variable value is still not defined, B variable value is 2. Now the game prints 2 on the screen.
  • "Not" is resulted in "true", when the argument is a "not" thing (i.e. it is false or nil), but it is resulted in "false", when the argument is not a "not" thing (i.e. it is true or set). - For example:

    ShowString(DisplayString(tostring(not A), 250, 100, Color(255,0,0)), 5)

    A variable value is 1. Now the game prints "false" on the screen.
    Or A variable value is still not defined. Now the game prints "true" on the screen.

----------

Notes:
  • Feel free to place the brackets in complex logical conditions, if that helps you to understand the condition. However, that is not necessary. - I mean, eg. the three versions have the same "true" result, when A = 1, B = 0 and C = 1:

    if A == 1 or B == 1 and C == 1 then
    if (A == 1 or B == 1) and C == 1 then
    if A == 1 or (B == 1 and C == 1) then

    But if it is still unclear to you, then feel free to use a "condition in condition", with the same meaning, eg.:

    Code:
    if C == 1 then
        if A == 1 or B == 1 then
        (...)
        end
    end
  • Complex examples with "not" conditions:

    if A == true and not B == true then

    If A is true and if B is not true.

    if A == true or not B == true then

    If A is true or if B is not true.

    if A == 1 and B ~= 1 then

    If A is 1 and if B is not 1.

    if A == 1 or B ~= 1 then

    If A is 1 or if B is not 1.

Last edited by AkyV; 16-10-24 at 22:49.
AkyV is online now  
Closed Thread

Bookmarks

Thread Tools

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off



All times are GMT. The time now is 17:04.


Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2024, vBulletin Solutions Inc.
Tomb Raider Forums is not owned or operated by CDE Entertainment Ltd.
Lara Croft and Tomb Raider are trademarks of CDE Entertainment Ltd.