GameMaker: Statics and method overriding

On the left, there's a regular-shaped dog that is captioned "Dog" and is saying "bof".
On the right, there's a small-looking dog that is captioned "LoudDog" and is saying "BOF".
If you remember

This is a post about how to call a function from a parent constructor in a same-named function in a child constructor and how static works in GameMaker Language in general.

What's a static, anyway

static in functions

Static variables persist between calls to the function:

function make_id() {
    static _id = 0;
    _id += 1;
    return _id;
}
trace(next_id()); // 1
trace(next_id()); // 2

This is similar to how static works in C++.

static in constructors

When used inside a constructor, it works the same as above, but also GM will look up any missing variables in a struct's constructor's statics:

function Test() {
    static a = 1;
    b = 2;
}
var test = new Test();
trace(test.a); // 1
trace(struct_exists(test, "a")); // false
trace(test.b); // 2
trace(struct_exists(test, "b")); // true

This is similar to how prototypes work in JavaScript.

Variables that aren't in a struct don't take up memory in it, which is particularly convenient for variables that are unlikely to be changed - such as methods!

static "sharing"

It might be tempting to mark all of your variables as static, but watch out! When used with by-reference types like arrays and structs, all of the structs will share that value:

function Test() constructor {
    static array = [];
}
var a = new Test();
var b = new Test();
trace(a.array); // []
trace(b.array); // []
array_push(a.array, 1);
trace(a.array); // [1]
trace(b.array); // [1] <- !!

To avoid this, give a struct a personal variable before modifying it:

function Test() constructor {
    static array = [];
}
var a = new Test();
var b = new Test();
trace(a.array); // []
trace(b.array); // []
a.array = [];
array_push(a.array, 1);
trace(a.array); // [1]
trace(b.array); // [] <- OK!

static inheritance

Just like how struct variables take priority over the constructor's statics, constructor's statics take priority over those from parent constructor(s):

function Par() constructor {
    static a = 1;
    static b = 2;
}
function Ctr() : Par() constructor {
    static b = 3;
    static c = 4;
}
var q = new Ctr();
trace(q.a); // 1
trace(q.b); // 3
trace(q.c); // 4

Method overriding

And with all that out of the way, we're finally here! Suppose you have the following

function Dog() constructor {
    static bark = function() {
        return choose("bof", "woof");
    }
}
function LoudDog() : Dog() constructor {
    static bark = function() {
        return string_upper(/*parent's bark*/());
    }
}

You cannot simply do bark() because that would refer to LoudDog's bark and lock up your program in a recursive call.

But there are options:

Modern

This works starting in whatever version where support for Script.staticVar was added. It does not work in LTS 2022.

function Dog() constructor {
    static bark = function() {
        return choose("bof", "woof");
    }
}
function LoudDog() : Dog() constructor {
    static bark = function() {
        return string_upper(Dog.bark());
    }
}

Static functions do not have a bound self so the call will use the current struct and it all works out!

Classic

This works in any version that has constructors.

function Dog() constructor {
    static bark = function() {
        return choose("bof", "woof");
    }
}
function LoudDog() : Dog() constructor {
    static Dog_bark = bark;
    static bark = function() {
        return string_upper(Dog_bark());
    }
}

Here we store the original bark() (from the parent constructor) before defining the one for LoudDog.

Performance

If you are doing this in a performance-critical context, I regret to inform that the "classic" method is 13..20% faster than the nicer-looking Dog.bark():

GMBenchmark test showing that `static Dog_bark = bark` is 13% faster than `Dog.back()` in VM and 20% faster in YYC

GMBenchmark code:

function Dog() constructor {
    static bark = function() {
        return 1;
    }
}
function LoudDog_dot() : Dog() constructor {
    static bark = function() {
        return Dog.bark();
    }
}
function LoudDog_rev() : Dog() constructor {
    static Dog_bark = bark;
    static bark = function() {
        return Dog_bark();
    }
}
Benchmarks = [
    new Benchmark("Overrides", [
        new TestCase("Parent.func()", function(iterations) {
            repeat (iterations) ldd.bark();
        }, function() {
            ldd = new LoudDog_dot();
        }),
        new TestCase("Parent_func()", function(iterations) {
            repeat (iterations) ldr.bark();
        }, function() {
            ldr = new LoudDog_rev();
        }),
    ]),
];

Bonus: caching

Bonus! Tero Hannula (drandula on GM forums) pointed out that you could cache the parent function in a static of a child function, like so:

function Dog() constructor {
    static bark = function() {
        return choose("bof", "woof");
    }
}
function LoudDog() : Dog() constructor {
    static bark = function() {
        static super = Dog.bark;
        return string_upper(super());
    }
}

Unfortunately, as of 2024.8 this is still slower than the "classic" approach. Implementation details?


And that's about it..?

Ah! But hold on, I have more dogs that I drew while figuring out what to use for the post intro image:

Three more dogs. One has a regular shape (though shorter-snouted than the one in the post intro), one is a kind of cartoon wolf shape, and one looks more like a cartoon dinosaur (first of the batch).

Related posts:

