-
-
Notifications
You must be signed in to change notification settings - Fork 21.1k
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
Comments
@olsonjeffery You realize you use setget with only one parameter to leave the getter intact? 😄 var a = 5 setget a_changed # only setter
var b = 6 setget ,b_taken # only getter |
Thanks, I hadn't realized that. Doesn't really change the overall argument,
|
You might event want to modify the default signature like so:
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?
ahhhhhh the possibilities. (Also I can could look into doing this change myself if someone would want to mentor me on it). |
@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... |
I like @bojidar-bg 's suggestion to do. It could be even simpler:
We could have something like this?
We are trying to avoid this use case:
simple syntax :) |
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 |
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 |
@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 |
@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: # 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 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" |
There'll be first-class functions/lambdas in 3.2 or 4.0 (whichever comes after 3.1). |
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. |
I really like the approach of making a property change a function:
I would tend it with optional old and new_val properties. and the I would make it look more like a function using "()". |
The |
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") |
- When there is a setter and an observer, value is not changed before calling the observer - Partially fixes godotengine#6491
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? |
@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! :) |
@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? |
@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 Now if I want to access the data inside of the observable I would do 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 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 💙 |
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. |
@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? |
@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 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 So maybe this is something we might be interested in as a feature for GDScript at some point. |
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. |
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 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 😋 |
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 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 Okay, now you have a refrence to the "Global" so in the _ready function you connect to it 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: 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 |
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.
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.
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.
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. |
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. |
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! |
If anyone is interested I'm working on an experiment for this kind of databinding here: |
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:
This could be streamlined down to:
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:
becomes:
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!
The text was updated successfully, but these errors were encountered: