In the previous chapter, we have learned the basics of installing modules and importing them into level scripts, by using the require() instruction.
I also mentioned that modules can come with or without customizable parameters. Whether a module has parameters or not is up to the coder, the author of the module. As (only) a builder, you are limited to modifying values of these given parameters. This is not to say you can’t (or shouldn’t) branch out and learn how to change module code yourself – if anything, I’d encourage it, once you get comfortable with using modules! But going by our idealized categorizations of plugin users, a builder (with no knowledge nor interest in coding) never interferes with module code directly, and only changes parameters implemented by the coder.
Parameters provide a way to modify selected aspects of the particle effect, without having to change the module code itself. An example could be changing the sprite texture used by the particles. Or their color, or size, or duration, or determine how many particles are spawned and how often. With more advanced modules, they can determine how much damage a particle can do to Lara or an enemy. If the particle has an area of effect, a parameter can describe the radius or range of that area. A parameter can specify the NGLE Script ID of the moveable object that will serve as a particle emitter or a target for a projectile. When dealing with mesh particles, parameters may determine which object slot and which mesh of the slot will be used for these particles. If the particles play certain sound effects, a parameter can control which sound effect is played.
As you see, parameters can control and influence many different things. Because of this, customizable module parameters are quite reminiscent of the Customize= and Parameters= script commands in TRNG, which can describe various customizations of some TRLE or TRNG feature. However, what exactly is customizable varies between one feature and another, and you cannot change any aspects that are not explicitly given a parameter in the script command. The same can be said about module customization: what is customizable for a module depends on the module itself (and the intentions of the module’s author). It is the responsibility of the coder to communicate how many parameters a module has, what their names are and provide information about what each parameter is responsible for in the module (the same way CUST_ and PARAM_ constants have their parameters explained in the TRNG reference).
In this chapter, I will explain how the parameter system works and how the syntax changes when importing a module with parameters (there is a bit more Lua syntax involved here, than for simply importing with require).
As mentioned above, module parameters can be well understood by TRNG users through an analogy to parameters in Customize= and Parameters= scripts. The parameter list for each individual CUST_ or PARAM_ constant is different, and the same applies to modules. Furthermore, just as parameters in the CUST_ / PARAM_ scripts often have default values (that are used if you don’t set a particular parameter, e.g. by typing IGNORE), module parameters have these default values too. If you do not set any value to the parameter yourself, the module will use the default value set by the coder instead (you literally ignore the parameters you do not wish to change). However, this does not mean that the default value that the parameter falls back to is always useful!
To illustrate this with an example: imagine that one parameter asks for the NGLE script ID of a moveable, which could be the emitting item to which the particle effect will be attached. The coder gave this parameter some arbitrary placeholder value, like 55. Notice, the moveable object which has that specific NGLE script ID will be different for each level map. It may even turn out that there is no object with that NGLE script ID in the level map yet! Hence the “default value” of this parameter is quite useless. And as such, with some module parameters, the placeholder values must always be changed after import, because the “default” is not meaningful or not applicable to your level map. Coders, as module authors, should communicate which parameters need obligatory adjustment on the builder’s part, after a module is imported into a level script.
Types of parameters
Parameters for modules can be numbers, booleans or strings. I believe that number parameters should be self-explanatory (e.g. “a number for the particle’s lifetime”, “the number values of red, green and blue”), but I feel I should explain what booleans and strings are for.
Booleans in Lua can have two possible values: true or false. These words are always typed as lowercase to be seen as booleans. Neither True nor FALSE will be recognized as a boolean value by Lua, it must be all-lowercase. There is a high likelyhood you have some idea of what booleans are, but in a nutshell, they are used in mathematics and computer science to determine the logical value of a statement. For example, 2 > 1 (two larger than one) is true, while 3 < 2 (three smaller than two) is false. Booleans also can also be used as an on/off setting for something, with true usually representing the on-state and false representing off-state. As an example, the module implements a damaging projectile, which can optionally set Lara on fire, or just do regular damage with no fire. The coder can add a boolean parameter, which will answer the question: “Does the projectile set Lara on fire?”. The builder can then use the boolean parameter to answer with true (“yes”), so the projectile will indeed set her on fire. Answering with false (“no”) instead, the projectile will only hurt Lara, but will not set her on fire. Essentially, a boolean is useful for answering a yes or no question, enabling or disabling, turning something on / off.
Finally, we can have strings. We have already seen strings before actually, when we were importing the modules with require() by their name. The name of the module typed in quotes, as in require("firehead"), was actually a string! Strings are nothing more that some arbitrary text placed inside a (matching) pair of quotes. String parameters can therefore be used to provide arbitrary text data as input to a module. Admittedly, not that many features of the plugin call for the use of strings. However, it is possible to access values of the TRNG text variables (BigText, Last Input Text, Text1-4) with the plugin, so perhaps a module could do something with the text you provide to it via the string parameter. I will leave it up to creative module coders to figure it out, though!
To recap, module parameters can come in 3 different types:
number parameters (e.g. 1, 4, 30, 0.25, -10)
boolean parameters (true or false)
string parameters (e.g. "some text")
The coder should list all the parameters of the module, describing the type of each parameter (number, boolean or string), explain what it does, what is its default value and whether or not the default value should be always changed after importing.
Leading with example, in the Particle-Module-Templates repository, there is a module_descriptions.txt file, which provides short descriptions of the effect achieved by each module script, and what parameters the modules have (if any).
The require() instruction, in the form it was shown in the last chapter, imports a module by disregarding any parameters it may have (and assuming the default values of the parameters, which the coder must also provide). If the module, as indicated by the coder, has no parameters, then you might as well use the simplified import instruction, since it makes no difference:
require("party")
There is a more elaborate import syntax though, which gives access to the parameters of a module after importing it, allowing the builder to modify them freely in the level script. But to get there, we must explain a bit more about Lua scripting. Specifically, the concept of identifiers.
Identifiers in Lua
An identifier is simply a label given to something inside Lua. Examine the below line:
local fruit = 'apple'
Here, we have a string value, 'apple'. Before the string, there is this peculiar local fruit = construct. What we are doing here is creating a label (identifier) for the "apple" string, with the name fruit (in other words, setting fruit to equal 'apple'). The local word that comes before the fruit label is simply a syntactic requirement. I’m deliberately refraining from explaining what local actually means here, because to do so, I would also have to introduce many programming ideas which are not strictly needed to understand the motivation behind identifiers (and may potentially obscure the picture to a less technical-minded person). If you are really curious, the coder path provides these explanations. Otherwise, treat the local as a necessity for defining labels, the same way require() is a necessity for importing modules.
What purpose do identifiers have, you might ask? By giving something a label, we have an easy way to refer to it in Lua later on, by using that label.
Importing a module with an identifier
Why did I bring up these labels all of a sudden? Well, because we also can give labels to imported modules. And by doing so (labelling the module), we gain access to any parameters it may have!
Let me demonstrate a “labeled” module import:
local mylabel = require("party")
This will not only import the party.lua module, but also give it the identifier mylabel.
Each module can have a unique, case-sensitive identifier (an identifier shouldn’t be reused for a different module within the same level script file, but it can be safely reused across different level scripts). On top of being unique, the identifier must follow these rules, which are enforced by Lua’s syntax requirements:
it does not contain any spaces (e.g. my label)
it does not begin with any digit characters (e.g. 123label)
it uses only alphanumeric characters (lower- and uppercase letters, digits), meaning no punctuation or symbol characters like ! , . @ # $ % ^ ( ) + - = etc. (with one exception, the underscore _ character is allowed)
it is not any one of the following lower-case, reserved keywords (though uppercase versions of these words can technically be used):
and
break
do
else
elseif
end
false
for
function
goto
if
in
local
nil
not
or
repeat
return
then
true
until
while
As long as you follow the above rules, the label (identifier) for a module can be anything you want. Some valid identifier examples include: fire_effect, IceSparkles, Debris1, Debris2, BIG_NUKE_EXPLOSION, PuppyKittenBunny12345 (preferably though, make the name somewhat relevant to the module effect, it helps to keep things tidy).
Alright, we can now give modules these identifiers, but what’s the purpose of that? Well, if you have an unlabeled module, you cannot access its parameters, even if it may have some. Certain parameters can fall back on default values, which is not the worst case, you will simply have a default effect. The worst case is when the module has a mandatory parameter that always needs adjustment (like the NGLE script index example from earlier), but the unlabeled import prevents you from accessing it! That is why importing with an identifier is generally more recommended, unless you know for a fact that the module does not have any parameters you want to change.
To access a specific parameter of a module (e.g. in order to change it), you first must know how the coder named the parameter in question (again, the coder’s responsibility is to disclose this kind of information when distributing the module). For example, party.lua has a parameter called size, which is a number parameter describing the general size of the particles which spawn around Lara.
We will attempt to import the module and change its size parameter. Let’s open up a level script file (if needed, refer back to the previous chapter on how to create one). In the first line, we import the module with an identifier (label), as described before:
local party1 = require("party")
We have imported the module and gave it the identifier party1. Thanks to this, we can now refer to the module’s parameters! It’s a bit of a tricky syntax, but I think you can get the hang of it.
Having the party1 identifier, in a new line below the import, we can type the identifier again, this time without the local word in front (we only needed it for defining the identifier):
local party1 = require("party")
party1
Right after party1, we type a single dot, and after the dot, we add the word params:
local party1 = require("party")
party1.params
We’re almost there! We now have to type another dot, and after it the name of the parameter we are trying to access, in our case size:
local party1 = require("party")
party1.params.size
Whew! I will explain the meaning of these dots now, by comparing it to file paths in the operating system. If you imagine the module’s identifier as a folder that contains other folders and files, we can then view the dots as slashes in the path, for example:
party1
party1/params
party1/params/size
Of course, party1 and params aren’t actual folders, but there is a similar idea of hierarchy going on here. In order to access the size parameter, you must navigate to it from the “root” (party1), through the intermediate (params), allowing you to reach the final destination (size). Hopefully, this is clear enough and needs no further explanation.
Okay, that’s nice and dandy, but how do we actually change the parameter? Easy, afterwards we just write an equals sign and type the new value we want the parameter to have. Since size is a number parameter, we should use a number:
local party1 = require("party")
party1.params.size = 150
The size parameter has a default value of 75, which we get if we ignore it. By changing it to 150 though, we should cause the particles to be twice as large! Check in game to see if that’s indeed the case.
If the module has more than one parameter, we can change the other parameters too! We just have to do it in a new line. For example, on top of changing size, let’s change the radius parameter, controlling the radius of the area around Lara, in which the particles can spawn:
local party1 = require("party")
party1.params.size = 150
party1.params.radius = 1024
With radius having a default value of 512, increasing it to 1024 will make this spawning area bigger.
The party.lua module has two more parameters: sprite, which determines the sprite texture (from DEFAULT_SPRITES) used for the particles (default 14), and amount, which controls the number of spawning particles. Default is 1, meaning 1 particle per frame, but you can use bigger numbers for more particles, or decimal values between 0 and 1 for less.
Something to be wary of when attempting to customize parameters, is mistyping anything along the way, for example:
All three parameter changes have mistakes in them and will fail. The size change is incorrect, because we mistyped the module’s identifier, part1 instead of party1. The second one will fail because of two typos: the first is in Param (it’s lowercase params, with ‘s’ at the end), the second is in the name of the parameter itself, radious. And the final sprite parameter change is missing the mandatory params before the name of the actual name of the parameter, sprite.
Another thing to be cautious of is making sure the value you are assigning to the parameter matches its type. A number parameter can only take a number, a boolean parameter can only take a boolean (true or false) and a string parameter can only take a string ("text"). Because size and radius are number parameters, the below value changes are incorrect due to giving them mismatched value types (we assigned boolean true to size and string "I like pizza" to radius):
party1.params.size = true
party1.params.radius = "I like pizza"
Be mindful of the exact spelling and assign only correct value types to parameters – you should have no problems then.
That’s pretty much the whole science of customizing module parameters. The drill is:
Import the module with an identifier
Navigate to the desired parameter by using the dots (identifier.params.someName)
Change the parameter value (respecting the value type) with the equals sign =
We could leave this topic at that, but there is actually one more thing that I want to reveal to you on the subject of importing modules. I promise that it’s really cool and useful, though!
Accessing parameters is not the only reason why importing a module with an identifier is worth the hassle. You see, we are not limited to importing a module only once…
What happens if we import the same module twice, but using different identifiers?
local party1 = require("party")
local party2 = require("party")
Well, we get two distinct copies of said module! One we’ve called party1 and the other party2. On its own, this effectively doubles the effect (the count of spawned particles). That is not the most interesting thing about module copies, though.
Importantly, these module copies are distinct, meaning each copy gets its own, independent set of parameters! This means that if you change party1.params.size, that change does not apply to party2.params.size. To understand how this works, you can compare this to copying a file, like a text file. First of all, the copied file must get a different name (if it’s copied inside the same directory, at least). After that, the contents of both files is the same. But once you open the duplicated file, do some changes and save it, that does not mean the same changes were carried over to the original file! After the duplication, they are completely distinct files, each with their own contents. The same goes for duplicated modules and their parameters.
Why is this significant? Well, imagine a hypothetical module (magic.lua) which allows a particle effect to spawn around a specific moveable, by providing its NGLE script ID via a parameter (here we will call it objID). You import the module once, change the moveable ID parameter (e.g. objID = 5) and you now have the effect spawning around the moveable with ID 5. What if you want to have the same effect spawn around another moveable, with ID 6, but keep the effect that’s already tied to moveable 5? It’s as simple as importing the magic.lua module a second time, with a different identifier, and configuring its ID parameter to 6:
local magic_5 = require("magic")
magic_5.params.objID = 5
local magic_6 = require("magic")
magic_6.params.objID = 6
We would now have the magic effect conveniently spawning near both moveables at once!
This duplication mechanism allows to have different variants of the same effect, with a different set of parameters, making modules even more reusable and versatile. For example, you can have the same smoke effect at different locations and with a different color (assuming suitable parameters exist to allow both position variation and color variation). Another example could be adding a ranged projectile attack to a group of enemies, but for one of the enemies (perhaps a boss) to have a stronger projectile, that deals more than the default damage to Lara. You could import duplicates of the projectile module and increase the damage parameter for one of the duplicates (for the boss).
Of course, the usefulness of the module duplication functionality very much depends on what parameters the coder has set up for the module. Coders are encouraged to add such parameters to modules in the Coder’s Path tutorials, if applicable, so hopefully they will not disappoint and provide highly customizable modules to builders’ disposal.
Up to this point, we have covered 90% of what a builder should know about modules and level scripts. The parameter system in tandem with the module copy mechanism allow the builder to get far more out of modules, when compared to the simple, unlabeled import we have seen in the previous chapter.
The remaining 10% is finally revealing how to protect the integrity of these plain-text Lua scripts, once you package the finished custom level project, in preparation to upload it to a hosting site (like trle.net or trcustoms.org). However, this 10% makes all the difference if you want to prevent any devious players from altering the script files (e.g. by making harmful traps in your level heal Lara, instead)!
Do not skip this upcoming chapter, it is especially important to level builders! Take a break here, then we will continue our endevours.