Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Observable properties in gdscript #6491

Closed
olsonjeffery opened this issue Sep 15, 2016 · 30 comments
Closed

Observable properties in gdscript #6491

olsonjeffery opened this issue Sep 15, 2016 · 30 comments

Comments

@olsonjeffery
Copy link

olsonjeffery commented Sep 15, 2016

Operating system or device - Godot version:
gdscript feature

Issue description (what happened, and what was expected):
I find myself frequently writing accessors around properties in classes to notify outside observers when some property on a Node changes (e.g. The position of a Sprite, or any number of properties of a game entity that other consumers (like UI elements) would like to know).

It can become tedious and repetitive, but currently is worth it as you get the flexibility of information and changes flow/propagating through layers of the application naturally and in a decoupled fashion (interested consumers subscribe to data streams they're interested in), instead of being pushed by the data's owners (which can be fragile as you adapt application structure over time and the owner of some property has to keep track of who is interested in its contents over time).

It would be great if there were a pattern to abstract this away, akin to what you see with the Observable pattern in other languages/frameworks.

Steps to reproduce:
Currently:

# all of this for one observable, ouch!
signal sprite_pos_changed(new_pos)

var __sprite_pos = Vector2(0,0)
var sprite_pos = Vector2(0,0) setget sprite_pos_setter, sprite_pos_getter

func sprite_pos_setter(new_pos):
  __sprite_pos = new_pos
  emit_signal("sprite_pos_changed", new_pos)
func sprite_pos_getter():
  return __sprite_pos

This could be streamlined down to:

observable var sprite_pos = Vector2(0,0)

And, by convention, it would be understood that this would initialize a new user signal for the class with a specification of: <property_name>_changed(new_val).


This is also possible, right now, by creating an Observable wrapper class to contain the value, but it makes things uglier when:

  var foo = self.bar
  self.bar = foo + 1

becomes:

  var foo = self.bar.get()
  self.bar.set(foo + 1)

Also, what's important about the Observable is that it's a data component of some larger data structure. Things like Observable wrappers just seek to atomize data in a way that can create liabilities down the road (like multiple data structures holding references to the same observable by accident, instead of a single "owner" with everyone else observing/subscripting to the data).


Additionally, with this pattern, you'll see it extended to collection types to fire an event when properties are added/removed to a collection (it should be noted that Array, Dictionary et al don't fire signals when their contents are modified and this is something you'd want to restrict to Nodes in the scene, anyways).

Also, once you have Observable collections, you can start modelling applications as handling streams of data over time, i.e. what's traditionally construed as "reactive programming". Just look at what's possible w/ RxPy (https://github.com/ReactiveX/RxPY)! I won't argue for/against-this as it's pretty advanced and something that requires a lot of discipline. There would be a lot of discovery to determine how it'd best fit into Godot (which is already very event driven, so these patterns would mostly be another approach to the same ideas/paradigms).

Link to minimal example project (optional but very welcome):
I know everyone is busy with lots of other great features and fixes for the community. This is not a high priority change, but I feel like it would be high value.

This is something that would greatly facilitate and make clear to members of the community how applications can be structured to allow data to propagating freely to interested consumers. The observer pattern allows data-owners to focus on their own logic, as opposed to worrying about implementation details of other entities interested in their contents.

Thanks for your time!

@bojidar-bg
Copy link
Contributor

@olsonjeffery You realize you use setget with only one parameter to leave the getter intact? 😄
E.g.

var a = 5 setget a_changed # only setter
var b = 6 setget ,b_taken # only getter

@olsonjeffery
Copy link
Author

Thanks, I hadn't realized that. Doesn't really change the overall argument,
though.
On Thu, Sep 15, 2016 at 2:39 AM Bojidar Marinov [email protected]
wrote:

@olsonjeffery https://github.com/olsonjeffery You realize you use
setget with only one parameter to leave the getter intact? 😄
E.g.

var a = 5 setget a_changed # only settervar b = 6 setget ,b_taken # only getter


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#6491 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAAoqMq6UQzr8whazzH9pQB0dZvU_75eks5qqPYqgaJpZM4J9dOa
.

@olsonjeffery
Copy link
Author

olsonjeffery commented Sep 16, 2016

You might event want to modify the default signature like so:

# all of this for one observable, ouch!
signal sprite_pos_changed(sender, new_pos)

var sprite_pos = Vector2(0,0) setget sprite_pos_setter

func sprite_pos_setter(new_pos):
  sprite_pos = new_pos
  emit_signal("sprite_pos_changed", self, new_pos)

This way, a single downstream consumer can subscribe/listen to the same property observable for multiple Nodes/Sprites/etc and be able to tell them apart. You would see this in something like a logger that shows a message whenever something (like hp?) changes. Also might want to capture old and new values?

# all of this for one observable, ouch!
signal sprite_pos_changed(sender, old, new)

var sprite_pos = Vector2(0,0) setget sprite_pos_setter

func sprite_pos_setter(new_pos):
  var old_pos = sprite_pos
  sprite_pos = new_pos
  emit_signal("sprite_pos_changed", self, old_pos, new_pos)

ahhhhhh the possibilities.

(Also I can could look into doing this change myself if someone would want to mentor me on it).

@bojidar-bg
Copy link
Contributor

bojidar-bg commented Oct 27, 2016

@olsonjeffery Wouldn't this syntax be better?

var sprite_pos setget signal sprite_pos_changed
# Emits sprite_pos_changed(sprite_pos) on set, nothing on get
# Or this, actually:
var sprite_pos signal sprite_pos_changed

Also, emitting the sender is bad practice in godot -- you can just mail it to yourself when binding, like:

stuff.connect("sprite_pos_changed", self, "it_changed", [stuff])

func it_changed(new_sprite_pos, stuff):
    # stuff is now the one that emitted the signal...

@blurymind
Copy link

blurymind commented Sep 27, 2017

I like @bojidar-bg 's suggestion to do. It could be even simpler:

var sprite_pos signal sprite_pos_changed

func sprite_pos_changed():
  print("pos changed!")

We could have something like this?
Perhaps it would be useful if the emitted signal contains both the before and after values?

export var width = 2 signal _set_size
export var height = 4 signal _set_size

func _set_size(oldValue,newValue):
   print("size has changed.. old value:",oldValue," new value:",newValue)

We are trying to avoid this use case:

export var width setget _set_width
export var height setget _set_height

func _set_width(new_value):
  width = new_value
  update()
func _set_height(new_value):
  height = new_value
  update()

simple syntax :)

@bojidar-bg
Copy link
Contributor

bojidar-bg commented Mar 14, 2018

I think have a probably better idea than my last one (after having needed it in some tool scripts...):

# Signal version
signal foo_changed
export var foo = 6.0 onchange: emit_signal("foo_changed")
# Func version
export var foo = 6.0 onchange: print_foo()
func print_foo():
    print(foo)
# Inline version
export var foo = 6.0 onchange: bar = foo * 5
# Block version
export var foo = 6.0 onchange:
    bar = foo * 5
    baz = sin(foo)

Potentially, the colon might be omittable, but this looks like a bit too much sugar:

export var foo = 6.0 onchange "foo_changed" # Signal
export var foo = 6.0 onchange print_foo # Function

@razcore-rad
Copy link
Contributor

I'm going to up the stakes here. I would argue that instead of doing half steps like these why not get the real deal? This would be kinda' like what Promises are in Javascript (a dying breed) because of their limitations. I would argue in favor of some reactive patterns as in RX, cyclejs etc. Most languages and libraries are moving in this direction so instead of Observables which are very limited in how you interact with them (especially in time) I'd say we should go for Streams (like in the cyclejs docs). They are very natural and composable and usable with other requested features like filter, map, reduce etc. (see #17268).

So instead of transmitting a value at a time (which is the problem with Promises in JS too) most of this stuff is moving towards Streams which work in time, they can be delayed, composed, dropped, etc. and safer in a sense cause most of the operations on them are done through very tiny functional operations that don't alter any state (or shouldn't).

