Skip to content

GameMaker Workflow

In this section, we will cover how ScaffScript fits into your GameMaker project lifecycle.

The scaff.config.cjs (from pnpm or npm) or scaff.config.ts (from bun) file is the main configuration file for ScaffScript. It contains all the settings for your project. This config is created automatically when you run bun|pnpm|npm create @scaffscript/project@latest.

Here’s minimal config you can use:

scaff.config.cjs
module.exports = {
clearOutputDir: false,
noIntegration: false,
path: {
// path aliases...
},
production: false,
tabType: "1t",
targetPlatform: "all",
useGmAssetPath: true,
// other options...
};

We’ll use this structure for this example:

  • Directorysrc/
    • Directoryfs/
      • control.ss
      • input.ss
      • output.ss
    • Directoryutils/
      • debug.ss
      • math.ss
    • index.ss

Let’s say we’ve code each module in fs and utils directory, and we want to use them in index.ss.

  1. Create the main logic first in the index.ss

    Section titled “Create the main logic first in the index.ss”
    src/index.ss
    import { log } from "./utils/debug"
    include { read_file } from "./fs/input"
    include { append_file } from "./fs/output"
    @content log
    var data = read_file("data.txt");
    log(data);
    append_file("data.txt", "Hello, World!");

    This code will read the data.txt file, log the content, then append the text “Hello, World!” to the data.txt file.

  2. src/index.ss
    intg { main } to "./scripts/Test/scrHello"
    import { log } from "./utils/debug"
    #[main]
    include { read_file } from "./fs/input"
    include { append_file } from "./fs/output"
    @content log
    var data = read_file("data.txt");
    log(data);
    append_file("data.txt", "Hello, World!");
    • #[main] integration block defines the content that will be generated when the main block is included in the intg statement.
    • intg { main } to "./scripts/Test/scrHello" statement maps the main integration block to the scrHello script asset under Test folder.
    • import statement is moved outside of the #[main] block, because we need to import the log function before the main block is executed.
    • Test in the intg statement is a virtual path. It will be created as a group/folder in the GameMaker IDE. Read more about virtual path in the Code Generation page.
.out/scripts/Test/scrHello/scrHello.gml
function read_file(file_path) {
// implementation...
}
function append_file(file_path, text) {
// implementation...
}
function log(message) {
// implementation...
}
var data = read_file("data.txt");
log(data);
append_file("data.txt", "Hello, World!");

We’ll integrate it to scrHello script asset.

  1. Run bun|pnpm|npm run generate to compile and integrate

    Section titled “Run bun|pnpm|npm run generate to compile and integrate”
    Terminal window
    bun run generate

    It will generate the scrHello.gml file in the .out/ directory, then integrate it to the scrHello script asset in the GameMaker project.

  2. Open your GameMaker project, and you should see a window like this:

    GameMaker IDE Reload Window

    Click the Reload button to reload the changes. Then, open the scrHello script asset in the GameMaker IDE, and you should see the compiled GML code.

  3. Check your terminal, it should waiting for your input to accept or revert the changes.

    Terminal window
    [INPUT] Revert file scrHello.gml from scripts/Test/scrHello? (y/N) ->

    Type y to revert the changes, or just press Enter to accept the changes.

That’s the basic workflow of using ScaffScript in your GameMaker project.


If you let ScaffScript to create the asset for you, you might won’t see the asset in the GameMaker IDE immediately. You need to re-open the project to see the created asset.

ScaffScript creates the asset in the .yyp file. If you see your GameMaker project name in the reload window, it means the GameMaker IDE is aware of the changes in the .yyp file. But, if you don’t, you need to re-open the project to see the created asset.

There are 2 solutions for this problem:

  1. Add the asset manually in the GameMaker IDE before running the generate command Recommended.
  2. Re-open the GameMaker project.

Back then in ScaffScript initial design, we can’t write to object events directly, not even any workaround. But we realize, it’s kind of “weird” that we can’t write to object events, even though we can write to script assets. So, we added a way to write to object events. Here’s how it works:

  • The last segment of the path in the intg statement is the object asset name.
  • The segment must include objects/ in the path to be recognized as an object asset.
  • The integration block must have an event targeting. For example: #[create as create], #[show_info as keydown:keyboard_f1], etc.
  • Integration blocks with no event won’t be written to the object asset.
  • ScaffScript will create the necessary event files for you.
src/index.ss
intg { create, show_info, keypress:keyboard_space, no_event } to "./objects/Test/objController"
#[create as create]
show_debug_message("create event!");
#[show_info as keydown:keyboard_f1]
show_debug_message("F1 pressed!");
#[keypress:keyboard_space Event]
show_debug_message("Space pressed!");
#[no_event]
show_debug_message("This won't be shown");

And then, run bun|pnpm|npm run generate to compile and integrate.

  • intg { create, show_info, keypress:keyboard_space, no_event } to "./objects/Test/objController" statement maps the create, show_info, keypress:keyboard_space, and no_event integration blocks to the objController object asset under Test folder.
  • #[create as create] integration block targets the create event of the objController object asset.
  • #[show_info as keydown:keyboard_f1] integration block targets the keydown:keyboard_f1 event of the objController object asset.
  • #[keypress:keyboard_space Event] integration block targets the keypress:keyboard_space event of the objController object asset.
  • #[no_event] integration block has no event targeting, so it won’t be written to the objController object asset.
  • You can fill the content inside each integration block as you would normally do in a script asset.
  • For safety purpose, you should create the objController object asset in the GameMaker IDE first, and create the necessary events before running the generate command.
  • Directory.out/
    • Directoryobjects/
      • DirectoryTest/
        • DirectoryobjController/
          • Create_0.gml
          • Keyboard_32.gml
          • Keyboard_112.gml
        • objController.gml // for the non-event integration block
