Property Binding - coldrockgames/gml-raptor GitHub Wiki

You may know the concept of data-binding, or property-binding, from other environments, like .net, where it is widely used, especially in UI applications with WPF or MAUI. Mobile Apps normally also use UIs, which are data bound.

raptor3 offers Property Binding too and takes the concept one step further.

What is it used for?

There are many situations, where property binding reduces boilerplate code and keeps things nice and tidy.

Example: Bind an object's position

One example could be, let's say in a Jump'n Run game, where your character has a temporary shield, preventing damage, where this shield shall follow the player.

You could create an object for the shield, set the player object as parent, define a step-event, where you make the x-position of the shield equal the x of the player, and so on. This needs functions, events, code.

Instead, in the Create event of your Shield object, you could simply write:

binder.bind_pull("x", player_instance, "x");
binder.bind_pull("y", player_instance, "y");

...and you're done! This object's x/y coordinates will be bound to the player_instances' x/y and the object will follow the player! No step-event, no draw, nothing special to do, just 2 lines of code.

Example: Bind the sell-value of an item at a merchant in an RPG

Assuming you do an RPG, and the player visits a merchant to sell some of the collected items. You could create that shop-item, get a parent, copy all the values, ... again... code, code, code.

Or, you could do (in the shop item):

// I assume here, that your shop-item is made of several objects, 
// like the icon of the item and the sell value
// and "icon" and "value" (TextBox) are the instances of these sub-items
icon.binder.bind_pull("sprite_index", inventory_item, "sprite_index");
value.binder.bind_pull("text", inventory_item, "sell_value", NUMBER_TO_STRING_CONVERTER);

This is just two of many thinkable scenarios, where you can keep your code simple and clean, without boilerplate copy-blocks of values. As a "binding" is a "live" value, you even have the guarantee, that everything on screen gets updated correctly, if something changes!

Tip

You can bind any property of your current object to any property of a source instance or struct. There's no restriction!
In the example above, a "text" property of a TextBox is bound to a sell_value of an item, and above that the position of one item is mirrored to another one. You can even bind struct-members (or entire structs!) freely.

Group Bindings

Especially in cases when you want to "bind two objects together" (like a kind of shield surrounding your player) you will face the situation, that you need to bind multiple properties from object A to object B, like x, y and image_angle or something similar.

It can become very tedious to bind multiple properties one-by-one from one object to another despite the fact, that multiple bindings need multiple calls per step and therefore put a higher burden on your framerate.

For these cases the binding system of raptor offers group bindings. Instead of a single property you may send an array of properties to any of the bind_* functions explained further down this page.

Instead of writing

binder.bind_pull("x", player_instance, "x");
binder.bind_pull("y", player_instance, "y");
binder.bind_pull("image_angle", player_instance, "image_angle");

you may simply write

binder.bind_pull([ "x", "y", "image_angle" ], player_instance, []);

Tip

Look closely at the example above. You see that there is no need to repeat the entire array of bound properties. Just provide an empty array [] and the function will mirror the first array into the second one for you. Of course, this only works, if the property names of the target instance are the same than those of the source instance.

Pull and Push binding

In the examples above you have seen, that a method called bind_pull was used. It also has a counterpart, bind_push.

The terms pull and push define the direction of the binding. A pull binding will receive the value from the instance, it is bound to and a push binding will write its own value to the destination field.

Why do we need two binding directions?

The reason for this is very simple: Every _raptorBase object has a binder variable, which you can use to bind properties of the object (in the one or the other direction).
The design idea of this feature was to bind anything to anything. This also includes structs or even other built-in objects in GameMaker, like the parameter structs of effect layers and things like that. And this is the point, where pull and push come into play.
While it is technically possible to modify GameMaker structs and add members to them, the question is: Should you do it all the time, just because you can?

So we need a way to inject a binding to such an object or struct class without modifying it. If you want that object to be the target of such a binding, create a push binding and modify any object or struct's member through one of your own properties. To receive, create a pull binding.

Watcher binding

Sometimes, you do not have the need to "mirror" a value from one object or struct to another one (like the shield following a player), but you just want to "monitor" a member of any type and get informed, when it changes.

This can be anything, even running values from an Animation Curve, or the image_index of an object, some value in a fx_struct of some effect layer.

For this scenario, bind_watcher exists, which binds a value to a callback function. This is even more performant than pull or push binding, as the callback gets invoked only when the value really changes, while in the other binding methods, the converter function needs to run every frame, because only after conversion, the binding can compare, whether the value changed.

The Bindable class

raptor offers a Bindable struct, which you can use as base class, to create your own bindable classes, that offer all binding functionality.

The two main objects/classes of raptor also offer a Bindable member:

  • StateMachine's data member. Just call states.binder().bind_* to bind to any value in the data of a StateMachine.
  • Animation's values member. Just call youranim.binder().bind_* to bind to any running curve channel in the animation.
    • This is extremely useful to animate effect layers or any other things that are not only x/y coordinates of instances

Create custom bindables: Just, instead of

my_member = {
    myval: 0
};

do

my_member = new Bindable(self); 
my_member.myval = 0;

Or derive from Bindable when you create your classes.

After that, you can watch any property or value in my_member, by using the binder offered:

my_member.binder().bind_watcher("myval", function(new_value, old_value, source_instance) {
    show_debug_message($"myvalue is now {new_value}");
});