It's a big change and a large topic... but I really hope people are interested in reactive style or programming, it's where this stuff is going to end up anyway, sooner or later

@bojidar-bg
Copy link
Contributor

bojidar-bg commented Jun 27, 2018

@razcore-art I was actually thinking of the same, but I'm not sure how much it would fit in the current godot philosophy. Basically, being about to do something like this would do wonders:

extends Node2D
const textures = [preload(...), preload(...), preload(...)]
export(int, "a", "b", "c") var item
$sprite.texture = textures[item]
# Or even:
$rigidbody.velocity = Vector2($slider_x.value, $slider_y.value)

Unfortunately, this would require some additions to the current API, which would allow subscribing to changes (via signals/observables or streams). So, I would rather stay away from it for the moment.

Edit: now #23115

@razcore-rad
Copy link
Contributor

razcore-rad commented Jun 27, 2018

@bojidar-bg what would be the current godot philosophy? I thought it was something like: "make stuff easy". I don't think I follow your first example, that's not exactly how I think about streams, you still have to operate on them, that is you can't just use them like that in array operations: textures[item], I see it more like:

# emits a signal every second from the beginning of the game
# it doesn't produce any values whatsoever tho'
var item = Stream.repeat(1000) # this is in miliseconds

# delay the signal by 2 seconds and convert to numeric value, start value being 0
# x would be the incoming stream value (if there was one, it's null here so we don't use it)
item_numeric = item.delay(2000).fold((acc, x) => acc + 1, 0) # borrowed the lambda function notation from JS
item_numeric.add_listener(self, "_on_item") # or item_numeric.connect(self, "_on_item")
# OR perhaps this
self.observe(item_numeric, "_on_item")

