Skip to content

Working with Modules

Let’s dive deeper into how to use the module system properly in ScaffScript.

Module in ScaffScript is a .ss file that only used for exporting variables, functions, etc. These are called exports. Here’s the common exports you can use in ScaffScript:

ExportDescription
export varExport a variable. Compiles to local variable (var) in GML.
export letExport an instance variable. Compiles to instance variable in GML.
export constExport a constant. Compiles to global constant (#macro) in GML.
export functionExport a function. Compiles to global function in GML, like when you’re creating a function in a script asset in the GameMaker IDE.
export enumExport an enum. Compiles to enum in GML.
export classExport a class. Compiles to struct constructor in GML.

ScaffScript provides two ways to import modules: import and include. They both serve the same purpose of importing modules, but they have different syntax and behavior. Here’s the difference between them:

importinclude
Syntaximport { ExportsName } from "path/to/file"include { ExportsName } from "path/to/file" | include { "filename" } from "path/to/dir"
Content Directives
Inline ContentNeed @content directiveDirect inlining
Use CaseGeneral purposeInlining module content
Exports Alias
.gml Inlining

Let’s use my-file module as an example. It exports a variable and a function.

my-file.ss
export var x_point = 10;
export function hello() {
show_debug_message("Hello, World!");
}
index.ss
import { x_point, hello } from "./my-file"
@content hello
hello();
@content x_point
show_debug_message($"x_point = @:x_point");
index.ss
import { x_point, hello } from "./my-file"
@content hello
function hello() {
show_debug_message("Hello, World!");
}
hello();
@content x_point
show_debug_message($"x_point = x_point");
var x_point = 10;
show_debug_message($"x_point = 10");
index.gml
function hello() {
show_debug_message("Hello, World!");
}
hello();
var x_point = 10;
show_debug_message($"x_point = 10");

export * from is called barrel export. It’s useful for re-exporting modules from a deep path with a more convenient path.

Let’s say you have this structure:

  • Directorysrc/
    • Directorylib/
      • Directorymath/
        • Directorysimple/
          • vector2.ss
          • vector3.ss
        • Directorycomplex/
          • matrix2-2.ss
          • matrix3-3.ss
        • index.ss
      • Directoryutils/
        • my-utils.ss
        • other-utils.ss
        • yet-another-utils.ss
      • index.ss
    • index.ss

Without the re-export, you would have to import the modules from the deep path, like this:

src/index.ss
import { read_multi_file } from "./lib/utils/my-utils"
import { Vector2, Vector3 } from "./lib/math/simple/vector2"
import * from "./lib/math/complex/matrix3-3"

It’s tedious and error-prone, especially when you’re working with a large project. This is where export * from shines. You can re-export all exports from a module in lib/index.ss, like this:

src/lib/math/index.ss
export * from "./simple/vector2"
export * from "./simple/vector3"
export * from "./complex/matrix2-2"
export * from "./complex/matrix3-3"
src/lib/index.ss
// ./utils doesn't have index.ss, so we import the exports from the deep path
export * from "./utils/my-utils"
export * from "./utils/other-utils"
export * from "./utils/yet-another-utils"
// ./math has index.ss, and we already re-export all exports from it in math/index.ss
export * from "./math"

Now you can import the exports from lib/index.ss like this:

src/index.ss
import { read_multi_file } from "./lib"
import { Vector2, Vector3 } from "./lib"
import * from "./lib"

You have a deeply nested module, but you don’t want either to use the full path, or re-export it? Path aliases are here to help you!

Let’s use the same structure as the export * from example above:

  • Directorysrc/
    • Directorylib/
      • Directorymath/
        • Directorysimple/
          • vector2.ss
          • vector3.ss
        • Directorycomplex/
          • matrix2-2.ss
          • matrix3-3.ss
        • index.ss
      • Directoryutils/
        • my-utils.ss
        • other-utils.ss
        • yet-another-utils.ss
      • index.ss
    • index.ss

You can add path aliases in scaff.config.* to make it easier to import the modules. Here’s an example:

scaff.config.cjs
module.exports = {
clearOutputDir: false,
noIntegration: true,
path: {
"@vec2": "./lib/math/simple/vector2",
"~vec3": "~/lib/math/simple/vector3",
"Math": "./lib/math",
"@utils-1": "./lib/utils/my-utils",
"Utils-other": "./lib/utils/other-utils",
"Lib": "./lib",
},
production: false,
tabType: "1t",
targetPlatform: "all",
useGmAssetPath: true
// other options...
};

And then, you can use the aliases in your modules:

src/index.ss
import { Vector2 } from "@vec2"
import { Vector3 } from "~vec3"
import * from "Math"
import { read_multi_file: read_multi_file_from_utils_1 } from "@utils-1"
import { read_multi_file: read_multi_file_from_utils_other } from "Utils-other"

A path that starts with ./ or without any prefix, will be resolved relative to the importing file’s directory.

scaff.config.*
// --- snip ---
path: {
"@vec2": "./lib/math/simple/vector2",
"Math": "./lib/math",
"@utils": "lib/utils"
},
// --- snip ---
src/index.ss
import { Vector2 } from "@vec2"
import { Vector3 } from "Math"
  1. Current directory is ./src.
  2. The @vec2 path alias is resolved to "./src/lib/math/simple/vector2".
    • So, it’ll import Vector2 from "./src/lib/math/simple/vector2".
  3. The Math path alias is resolved to "./src/lib/math".
    • So, it’ll import Vector3 from "./src/lib/math".

Opposite to relative paths, absolute paths are resolved from the root of the project. It uses tilde (~) as the prefix. The root itself is based on the source option in scaff.config.*.

scaff.config.*
// --- snip ---
path: {
"~vec2": "~/lib/math/simple/vector2",
"Math": "~/lib/math",
"~utils": "~/lib/utils"
},
source: "./src",
// --- snip ---
src/index.ss
import { Vector2 } from "~vec2"
import { Vector3 } from "Math"
  1. Absolute path doesn’t care about the importing file’s directory.
  2. The ~vec2 path alias is resolved to "/src/lib/math/simple/vector2".
    • So, it’ll import Vector2 from "/src/lib/math/simple/vector2".
  3. The Math path alias is resolved to "/src/lib/math".
    • So, it’ll import Vector3 from "/src/lib/math".

ScaffScript supports wildcard paths in path aliases. It uses * as the wildcard character. Here are the rules:

  • * must be the last character of both the path alias name and the resolved path alias.
  • * matches any number of characters and path segments, including none.
  • You can use * in both relative and absolute paths.
scaff.config.*
// --- snip ---
path: {
"@math/*": "./lib/math/*",
"~utils/*": "~/lib/utils/*"
},
source: "./src",
// --- snip ---
src/index.ss
import { Vector2 } from "@math/simple/vector2"
import { Matrix33 } from "@math/complex/matrix3-3"
import { read_file } from "~utils/my-utils"
  1. Current directory is ./src.
  2. The @math/simple/vector2 path alias is resolved to "./src/lib/math/simple/vector2".
    • So, it’ll import Vector2 from "./src/lib/math/simple/vector2".
    • It’ll work, because the module ("./src/lib/math/simple/vector2") exists.
  3. The @math/complex/matrix3-3 path alias is resolved to "./src/lib/math/complex/matrix3-3".
    • So, it’ll import Matrix33 from "./src/lib/math/complex/matrix3-3".
    • It’ll work, because the module ("./src/lib/math/complex/matrix3-3") exists.
  4. The ~utils/my-utils path alias is resolved to "/src/lib/utils/my-utils".
    • So, it’ll import read_file from "/src/lib/utils/my-utils".
    • It’ll work, because the module ("/src/lib/utils/my-utils") exists.
  1. Use @ prefix, ~ prefix, or uppercase for path alias names to avoid conflicts with normal paths.
  2. Use absolute paths over relative paths, as its persistent behavior makes it more predictable.
  3. Use * wildcard character to make your path aliases more flexible. So, you won’t waste your time remapping your path aliases when your project grows.

After importing a module with import, you can use the content directives to inline the module’s content. Here are the commonly used content directives:

DirectiveUsage
@contentInlines the full compiled GML declaration of the module.
@valueofInlines the right hand side or the body of the module.
@:Shorthand for @valueof.
my-file.ss
export var x_point = 10;
export function hello() {
show_debug_message("Hello, World!");
}
index.ss
import { x_point, hello } from "./my-file"
@content hello
hello();
var my_method = function() {
show_debug_message("This is my method!");
@content x_point
var my_x = @:x_point;
show_debug_message($"my_x * x_point = {my_x * x_point}");
}
index.ss
import { x_point, hello } from "./my-file"
@content hello
function hello() {
show_debug_message("Hello, World!");
}
hello();
var my_method = function() {
show_debug_message("This is my method!");
@content x_point
var my_x = @:x_point;
var x_point = 10;
var my_x = 10;
show_debug_message($"my_x * x_point = {my_x * x_point}");
}

  • import alone doesn’t emit anything.
  • If you set a path alias target to "./some/path", the resolved paths are relative to the importing file, not from the root of the project.
  • Forgetting that include inlines the full declaration, not just the value.
  • Using export * from recklessly leads to naming conflicts.