Property Binding - Grisgram/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.

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) {
    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" and "binder()"

Each child of _raptorBase holds a variable 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).

Tip

Object instances, that are child of raptorBase, hold a binder variable.
Struct classes, like the StateMachine or Animation or any Bindable() child, hold a binder() function.

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.

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.

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.

unbind

/// @function unbind = function(_my_property)
/// @description Deletes the binding of the specified property.

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.

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.

Converter Functions

These are small functions, that take one input value (the new value from the source of the binding), and convert it to a target value/format, the receiving object can use or assign.

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

#macro STRING_TO_NUMBER_CONVERTER	function(_value) { return real(_value); }
#macro NUMBER_TO_STRING_CONVERTER	function(_value) { return string(_value); }
⚠️ **GitHub.com Fallback** ⚠️