func _on_item(i):
    $sprite.texture = textures[i] # textures here is just an array, not a stream

Now like I said, this would require functional programming to some degree, first class functions, probably a lambda/arrow notation, etc. There's a lot of change, but I think it would be a good change.

Note that the above (except for the lambda function/functions as 1st class citizens) this could already be implemented in gdscript as a library and I was reading that there's some work done on lambda functions and there's already requests for basic functional implementations like map, fold, reduce etc.

the xstream micro-streams library is a really good starting place for looking at some of the implementations/usages. Compared to RX it's super tiny and I think it falls very well in the philosophy of "keep it simple"

@vnen
Copy link
Member

vnen commented Jun 27, 2018

There'll be first-class functions/lambdas in 3.2 or 4.0 (whichever comes after 3.1).

@larpon
Copy link
Contributor

larpon commented Oct 20, 2018

I'd like to chip in my suggestion (#23115) to add property bindings like they are found in QML (Qt Modeling Language). They provide observables AND expression value binding.

@toger5
Copy link
Contributor

toger5 commented Feb 3, 2019

I really like the approach of making a property change a function:

export var a = 6.0 onchange(): print_foo()

export var b = 6.0 onchange(new_val): 
    position = new_val*6

export var c = 6.0 onchange(new_val, old_val): 
    $label.text = str(old_val) + " was the previous value for the size"
    size.x = new_val

I would tend it with optional old and new_val properties. and the I would make it look more like a function using "()".
Also it should definitely support multiline function implementations.

@Chaosus Chaosus changed the title Feature request: observable properties in gdscript Observable properties in gdscript Mar 16, 2019
@creikey
Copy link
Contributor

creikey commented Oct 12, 2019

The onchange syntax seems like it would be helpful when writing an editor tool script, and you simply want the node to redraw with update every time certain properties are changed, that way instead of making a new setter/getter function for each property that just calls that update function and sets the value, you can use the same update function for every property that affects what the editor preview looks like.

@creikey
Copy link
Contributor

creikey commented Oct 14, 2019

What if the setter method's argument did not have to exist, and it would still be valid? An example:

var onchange_val = 0 setget onchange_val_updated

func onchange_val_updated():
	# onchange_val still set, but function does not have to set the variable
	print("updated")

creikey added a commit to creikey/godot that referenced this issue Oct 15, 2019
 - When there is a setter and an observer, value is not changed
   before calling the observer
 - Partially fixes godotengine#6491
@CreativeBuilds
Copy link

