[JS] Writing javascript modules for C3

6

Features on these Courses

Attached Files

The following files have been attached to this tutorial:

.c3p

Stats

2,484 visits, 3,262 views

Tools

Translations

This tutorial hasn't been translated.

License

This tutorial is licensed under CC BY 4.0. Please refer to the license text if you wish to reuse, share or remix the content contained within this tutorial.

Published on 25 Feb, 2020. Last updated 19 Apr, 2021

C3 does Javascript in a bit of an odd way. Things don't exactly work the way you might expect them to work if you're used to regular Javascript in a browser.

In fact there are two main problems you absolutely need to deal with when writing JS scripts in C3:

1 - You can not predict in which order the js files will be executed

2 - You can not use import and export because all the script files are compiled to a single javascript file when the game is exported, AND because files don't have a predictable path to import from.

All you're left with is the will to write code that should work no matter the order in which scripts are executed, and that should not overlap on each other because they all end up in the same scope.

The solution

The solution to this is by using closures.

In Javascript, you can create a local scope by writing your code inside a function:

function code() {
    let localVariable = 10
}
code();
console.log(localVariable); //This will fail to find the variable

However, to define that new scope, you still define a function in the global scope that might cause an overlap. Moreover, naming the function means that anyone can call it again and run your script again, which is not what you want to do.

Instead, we can use anonymous functions

(function() {
    let localVariable = 10
})()

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:

.C3P

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.

  • 2 Comments

  • Order by
Want to leave a comment? Login or Register an account!
  • that is awesome, this pattern is great for code reuseability.

      • [-] [+]
      • 1
      • skymen's avatar
      • skymen
      • Affiliate
      • 1 points
      • *
      • (0 children)

      Indeed :)

      I want to make more JS modules like that and expand on what we can actually do in JS, and especially make it easier for non developers to use them