That way, we have the same local scope, and it's still executed, but the function doesn't have a name, and doesn't exist outside of these parenthesis.
If you're wondering why it looks like this, it is because of the way JS treats expressions.
(value)
using parenthesis is just like using a variable that contains the return value of the content of said parenthesis. This means that if you initalize variables, make calculations, and then end your code with a value, the parenthesis will hold that last value. In our case, it holds the definition of our anonymous function. Since these parentheses are now a function, we can execute it. By using a new set of parenthesis ()
.
Functional programming
This expression system works very similarly to functional programming languages, and it's both confusing and fun to mess around with. For instance, here is a simple function that translates a hex color string to rgba:
function hexToRGB(hex, alpha = 1) {
let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
let rgb = result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
}
: {
r: 0,
g: 0,
b: 0
};
return `rgba(${rgb.r},${rgb.g},${rgb.b},${alpha})`;
}
hexToRGB("#FFFFFF", 0.5);
Now here is the same function using the expression system:
((hexToRGB = (hex, alpha) => ((result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)), (rgb = result && {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} || {
r: 0,
g: 0,
b: 0
}), (`rgba(${rgb.r},${rgb.g},${rgb.b},${alpha})`))),
(hexToRGB("#FFFFFF", 0.5)))
Anyway, I digress...
An example of a module
Because a concrete example is much clearer than lengthy explanations, here is a module that I wrote:
/*****=== MODULE: TIMEOUT AND INTERVALS ====**********/
/*****Author: skymen **********/
/*****Description: Exports two functions: **********/
/***** interval: works like setInterval **********/
/***** timeout: works like setTimeout **********/
/***** **********/
/*****Both count time in seconds instead of **********/
/*****milliseconds and respect time scale **********/
/***** **********/
/*****======================================**********/
(function () {
// Init and get runtime
runOnStartup(async runtime =>
{
// Code to run on the loading screen.
// Note layouts, objects etc. are not yet available.
runtime.addEventListener("beforeprojectstart", () => OnBeforeProjectStart(runtime));
});
function OnBeforeProjectStart(runtime)
{
// Code to run just before 'On start of layout' on
// the first layout. Loading has finished and initial
// instances are created and available to use here.
runtime.addEventListener("tick", () => Tick(runtime));
runtime.getAllLayouts().forEach(layout => {
layout.addEventListener("beforelayoutstart", () => StartOfLayout(layout, runtime))
});
}
// Init local vars
let timeouts = [];
let curTime = 0;
// Export functions to global scope
globalThis.timeout = (callback, duration, isInterval = false) => {
timeouts.push({
callback,
duration,
current: duration,
isInterval
});
}
globalThis.interval = (callback, duration) => {
globalThis.timeout(callback, duration, true);
}
// Local functions for more processing
function StartOfLayout(layout, runtime) {
timeouts = [];
}
function Tick(runtime)
{
let dt = runtime.gameTime - curTime;
curTime = runtime.gameTime
for(let i = 0; i < timeouts.length; i++) {
let cur = timeouts[i];
cur.current -= dt;
if (cur.current <= 0) {
cur.callback()
if (cur.isInterval) {
cur.current = cur.duration;
} else {
timeouts.splice(i, 1);
i--;
}
}
}
}
})()
What that module does is that it exports two functions that work exactly like setInterval and setTimeout, but with 2 changes:
1 - It respects the time scale
2 - The callbacks are not executed if the layout changes or is restarted
It works kinda like the timer behavior, but in JS.
Here is a c3p file that makes use of this behavior if you want to look into it:
The structure
Let's analyse its structure
(function(){
runOnStartup(async runtime =>
{
// Code to run on the loading screen.
// Note layouts, objects etc. are not yet available.
runtime.addEventListener("beforeprojectstart", () => OnBeforeProjectStart(runtime));
});
function OnBeforeProjectStart(runtime)
{
// Code to run just before 'On start of layout' on
// the first layout. Loading has finished and initial
// instances are created and available to use here.
}
let localVar; //Defines a local variable
globalThis.globalVar; //Defines a global variable
globalThis.globalFunction = () => {} //Defines a global function
function localFunction () {} //Defines a local function
)()
Conclusion
Using that new structure, you make absolutely sure that each module will work completely independently, no matter what other JS code is ran. The only possible overlap happens in the global variables and global functions definition, but that is pretty much inevitable.
You also make sure that runtime is accessed cleanly and doesn't need to be exposed to the global scope.
And in case you'd rather not make functions accessible globally, you can define your global variables and functions on the runtime object, they will still be globally accessible anywhere in your code, but not to the end user. It would look like this in that case:
runOnStartup(async runtime =>
{
let localVar; //Defines a local variable
runtime.globalVar; //Defines a global variable
runtime.globalFunction = () => {} //Defines a global function
function localFunction () {} //Defines a local function
});
The difference in that case is that your module will not run until the game starts, but most of the time, this will not matter.