Not sure where this is at exactly with 3.2 being out now. But is there anything to easily make Rx-like Observable streams now?

@Calinou
Copy link
Member

Calinou commented May 16, 2020

@CreativeBuilds There are no known plans to implement this in 4.0, but there are no decisions taken against it either.

@CreativeBuilds
Copy link

@CreativeBuilds There are no known plans to implement this in 4.0, but there are no decisions taken against it either.

@Calinou sadly I'm still a newbie when it comes to actually working on the actual engine, but if I stick around long enough then I may try and get it added in a future 4.x release 😉

Observables are one of those things from web dev JS land that I love and I feel like it would simplify so much of my Godot code, but then again I'm doing game dev code as a web dev so I probably just don't know how to make things the best way yet. Have much to learn! :)

@creikey
Copy link
Contributor

creikey commented May 20, 2020

@CreativeBuilds I still don't really understand the concept of Javascript observables enough to accurately translate the concept to GDScript, could you link an online tutorial of some sort or explain it yourself here?

@CreativeBuilds
Copy link

@creikey Its probably best to link you the RxPi version instead of RxJS https://github.com/ReactiveX/RxPY since GDScript is more like python anyways.

Rx though is a pattern where you have something called an observable. When an observable is created you have the option to pass a data object I.E. string/object/whatever like new Observable("First String").

Now if I want to access the data inside of the observable I would do var subscription = someObservable.subscribe(func_to_run_on_update). As soon as subscribe is called, func_to_run_on_update is also called with one parameter, that being whatever is inside of the observable at the time.

The benefit this offers over the current system in GDScript is that you can make some helper file lets say "rxBalance.gd". What this file will do, is return an Observable that contains an integer. If I want to get the users balance I would just need to import that file and then call rxBalance.subscribe(my_own_handler_function) then whenever the users balance updates I dont have to worry about triggering a set function then emitting a signal, making sure the other node in my game finds that other node and listens to the signal.

If you want I can explain this a lot easier and with examples in discord. CreativeBuilds#0001

Keep in mind im a Godot noobie and have no idea on how this type of data handling would work inside GDScript 💙

@vnen
Copy link
Member

vnen commented May 24, 2020

I thought I had commented here, but I don't see it now... so let me rewrite.

Usually those reactive extensions, be in JavaScript or Python, are implemented without any special feature from the language itself. Is there anything missing in GDScript that prevents someone of writing such a library? Why does this need to be a language feature when other languages don't have but still can replicate the pattern anyway?

I truly believe that with the feature-set I have in mind for GDScript (first-class functions and signals, lambdas/closures, traits, etc.) it's entirely possible to implement a library like that in userland without require reactive stuff embedded in the language itself.

@CreativeBuilds
Copy link

@vnen I may have the wrong impression from plugins/libraries with Godot but it seems like they're more "niche" things for certain types of games. Something like rx can widely be used by all types of games and be taken advantage of by anyone regardless of what game they're working on (generally speaking). Wouldn't it make sense if you're going to spend roughly the same energy to make it first-class?

@creikey
Copy link
Contributor

creikey commented May 25, 2020

@CreativeBuilds Typically when another node needs to be notified of the state change of another node, you use a signal emitted by the variable's setget. In that case syntax sugar to just make some variables available as a signal like "observable" would work fine.

@razcore-rad
Copy link
Contributor

This is far fetched, but I'll throw it in here just for future reference. My issue with libraries is that they really work in spite of the language instead of working with it. Lately I found about https://svelte.dev/ which is the closest thing I could find to "real" reactive programming. They transpile to JavaScript, it's for front-end development, but the way they approach the issue is very interesting. It's a language 90% compatible with JavaScript to begin with, except with a few changes, one of the bigger one is that variables are inherently reactive. For example:

<script>
	let count = 0;

	function handleClick() {
		count += 1;
	}
</script>

<button on:click={handleClick}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

Every time handleClick runs, the button inner HTML gets updated too, the curly braces parts. This is built into the language. At a first impression, it looks to be incompatible with FP paradigm, because in FP immutability is a big thing. Svelte just compiles to JavaScript highly optimized imperative code, or so they say.