This callback gets invoked, when myval changes.

What about performance of these things?

Of course, everything comes with a performance cost, but for this feature it is extremely low. I have tested with 1500 bindings in a scene with roughly 200 active moving objects and multiple particle emitters active. On my machine (3070RTX), GameMaker still rendered way more than 1000 FPS in 1080p. If you create many thousands of bindings, you might run into performance issues, but I don't think, that's a standard case. There's no problem in binding a couple, or even some hundreds of objects to each other, or to bind values like the Score or Game Time to some UI Element. It just reduces code.
Go ahead! Feel free to bind what you want!

"binder()"

Each child of _raptorBase offers a function called binder().
With this, you can bind any value of the current object to any value from a source object (your current object is always the receiver of a bound value, not the sender).

This binder() is of type PropertyBinder and offers these functions:

bind_pull

/// @function bind_pull(_my_property, _source_instance, _source_property, _converter = undefined,
			_on_value_changed = undefined)
/// @description Receiving binding. Bind any of your properties to receive any source property.
/// @param {string} _my_property	The name of your local property to bind
/// @param {instance/struct} _source_instance	The object or struct, that holds the value, you want to receive
/// @param {string}	     _source_property	The name of the property, you want to receive
/// @param {function}	     _converter		A function receiving 1 argument: the value from the source.
///						Must return a converted value, that the object can use.
/// @param {function}	     _on_value_changed	A callback function receiving 2 arguments: (new_val, old_val)
///						This callback is only invoked, if the bound value changed.
static bind_pull = function(_my_property, _source_instance, _source_property, 
			    _converter = undefined, _on_value_changed = undefined) {

bind_push

/// @function bind_push(_my_property, _target_instance, _target_property, _converter = undefined,
			_on_value_changed = undefined)
/// @description Sending binding. Bind any of your properties to write to any target property.
/// @param {string} _my_property	The name of your local property to bind
/// @param {instance/struct} _target_instance	The object or struct, that holds the value, you want to overwrite
/// @param {string}	     _target_property	The name of the property, you want to overwrite
/// @param {function}	     _converter		A function receiving 1 argument: the value from the source.
///						Must return a converted value, that the object can use.
/// @param {function}	     _on_value_changed	A callback function receiving 2 arguments: (new_val, old_val)
///						This callback is only invoked, if the bound value changed.
static bind_push = function(_my_property, _target_instance, _target_property, 
			    _converter = undefined, _on_value_changed = undefined) {

The converter function (if set), is invoked every time, the value shall be compared to the previous value.
The on_value_changed callback only runs, if a difference between the old and the new value is found.

bind_watcher

/// @function bind_watcher(_my_property, _on_value_changed)
/// @description Binds only a function on value change to a property. This is useful, if you
///		 do not want to mirror the bound value to any other member, but just get informed,
///		 when the watched value changes. The callback receives two arguments:
///		 (new_value, old_value)
/// @param {string}	_my_property		The name of your local property to bind
/// @param {function}	_on_value_changed	A callback function receiving 2 arguments: (new_val, old_val)
///						This callback is only invoked, if the bound value changed.
static bind_watcher = function(_my_property, _on_value_changed) {

unbind_source

/// @function unbind_source = function()
/// @description Deletes ALL bindings where the current object instance is registered
///              as the SOURCE of a binding.
///		 NOTE: This is called for you in the "CleanUp" event of _raptorBase!
///		 It ensures, that your game doesn't crash, when an object gets destroyed.
///		 Due to the nature of Step, it is impossible to say, whether the binding
///		 engine or the instance, that receives a binding will processed first.
///		 So see this as a security belt in case of an unlucky processing order.
static unbind_source = function() {

unbind_all

/// @function unbind_all = function()
/// @description Deletes ALL bindings of the current object.
///		 NOTE: This is called for you in the "CleanUp" event of _raptorBase!
///		 Normally, you do not need to deal with this method.
static unbind_all = function() {

Converter Functions

These are small functions, that can convert the new value of a property. They get invoked every frame and receive 3 arguments:

<converter_function>(_name, _value, _source_instance)

Here are two examples of "standard" converters, which are delivered with raptor:

#macro STRING_TO_NUMBER_CONVERTER	function(_name, _value) { return real(_value); }
#macro NUMBER_TO_STRING_CONVERTER	function(_name, _value) { return string(_value); }

Caution

As the converter functions determine, whether a value will change, they are invoked every frame. Keep the logic in these function at a minimum.
If the returned value differs from the last known value of a property, the on_value_changed callback will be invoked afterwards.

Example: Bind position with an offset

Another example for a converter function could be a binding of the position between two objects, where object shall follow another with some offset/distance. It could be achieved like this:

binder().bind_push(["x","y"], my_shield, [], function(_name, _value) {
    // Just for demonstration, we follow the object
    // with an offset of 100 in x and -50 in y
    if (_name == "x") return _value + 100 else return _value + 50;
});

on_value_changed

This callback can be added to any binding and will receive 4 arguments.
It gets invoked whener any of the bound properties changes (see Group Bindings). That's why the first argument will contain the name of the property that changed its value.

on_value_changed(name, new_value, old_value, source_instance)
⚠️ **GitHub.com Fallback** ⚠️