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:

10 thoughts on “GameMaker: Statics and method overriding

  1. Some will say that the GML language has been enriched by the introduction of these things, but for an eternal beginner like me, GML is becoming unreadable like a hieroglyph

  2. 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

          • A more concrete question would be. Do you know how can I implement a funtion that restarts the counter number_of_weapons for all the instances of the weapon constructor and not for a single one that gets desyncronized?

            • weapon.number_of_weapons would read/write the static variable that is returned for structs that do not have their own.

              • for read yes. But for write, not actually, that’s why I made all those tests on the message, to try to modify the static variable. I don’t know if I’m too dump to explain, but I’ll keep trying.

                if I do:
                var w1 = new weapon();
                var w2 = new weapon();
                trace(w1.n_o_w, w2.n_o_w);
                ///Here I get: 2, 2 (nice)
                //but if after I do:
                w1.n_o_w = 0;
                trace(w1.n_o_w, w2.n_o_w);
                //Here I get: 0,1 (not nice to me)…
                //I would expect: 0,0

                Why I would expect 0,0? because I don’t know why I should expect other thing, in my mind the SAME static variables is linked to all the instances of weapon, so if you read or modify any, it is modified on all. But it seems that for you this behavior is not the expected and the expected behavior is what we actually get…

                I need more information of how static variables are implemented because the manual does not make me think that they are implemented the way they react that you seem to take as natural, so maybe I should read another language docs of how are static implemented in order to solve my question, do you think so?

              • OK now I see what you mean hehe I had to use “weapon” instead of “_weapon123”. Ok that does exactly what I would expect. Thank you very much!

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.