So maybe this is something we might be interested in as a feature for GDScript at some point.

@vnen
Copy link
Member

vnen commented May 25, 2020

Something like rx can widely be used by all types of games and be taken advantage of by anyone regardless of what game they're working on

Yes, but it can also not be used at all, as have been the case so far... The thing is that "widely used" vs "niche" is not only applied to game genres, but programming paradigms as well. What if I spend time implementing this and only a handful of people uses the feature? I would classify that as a "niche" too.

I know GDScript is more humble and accessible language, so it's easy to ask "just for this feature" but I don't really see why games would rely heavily on this since they pretty much haven't so far. Why is it not in other mainstream language by default? Why it has to be the case in GDScript?

I say that someone should open a proper proposal with more detailed use-cases. The OP here says it's used a lot in the project but... why? Can't you approach it differently?

Better yet: make a library that uses this pattern and show how it is better than the current way. I mean, if you're gonna use this style of programming, making a library will help you a lot, especially if the feature is never added.

Reactive programming is the "next new thing" and all that but I'm not so sure it really applies to a lot of game projects.

@CreativeBuilds
Copy link

Its easier to describe in how it worked with web before/after I found it and how it changed the way I made websites. In both cases I use React.

React is very similar to how Godot works in the context of how the nodes/event system works, data down, events up. This works for elements that are close together, UI components that are all in the same little box. But wait now you need to move variables that were just for those components and you need them on a completely different page. You have a couple options, you can

a) copy and paste the code over/not share state

b) refactor and move the data up the tree until you reach a common parent node. or

c) extract the variable to an observable and in both of those components just import the observable and subscribe to it.

The first is obviously out of the question, duplicated code is a no-no. The second option sounds fine, and it is! For smaller projects, we ran into a problem at my old workplace where we ended up having nodes/components that were STUFFED with information because we needed to use that info everywhere and it made that file gigantic. Very quickly it got jumbled up, felt like you couldn't easily trace where events where coming from/data was being stored unless in a global state, so we looked for something new.

That's pretty much where Rx came in. All the global states were abstracted out of those couple giant files, and we made a rx folder and used a naming convention of rxVariableName in order to quickly find what we needed.

Generally speaking the feeling I get from godot and how it works is exactly how I felt in that "jumbled" state pre-rx. Its not necessarily that I want "Rx" added to Godot, its more so I want a clearer way to handle variables so I dont need to get a tree_node and couple all my function calls to a specific path structure.

If you don't think any of this is important and that Godot works fine without, I'm sure you're right. But for me, as a new person coming into the engine, it feels like the difference between working "with a cluttered desk" and "working with a clean desk" sort-of-speak. You can get all the work done either way, but at some point some people snap and have to clean their desk if that makes sense 😋

@CreativeBuilds
Copy link

CreativeBuilds commented May 25, 2020

@CreativeBuilds Typically when another node needs to be notified of the state change of another node, you use a signal emitted by the variable's setget. In that case syntax sugar to just make some variables available as a signal like "observable" would work fine.

This is true, but then you're tying the signal to the actual node itself. My thing is more of I need some global variable like balance. In this case lets say I have a Global node I create which is just a Node with a script on it setup to emit_signal when balance changes.

Now I need to access the balance inside of a "on_purchase" script that runs in a scene separate from the root scene. In order to get notified you now need to find that global node which generally consists of me doing something like get_tree().get_child(0).get_node("Global") each time time every component needs to reference anything from global.

Okay, now you have a refrence to the "Global" so in the _ready function you connect to it Global.connect("balance_onchange", self, "do_something") but wait! You forgot to check if that node exists! So now you also need an if check, because it is possible it doesnt exist. a) someone else in the repo removes something not knowing you needed it in that component or more likely b) You just wanted to test the "Shop" scene, so you hit the "play scene" button. Well now there is no Global node because its not starting the tree from your project root anymore so this connect will error.