.out/objects/Test/objController/Create_0.gml
show_debug_message("create event!");

ScaffScript has a feature to filter out integration blocks based on the dev or prod flags. This is useful when you want to have different behavior in development1 and production2 stage. Here’s how it works:

  • Content inside #[main -- dev] or #[main -- development] will be removed when production: true in the scaff.config.cjs|ts.
  • Content inside #[main -- prod] or #[main -- production] will be removed when production: false in the scaff.config.cjs|ts.
  • You can have multiple dev and prod blocks in a single file, even with the same block name.
src/index.ss
intg { main } to "./scripts/Test/scrHello"
#[main -- dev]
show_debug_message("Only in development!");
#[main -- prod]
show_debug_message("Only in production!");

production: false in scaff.config.cjs|ts:

.out/scripts/Test/scrHello/scrHello.gml
show_debug_message("Only in development!");

It’s a simple yet powerful feature that can help you to test your code in different stages. Here’s other example of how you can use it:

src/index.ss
intg { main } to "./scripts/Test/scrHello"
import { log } from "./utils/debug"
#[main]
include { read_file } from "./fs/input"
include { append_file } from "./fs/output"
@content log
#[main -- dev]
var data = undefined;
try {
data = read_file("data.txt");
log(data);
show_debug_message("Data read successfully!");
}
catch (err) {
show_debug_message("Error reading data: " + err);
}
try {
append_file("data.txt", "Hello, World!");
show_debug_message("Data written successfully!");
}
catch (err) {
show_debug_message("Error writing data: " + err);
}
#[main -- prod]
var data = read_file("data.txt");
log(data);
append_file("data.txt", "Hello, World!");
  • #[main] block will be included in both development and production stage.
  • #[main -- dev] block is used for testing the read_file and append_file functions in development stage. It will be removed in production stage.
  • #[main -- prod] block is used for the actual logic of the script. It won’t be included in development stage.
  • After testing phase in development stage is done, you can be sure that the read_file and append_file functions are working as expected, so you can remove the prod flag in the third main integration block, and safely use them in production stage.
    • Why removing the prod flag instead of removing the #[main -- dev] block? Because you might need to re-test the functions again in the future, and you don’t want to re-write the test code every time.
    • And in case you need to test it again, you can just add the prod flag back to the third main integration block, and re-run the generate command.

production: false in scaff.config.cjs|ts:

.out/scripts/Test/scrHello/scrHello.gml
function read_file(file_path) {
// implementation...
}
function append_file(file_path, text) {
// implementation...
}
function log(message) {
// implementation...
}
var data = undefined;
try {
data = read_file("data.txt");
log(data);
show_debug_message("Data read successfully!");
}
catch (err) {
show_debug_message("Error reading data: " + err);
}
try {
append_file("data.txt", "Hello, World!");
show_debug_message("Data written successfully!");
}
catch (err) {
show_debug_message("Error writing data: " + err);
}

ScaffScript also has a feature to write platform specific code. This is useful when you want to have different behavior in different platforms. Here’s how it works:

  • Content inside #[main -- <platform>] will be included only when targetPlatform: "<platform>" in the scaff.config.cjs|ts.
  • Content inside #[main -- !<platform>] will be included only when targetPlatform is not set to "<platform>" in the scaff.config.cjs|ts.
  • You can have multiple platform flags in a single block, even with the same block name.
index.ss
#[main]
show_debug_message("On all platforms!");
#[main -- windows]
show_debug_message("Windows only extra content!");
#[htmlBlock -- !html5]
show_debug_message("Anything but HTML5!");
#[htmlBlock -- html5]
show_debug_message("This is from HTML5!");

Let’s test it with configuring the targetPlatform to windows, android, and html5 in the scaff.config.cjs|ts.

targetPlatform: "windows" in scaff.config.cjs|ts:

index.ss
#[main]
show_debug_message("On all platforms!");
show_debug_message("Windows only extra content!");
#[htmlBlock]
show_debug_message("Anything but HTML5!");

By default, after you run the generate command, ScaffScript will prompt you to review the changes, whether to accept or revert the changes.

Terminal window
[INPUT] Revert file <filename> from <path>? (y/N) ->

Type y to restore the original content. Otherwise, the new content will be kept.

Terminal window
[INPUT] Remove file <filename> from <path>? (y/N) ->

Type y to delete the file and remove it from the .yyp. Otherwise, the file will be kept.


Here’s the TL;DR of the development loop using ScaffScript:

  1. Create your logic in .ss files.
  2. Add integration blocks (#[...]) and intg statement to one or more .ss files (index.ss is preferred).
    • Optional: Create the target asset in the GameMaker IDE based on the last segment of the intg path. For example, if your intg statement is intg { main } to "./scripts/scrHelloWorld", create a scrHelloWorld script asset in the GameMaker IDE.
  3. Run bun|pnpm|npm run generate to compile it.
    • Make sure you’ve set the correct GameMaker project path in the package.json scripts.
    • Recommended: Review the compiled GML code in the .out/ directory.
  4. Apply changes in GameMaker IDE by clicking the Reload button.
    • Choose whether to accept or revert the changes for each file when prompted.
  5. Repeat!
  1. Development: In ScaffScript context, it means a condition where your ScaffScript project isn’t ready to be published yet. In integration block context, it means the content inside the block is only relevant in development stage, and should be removed in production stage.

  2. Production: The opposite of development.