GameMaker: Simplest possible instance methods

A tutorial about implementing instance methods in GameMaker.

After seeing series of increasingly strange uses of "user events" in GameMaker games for object-bound actions, it came to my attention that most people are only vaguely aware of other ways of doing things, so I decided to write a small blog post on the matter.

There are many uses for these - for example, you might want to have a "take X damage" method on your enemy objects so that it can be varied depending on requirements - some enemies might just take damage, some should retaliate on being hit, some might have a fancy damage reduction formula. Being able to comfortably define/redefine methods on per-object-type basis can help a lot.

The idea

The premise is pretty simple,

  • GM allows referencing and execution of scripts via script_execute;
  • To have instance methods, the script-method should be called on it's "owner" instance;
  • Therefore, packing the instance+script pair into a tiny array is enough to execute it with correct context while only having the method-reference.

Ideally, of course, you would use closures. But those are still on the roadmap for now.

The code

In GMS2, things are easy:

take_damage = [self, scr_enemy_take_damage];

In GMS1 you'd have a script instead, say, mt_bind:

/// mt_bind(instance, script, ...)
var i = argument_count;
var m = array_create(i);
while (--i >= 0) m[i] = argument[i];
return m;

and do

take_damage = mt_bind(self, scr_enemy_take_damage);

(and to override a method you'd just reassign a method variable in the child object(s))

Then, you'd have a script to call such method-arrays, called mt_call or maybe even just mt:

/// mt(method, ...args)
/// @param method
/// @param ...args
var m = argument[0];
with (m[0]) switch (argument_count) {
    case 1: return script_execute(m[1]);
    case 2: return script_execute(m[1], argument[1]);
    case 3: return script_execute(m[1], argument[1], argument[2]);
    // and so on...
    default: show_error("Too many arguments for mt!", true); exit;
show_error("Couldn't find instance " + string(m[0])
    + " for " + script_get_name(m[1]) + " call.", true);

and later do

// in collision between projectile and enemy
var damage_dealt = mt(other.take_damage, damage);

and that's it.

Advanced uses

If your setup could benefit from being able to give method-scripts additional data without having to define variables for it, that can be done as well. First, you would feed those additional arguments into array literal/script:

take_damage = [self, scr_explode_on_hit, obj_explo_big];

and slightly expand the mt script:

/// mt(method, ...args)
/// @param method
/// @param ...args
gml_pragma("global", @'// remove "@" in GMS1
// 4 -> max argument count as per switch block below
global.mt_temp_0 = array_create(4);
global.mt_temp_1 = array_create(4);
var m = argument[0];
var n = array_length_1d(m) - 2; // number of arguments in method-reference
var w = global.mt_temp_1; // temp array to store combined arguments
array_copy(w, 0, m, 2, n); // copy arguments from method-reference to temp array
// copy script arguments to temp array:
for (var i = 1; i < argument_count; i++) w[@n++] = argument[i];
var found = false, result;
with (m[0]) {
    found = true;
    switch (n) {
        case 0: result = script_execute(m[1]); break;
        case 1: result = script_execute(m[1], w[0]); break;
        case 2: result = script_execute(m[1], w[0], w[1]); break;
        case 3: result = script_execute(m[1], w[0], w[1], w[2]); break;
        case 4: result = script_execute(m[1], w[0], w[1], w[2], w[3]); break;
        // and so on...
        default: show_error("Too many arguments for mt!", true); exit;
array_copy(w, 0, global.mt_temp_0, 0, n); // reset the temp array
if (found) return result;
show_error("Couldn't find instance " + string(m[0])
    + " for " + script_get_name(m[1]) + " call.", true);

and then your script would read init-arguments in front of call-arguments:

/// scr_explode_on_hit(explosion_object, damage)
var obj = argument0, dmg = argument1;
my_health -= dmg;
instance_create(x, y, obj);

and called as usual.

This can also be used to chain methods for inheritance (so that the overriden method would get the original as an argument and be able to call it that way).

Conclusion and considerations

The primary thing to keep in mind with this that with cannot be applied to deactivated or freshly destroyed instances so you can't call methods for those this way. Not that this is a good idea to begin with or anything.

Have fun !

Related posts:

Leave a Reply

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