5 thoughts on “GameMaker: Statics and method overriding

  1. I don’t understand the example that u used on the “sharing” section. I mean of course they will share the array between all the instances of Test object, that’s the purpose of static, isn’t it? otherwise u would not use it.

    I mean that would also happen on non by-reference types, except that there there’s nothing you can do to avoid it:

    function Test() constructor {
    static value = 0;
    }
    var a = new Test();
    var b = new Test();
    trace(a.value); // 0
    trace(b.value); // 0
    a.value = 1;
    trace(a.value); // 1
    trace(b.value); // 1 <- !!

    For me that's just the usual way it should work and Idk if on array behavior that an instance can have a static value different from other one is a bug or a feature…

    Nice post btw

    • That does not happen with non-reference types, the output from your snippet is

      scr_test:6: 0 // is Test.value
      scr_test:7: 0 // is Test.value
      scr_test:9: 1 // is a.value
      scr_test:10: 0 // is Test.value
      
      • Woah, never mind, that’s why I never got to work right with static variables. To be sincere I don’t see why are they implemented like this or why is this expected. I read the docs and they still make me think that they should work just like a global variable attached to the struct or something, do you know what I mean?

        Maybe you could explain this behavior in deep on the article as I think others could have the same idea that me. Btw my reference is the manual: https://manual.gamemaker.io/monthly/en/#t=GameMaker_Language%2FGML_Overview%2FFunctions%2FStatic_Variables.htm&rhsearch=static

        I modified the example from there and I get this answer just like on the example that u just clarified for me but extended:

        function weapon() constructor
        {
        static number_of_weapons = 0;
        number_of_weapons+=1;

        f = function(){
        number_of_weapons+=10;
        }
        }

        var _weapon1 = new weapon();
        var _weapon2 = new weapon();
        trace(“test 0:”, “Create two weapon instances with static ++n-of-wep”);
        trace(_weapon1.number_of_weapons); // 2
        trace(_weapon2.number_of_weapons); // 2
        trace(“result”, “two variables syncronized (or only one for two instances)”);

        _weapon1.number_of_weapons+=2;

        var _weapon3 = new weapon();

        trace(“test 1:”, “add 2 to _weapon1 & create a new _w3”);
        trace(_weapon1.number_of_weapons); // 4
        trace(_weapon2.number_of_weapons); // 3
        trace(_weapon3.number_of_weapons); // 3
        trace(“result:”, “added 2 but w1 is now desyncronized”);

        _weapon1.f();
        trace(“test 2:”, “add 10 to w1 through a function call”);
        trace(_weapon1.number_of_weapons); // 14
        trace(_weapon2.number_of_weapons); // 3
        trace(_weapon3.number_of_weapons); // 3
        trace(“result:”, “added but desyncronized”);

        _weapon2.f();
        trace(“test 3:”, “add 10 to w2 through a function call”);
        trace(_weapon1.number_of_weapons); // 14
        trace(_weapon2.number_of_weapons); // 13
        trace(_weapon3.number_of_weapons); // 3
        trace(“result:”, “w2 desyncronized”);

        var _weapon4 = new weapon();
        trace(“test4:”, “just create a new weapon”);
        trace(_weapon1.number_of_weapons); // 14
        trace(_weapon2.number_of_weapons); // 13
        trace(_weapon3.number_of_weapons); // 4
        trace(_weapon4.number_of_weapons); // 4
        trace(“result:”, “(only w3 and w4 are still sincronized)”);

        //RESULTS:
        Object1_Create_0:15: test 0: Create two weapon instances with static ++n-of-wep
        Object1_Create_0:16: 2
        Object1_Create_0:17: 2
        Object1_Create_0:18: result two variables syncronized (or only one for two instances)
        Object1_Create_0:24: test 1: add 2 to _weapon1 & create a new _w3
        Object1_Create_0:25: 4
        Object1_Create_0:26: 3
        Object1_Create_0:27: 3
        Object1_Create_0:28: result: added 2 but w1 is now desyncronized
        Object1_Create_0:31: test 2: add 10 to w1 through a function call
        Object1_Create_0:32: 14
        Object1_Create_0:33: 3
        Object1_Create_0:34: 3
        Object1_Create_0:35: result: added but desyncronized
        Object1_Create_0:38: test 3: add 10 to w2 through a function call
        Object1_Create_0:39: 14
        Object1_Create_0:40: 13
        Object1_Create_0:41: 3
        Object1_Create_0:42: result: w2 desyncronized
        Object1_Create_0:45: test4: just create a new weapon
        Object1_Create_0:46: 14
        Object1_Create_0:47: 13
        Object1_Create_0:48: 4
        Object1_Create_0:49: 4
        Object1_Create_0:50: result: (only w3 and w4 are still sincronized)

        • Modifying a static variable inside a constructor will modify the static variable, just like how it does inside a function. If you want to both declare a variable as static in a constructor and give the struct its own variable, use self.number_of_weapons.

          • What I would like to do is modify the static variable from outside and that it modifies the static variables of all the instances. That’s what my intuition tells me that the static keyword do, what it does is not intuitive

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.