When Construct's JavaScript coding feature was launched in 2019, all the scripts in a project ran as classic mode scripts. This is how JavaScript has run in web pages since its introduction in the 1990s. However modern JavaScript now uses modules - a newer and better way to organise scripts and their dependencies.
Construct 3 r226 added support for using modules for your project's JavaScript code. In Construct 3 r251, support for the legacy classic mode was removed. This tutorial will guide you through what's changed and how to update your code, in case you have any old projects still using classic mode.
Classic mode vs. modules
In classic mode:
- All your project's script files are loaded automatically on startup.
- You can't control the order script files are loaded in.
- Global variables and functions can be used everywhere, including in other scripts, and in scripts in event blocks.
In module mode:
- Only the project main script is loaded automatically on startup.
- Other script files aren't used unless you
import
them.
- You can control the order script files are loaded by the order you import them.
- Each module has its own scope. This means top-level variables and functions cannot be used outside the module, unless:
- they are exported, and imported where they are used (recommended)
- they are explicitly made global by adding them as properties on the global object (not recommended)
Using modules helps you organise your code better. For example, making all your variables and functions global is generally regarded as poor design, is prone to problems like naming clashes, and makes re-using code more difficult. Modules allow you to write clean, self-contained pieces of code that have clearly defined dependencies, and export a clearly defined interface for other code to use. Individual modules can then easily be re-used in different places.
You can learn more about how modules work in JavaScript at the MDN guide on JavaScript modules.
Changing modes
In Project Properties inside the Advanced section is a Scripts type setting. You can use this to change between Classic and Module modes. You should switch your project to use Modules mode. This affects how script files in your project are loaded, and you may have to make changes, as explained in the following steps.
Changing modes only affects script files. If you only use scripts in event blocks and don't have any script files at all, you don't need to do anything. Construct will automatically switch your project to use modules and everything will continue working as it did before.
assign a main script
Once you've switched to modules mode, the first thing you need to do is tell Construct which is your main script. This is the only script that will be loaded on startup. All other scripts then need to be imported by this script.
Find the script you want to use as your main script in the Project Bar. Usually this is the script which has a call to runOnStartup
. Select the script in the Project Bar, and some properties appear in the Properties Bar for it. Change the Purpose property to Main script. The main script also appears bold in the Project Bar.
add exports
Next, for every other script file your project uses, look through it and identify which functions, classes and objects need to be made available to other script files. These need to be exported using the export
keyword.
For example consider a JavaScript file utilities.js that defines a function add:
// utilities.js
function add(a, b)
{
return a + b;
}
This function can be exported by adding the export
keyword:
// utilities.js
export function add(a, b)
{
return a + b;
}
The module now exports a function named add. You can add lots of exports, as well as a default export.
You can learn more about the export syntax at the MDN guide on the export statement.
add imports
Now starting with the main script, add imports to load other script files, and make the things they export available. Continuing the previous example, the main script may have formerly relied on calling a global function add, since it was defined at the top level:
// main.js
console.log(add(2, 3));
Now you can tell the main script to load utilities.js and use the things it exports with the import
statement:
// main.js
import * as Utils from "./utilities.js";
console.log(Utils.add(2, 3));
Note that:
- The
*
means "import everything from this script"
- The script's exports are given the name
Utils
, so the exported method add is accessed with Utils.add()
.
- The script URL has to start with
./
You may wish to think about how to arrange all your scripts and their imports and exports so everything is organised and grouped in to relevant modules.
You can learn more about the import syntax at the MDN guide on the import statement. There are lots of ways to import scripts, including only importing certain things, renaming imports, importing just the default export, and so on.
update scripts in events
If you don't use any scripts in event blocks, you can skip this step.
Suppose you have a script block in an event sheet that also called add
, using it as a global function. Once you've changed your script files to modules, this method is no longer available globally - it has to be imported. However you cannot use import
in script blocks. (Internally each script block is its own function, and you cannot import inside a function.)
To solve this, you can add another script with the purpose Imports for events. Add a new script file named importsForEvents.js. Select it in the Project Bar, and in the Properties Bar, set its Purpose property to Imports for events. Now anything you import in this file can be accessed by all scripts in events.
For example you can also import utilities.js here the same way the main script does:
// importsForEvents.js
import * as Utils from "./utilities.js";
Now your script blocks in event sheets can also use Utils.add()
.
You can also define functions and variables in this file which will be accessible to all scripts in events (but not other script files).
Useful patterns
Utilities/helper functions
It's often useful to have a series of helper or utility functions that are used across a range of scripts. The prior example demonstrates importing a utilities script. You can import scripts in multiple places, e.g. using import * as Utils from "./utilities.js";
in several scripts, and re-using the same set of helper functions everywhere. The module will only be loaded once even if it's imported multiple times, ensuring everything works efficiently.
A module for globals
Remember that modules have their own scope. If you declare a top-level variable, it's not global - it's only available in that module. So, similar to a utilities script, you may want to make a module to represent all global variables your code uses, and import that in to every script.
However module exports are read-only. This means if you export a variable like this:
export let myGlobalVariable = 0;
...then myGlobalVariable
cannot be changed by any scripts importing it, as if it were actually declared with const
. So this approach only works for constants.
Instead you can export globals as an object with properties instead, like so:
// globals.js
const Globals = {
myGlobalVariable: 0,
myOtherGlobal: 1,
// Example global function
someGlobalFunction()
{
/* ... */
}
};
export default Globals;
Now scripts can access global state like so:
// main.js
import Globals from "./globals.js";
// Change a global variable
Globals.myGlobalVariable = 1;
Note this example uses a default export. The import Globals from ...
statement only imports the default export, which in this case is the object with all our global variables.
This approach is better than using global variables, since it keeps everything better organised, avoids clashing with hundreds of names in the browser's own global namespace, and makes it easier to debug since you can easily view just your own global variables rather than everything.
Access global scope
The prior example of a global module is the recommended way to manage global state in your game. However if you really need to break out of the module's own scope and access the full global scope, you can do it by accessing properties on globalThis
, e.g.:
globalThis.myGlobalScopeVariable = 0;
globalThis.myGlobalScopeFunction = function ()
{
/* ... */
}
These can then be accessed everywhere so long as you consistently use them as properties of globalThis
.
Import the main script
In your Imports for events script, you can also import the main script, like so:
// importsForEvents.js
import * as Main from "./main.js";
Now your scripts in events can call exported functions in the main script, e.g. Main.myFunction()
. Remember to add the export
keyword to those functions in the main script, since otherwise they won't be imported.
Conclusion
Support for classic mode scripts has now been removed, so any old projects will need updating. Using modules is a modernised and better-organised way to manage scripting in Construct. You may have to make some changes to your code, but the result should be cleaner code that is easier to manage and re-use in other projects.