This all usually takes around 3-4 minutes each time for me to go through and hook up all the signals correctly. The alternative version from a thing like Rx would be to have the file you import be the Observer that you subscribe to, and you just take the "do_something" from the last part and just plug it in, Example in JS: rxBalance.subscribe(current_balance => do_something(current_balance) and then boom everything works exactly the way you want it to.

Edit: Didn't read the last part, but yea even something like just syntax for setting up that kind of easy subscription model for variables is fine. @creikey

@vnen
Copy link
Member

vnen commented May 25, 2020

c) extract the variable to an observable and in both of those components just import the observable and subscribe to it.

How do you "import" it? Don't you need to know where it is to import it anyway? Doesn't it give the same problem as relying on the tree? GDScript doesn't have an "import" statement like JavaScript does, because it works in a different way.

If you don't think any of this is important and that Godot works fine without, I'm sure you're right. But for me, as a new person coming into the engine, it feels like the difference between working "with a cluttered desk" and "working with a clean desk" sort-of-speak. You can get all the work done either way, but at some point some people snap and have to clean their desk if that makes sense

It's not that I don't think it's important to have clean code, just that you can do it without baking Rx into the language. I do believe it's already possible to implement this with scripts alone.

Now I need to access the balance inside of a "on_purchase" script that runs in a scene separate from the root scene. In order to get notified you now need to find that global node which generally consists of me doing something like get_tree().get_child(0).get_node("Global") each time time every component needs to reference anything from global.

Okay, now you have a refrence to the "Global" so in the _ready function you connect to it Global.connect("balance_onchange", self, "do_something") but wait! You forgot to check if that node exists! So now you also need an if check, because it is possible it doesnt exist. a) someone else in the repo removes something not knowing you needed it in that component or more likely b) You just wanted to test the "Shop" scene, so you hit the "play scene" button. Well now there is no Global node because its not starting the tree from your project root anymore so this connect will error.

If you have something that's supposed to be global, why not make it an autoload? People tend to think that singletons "are evil", but that's pretty much what you're doing here: having a single object to handle all transactions. So use the tools Godot is already providing: the autoloads. They will work even if you play a single scene.

This all usually takes around 3-4 minutes each time for me to go through and hook up all the signals correctly. The alternative version from a thing like Rx would be to have the file you import be the Observer that you subscribe to, and you just take the "do_something" from the last part and just plug it in, Example in JS: rxBalance.subscribe(current_balance => do_something(current_balance) and then boom everything works exactly the way you want it to.

Again how do you "import" something in GDScript? Do you load the script? If so, what's preventing you from doing it now?

I also think that people use patterns just because they're there. Do you really need to make everything observable? The setget might be boring to repeat, but how many times do you really need to do it?


Once again: I'm not saying it's wrong to use reactive pattern or that it's never a solution. All I'm saying is that it can already be done using the tools that Godot provides and there isn't a need to bake stuff into the language itself, like pretty much all other languages out there.

@vnen
Copy link
Member

vnen commented May 25, 2020

Again, please open a proposal in https://github.com/godotengine/godot-proposals with actual, real-world use cases for which this is needed, and why can't be done (or is too difficult to do) with current features.

@akien-mga
Copy link
Member

Feature and improvement proposals for the Godot Engine are now being discussed and reviewed in a dedicated Godot Improvement Proposals (GIP) (godotengine/godot-proposals) issue tracker. The GIP tracker has a detailed issue template designed so that proposals include all the relevant information to start a productive discussion and help the community assess the validity of the proposal for the engine.

The main (godotengine/godot) tracker is now solely dedicated to bug reports and Pull Requests, enabling contributors to have a better focus on bug fixing work. Therefore, we are now closing all older feature proposals on the main issue tracker.

If you are interested in this feature proposal, please open a new proposal on the GIP tracker following the given issue template (after checking that it doesn't exist already). Be sure to reference this closed issue if it includes any relevant discussion (which you are also encouraged to summarize in the new proposal). Thanks in advance!

@jamie-pate
Copy link
Contributor

If anyone is interested I'm working on an experiment for this kind of databinding here:
https://github.com/jamie-pate/godot-control-data-binds/blob/main/addons/DataBindControls/DataModel.gd

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.