09-09-24, 19:23 | #1 |
Moderator
Joined: Dec 2011
Posts: 5,074
|
TEN - Script elements and operations (advanced)
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 tutorial sequence about TEN scripting techniques. The further parts of the sequence so far: functions, basic scripting. So this current tutorial is the fourth part about scripting techniques in TEN. It is about the advanced script elements and operations. One day I'll probably extend this tutorial with some further LUA features. But I just don't know enough about advanced LUA scripting, to do that now. However, after this tutorial don't wait for a "TEN - Script elements and operations (expert)" tutorial to be released any time soon. I will surely not be clever enough to write something like that, nowadays or in the near future. So the tutorial sequence will continue with something else... CONTENTS: 1. Tables 2. Loops 3. Multiple assignments 4. Advanced mathematical operations 5. Bitwise operations 6. String operations ---------- Notes:
Last edited by AkyV; 04-11-24 at 20:23. |
09-09-24, 19:37 | #2 |
Moderator
Joined: Dec 2011
Posts: 5,074
|
1. Tables
In this tutorial sequence we previously mentioned "tables": TEN master table has sub-tables ("modules") which has sub-subtables ("classes"), and they can also have functions and constants. Yes, a purpose of a table is to store multiple values at the same time - unlike variables which can store only one value at the same time. If you are an advanced or expert TRNG builder, then tables should be familiar to you. See for example this script command, whose purpose is to store big numbers: Parameters = PARAM_BIG_NUMBERS, 1286, -31713, 18565, -771, 5654, 397 So, for example, when you want to use the number of 18565, then you will call a trigger (or another script command) which refers to the 3rd position of PARAM_BIG_NUMBERS "table". The important things you initially need to know about TEN tables are:
As I said above, we need to identify a position of a value in a table, so we will able to call that table value. The method for this is we use our tables in the form of arrays, i.e. using indices for the values. If you don't want any specific index (I will tell below how you can), then the first value of the table will always have ID 1, the second value of the table will always have ID 2, the third value of the table will always have ID 3 etc. (So this is what I wall call "regular indices".) The required index always needs to be typed in [] brackets. A table value can be called if you name the table, with the required index together. For example now "Pierre" will be printed on the screen: Code:
local NatlasTeam = {"Natla", "Pierre", "Larson", "Kold", "Kid", "Cowboy"} printMember = DisplayString(NatlasTeam[2], 250, 250, Color(255,0,0)) ShowString(printMember, 10) That is why you can set a table value even as a variable value: Code:
local A ="Natla" local NatlasTeam = {A, "Pierre", "Larson", "Kold", "Kid", "Cowboy"} Code:
local A = TEN.Objects.GetMoveableByName("Natla") local NatlasTeam = {A:GetName(), "Pierre", "Larson", "Kold", "Kid", "Cowboy"} You don't need to create a table with values everyway. I mean, you are allowed to create an empty table, which will be stuffed only later with values - all you naturally need for an empty table is to use empty brackets: local NatlasTeam = {} Now I show the method how to fill the empty table with values, using regular indices: When you insert a value in an empty table (with a table.insert initial LUA function), that will surely go into the first table position (i.e. index = 1), which is now the first empty table position. The next value inserted in that table will surely go into the second table position (index = 2), which is now the first empty table position, etc. For example it makes the same table, as above: Code:
local NatlasTeam = {} table.insert(NatlasTeam, "Natla") table.insert(NatlasTeam, "Pierre") table.insert(NatlasTeam, "Larson") table.insert(NatlasTeam, "Kold") table.insert(NatlasTeam, "Kid") table.insert(NatlasTeam, "Cowboy") Code:
local NatlasTeam = {"Natla", "Pierre", "Larson"} table.insert(NatlasTeam, "Kold") table.insert(NatlasTeam, "Kid") table.insert(NatlasTeam, "Cowboy") table.insert(NatlasTeam, 5, "Winston") But it doesn't overwrite the previous value of that position. Instead, it pushes away that by one position. So now "Kid" automatically will be pushed away from ID5 to ID6. And that is why "Cowboy" automatically will be pushed away from ID6 to ID7. But if you want to overwrite the previous value, then first remove it (with a table.remove initial LUA function), and use the inserting function only after that, for that position: table.remove(NatlasTeams, 5) table.insert(NatlasTeam, 5, "Winston") Now first "Kid" will be removed from ID5, and "Cowboy" will be pulled back from ID6 into the empty ID5. And then "Winston" will be inserted in ID5, pushing "Cowboy" back into ID6. There is another way to add a value to an empty table position - but this time you always need to name exactly the index which that value is inserted into. On the other hand, this time you don't need to add the values in the order of indices - so this time eg. you will still insert "Cowboy" into ID6, even if you added "Kid" in the 6th line: Code:
local NatlasTeam = {} NatlasTeam[1] = "Natla" NatlasTeam[2] = "Pierre" NatlasTeam[3] = "Larson" NatlasTeam[6] = "Cowboy" NatlasTeam[4] = "Kold" NatlasTeam[5] = "Kid" printMember = DisplayString(NatlasTeam[6], 250, 250, Color(255,0,0)) ShowString(printMember, 10) This time referring to an existing index won't push the other values away, but simply overwrites the value in that index: NatlasTeam[5] = "Winston" So this time "Kid" was overwritten at ID5, that index already belongs to "Winston". "Cowboy" won't leave ID6, it remains always there. But you can also use this "another" method as a way to set irregular indices, which means:
You can also use # sign, which tells the length of a table. It could be important eg. if your table is really long (so eg. you can't remember/you don't want to count what the index of the last value of the table is), and you want to insert a new value in the last but one position of the "longtable" numerical table: longtable[#longtable - 1] = 125 Yes, "#longtable" means "the index of the last value of longtable table". But it works nicely only if you used the regular way (1, 2, 3, 4 etc.) to set indices for this table. A table of tables As you could see above, eg. the parts of TEN master table (i.e. the modules) are other tables. I.e. you could make even "a table of tables" - eg. splitting "NatlasTeam" table into two subtables: Code:
local NatlasTeam = {{"Natla", "Pierre", "Larson"}, {"Kold", "Kid", "Cowboy"}} printMember = DisplayString(NatlasTeam[2][3], 250, 250, Color(255,0,0)) ShowString(printMember, 10) Code:
local NatlasTeam = { {"Natla", "Pierre", "Larson"}, {"Kold", "Kid", "Cowboy"} } printMember = DisplayString(NatlasTeam[2][3], 250, 250, Color(255,0,0)) ShowString(printMember, 10)
So in the case of that position in the printing function (NatlasTeam[2][3]):
Naturally now you need the same methods to insert/remove values, as I said above: Code:
local NatlasTeam = {} table.insert (NatlasTeam, {"Natla", "Pierre", "Larson"}) table.insert (NatlasTeam, {"Kold", "Kid", "Cowboy"}) Code:
local NatlasTeam = {} NatlasTeam[1] = {"Natla", "Pierre", "Larson"} NatlasTeam[2] = {"Kold", "Kid", "Cowboy"} Code:
local NatlasTeam = {} NatlasTeam["Team1"] = {"Natla", "Pierre", "Larson"} NatlasTeam["Team2"] = {"Kold"} NatlasTeam["Team2"][2] = "Kid" NatlasTeam["Team2"][3] = "Cowboy" printMember = DisplayString(NatlasTeam["Team2"][3], 250, 250, Color(255,0,0)) ShowString(printMember, 10) Code:
local NatlasTeam = {"Natla", "Pierre", "Larson", {"Kold", "Kid", "Cowboy"}} printMember = DisplayString(NatlasTeam[4][3], 250, 250, Color(255,0,0)) ShowString(printMember, 10) Enemies["animals"]["big ones"][5] Further functions for tables Further initial LUA functions to handle tables (just like at other "table..." functions, recommended to use them only with regular indices):
Notes:
Last edited by AkyV; 08-11-24 at 17:45. |
10-09-24, 09:56 | #3 |
Moderator
Joined: Dec 2011
Posts: 5,074
|
2. Loops
Previously we talked about the OnLoop game phase which is running all through the effective playtime of your current level (except when menus are open). So whatever you type in this phase of the script, that will be called again and again, once in each moment of this playtime. Naturally usually you do not run (call) anything during all the playtime. Conditions helps you to narrow it down. Eg. if the condition is "if Lara is holding pistols", then that game event (eg. to spawn an enemy) will happen only when Lara has pistols in the hands. And if you don't want to call that in each moment when Lara is holding pistols, then you need further conditions to narrow it more down: "if Lara is holding pistols and if...". - Yes, it is a typical TRNG GlobalTrigger logic. And if it is still not enough to you, then you have further tools to control more these "TEN globaltriggers": control them by some timers, or enable/disable them in specific moments. - But things like that will be discussed in a latter tutorial, those are not parts of the scripting techniques tutorial sequence (because you will use techniques for them, which you have already learnt in the meanwhile). I say that because you need to understand: when I am talking about looped operations (i.e. loops), that is something completely different, it has nothing to do with OnLoop logic. I mean loops are initial LUA operations. In their case the loop doesn't mean "many moments after each other". In their case loop means "many happenings after each other, in the same moment". (Which means if a loop is typed in OnLoop then you also need conditions and such, to tell the moment when the loop will be running. Otherwise the whole loop will fully run once in each moment of OnLoop: once fully in the first moment, then once fully in the second moment, then once fully in the third moment etc.) Loops are useful when you want to script numerous similar happenings, which are for the same purpose, but each of these happenings have one or more different parameters. I.e. in this case you can solve it by typing one line in the script, for each happening - or you won't type numerous lines, instead you type only a few lines for a loop. Let's see for example when you want to print "Hello, Lara!" six times, in the moment when the level starts, printing it in six different positions: one of the coordinates is constant, it is 250 pixels from the left edge of the screen. What is different is the other coordinate: it is 250, 350, 450, 550, 650, 750 pixels from the upper edge of the screen: Without loops, it works only with some tiresome, long scripting: Code:
LevelFuncs.OnStart = function() local printLara1 = DisplayString("Hello, Lara!", 250, 250, Color(255,0,0)) ShowString(printLara1, 10) local printLara2 = DisplayString("Hello, Lara!", 250, 350, Color(255,0,0)) ShowString(printLara2, 10) local printLara3 = DisplayString("Hello, Lara!", 250, 450, Color(255,0,0)) ShowString(printLara3, 10) local printLara4 = DisplayString("Hello, Lara!", 250, 550, Color(255,0,0)) ShowString(printLara4, 10) local printLara5 = DisplayString("Hello, Lara!", 250, 650, Color(255,0,0)) ShowString(printLara5, 10) local printLara6 = DisplayString("Hello, Lara!", 250, 750, Color(255,0,0)) ShowString(printLara6, 10) end Code:
LevelFuncs.OnStart = function() ShowString(DisplayString("Hello, Lara!", 250, 250, Color(255,0,0)), 10) ShowString(DisplayString("Hello, Lara!", 250, 350, Color(255,0,0)), 10) ShowString(DisplayString("Hello, Lara!", 250, 450, Color(255,0,0)), 10) ShowString(DisplayString("Hello, Lara!", 250, 550, Color(255,0,0)), 10) ShowString(DisplayString("Hello, Lara!", 250, 650, Color(255,0,0)), 10) ShowString(DisplayString("Hello, Lara!", 250, 750, Color(255,0,0)), 10) end "For" loops "For" loops are probably the loops which are used most of the time. Our previous example should be typed with a "for" loop in a way like this: Code:
LevelFuncs.OnStart = function() for i = 0, 500, 100 do local printLara = DisplayString("Hello, Lara!", 250, 250 + i, Color(255,0,0)) ShowString(printLara, 10) end end When you define the purpose of a loop, then it always starts with defining different values for a variable. Yes, this time 0, 500 and 100 are all to define different values, for the same variable. Usually we name this variable of loops always "i" (but it is not a must at all), and it is always local for this block, even if it is not declared. After the "do" tag you should type the functions (or other operations) what this loop will do. Your task is to use that variable value everyway in these functions. As you can see, this time what we do is adding the variable value to the coordinate of the text, which coordinate is compared to the upper edge of the screen. The three values for i variable are:
Seeing that "250 + i" addition, that value will be 250 + 0 = 250 first, then 250 + 100 = 350, then 250 + 200 = 450 etc. So these will be the values of that coordinate: 250 at the first running, 350 at the second running, 450 at the third running, 550 at the fourth running, 650 at the fifth running, 750 at the sixth (last) running. So the contents of the loop will be executed six times in the moment when the level starts, printing "Hello, Lara!" text on (250, 250), (250, 350), (250, 450), (250, 550), (250, 650) and (250, 750) positions of the screen. Some examples for "for" loops and tables:
"Pairs" and "ipairs" functions You will use pairs or ipairs initial LUA functions for "for" loops:
Code:
-- FILE: Levels\My_level.lua dogrooms = {"Dog Room1", "Dog Room2", "Dog House", "Dog Arena", "Dog Lair"} function RoomPrint(roomtable) for i, v in ipairs(roomtable) do local pos = i * 100 - 100 ShowString(DisplayString(v .. " has ID" .. i, 300, 300 + pos, Color(255,0,0)), 10) end end LevelFuncs.OnLoad = function() end LevelFuncs.OnSave = function() end LevelFuncs.OnStart = function() RoomPrint(dogrooms) end LevelFuncs.OnLoop = function() end LevelFuncs.OnEnd = function() end Dog Room1 has ID1 in position (300, 300) Dog Room2 has ID2 in position (300, 400) Dog House has ID3 in position (300, 500) Dog Arena has ID4 in position (300, 600) Dog Lair has ID5 in position (300, 700) So the argument of "ipairs" function is the table name. This time "i" variable value is the regular index of a table value. The "for" loop will check them one by one, in an ascending order: 1, 2, 3, 4 etc. The value of "v" variable is a value of the table, which belongs to the current index. (Again, i and v only default names, you can change them, if you want.) Naturally you can do that even with a "casual" "for" - but only if you know exactly that how many values (5) you want to check in the table: Code:
function RoomPrint(roomtable) for i = 1, 5 do local pos = i * 100 - 100 ShowString(DisplayString(roomtable[i] .. " has ID" .. i, 300, 300 + pos, Color(255,0,0)), 10) end end Code:
-- FILE: Levels\My_level.lua dogrooms = {["r1"] = "Dog Room1", ["r2"] = "Dog Room2", ["h"] = "Dog House", ["a"] = "Dog Arena", ["l"] = "Dog Lair"} function RoomPrint(roomtable) for k, v in pairs(roomtable) do if k == "r1" then pos = 300 elseif k == "r2" then pos = 400 elseif k == "h" then pos = 500 elseif k == "a" then pos = 600 else pos = 700 end ShowString(DisplayString(v .. " has ID" .. k, 300, pos, Color(255,0,0)), 10) end end LevelFuncs.OnLoad = function() end LevelFuncs.OnSave = function() end LevelFuncs.OnStart = function() RoomPrint(dogrooms) end LevelFuncs.OnLoop = function() end LevelFuncs.OnEnd = function() end Dog Room2 has IDr2 in position (300, 400) Dog House has IDh in position (300, 500) Dog Arena has IDa in position (300, 600) Dog Lair has IDl in position (300, 700) And here is a complex example with "ipairs" - the game will place a flaming effect on the head top of the dogs which are just in "Dog Room1" or "Dog Room2", removing the effect while they are just out of these rooms. The problem with this setup is that this effect is harmless to Lara. (Or even for the dogs themselves - but let's say the dogs are resistant to their own flames.) That is why we enhance the features of this effect with some additional scripting:
Code:
-- FILE: Levels\My_level.lua function FlamingDogHeads() local doggroup = {} for dog_i = 1, 7 do doggroup[dog_i] = GetMoveablesBySlot(ObjID.DOG)[dog_i] end local meshgroup = {} for mesh_i = 1, 15 do meshgroup[mesh_i] = Lara:GetJointPosition(mesh_i - 1) end for index1, dog in ipairs(doggroup) do local room = dog:GetRoom() if dog:GetStatus() == MoveableStatus.ACTIVE then if room:GetName() == "Dog Room 1" or room:GetName() == "Dog Room 2" then local headmesh = dog:GetJointPosition(3) local flamepos = Vec3(headmesh.x, headmesh.y - 100, headmesh.z) EmitFire(flamepos, 1.5) local larapos = Lara:GetPosition() if larapos:Distance(flamepos) < 768 then Lara:SetHP(Lara:GetHP() - 1) end for index2, laramesh in ipairs(meshgroup) do if laramesh:Distance(flamepos) < 100 then Lara:SetEffect(EffectID.FIRE) end end end end end end LevelFuncs.OnLoad = function() end LevelFuncs.OnSave = function() end LevelFuncs.OnStart = function() end LevelFuncs.OnLoop = function() FlamingDogHeads() end LevelFuncs.OnEnd = function() end
for index1, dog in ipairs(doggroup) do The "for" loop will take the dogs of "doggroup" table, one by one, once in each moment of OnLoop. Unlike a "casual" "for", this is a place where you don't need to calculate "X to Y" intervals for the indices, because the loop will end now automatically (for this moment of OnLoop) when it has just counted all the dogs: First it takes the dog at ID1. So the value of "dog" variable where "index1" value is 1. Then it takes the dog at ID2. So the value of "dog" variable where "index1" value is 2. Etc. Now this is what this "for" will do with the current dog, in the current moment of OnLoop:
I mean, the game seemingly nicely handles the issues of inactive objects. I mean, eg. even if the dog is placed inside Dog Room1 and Dog Room2, then the game won't place a flame effect on the invisible head of the dog, before it is getting spawned. On the other hand, I experienced a bug about this: when a dog like this is already living, with a flaming head, but I go back to the title without killing it. In this case, if I restart the level now from the title, without quitting the game, then one or more "phantom flames" will be floating on invisible dog heads, before they are getting spawned in these rooms. That is why I used "dog:GetStatus() == MoveableStatus.ACTIVE" to check if the current dog is already living when I also check the required rooms. (Which naturally will give the same true or false answer now, as if I used GetActive().) This complex condition technically could be typed even only in one row, like: if room:GetName() == "Dog Room 1" or room:GetName() == "Dog Room 2" and dog:GetStatus() == MoveableStatus.ACTIVE then But I had a good reason two separate them into two conditions: one will "wrap" the other one inside in it. I mean, you cannot type those conditions in the same row, because the result could be wrong, the phantom flames will be still there. - That is why I first check if the dog is living, I only check its current room only after that: a "condition in condition". Now there will surely not be phantom flames. So if the current dog is in one of the required rooms (and living), then it can get that flame effect on its head:
The EmitFire function will ignite the flame only for one moment - but that is not a problem, because the next moment of OnLoop will keep it burning further, ignite it for a further one moment, then again in the next moment etc. - Except: if the dog is not in any of the required rooms any more, then the flame won't be ignited again (till the dog comes back into these rooms). Now that "condition in condition" starts checking Lara (i.e. if the current dog is in one of the required rooms and living):
Then in the further moments of OnLoop, it will check all the dogs one by one again and again on her, for this. Now that "condition in condition" continues checking Lara (i.e. if the current dog is in one of the required rooms and living), but now for something else - to set her on fire or not:
Then the "for-ipairs" loop of the dogs will jump to the next dog, so her "for-ipairs" loop will compare her meshes even to that dog. And the dogs' loop will jump to the further dogs, to check also there the distances. Then in the further moments of OnLoop, it will check all the dogs one by one again and again on her, for this. (So checking her meshes continue even if she is already burning, so the game tries to set her on fire again, unnecessarily. Oh, well. I mean, she will die soon, anyway. Or, if she extinguishes the flame in the meanwhile, then that is naturally a good reason to continue the checking, to ignite her again.) "While" loops For example our "tiresome" example to print "Hello, Lara!" six times, looks this way with a "while" loop: Code:
LevelFuncs.OnStart = function() local i = 0 while i ~= 600 do local printLara = DisplayString("Hello, Lara!", 250, 250 + i, Color(255,0,0)) ShowString(printLara, 10) i = i + 100 end end The first i value is 0, so the first "Hello, Lara" will be printed in (250, 250 + 0 = 250) position. Then the loop will turn i into 100, with the i = i + 100 addition. Then the actual step of the loop ends, the loop starts the next step, first printing the second "Hello, Lara" in (250, 250 + 100 = 350) position. Etc. The last (sixth) "Hello, Lara!" will be printed in (250, 250 + 500 = 750) position. I mean, the i variable value won't turn in this step into 600 with i = i + 100, because we said in the condition that i cannot be 600. - So the loop has been stopped now. (Naturally I could write "i < 600" now instead of "i ~= 600": "i must be less than 600".) Or let's see our example with the flaming dog heads. This time we use a "while" loop to fill the "doggroup" table with seven dogs (naturally I could write "i ~= 8" now instead of "i < 8"): Code:
local doggroup = {} local dog_i = 1 while dog_i < 8 do doggroup[dog_i] = GetMoveablesBySlot(ObjID.DOG)[dog_i] dog_i = dog_i + 1 end
On the other hand, for the "while" loop, you "only" tell that i cannot reach 600 (i < 600). Because the step is 100 now, and you don't want i to turn from 500 to 600. But, for this reason, any number is a proper limit now for a < relation, between 501 and 600: i < 501, i < 502, i < 503, ... i < 598, i < 599, i < 600. So, with an approximate value, you will stop the loop at the proper point. "Repeat/until" loops "Repeat/until" loops are also about unclear ending values, just like "while" loops. The difference is:
Code:
LevelFuncs.OnStart = function() local i = 0 repeat local printLara = DisplayString("Hello, Lara!", 250, 250 + i, Color(255,0,0)) ShowString(printLara, 10) i = i + 100 until i > 514 end This time the condition should be typed in the line of "until". Again: 500 should be the highest i, 600 shouldn't be used. So you can stop the loop by an approximate minimum limit, if the limit value is i > 500, i > 501, i > 502, i > 503, ... i > 598, i > 599. This time I intentionally chose a random limit now: i > 514. (But "until i == 600" is also a proper value now, to stop the loop, not printing a next line in 600 position. I.e. this is the case when the loop works in WHEN mode, not AFTER mode.) "Break" statements "Break" statements are actions in additional conditions of the loops (in any loop type). The purpose of a "break" is to stop the loop before it reaches the limit defined in i variable. See the six lines above about "Hello, Lara!" text, when you use a "while" loop. This time we expand that example. I mean, you say now that you don't want to print after the position of 500, even if you originally wanted to print six lines on the screen. So you don't want/cannot calculate that 500 limit is enough for this or not. That is why you'll use a "break" not to print the lines over that position - also printing a warning on the screen, if you'd exceed this limit with further printings: Code:
LevelFuncs.OnStart = function() local i = 0 while i ~= 600 do local pos = 250 + i if pos > 500 then ShowString(DisplayString("Too long list!", 250, 600, Color(255,0,0)), 10) break end local printLara = DisplayString("Hello, Lara!", 250, 250 + i, Color(255,0,0)) ShowString(printLara, 10) i = i + 100 end end "Goto" statements The "goto" statement is usually used in loops, but it is not mandatory. The "goto" makes the game jump to a specific place of the script, when just reading the line of "goto". This place is called a "label", and the line of "goto" will name this label. Repeat the name in the script, between two :: signs: this is the place of the label. Code:
LevelFuncs.OnStart = function() for i = 0, 500, 100 do if i == 200 or i == 300 then goto emptyline end local printLara = DisplayString("Hello, Lara!", 250, 250 + i, Color(255,0,0)) ShowString(printLara, 10) ::emptyline:: end end However, in this case probably it is an easier version of the setup: Code:
LevelFuncs.OnStart = function() for i = 0, 500, 100 do if i < 200 or i > 300 then local printLara = DisplayString("Hello, Lara!", 250, 250 + i, Color(255,0,0)) ShowString(printLara, 10) end end end Notes:
Last edited by AkyV; 13-11-24 at 09:45. |
13-09-24, 23:33 | #4 |
Moderator
Joined: Dec 2011
Posts: 5,074
|
3. Multiple assignments
You have already read about multiple assignments in this tutorial. - Just remember table.unpack function: A, B and C variables will take values from "NatlasTeam = {"Natla", "Pierre", "Larson"}" table, in the same order. So the first variable (A) will take the first table value (Natla), the second variable (B) will take the second table value (Pierre) etc. Or the example when this function turned the table values into function arguments. Let's see some further examples, to understand that how many similar things can be done with multiple assignments: local X = 48 local A, B = 163, 6 * X, 82 Now A is 163, B is 288. The value of 82 will be ignored. local A = 26 local B = 42 A, B = B, A Now A is 42, B is 26. local NatlasTeam1 = {"Natla", "Pierre", "Larson"} local NatlasTeam2 = {"Kold", "Kid", "Cowboy"} NatlasTeam1[1], NatlasTeam2[1] = NatlasTeam2[1], NatlasTeam1[1] Now NatlasTeam1[1] is Kold, NatlasTeam2[1] is Natla. local flame = TEN.Objects.GetMoveableByName("flame_emitter2_117" ) local A, B = "Hello, Lara!", flame:GetOCB() Now A is "Hello, Lara!", B is the current OCB value of the Moveable object with flame_emitter2_117 name. There can be some more complex situations. Keep reading the tutorial for some more info. ---------- Note: local _, A = "Hello, Lara!", flame:GetOCB() means that I don't care about "Hello, Lara" text, I only want to record the flame OCB, in variable A. I.e. _ means that is a "dummy variable". However, this doesn't mean _ is not a valid variable name, so, after all, "Hello, Lara!" will be recorded in it. Last edited by AkyV; 03-11-24 at 12:42. |
02-10-24, 23:15 | #5 |
Moderator
Joined: Dec 2011
Posts: 5,074
|
4. Advanced mathematical operations
The initial LUA functions to make advanced mathematical operations:
Notes:
Last edited by AkyV; 07-11-24 at 17:50. |
03-10-24, 19:34 | #6 |
Moderator
Joined: Dec 2011
Posts: 5,074
|
5. Bitwise operations
In the case of bitwise operations you do your things with binary numbers. If we'd like to explain the binary numbers, based on decimal numbers, then we need to say that the digits of the binary numbers are composed of the powers of 2. Each digit like that is called "bit", their ID is that power value: the first digit = Bit0 = 2 to power of 0 = 1 the second digit = Bit1 = 2 to power of 1 = 2 the third digit = Bit2 = 2 to power of 2 = 4 the fourth digit = Bit3 = 2 to power of 3 = 8 the fifth digit = Bit4 = 2 to power of 4 = 16 Etc. The value of that bit is 0, if that bit is unset, or 1, if it is set. The bits should be typed in a descending order - for example: 6-----5-----4---3---2---1---0 → powers of 2 (bit ID) 64---32---16---8---4---2---1 → decimal bit value 1-----0----1----1---0---0---1 → bit set/unset If you want to know the decimal value of a binary number, then sum the decimal values of all the bits, which bits are set: So the decimal value of this binary number (1011001) is 64 + 16 + 8 + 1 = 89. (Really naturally you don't need this manual conversion. I mean, I mentioned the converter function in the previous chapter.) Anyway, you can easily calculate the amount of the needful bits of a binary number, searching for the largest power which fits that value. So eg. now 64 (6) can be stuffed in 89, but the next power (7, with 128 value) is not. So there will be seven digits now: 0, 1, 2, 3, 4, 5, 6. Bitwise "AND" operation The OCB (Object Code Bit) values of an object could be even very simple: 0, 1, 2, 3, 4 etc. So eg. 0 is for the basic behavior, and it will execute some specific thing if OCB1 is typed at its object panel, or it will execute some other specific thing if OCB2 is typed etc. But that doesn't work in many cases: if the object should have even more than one specific behavior sometimes, at the same time. I mean, what if eg. you want the object have specific behaviors marked by both OCB1 and 2 values? Then how can you mark it? With their sum: 1 + 2 = 3? But what if there is already another behavior set at OCB3? The solution is available with the bitwise "AND" operation. The operation is marked by AND word or ∧ symbol. (Which is not the ^ symbol of the LUA exponentiation.) Technically it works like multiplying two decimal numbers by each other. But this time you multiply the bit of a binary number by the same bit of another binary number. For example, multiplying the 0/1 value of Bit3 at Number A by the 0/1 value of Bit3 at Number B. Naturally there could be only three operations: 0 ∧ 0, 0 ∧ 1, 1 ∧ 1. As I said they work like decimal multiplications (0 x 0 = 0, 0 x 1 = 0, 1 x 1 = 1), so the results could be: 0 ∧ 0 = 0, 0 ∧ 1 = 0, 1 ∧ 1 = 1. - I.e. the result will be "set"/"unset" if both the bits are set/unset. Otherwise it is "unset". Let's try it with these two numbers now: 89 (1011001) above, and 923 (1110011011) - naturally you need to fill the shorter number with unnecesary 0 values, so the length of the two numbers will be the same: 9-----8-----7------6-----5-----4---3---2---1---0 → powers of 2 (bit ID) 512--256--128---64---32---16---8---4---2---1 → decimal bit value 0-----0-----0------1-----0----1----1---0---0---1 → 89 1-----1-----1------0-----0----1----1---0---1---1 → 923 0-----0-----0------0-----0----1----1---0---0---1 → 25 So the result is 89 AND 923 = 25, not 89 x 923 = 82147. When I say that this operation is a solution for multiplied OCB values, that means that OCB values will be the decimal bit values: 0, 1, 2, 4, 8, 16 etc., instead of 0, 1, 2, 3, 4 etc. And why is it good? Let's say we want the multiplied OCB values of both OCB 2 and 8. Their sum is 10 - and there is no OCB defined at the value of 10. Besides, there is no other sum, which can be 10: eg. 1+8, 1+4, 2+4, 1+4+8 etc. are surely not 10. - So if the sum is 10, then you can be sure, that OCB2 and 8 are set (and only they are set) at this object. This means that only Bit1 and Bit3 are set at that object, for the OCB values of the object. - So when you adjust a multiplied OCB for an object, then you type this sum (like 10 now) on its OCB panel. If you want to check if a multiplied OCB has a specific OCB value, then you check if a bit is set or unset at that value: Does OCB1 is used? No, Bit0 cannot be set in 10. Does OCB2 is used? Yes, Bit1 is set in 10. Does OCB4 is used? No, Bit2 cannot be set in 10. Does OCB8 is used? Yes, Bit3 is set in 10. Etc. The bitwise "AND" operation will do this check this way, eg. for OCB=8 now: 3---2---1---0 → powers of 2 (bit ID) 8---4---2---1 → decimal bit value 1---0---1---0 → OCB sum = 10 1---0---0---0 → checked OCB = 8 1---0---0---0 → result = 8 So 10 AND 8 = 8. Which means:
if object:GetOCB() & 8 == 8 then I.e. it checks the result of "OCB sum AND 8". A full condition - if the OCB sum has OCB 8, then remove this OCB from there, not hurting the other OCBs of the sum: Code:
if object:GetOCB() & 8 ~= 0 then object:SetOCB(object:GetOCB() - 8) end Code:
if object:GetOCB() & 8 == 0 then object:SetOCB(object:GetOCB() + 8) end Custom OCB values Technically you can easily set a custom OCB value. After all, all you eg. need is a condition which checks if a bit is set in the OCB. If that is true, then the condition will execute that custom thing - something like this: Code:
if object:GetOCB() & X == X then (...) end That is why I suggest leaving OCB only for official using. Instead, let's create the term of "Custom Code Bit" (CCB), which is useable not only for Moveable objects, but for anything which can be exactly identified in the map: Statics, rooms, sinks etc. Let's see for example the example again with the flaming dog heads. This time we don't care about nice flame positions - so this time (to have a simple example) we place the flames on the mesh pivots. (And this time these will be harmless flames.) Let's see first the case when you want only maximum one flame per dog, so you don't need multiplied CCBs: We use three CCB numbers, to place flames on three different meshes of dogs: back (mesh1, CCB1), head (mesh3, CCB2), tail end (mesh25, CCB3). (But it is not a must. You could choose even 25, 68, 192 CCB values for it. Whatever.) Code:
-- FILE: Levels\My_level.lua function SetDogCCB() local dogs = {} for index = 1, #LevelVars.dogtableCCB1 do dogs[index] = TEN.Objects.GetMoveableByName(LevelVars.dogtableCCB1[index]) if dogs[index]:GetStatus() == MoveableStatus.ACTIVE then local backmesh = dogs[index]:GetJointPosition(1) EmitFire(backmesh, 1.5) end end for index = 1, #LevelVars.dogtableCCB2 do dogs[index] = TEN.Objects.GetMoveableByName(LevelVars.dogtableCCB2[index]) if dogs[index]:GetStatus() == MoveableStatus.ACTIVE then local headmesh = dogs[index]:GetJointPosition(3) EmitFire(headmesh, 1.5) end end for index = 1, #LevelVars.dogtableCCB3 do dogs[index] = TEN.Objects.GetMoveableByName(LevelVars.dogtableCCB3[index]) if dogs[index]:GetStatus() == MoveableStatus.ACTIVE then local tailendmesh = dogs[index]:GetJointPosition(25) EmitFire(tailendmesh, 1.5) end end end LevelFuncs.dogswap = function() table.move(LevelVars.dogtableCCB2, 1, #LevelVars.dogtableCCB2, #LevelVars.dogtableCCB1 + 1, LevelVars.dogtableCCB1) table.move(LevelVars.dogtableCCB3, 1, #LevelVars.dogtableCCB3, #LevelVars.dogtableCCB1 + 1, LevelVars.dogtableCCB1) for i = 1, #LevelVars.dogtableCCB2 do LevelVars.dogtableCCB2[i] = nil end for i = 1, #LevelVars.dogtableCCB3 do LevelVars.dogtableCCB3[i] = nil end end LevelFuncs.OnLoad = function() end LevelFuncs.OnSave = function() end LevelFuncs.OnStart = function() LevelVars.dogtableCCB1 = {"dog_14", "dog_15", "dog_16"} LevelVars.dogtableCCB2 = {"dog_17", "dog_20"} LevelVars.dogtableCCB3 = {"dog_18", "dog_19"} end LevelFuncs.OnLoop = function() SetDogCCB() end LevelFuncs.OnEnd = function() end SetDogCCB function will run in OnLoop, putting the one frame long flame in each moment on the current position of these meshes. There are three loops in the function, each for one CCB value. (I used independent loops, ignoring elseifs or else. Probably it is clearer this way now, to explain.) I also made that "dogs" custom table to make the things easier. (So you don't need to refer that long "GetMoveableByName(LevelVars.dogtable...[index])" way to the dogs in this function, again and again.) Each loop uses dogs table, but it is not a problem: because when one loop needs it, then the other ones don't. The current loop will overwrite the values of the previous loop, in this table. (DogtableCCB1 is longer than the other two tables. But it is not a problem, either. Because loops uses the last indices of their own tables. So when loop2 and then loop3 goes maximum to ID2, then they will ignore the value of DogtableCCB1 left at ID3.) The way the loops place the flames on the dogs, should be familiar to you, from "Loops" chapter. There also is a LevelFuncs.dogswap function. This is called as a volume (local) event set, through TE. When Lara (or else) activates this volume trigger, then the contents of the LevelVars tables changes: the full contents of dogtableCCB2 and dogtableCCB3 tables will be copy/pasted into dogtableCCB1 table, then both dogtableCCB2 and dogtableCCB3 will be fully emptied. The result: all these dogs still living will swap their flame, if that is just not on their back, so all the head flames and tailend flames will be removed to the backs. (I.e. eg. this is a way to change a CCB value.) This setup naturally looks differently when you want to use these CCB values for multiplied cases (so eg. when a dog can have flames not only on one body part at the same time): Code:
-- FILE: Levels\My_level.lua function SetDogCCB() local dogs = {} local doggroup = GetMoveablesBySlot(ObjID.DOG) for i, dog in ipairs(doggroup) do CCB = 0 for index = 1, #LevelVars.dogtableCCB1 do dogs[index] = TEN.Objects.GetMoveableByName(LevelVars.dogtableCCB1[index]) if dog == dogs[index] then CCB = 1 end end for index = 1, #LevelVars.dogtableCCB2 do dogs[index] = TEN.Objects.GetMoveableByName(LevelVars.dogtableCCB2[index]) if dog == dogs[index] then CCB = CCB + 2 end end for index = 1, #LevelVars.dogtableCCB4 do dogs[index] = TEN.Objects.GetMoveableByName(LevelVars.dogtableCCB4[index]) if dog == dogs[index] then CCB = CCB + 4 end end if CCB & 1 == 1 then if dog:GetStatus() == MoveableStatus.ACTIVE then local backmesh = dog:GetJointPosition(1) EmitFire(backmesh, 1.5) end end if CCB & 2 == 2 then if dog:GetStatus() == MoveableStatus.ACTIVE then local headmesh = dog:GetJointPosition(3) EmitFire(headmesh, 1.5) end end if CCB & 4 == 4 then if dog:GetStatus() == MoveableStatus.ACTIVE then local tailendmesh = dog:GetJointPosition(25) EmitFire(tailendmesh, 1.5) end end end end LevelFuncs.OnLoad = function() end LevelFuncs.OnSave = function() end LevelFuncs.OnStart = function() LevelVars.dogtableCCB1 = {"dog_14", "dog_15", "dog_16", "dog_19"} LevelVars.dogtableCCB2 = {"dog_15", "dog_16", "dog_17", "dog_20"} LevelVars.dogtableCCB4 = {"dog_16", "dog_17", "dog_18", "dog_19"} end LevelFuncs.OnLoop = function() SetDogCCB() end LevelFuncs.OnEnd = function() end This time there is a main loop, in which we will check all the dogs placed in the level. The sub-loops in the main loop will check the contents of each LevelVars.dogtableCCB... table:
Bitwise "OR" operation and custom bit flags There could also be an "OR" bitwise operation between the same bits of two binary numbers. The operation is marked by OR word or ∨ symbol. (Which is not the "v" letter.) If any of the bits is set, then the result is "set", but if none of them is set, then the result is "unset": 0 ∨ 0 = 0, 0 ∨ 1 = 1, 1 ∨ 1 = 1. For example: 9-----8-----7------6-----5-----4---3---2---1---0 → powers of 2 (bit ID) 512--256--128---64---32---16---8---4---2---1 → decimal bit value 0-----0-----0------1-----0----1----1---0---0---1 → 89 1-----1-----1------0-----0----1----1---0---1---1 → 923 1-----1-----1------1-----0----1----1---0---1---1 → 987 So the result is 89 OR 923 = 987. This method should be also familiar, even in the classic Tomb Raider level building: when you used a group activation to activate Moveable objects. (Or flipmaps.) I mean, in these cases you had to be careful with the five bit options (Bit1, 2, 3, 4, 5: "bit flags") set or unset on the object panel and/or the trigger panel - because the object has been activated when all of these options have been set:
5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 0----0-----1---1---0---0 → Door: 12 0----1-----1---0---0---0 → Pad: 24 0----1-----1---1---0---0 → Result: 28 Bit1 and Bit5 is still unset, so the door still won't open. (It is meaningless to set Bit3 at Pad now, because that is already set at the door object. However, it won't ruin the procedure.) Then we also activate the "Trigger": 5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 0----1-----1---1---0---0 → Door+Pad: 28 1----0-----0---0---1---0 → Trigger: 34 1----1-----1---1---1---0 → Result: 62 Now Bit1, Bit2, Bit3, Bit4 and Bit5 are all set, the door will open. Or aggregated: 5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 0----0-----1---1---0---0 → Door: 12 0----1-----1---0---0---0 → Pad: 24 1----0-----0---0---1---0 → Trigger: 34 1----1-----1---1---1---0 → Result: 62 So 12 OR 24 OR 34 = 62. (So 62 = all the five bits are set.) TEN supports this classic feature. However, it is not a TEN feature - so if you want more control on this (eg. to check that which bits are already set), then you need to write a fully custom TEN script for this. However, its advantage is that in a TEN script like that you can use even more than five bits (including Bit0), so you can create even a more complex setup: eg. pull ten (or even more) switches to open a door. This example will use seven big wall mechanical levers (the most classic Tomb Raider switch) to open a door. As I said, originally it is not a TEN feature, so you can't use bit flags on the object panel or on the trigger panel for this TEN script - but, as you can see just below, you don't need them, anyway: Code:
-- FILE: Levels\My_level.lua function OpenDoor() bitflags = 0 if GetMoveableByName("switch_type4_17"):GetAnim() == 1 then bitflags = 1 end if GetMoveableByName("switch_type4_18"):GetAnim() == 1 then bitflags = bitflags + 2 end if GetMoveableByName("switch_type4_19"):GetAnim() == 1 then bitflags = bitflags + 4 end if GetMoveableByName("switch_type4_20"):GetAnim() == 1 then bitflags = bitflags + 8 end if GetMoveableByName("switch_type4_21"):GetAnim() == 1 then bitflags = bitflags + 16 end if GetMoveableByName("switch_type4_22"):GetAnim() == 1 then bitflags = bitflags + 32 end if GetMoveableByName("switch_type4_23"):GetAnim() == 1 then bitflags = bitflags + 64 end if bitflags & 1 == 1 and bitflags & 2 == 2 and bitflags & 4 == 4 and bitflags & 8 == 8 and bitflags & 16 == 16 and bitflags & 32 == 32 and bitflags & 64 == 64 then GetMoveableByName("door_type4_16"):Enable() else GetMoveableByName("door_type4_16"):Disable() end end LevelFuncs.OnLoad = function() end LevelFuncs.OnSave = function() end LevelFuncs.OnStart = function() end LevelFuncs.OnLoop = function() OpenDoor() end LevelFuncs.OnEnd = function() end So this is what happens now: first the function (running in OnLoop) will declare that bitflags variable is 0 initially, in each moment of the playtime. (Again: it has nothing to do with the bit flags of trigger and object panels.) Animation1 of these switches is the one frame long animation when the switch is in "on" position. So seven "if"s check the seven switches if they are in this position. If one of them is just performing Animation1, then that switch will add a bit to bitflags variable. - If all of them is performing that animation, then the eighth "if" opens the door (checking if all those bits are set) - otherwise it remains open. (Or if that has been already opened, then it will be closed, when Lara moves a switch back to "off" position, not adding that bit again to bitflags variable, in the further moments of OnLoop.) - Instead of that long "if bitflags & 1 == 1 and..." you could use a short condition, to check the decimal sum of all those bits: if bitflags == 127 then It is always easy to calculate a sum like that: highest_bit * 2 - 1 = 64 * 2 - 1 now. (Because we also used Bit0, i.e. 1 now.) As you can see, we didn't use a bitwise "OR" in this example. In LUA scripts, we use | symbol (not "i", not "L" letter) instead of the general OR/∨ signs. - The example looks this way with a bitwise "OR": Code:
-- FILE: Levels\My_level.lua function OpenDoor() bitflags1, bitflags2, bitflags4, bitflags8, bitflags16, bitflags32, bitflags64 = 0, 0, 0, 0, 0, 0, 0 if GetMoveableByName("switch_type4_17"):GetAnim() == 1 then bitflags1 = 1 end if GetMoveableByName("switch_type4_18"):GetAnim() == 1 then bitflags2 = 2 end if GetMoveableByName("switch_type4_19"):GetAnim() == 1 then bitflags4 = 4 end if GetMoveableByName("switch_type4_20"):GetAnim() == 1 then bitflags8 = 8 end if GetMoveableByName("switch_type4_21"):GetAnim() == 1 then bitflags16 = 16 end if GetMoveableByName("switch_type4_22"):GetAnim() == 1 then bitflags32 = 32 end if GetMoveableByName("switch_type4_23"):GetAnim() == 1 then bitflags64 = 64 end if bitflags1 | bitflags2 | bitflags4 | bitflags8 | bitflags16 | bitflags32 | bitflags64 == 127 then GetMoveableByName("door_type4_16"):Enable() else GetMoveableByName("door_type4_16"):Disable() end end LevelFuncs.OnLoad = function() end LevelFuncs.OnSave = function() end LevelFuncs.OnStart = function() end LevelFuncs.OnLoop = function() OpenDoor() end LevelFuncs.OnEnd = function() end Code:
-- FILE: Levels\My_level.lua function OpenDoor() bitflags1, bitflags2, bitflags4, bitflags8, bitflags16 = 0, 0, 0, 0, 0 if GetMoveableByName("switch_type4_17"):GetAnim() == 1 then bitflags1 = 1 end if GetMoveableByName("switch_type4_18"):GetAnim() == 1 then bitflags2 = 2 end if GetMoveableByName("switch_type4_19"):GetAnim() == 1 then bitflags4 = 4 end if GetMoveableByName("switch_type4_20"):GetAnim() == 1 then bitflags8 = 8 end if GetMoveableByName("switch_type4_21"):GetAnim() == 1 then bitflags16 = 16 end if GetMoveableByName("switch_type4_22"):GetAnim() == 1 then bitflags8 = 8 end if GetMoveableByName("switch_type4_23"):GetAnim() == 1 then bitflags16 = 16 end if bitflags1 | bitflags2 | bitflags4 | bitflags8 | bitflags16 == 31 then GetMoveableByName("door_type4_16"):Enable() else GetMoveableByName("door_type4_16"):Disable() end end LevelFuncs.OnLoad = function() end LevelFuncs.OnSave = function() end LevelFuncs.OnStart = function() end LevelFuncs.OnLoop = function() OpenDoor() end LevelFuncs.OnEnd = function() end Or, if you want that that Lara can use only one-one alternative for the fourth and fifth switches: Code:
-- FILE: Levels\My_level.lua function OpenDoor() bitflags1, bitflags2, bitflags4, bitflags8, bitflags16 = 0, 0, 0, 0, 0 if GetMoveableByName("switch_type4_17"):GetAnim() == 1 then bitflags1 = 1 end if GetMoveableByName("switch_type4_18"):GetAnim() == 1 then bitflags2 = 2 end if GetMoveableByName("switch_type4_19"):GetAnim() == 1 then bitflags4 = 4 end if GetMoveableByName("switch_type4_20"):GetAnim() == 1 then bitflags8 = bitflags8 + 8 end if GetMoveableByName("switch_type4_21"):GetAnim() == 1 then bitflags16 = bitflags16 + 16 end if GetMoveableByName("switch_type4_22"):GetAnim() == 1 then bitflags8 = bitflags8 + 8 end if GetMoveableByName("switch_type4_23"):GetAnim() == 1 then bitflags16 = bitflags16 + 16 end if bitflags1 | bitflags2 | bitflags4 | bitflags8 | bitflags16 == 31 then GetMoveableByName("door_type4_16"):Enable() else GetMoveableByName("door_type4_16"):Disable() end end LevelFuncs.OnLoad = function() end LevelFuncs.OnSave = function() end LevelFuncs.OnStart = function() end LevelFuncs.OnLoop = function() OpenDoor() end LevelFuncs.OnEnd = function() end Bitwise "exclusive OR" ("XOR") operation and custom "Midas switches" There could also be an "XOR" bitwise operation between the same bits of two binary numbers. The operation is marked by XOR word or ⊕ symbol. If the bits are the same (both set or both unset), then the result is "unset", but if they are different (one of them is set, the other one is unset), then the result is "set": 0 ⊕ 0 = 0, 0 ⊕ 1 = 1, 1 ⊕ 1 = 0. For example: 9-----8-----7------6-----5-----4---3---2---1---0 → powers of 2 (bit ID) 512--256--128---64---32---16---8---4---2---1 → decimal bit value 0-----0-----0------1-----0----1----1---0---0---1 → 89 1-----1-----1------0-----0----1----1---0---1---1 → 923 1-----1-----1------1-----0----0----0---0---1---0 → 962 So the result is 89 XOR 923 = 962. This method should be also familiar, even in the classic Tomb Raider level building: I mean, I am sure you can remember the unforgettable multiswitch setup of Palace Midas. (Or I also discuss it here.) I mean, the essence of the setup is that you need to use the same switches to open different doors - but each door has its own specific switch combination, using only some of all the switches. If all the bit flags (Bit1, Bit2, Bit3, Bit4, Bit5) are set for a specific door, then that door will open. Let's see the explanation, with an example: There are three switches, with these bit flags ticked at their triggers: 5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 1----0-----0---1---1---0 → Switch trigger1: 38 0----0-----1---1---0---0 → Switch trigger2: 12 1----1-----1---0---0---0 → Switch trigger3: 56 You want switch1 and switch2 to open together Door A, and switch1 and switch3 to open together Door B. Let's see first what switch1 and switch2 do together: 5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 1----0-----0---1---1---0 → Switch trigger1: 38 0----0-----1---1---0---0 → Switch trigger2: 12 1----0-----1---0---1---0 → Result: 42 If you want Door A to open, then you need the final result to be 62 (11111), so these bit flags need to be set at the door object: 5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 1----0-----1---0---1---0 → Switch trigger1+Switch trigger2: 42 0----1-----0---1---0---0 → Door A: 20 1----1-----1---1---1---0 → Result: 62 Now Bit1, Bit2, Bit3, Bit4 and Bit5 are all set, the door will open. Or aggregated: 5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 1----0-----0---1---1---0 → Switch trigger1: 38 0----0-----1---1---0---0 → Switch trigger2: 12 0----1-----0---1---0---0 → Door A: 20 1----1-----1---1---1---0 → Result: 62 So 38 XOR 12 XOR 20 = 62. (So 62 = all the five bits are set.) And now let's see first what switch1 and switch3 do together: 5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 1----0-----0---1---1---0 → Switch trigger1: 38 1----1-----1---0---0---0 → Switch trigger3: 56 0----1-----1---1---1---0 → Result: 30 So Door B should be: 5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 0----1-----1---1---1---0 → Switch trigger1+Switch trigger3: 30 1----0-----0---0---0---0 → Door B: 32 1----1-----1---1---1---0 → Result: 62 Or aggregated: 5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 1----0-----0---1---1---0 → Switch trigger1: 38 1----1-----1---0---0---0 → Switch trigger3: 56 1----0-----0---0---0---0 → Door B: 32 1----1-----1---1---1---0 → Result: 62 So 38 XOR 56 XOR 32 = 62. (So 62 = all the five bits are set.) Any other combinations won't open any door - for example: 5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 0----0-----1---1---0---0 → Switch trigger2: 12 1----1-----1---0---0---0 → Switch trigger3: 56 1----1-----0---1---0---0 → Result: 52 and then: 5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 1----1-----0---1---0---0 → Switch trigger2 + Switch trigger3: 52 0----1-----0---1---0---0 → Door A: 20 1----0-----0---0---0---0 → Result: 32 Or: 5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 1----0-----1---0---1---0 → Switch trigger1+Switch trigger2: 42 1----0-----0---0---0---0 → Door B: 32 0----0-----1---0---1---0 → Result: 10 Just as I said above for bitwise OR, this feature is also available in TEN, but you need a full TEN setup for some customization. Eg. I want a setup, where seven switches open two doors - switch17, 18, 21 and 23 to open door16 and switch 18, 19, 22 and 23, to open door26. (Switch20 is a trick now, it doesn't trigger anything.) And I will use seven bits this time - it has nothing to do with using the same amount of switches now, I could use eg. even twelve bits, to make a really complex code. (Again: I can't set these bits at trigger/object panels now.) - Please note that is also accidental that we use the same amount of switches for each door, I mean you could make a setup eg. even having five switches for one door and nine switches for another one. 6----5-----4----3---2---1---0 → powers of 2 (bit ID) 64---32---16---8---4---2---1 → decimal bit value 1----1----0-----1---0---1---0 → Switch17: 106 0----1----1-----1---0---0---1 → Switch18: 57 1----0----1-----0---0---1---1 → Result17+18: 83 -------------------------------- 1----0----0-----1---1---1---0 → Switch21: 78 0----0----1-----1---1---0---1 → Result17+18+21: 29 -------------------------------- 1----1----0-----0---0---1---1 → Switch23: 99 1----1----1-----1---1---1---0 → Result17+18+21+23: 126 -------------------------------- 0----0----0-----0---0---0---1 → Door16: 1 1----1----1-----1---1---1---1 → Final Result: 127 106 XOR 57 XOR 78 XOR 99 XOR 1 = 127 6----5-----4----3---2---1---0 → powers of 2 (bit ID) 64---32---16---8---4---2---1 → decimal bit value 0----1----1-----1---0---0---1 → Switch18: 57 0----0----1-----0---1---0---1 → Switch19: 21 0----1----0-----1---1---0---0 → Result18+19: 44 -------------------------------- 1----1----0-----0---0---0---1 → Switch22: 97 1----0----0-----1---1---0---1 → Result18+19+22: 77 -------------------------------- 1----1----0-----0---0---1---1 → Switch23: 99 0----1----0-----1---1---1---0 → Result17+18+21+23: 46 -------------------------------- 1----0----1-----0---0---0---1 → Door26: 81 1----1----1-----1---1---1---1 → Final Result: 127 57 XOR 21 XOR 97 XOR 99 XOR 81 = 127 In LUA scripts, we use ~ symbol instead of the general XOR/⊕ signs (the 1 and 81 values, which should be adjusted at the doors, are constant numbers in this setup): Code:
-- FILE: Levels\My_level.lua function OpenDoors() switch17, switch18, switch19, switch21, switch22, switch23 = 0, 0, 0, 0, 0, 0 if GetMoveableByName("switch_type4_17"):GetAnim() == 1 then switch17 = 106 end if GetMoveableByName("switch_type4_18"):GetAnim() == 1 then switch18 = 57 end if GetMoveableByName("switch_type4_19"):GetAnim() == 1 then switch19 = 21 end if GetMoveableByName("switch_type4_21"):GetAnim() == 1 then switch21 = 78 end if GetMoveableByName("switch_type4_22"):GetAnim() == 1 then switch22 = 97 end if GetMoveableByName("switch_type4_23"):GetAnim() == 1 then switch23 = 99 end if switch17 ~ switch18 ~ switch19 ~ switch21 ~ switch22 ~ switch23 ~ 1 == 127 then GetMoveableByName("door_type4_16"):Enable() else GetMoveableByName("door_type4_16"):Disable() end if switch17 ~ switch18 ~ switch19 ~ switch21 ~ switch22 ~ switch23 ~ 81 == 127 then GetMoveableByName("door_type4_26"):Enable() else GetMoveableByName("door_type4_26"):Disable() end end LevelFuncs.OnLoad = function() end LevelFuncs.OnSave = function() end LevelFuncs.OnStart = function() end LevelFuncs.OnLoop = function() OpenDoors() end LevelFuncs.OnEnd = function() end "switch17 ~ switch18 ~ switch19 ~ switch21 ~ switch22 ~ switch23 ~ 1" for door16: 106 XOR 57 XOR 0 XOR 78 XOR 0 XOR 99 XOR 1 = 127 and "switch17 ~ switch18 ~ switch19 ~ switch21 ~ switch22 ~ switch23 ~ 81" for door26: 0 XOR 57 XOR 21 XOR 0 XOR 97 XOR 99 XOR 81 = 127 So when a "wrong" switch isn't used, then it won't change the sum value, because its own value is 0. Otherwise it will, having a different own value, so the sum value cannot be 127, even when when all the "right" switches have been used yet. Don't forget to place one Switch trigger and one fake Trigger on the sector of each trigger, just like in the bitwise "OR" example. (Including the sector of that tricky switch, which also needs to work as if that did something.) The function with binary numbers - use them if you wish: Code:
function OpenDoors() switch17, switch18, switch19, switch21, switch22, switch23 = 0, 0, 0, 0, 0, 0 if GetMoveableByName("switch_type4_17"):GetAnim() == 1 then switch17 = tonumber("1101010", 2) end if GetMoveableByName("switch_type4_18"):GetAnim() == 1 then switch18 = tonumber("0111001", 2) end if GetMoveableByName("switch_type4_19"):GetAnim() == 1 then switch19 = tonumber("0010101", 2) end if GetMoveableByName("switch_type4_21"):GetAnim() == 1 then switch21 = tonumber("1001110", 2) end if GetMoveableByName("switch_type4_22"):GetAnim() == 1 then switch22 = tonumber("1100001", 2) end if GetMoveableByName("switch_type4_23"):GetAnim() == 1 then switch23 = tonumber("1100011", 2) end if switch17 ~ switch18 ~ switch19 ~ switch21 ~ switch22 ~ switch23 ~ tonumber("0000001", 2) == tonumber("1111111", 2) then GetMoveableByName("door_type4_16"):Enable() else GetMoveableByName("door_type4_16"):Disable() end if switch17 ~ switch18 ~ switch19 ~ switch21 ~ switch22 ~ switch23 ~ tonumber("1010001", 2) == tonumber("1111111", 2) then GetMoveableByName("door_type4_26"):Enable() else GetMoveableByName("door_type4_26"):Disable() end end I think it is enough if you simply keep this formula in mind: NOT X = - X - 1 So eg. NOT 89 = -90. Just at XOR operations, the sign of the operation is ~ in LUA - but this time not between two numbers, but only before one number: ~ 89. (Not really a useful operation, for a casual builder, like you, though.) There also are "not AND" ("NAND") and "not OR" ("NOR") bitwise operations, but they are not really important to you in TEN scripting. Besides, they don't have initial LUA functions. Binary shifting At binary shifting each bit of the binary number moves to left or right, with scripting like this: A << X → The bits of Number A move left with X position B >> Y → The bits of Number B move right with Y position For example: 5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 0----0-----1---0---1---0 → The number which will move left: 10 0----1-----0---1---0---0 → 10 << 1 = 20 1----0-----1---0---0---0 → 10 << 2 = 40 5-----4----3---2---1---0 → powers of 2 (bit ID) 32---16---8---4---2---1 → decimal bit value 0----0-----1---0---1---0 → The number which will move right: 10 0----0-----0---1---0---1 → 10 >> 1 = 5 0----0-----0---0---1---0 → 10 >> 2 = 2 As you can see:
Many times this operation could be useful for color codes. A color code is a really huge number, and you need shifts to split it into red, green, blue components. Or the opposite: to merge the components into the huge code. - However, in TEN you could already see Color(R, G, B) preset TEN function to define a color with its components, you don't need a workaround for that with binary shifting. Last edited by AkyV; 12-11-24 at 12:08. |
09-10-24, 19:40 | #7 |
Moderator
Joined: Dec 2011
Posts: 5,074
|
6. String operations
Let's sum up that how many things you have already learnt so far about texts (I mean: strings) in my tutorials about scripting techniques:
Quotation Instead of double quotation marks, you can also place the text even between single quotation marks or double square brackets - so these are all the same: "Hello, Lara!" 'Hello, Lara!' [[Hello, Lara!]] The thing that you can have more than one possibility, could be really useful if you'd like to print quotation marks in that text. In this case use a mixed solution: so one of "", '' or [[]] signs will signal it as a text, while the other symbol, inside this text, will be printed as a quotation mark - for example: 'Zip said: "Hello, Lara!"' "Zip said: 'Hello, Lara!'" The version with brackets is a splendid solution when you have no choice. I mean, when you need to type both quotation marks and apostrophes in the text: [[Lara said: "it's splendid!"]] If you would like to use the same quotation marks, both to signal the text and to print these marks, then type the printable ones with a backslash:
'Lara said: "it\'s splendid!"' (That should be not '' - i.e. twice a single quote - but " - i.e. once a double quote - where you can see \'' - backslash+2 x single quote -, but if I type " - double quote - there, then the forum engine will remove the backslash.) Cursor position You could change the cursor position while the text is being printed (don't work in texts signaled by double square brackets):
This function tells that there are how many characters in a text (including spaces, exclamation marks etc.): string.len("Hello, Lara!") → 12 Character position This function will search for the coordinates of a part in some text: local a, b = string.find("Hello, Lara!", "Hello,") → 1, 6 So the "Hello," part in "Hello, Lara!" text starts at the first character of the text, and it ends at the sixth character of the text. You also need to know:
This function will search for the presence of a part in some text: local a = "Hello, Lara!" string.match(a, "Hello") → Hello So the "Hello" part is presented in "Hello, Lara!" text. That is why the function result is this part itself. If there is no match, then the function result is nil. So you can do a condition here: "if the function result is not nil, then print the function result". So if local a is "Hello, Lara!", then "Hello" will be printed. But if you change the variable value meanwhile, eg. to "Good bye, Lara!", then nothing will be printed. You also need to know:
A substring is a text separated from some bigger text. This function will do substrings: local a = "Hello, Lara!" string.sub(a, 1, 6) → Hello, string.sub(a, 8) → Lara! The further arguments after the text argument:
Substitution The function of string.gsub will swap a part of some text for other characters: ShowString(DisplayString(string.gsub("Hello, Winston!", "Winston!", "Zip!"), 250, 350, Color(255,0,0)), 1/30) → Hello, Zip! You also need to know:
The function of string.rep will repeat the text the required times: ShowString(DisplayString(string.rep("Lara", 3), 250, 350, Color(255,0,0)), 1/30) → LaraLaraLara Use one more argument for a separator between them: string.rep("Lara", 3, " ") → Lara Lara Lara Printing backwards This function will turn the characters of a text into the opposite order, so they will be printed backwards: string.reverse("Hello, Lara!") → !araL ,olleH ASCII codes For any reason, you can call a character even via its ASCII (decimal) code, using this function (except ASCII extended characters): string.char(97) → a ShowString(DisplayString(string.char(76) .. string.char(97) .. string.char(114) .. string.char(97), 250, 350, Color(255,0,0)), 1/30) → Lara Alternatively, you can also type a backslash, to call that code in a text: "L\97r\97" → Lara This function does the opposite, telling the ASCII code of a character: string.byte("a") → 97 Or if you search for a code in a longer text, then use a second argument, which tells the position of the required character in this text (otherwise it will be the code of the first character): string.byte("Lara", 4) → 97 Please note that in this tutorial we don't examine how you can print ASCII extended characters or Unicode characters in TEN. Inserting values in a string You already know tostring function to convert a number into some text. But you can be more specific to insert values in strings, if you use string.format function - for example: local sum = 30 string.format("%i plus %i is %i", 10, 20, sum) → 10 plus 20 is 30 So the function could have numerous arguments. The first is the input text with one or more % directives, and the others are values to be inserted in the text. The function result is almost the input text itself - except that you will see the values-to-be-inserted in the output text where you can see the directives in the input text: the first value-to-be-inserted will be written where you can see the first directive, the second value-to-be-inserted will be written where you can see the second directive etc. The letter code in the directive tells that how the directive will recognize the input value. For example, %i means that value will be used as a (decimal) integer number, even if the value is defined in a different form. So either you really type 20 or its hexadecimal version (0x14), the value will be inserted (and printed) as 20. The directives are:
ShowString(DisplayString(string.format("%+014.5f", 127.68), 250, 350, Color(255,0,0)), 1/30) → +0000127.68000 As you can see, even if the + sign of the number (just like the - sign, anyway) is counted in the 14 characters of the width value, the meaningless zeros are (naturally) printed after the + sign. - But if that 0 isn't in the code (%+14.5f), then the empty spaces instead of the zeros are naturally printed before the + (or -) sign. string.format("%#+-8g", 76.4) → +76.4000 As I said above at +-/-+, the order of the flags could be anything, so that #+- could be even #-+, -#+, -+#, +-#, +#-. string.format("%084.6g", 79) → 000079 The precision makes six digits, including four zeros before the number. That is why that 0 in the code doesn't have an effect now, further zeros won't be printed, instead empty spaces will be printed. string.format("%#.10x", 0xFA73D8) → 0x0000fa73d8 As you can see, the 0x prefix isn't included in the ten digits defined by the precision. Patterns A pattern could be, after all, any key which you use to search for matches in a text. For example, in the "for part in string.gmatch(a, "Lara") do" example above "Lara" word is a pattern. Or, in the "string.gsub("Hello, Winston?", "Winston%?", "Zip?")" example above "Winston%?" is also a pattern. But there are not only "normal" patterns like that, but even special patterns, which are not printable characters, because they refer to character groups. For example there is a special pattern which refers to the character group of letters. Just like when you signal an inserting directive, or a magic character as a printable one, special patterns are basically also signaled with % sign in LUA. Eg. "%a" pattern is for the letter characters. Or "%s" pattern is for the empty space characters. Each appearance of a special pattern is only for one character. For example, the matches for %a could be a, A, b, B, c, C etc. So the text of "aBc" could have even three matches for %a. It depends on the searching type that how many matches a pattern will have in a text - for example:
The special patterns are:
Notes:
Last edited by AkyV; 05-11-24 at 18:29. |
Bookmarks |
Thread Tools | |
|
|