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.
There are many situations, where property binding reduces boilerplate code and keeps things nice and tidy.
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.
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.
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.
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.
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.
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.
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 callstates.binder().bind_*
to bind to any value in the data of a StateMachine. -
Animation's
values
member. Just callyouranim.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.
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!
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:
/// @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) {
/// @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.
/// @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) {
/// @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() {
/// @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() {
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.
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;
});
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)