Skip to content

Real-World Examples

In this section, we will re-write some real-world GameMaker libraries using ScaffScript.

We’ll only generate the GML code to the .out/ directory in this section. So, we need to set noIntegration: true in the scaff.config.cjs|ts.

scaff.config.cjs
module.exports = {
clearOutputDir: false,
noIntegration: false,
noIntegration: true,
production: false,
tabType: "1t",
targetPlatform: "all",
useGmAssetPath: true
// other options...
};
A lightweight, self-contained GML script for creating smooth animations
Section titled “A lightweight, self-contained GML script for creating smooth animations”

Author: @brodady

Repo: https://github.com/brodady/cassette

License: MIT

We choose this library because it’s a simple library that doesn’t have any external dependencies. It’s a good starting point to learn how to use ScaffScript in a real-world scenario. The code we’ll use in this example is the code from the main branch of the repository.

  1. We’ll only cover the src/ directory in this example.

    If you’re creating the library from GameMaker IDE, your project structure may look like this:

    • DirectoryCassette/
      • Constants // enum and #macro
      • Main // Cassette main struct constructor
      • Tape // CassetteTape internal struct constructor

    Or maybe like this:

    • Cassette // just one big script asset

    If you’re using ScaffScript, your project structure may look like this:

    • Directorysrc/
      • Directorycassette/ // Cassette main struct constructor
        • Directorycontrol/
          • navigation.ss
          • setter.ss
          • state.ss
        • Directoryeasing/
          • back.ss
          • bounce.ss
          • circ.ss
          • cubic.ss
          • elastic.ss
          • expo.ss
          • quad.ss
          • quart.ss
          • quint.ss
          • sine.ss
        • Directorygeneral/
          • custom.ss
          • getter.ss
          • transition.ss
          • update.ss
        • Directoryinternal/
          • calc.ss
          • manager.ss
          • track.ss
        • Cassette.ss
      • Directorycassette-tape/ // CassetteTape internal struct constructor
        • callback.ss
        • CassetteTape.ss
        • control.ss
        • setter.ss
      • Directoryconstants/ // exported constants
        • back.ss
        • bounce.ss
        • elastic.ss
        • index.ss
      • index.ss
      • types.ss

    It may look scary at first, but it’s actually pretty simple. Each file is responsible for a specific functionality, and the files are organized into folders based on their purpose.

    Of course, this is just one way to organize the code, and you can choose a different structure that works best for your project.

  2. We’re gonna export some types and constants first, because they’re used in multiple places.

    types.ss
    export enum CASSETTE_ANIM {
    ONCE,
    LOOP,
    PING_PONG,
    HOLD
    }
    constants/back.ss
    export const CASSETTE_BACK_S1 = 1.70158;
    export const CASSETTE_BACK_S2 = (CASSETTE_BACK_S1 * 1.525);
    export const CASSETTE_BACK_C1 = CASSETTE_BACK_S1;
    export const CASSETTE_BACK_C2 = CASSETTE_BACK_S2;
    export const CASSETTE_BACK_C3 = (CASSETTE_BACK_S1 + 1);
  3. cassette-tape/CassetteTape.ss
    export class __CassetteTape {
    constructor(_managerRef)
    __manager = _managerRef;
    __queue = _managerRef.__queue;
    }
    cassette-tape/setter.ss
    impl __CassetteTape {
    /// @function from(valueOrStruct)
    /// @desc Sets the start value. Can be a Real or a Struct.
    /// @return {Struct.__CassetteTape}
    static from = function(_val) {
    var _last = array_last(__queue);
    _last.__fromVal = _val;
    if (array_length(__queue) == 1) {
    __manager.__currentVal = _val;
    }
    return self;
    };
    59 collapsed lines
    /// @function to(valueOrStruct)
    /// @desc Sets the end value.
    /// @return {Struct.__CassetteTape}
    static to = function(_val) {
    var _last = array_last(__queue);
    _last.__toVal = _val;
    return self;
    };
    /// @function duration(secondsOrFrames)
    /// @return {Struct.__CassetteTape}
    static duration = function(_val) {
    var _last = array_last(__queue);
    _last.duration = _val;
    return self;
    };
    /// @function ease(functionOrCurve)
    /// @desc Sets the easing function or Animation Curve struct.
    /// @return {Struct.__CassetteTape}
    static ease = function(_func) {
    var _last = array_last(__queue);
    _last.__easingFunc = _func;
    if (is_struct(_func) && variable_struct_exists(_func, "__isAnimCurve")) {
    _last.__isCurve = true;
    } else {
    _last.__isCurve = false;
    }
    return self;
    };
    /// @function loop([times])
    /// @desc Repeats THIS track. -1 = Infinite.
    /// @return {Struct.__CassetteTape}
    static loop = function(_times = -1) {
    var _last = array_last(__queue);
    _last.__animState = CASSETTE_ANIM.LOOP;
    if (_times != -1) _times += 1;
    _last.__loopsRemaining = _times;
    if (array_length(__queue) == 1) __manager.__loopsRemaining = _times;
    return self;
    };
    /// @function pingpong([times])
    /// @desc PingPongs THIS track. -1 = Infinite.
    /// @return {Struct.__CassetteTape}
    static pingpong = function(_times = -1) {
    var _last = array_last(__queue);
    _last.__animState = CASSETTE_ANIM.PING_PONG;
    _last.__loopsRemaining = _times;
    if (array_length(__queue) == 1) __manager.__loopsRemaining = _times;
    return self;
    };
    }
  4. cassette-tape/CassetteTape.ss
    export class Cassette {
    constructor(_useDeltaTime = false, _autoStart = false, _defaultLerp = lerp)
    __activeTransitions = {};
    __activeKeyList = [];
    __scheduler = [];
    __useDeltaTime = _useDeltaTime;
    __defaultAutoStart = _autoStart;
    __defaultLerp = _defaultLerp;
    }
    cassette/calc.ss
    impl Cassette {
    /// @desc (Internal) Interpolates Reals or Structs.
    __calculateCurrentValue = function(_from, _to, _progress, _lerpFunc) {
    if (is_struct(_from)) {
    var _result = {};
    var _keys = variable_struct_get_names(_to);
    var _len = array_length(_keys);
    var _i = 0;
    repeat(_len) {
    var _k = _keys[_i];
    // FIXED: Safety check to prevent crash if 'from' is missing the key
    var _fromVal = variable_struct_exists(_from, _k) ? _from[$ _k] : _to[$ _k];
    _result[$ _k] = _lerpFunc(_fromVal, _to[$ _k], _progress);
    _i++;
    }
    return _result;
    } else {
    return _lerpFunc(_from, _to, _progress);
    }
    };
    17 collapsed lines
    /// @desc (Internal) Calculates and sets the current value based on time/queue.
    __evaluateAndSetValue = function(_manager) {
    var _def = _manager.__queue[_manager.__currentIndex];
    if (_def.__isWait) return;
    var _progress = (_def.duration <= 0) ? 1 : clamp(_manager.__timer / _def.duration, 0, 1);
    var _eased = 0;
    if (_def.__isCurve) _eased = animcurve_channel_evaluate(_def.__easingFunc.channel, _progress);
    else _eased = _def.__easingFunc(_progress);
    _manager.__currentVal = __calculateCurrentValue(_def.__fromVal, _def.__toVal, _eased, _manager.__lerpFunc);
    if (is_method(_def.__onUpdate)) _def.__onUpdate(_manager.__currentVal);
    };
    }
    cassette/transition.ss
    impl Cassette {
    /// @function transition(key, [lerp_func])
    /// @desc Creates a new transition chain.
    /// @param {String} key Unique identifier for this transition.
    /// @param {Function} [lerp_func] Optional custom lerp function.
    /// @return {Struct.__CassetteTape} A ChainBuilder instance to configure the animation.
    transition = function(_key, _lerpFunc = __defaultLerp) {
    var _firstDef = {
    label: "Start",
    __fromVal: 0, __toVal: 0, duration: 1.0,
    __easingFunc: Cassette.InQuad,
    __isCurve: false,
    __animState: CASSETTE_ANIM.ONCE, __loopsRemaining: -1,
    __onTrackEnd: undefined,
    __onUpdate: undefined,
    __isWait: false
    };
    36 collapsed lines
    var _manager = {
    __queue: [_firstDef],
    __currentIndex: 0,
    __lerpFunc: _lerpFunc,
    __onSequenceEnd: undefined,
    // Control Callbacks
    __onPlayCb: undefined,
    __onPauseCb: undefined,
    __onStopCb: undefined,
    __onRewindCb: undefined,
    __onFfwdCb: undefined,
    __onSeekCb: undefined,
    __onSkipCb: undefined,
    __onBackCb: undefined,
    // State
    __currentVal: 0,
    __reactVel: 0,
    __timer: 0,
    __direction: 1,
    __loopsRemaining: 1,
    __isPaused: !__defaultAutoStart,
    __playbackSpeed: CASSETTE_DEFAULT_PLAYBACK_SPEED,
    __isFinished: false
    };
    if (!variable_struct_exists(__activeTransitions, _key)) {
    array_push(__activeKeyList, _key);
    }
    __activeTransitions[$ _key] = _manager;
    return new __CassetteTape(_manager);
    };
    }
    cassette/navigation.ss
    impl Cassette {
    /// @function ffwd([keys])
    ffwd = function(_keys = undefined) {
    __applyToManagers(_keys, function(_manager, _data, _key) {
    if (is_method(_manager.__onFfwdCb)) _manager.__onFfwdCb();
    _manager.__currentIndex = array_length(_manager.__queue) - 1;
    var _lastDef = _manager.__queue[_manager.__currentIndex];
    _manager.__timer = _lastDef.duration;
    _manager.__direction = 1;
    _manager.__loopsRemaining = 0;
    if (!_lastDef.__isWait) _manager.__currentVal = _lastDef.__toVal;
    if (is_method(_manager.__onSequenceEnd)) _manager.__onSequenceEnd();
    variable_struct_remove(__activeTransitions, _key);
    });
    };
    47 collapsed lines
    /// @function rewind([keys])
    rewind = function(_keys = undefined) {
    __applyToManagers(_keys, function(_manager) {
    if (is_method(_manager.__onRewindCb)) _manager.__onRewindCb();
    __initTrack(_manager, 0);
    if (!__defaultAutoStart) _manager.__isPaused = true;
    });
    };
    /// @function seek(amount, [keys])
    seek = function(_amount, _keys = undefined) {
    // Wrap __seekManager to inject the callback trigger
    __applyToManagers(_keys, function(_manager, _amt, _k) {
    if (is_method(_manager.__onSeekCb)) _manager.__onSeekCb();
    other.__seekManager(_manager, _amt, _k);
    }, _amount);
    };
    /// @function skip([keys])
    skip = function(_keys = undefined) {
    __applyToManagers(_keys, function(_manager, _data, _key) {
    if (is_method(_manager.__onSkipCb)) _manager.__onSkipCb();
    if (_manager.__currentIndex + 1 < array_length(_manager.__queue)) {
    __initTrack(_manager, _manager.__currentIndex + 1);
    } else {
    var _lastIndex = array_length(_manager.__queue) - 1;
    var _lastDef = _manager.__queue[_lastIndex];
    __initTrack(_manager, _lastIndex, _lastDef.duration);
    if (is_method(_manager.__onSequenceEnd)) _manager.__onSequenceEnd();
    variable_struct_remove(__activeTransitions, _key);
    }
    });
    };
    /// @function back([keys])
    back = function(_keys = undefined) {
    __applyToManagers(_keys, function(_manager) {
    if (is_method(_manager.__onBackCb)) _manager.__onBackCb();
    if (_manager.__currentIndex > 0) __initTrack(_manager, _manager.__currentIndex - 1);
    else __initTrack(_manager, 0);
    });
    };
    }
    cassette/back.ss
    impl Cassette {
    /// @function InBack(progress)
    static InBack = function(_progress) {
    return CASSETTE_BACK_C3 * _progress * _progress * _progress - CASSETTE_BACK_C1 * _progress * _progress;
    };
    13 collapsed lines
    /// @function OutBack(progress)
    static OutBack = function(_progress) {
    return 1 + CASSETTE_BACK_C3 * power(_progress - 1, 3) + CASSETTE_BACK_C1 * power(_progress - 1, 2);
    };
    /// @function InOutBack(progress)
    static InOutBack = function(_progress) {
    return (_progress < 0.5)
    ? (power(2 * _progress, 2) * ((CASSETTE_BACK_C2 + 1) * 2 * _progress - CASSETTE_BACK_C2)) / 2
    : (power(2 * _progress - 2, 2) * ((CASSETTE_BACK_C2 + 1) * (_progress * 2 - 2) + CASSETTE_BACK_C2) + 2) / 2;
    };
    }
    cassette/expo.ss
    impl Cassette {
    /// @function InExpo(progress)
    static InExpo = function(_progress) {
    return (_progress == 0) ? 0 : power(2, 10 * _progress - 10);
    };
    14 collapsed lines
    /// @function OutExpo(progress)
    static OutExpo = function(_progress) {
    return (_progress == 1) ? 1 : 1 - power(2, -10 * _progress);
    };
    /// @function InOutExpo(progress)
    static InOutExpo = function(_progress) {
    if (_progress == 0) return 0;
    if (_progress == 1) return 1;
    return (_progress < 0.5) ?
    power(2, 20 * _progress - 10) / 2 : (2 - power(2, -20 * _progress + 10)) / 2;
    };
    }
  5. All exports are set, the next step is to generate the Cassette library using ScaffScript.

    In this example, we’ll use the include statements, because it’s the most straightforward way to do it, but you can’t control the exports fully like import does.

    index.ss
    intg { main } to "./scripts/scrCassette"
    #[main]
    /// --- Cassette ---
    /// @desc A lightweight, self-contained GML script for creating smooth animations.
    /// @ver 2.3.1 (Patched lerp function error on the react method, temporary fix for now)
    include * from "./constants";
    include { CASSETTE_ANIM } from "./types";
    include { __CassetteTape } from "./cassette-tape/CassetteTape";
    include { Cassette } from "./cassette/Cassette";
    • intg { main } to "./scripts/scrCassette"

    Generate a new script named scrCassette and export the main integration block to it.

    • #[main]

      This is an integration block called main.

    • /// --- Cassette --- ...

      This is a documentation comment.

    • include * from "./constants";

      Include all exports from the constants file to the main integration block.

    • include { CASSETTE_ANIM } from "./types";

      Include the CASSETTE_ANIM export from the types file to the main integration block.

    • include { __CassetteTape } from "./cassette-tape/CassetteTape";

      Include the __CassetteTape export from the cassette-tape/CassetteTape file to the main integration block.

    • include { Cassette } from "./cassette/Cassette";

      Include the Cassette export from the cassette/Cassette file to the main integration block.

  6. Run the generate command to generate the Cassette library.

    Terminal window
    bun run generate

    And then, check the generated file at .out/scripts/scrCassette/scrCassette.gml.

In this example, we’ve covered:

  • How to design ScaffScript project structure.
    • Module organization
    • Naming conventions
    • Integration points
  • How to use ScaffScript to generate code.