-
-
Notifications
You must be signed in to change notification settings - Fork 98
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
Allow custom GDScript annotations which can be read at runtime #1316
Comments
You could also make your variables exported and save the data as |
AFAIK, this doesn't work for embedded classes. |
+1 for this. So I'm basically working on something similar. exposing an annotation property would be nice. My work around is to use some naming prefix convention and yes iterate the object properties. Example:
Then a net synchronizer just iterates the properties and checks for that prefix. But it's not ideal. I'd prefer not to rely on a naming convention here, as a custom annotation would be much less hacky and easier to use. |
My main question about this is how do you expect to gather this information? Can you mock up a sample code with the functions you would call to retrieve the annotations? I also want to make annotations explicitly registered, so a typo won't break the functionality without the user noticing. |
DefineTo register it would be nice an API like this: @tool
func _ready():
Engine.add_annotation("custom_annotation") C++ version: void register_custom_module() {
Engine::get_singleton()->add_annotation("custom_annotation")
} FetchI think that a good way to retrieve the annotations would the following: GDscript version: # Returns the scripts path
var annotated_scripts = Engine.get_annotated_scripts("custom_annotation")
for script in annotated_scripts:
print("Script: ", script)
var annotated_functions = Engine.get_annotated_functions("custom_annotation", script)
var annotated_variables = Engine.get_annotated_variables("custom_annotation", script)
func f in annotated_functions:
print("-- Function: ", f)
var arguments = Engine.get_annotated_functions_arguments("custom_annotation", script, f)
for key in arguments.key():
print("---- ", key, " = ", arguments[key])
func v in annotated_variables:
print("-- Variable: ", v)
var arguments = Engine.get_annotated_variables_arguments("custom_annotation", script, v)
for key in arguments.key():
print("---- Annotation: ", key, " = ", arguments[key]) C++ version: struct ScriptAnnotationData {
OHashMap<StringName, Dictionary> function_data;
OHashMap<StringName, Dictionary> variable_data;
};
const OAHashMap<String, ScriptAnnotationData> &annotated_scripts = Engine::get_singleton()->get_annotated_scripts("custom_annotation")
// Returns the stored annotations for each script. The key is the script path.
[.....] UsageThis kind of syntax allows the annotation to contains any kind of data. So that it's possible to define dynamic annotations; allowing: @Serialize(name="json_foo")
var foo: int
@Serialize
var bar: String
@Serialize(max_length=20)
var long_string: String
@Serialize(networked=true)
var long_string_2: String
@Serialize(networked=false, max_length=20, name="json_foo")
var long_string_3: String |
We also need a way to take the script level annotations like |
+1 for this. Like @AndreaCatania we have a client-predicted, server-auth, (rollback re-sim) network module. Right now our net 'state' is explicity defined under list of exported properties of our "NetworkGameObject"s. GameData, Player1, and Player2 in below screenshot: We inspect this list at runtime to serialize the game's state, compare, and do any rollback and resims.. The downside to using just this list we can't add any other properties to these objects without them getting serialized along with whats currently exported. Likewise some properties we will want to predict client-side, and some properties we will want to render lag and show/draw in different timestream. And even like the above comment by @AndreaCatania perhaps serialize in different ways, etc. From the c++/native side, it would be great to access any custom annotations at runtime say in here somewhere: Right now we rely and default storage attribute flags for processing, but access to custom annotations here in the object runtime would be quite powerful and flexible. |
OFFTOPIC: @jordo this may interest you: godotengine/godot#37200 |
But ya, for sure +1 for what @AndreaCatania proposing below, this is great:
OFFTOPIC: @AndreaCatania yes I've followed what you've been doing here, godotengine/godot#37200 It's quite an effort! But we needed more explicit control and a more data-orientated approached with our module. Simpler and not tied into as many different godot systems. |
+1 for this proposal. Because that's how I solve the simular problem now. Perhaps this suggestion will allow me to get rid of the initialization of # CHILD CLASS ON SERVER
var mana
var mana_max: float = 418.0
var mana_regen: float = 8.0
var ability_power := 9.0
var ability_haste := 0.0
var level_int := 1
var level_frac
var exp_to_next_level := 280
var gold := 500.0
var creep_score := 0
var kills := 0
var deaths := 0
var assists := 0
func generate_variable_attributes():
.generate_variable_attributes()
var_attrs["mana"] = Attrs.UNRELIABLE | Attrs.VISIBLE_TO_EVERYONE
var_attrs["mana_max"] = Attrs.VISIBLE_TO_EVERYONE
var_attrs["mana_regen"] = Attrs.VISIBLE_TO_OWNER_AND_SPEC
var_attrs["ability_power"] = Attrs.VISIBLE_TO_EVERYONE
var_attrs["ability_haste"] = Attrs.VISIBLE_TO_EVERYONE
var_attrs["strike_chance"] = Attrs.VISIBLE_TO_EVERYONE
var_attrs["level_int"] = Attrs.VISIBLE_TO_EVERYONE
var_attrs["level_frac"] = Attrs.VISIBLE_TO_OWNER_AND_SPEC
var_attrs["gold"] = Attrs.VISIBLE_TO_OWNER_AND_SPEC
var_attrs["kills"] = Attrs.VISIBLE_TO_EVERYONE
var_attrs["deaths"] = Attrs.VISIBLE_TO_EVERYONE
var_attrs["assists"] = Attrs.VISIBLE_TO_EVERYONE
var_attrs["creep_score"] = Attrs.VISIBLE_TO_EVERYONE
func level_up_stats():
# damagable
var health_change = 92 * (0.65 + 0.035 * level_int)
sync_set("health_max", health_max + health_change)
sync_set("health_regen", health_regen + 0.06 * (0.65 + 0.035 * level_int))
sync_set("health", min(health + health_change, health_max))
sync_set("armor", armor + 3.5 * (0.65 + 0.035 * level_int))
sync_set("mr", mr + 0.5 * (0.65 + 0.035 * level_int))
# autoattacking
sync_set("attack_damage", attack_damage + 3 * (0.65 + 0.035 * level_int))
# champion
var mana_change = 92 * (0.65 + 0.035 * level_int)
sync_set("mana_max", mana_max + 25 * (0.65 + 0.035 * level_int))
sync_set("mana_regen", mana_regen + 0.08 * (0.65 + 0.035 * level_int))
sync_set("mana", min(mana + mana_change, mana_max))
# BASE CLASS ON SERVER
enum Attrs {
RELIABLE = 0, # default
UNRELIABLE = 1,
CONST = 2,
VISIBLE_TO_EVERYONE = 4,
VISIBLE_TO_TEAM_AND_SPEC = 8,
VISIBLE_TO_OWNER_AND_SPEC = 16,
}
var var_attrs := {}
func generate_variable_attributes():
pass
func _init():
generate_variable_attributes()
func sync_set(key, value = null):
if value == null:
value = get(key)
else:
set(key, value)
var attrs = var_attrs.get(key) if var_attrs else null
if attrs:
var func_name = "rpc_unreliable_id" if attrs | Attrs.UNRELIABLE else "rpc_id"
if attrs & Attrs.VISIBLE_TO_EVERYONE:
for client in Game.clients.values():
if should_sync_to_client(client):
avatar.call(func_name, client.id, "set_remote", key, value)
else:
if attrs & Attrs.VISIBLE_TO_TEAM_AND_SPEC:
for player in Game.lists[team].values():
if should_sync_to_client(player):
avatar.call(func_name, player.id, "set_remote", key, value)
elif attrs & Attrs.VISIBLE_TO_OWNER_AND_SPEC:
avatar.call(func_name, id, "set_remote", key, value)
for spectator in Game.spectators.values():
if should_sync_to_client(spectator):
avatar.call(func_name, spectator.id, "set_remote", key, value)
return true
return false
func should_sync_to_client(client):
return client.team == team || client.team == Types.Team.Spectators || seen_by_teams[client.team]
# BASE CLASS ON CLIENT (avatar)
puppetsync func set_remote(key, value):
self[key] = value
# CHILD CLASS ON CLIENT
var health := 0.0 setget set_health
var health_max := 0.0 setget set_health_max
var health_regen := 0.0 setget set_health_regen
var armor := 0.0 setget set_armor
var mr := 0.0 setget set_mr
func set_health(to):
emit_stat("health", to)
health = to
func set_health_max(to):
emit_stat("health_max", to)
health_max = to
func set_health_regen(to):
emit_stat("health_regen", to)
health_regen = to
func set_armor(to):
emit_stat("armor", to)
armor = to
func set_mr(to):
emit_stat("mr", to)
mr = to |
DefineWe could use annotations for defining annotations: # Console.gd
var commands = {}
@annotation
func register_command(function: Callable):
commands[function.name] = function This is similar to decorators in Python, a annotation could possibly call back the function it got annotated with. The function will have one argument, function, extra arguments could be added, extra arguments must be passed using the decorator itself. So the above code could be written this way: var commands = {}
@function_annotation
func register_command(function: Callable):
commands[function.name] = function I have a mockup for variables too. # Settings.gd
var settings = {}
@variable_annotation
func setting(name, type, value):
settings[name] = {name, type, value} As we don't have a class for variables we'd be passing name, type and the value, if type is not defined, we'd pass inferred type. FetchAs for fetching functions, we can possibly callback the function that has been annotated as annotation, with the function or variable that got annotated in the first argument and the arguments that got passed with it. @function_annotation and @variable_annotationAs said previously, this comment introduces two new annotations, function and variable annotations to declare custom annotations. These are built in annotations and will work like other built-in annotation. UsageVariable/Property Annotation@Settings.setting()
var speed: int = 50
@Settings.setting()
var start_money := 10.0
@Settings.setting() # I'm yet to decide how we should override variable names that get passed.
var this_name_will_be_overriden := "Hi!"
@Settings.setting("range", 10, 11, 1.0) # Extra arguments
var extra_args = 10 # will be inferred at runtime Function Annotation@Console.command()
func this_is_a_console_command(arg: int, arg_2: int) -> bool:
return arg > arg_2
@Console.command("test")
func set_start_money(money: int) -> bool:
start_money = money
return true This comment is kinda of a new proposal but still I'm commenting it here. |
This comment has been minimized.
This comment has been minimized.
@JuerGenie Please don't bump issues without contributing significant new information. Use the 👍 reaction button on the first post instead. |
I'll probably go "out of bounds" a bit, but imo most annotation requests we see here are either these, implementation wise:
"Type information" heavily overlaps with the current meta data field imo, and in the userspace could be implemented as a decorator that is a function writing metadata. This would also give room for other languages to interact with this information through that interface. I also do not fully understand how decorators would work in Godot. |
Something like |
https://github.com/tc39/proposal-decorator-metadata So, the "connect signal to" decorator is implementable in gdscript, by calling the If there is a setter for something, then decorators can and will be able to interact with it. I think this could be a quite elegant solution for gdscript, but again, I'm not quite certain exactly how it would interact with the rest of the system as in the current version it can not use IF there was a GDScript api to set export parameters one by one then all problems are solved with it imo. |
Is this like Python decorators or something different? |
See #6750 (comment) and #6750 (comment). |
This feature would be greatly appreciated. I'm not aware of any annotations for methods, but having custom ones for both methods and properties would be very useful. Just being able to filter results using annotations with the object's class get_property_list and get_method_list would make things like serializing data or exposing certain properties/methods to designer tools a lot easier. Functionally, just them being tags for other modules to process would be enough imo. |
An imo decent approach for implementing custom annotations could be copied from how Rust handles macros (see (https://doc.rust-lang.org/book/ch19-06-macros.html)). In pseudo-code this could look something like the following:
Where
|
Fully custom annotations could potentially have a huge scope, so I think it's worth considering whether there is some feature with smaller scope that could cover a large percentage of the use cases for custom annotations. Godot's development mantra is simple problem -> simple solution. For example, I've been thinking about an |
Tags solve one aspect, certainly. But I'd also like to use annotations to remove boilerplate, ex: a To me, it's a lot like the justification for the Personally, I like the elegance of python decorators, and think they would have good user ergonomics for gdscript. They're just a function, and they take what they decorate, do some work on it, and return a replacement for it (often just the original item). If a decorator takes arguments, it's called as a function with just those two arguments, and then expected to return a standard decorator function that just takes what to decorate. (i.e. if a normal decorator is the equivalent of Check out the Python decorator proposal, they have some good rationale for why they made the decisions they did, the problems they ran into, and the community reception since. It's useful to see other people thinking through the same problem, IMHO. |
Python approach may be too complicated and beginner unfriendly. When you put more than one decorator over a function, nested execution becomes harder to read and predict. Since C# is a first-party language now, I would suggest attribute approach. User defines custom annotation as a data class, that extends special Annotation class. Constructor executed once per build and can’t have side effects, so it’s a class metadata, not instance one. In the runtime it should be possible to access this metadata, by running a function, that retrieves all annotations of entity. In case of properties, I suggest, that it should return an array of annotations for a given property name. Same for annotation funcs and classes. Like This approach does not have a big scope. Other big advantage is similarity with C# approach. It’s not only ensures feature parity, but also makes possible to retrieve annotated metadata of GDScropt class from C# code. Than it can be handled the same as C# attribute, making possible creation C# plugins, that work with both languages metadata. Example here is potential plugin, that adds button to the inspector, based on |
I was hoping to be able to do some dependency injection by defining custom annotations. This feature would be great! |
This comment was marked as off-topic.
This comment was marked as off-topic.
@MarouaneMebarki Please don't bump issues without contributing significant new information. Use the 👍 reaction button on the first post instead. |
@daihaminkey I like your arguments in favor of the attribute approach. However, I'm not familiar with it first-hand. Could you give a rough idea of what it would look like in GDScript? For example, how would I create an |
@nlupugla I'm not @daihaminkey, but I have experience with C# attributes and want them in Godot, so I'll give a shot at showing what that could look like. # File: Serialize.gds
extends Annotation # All annotations must extend this.
class_name Serialize
# Defines which elements the annotation is valid on.
@annotation_target AnnotationTarget.Property
# The name the field should have in the serialized format.
@export var name: String
# If set, truncate the length of the string or array to this length.
@export var max_length: int # File: MyDataClass.gds
extends Reference
class_name MyDataClass
@Serialize(name="json_foo")
var foo: int
@Serialize
var bar: String
var x: Boolean # File: Serializer.gds
func convert_to_json( obj: Object ) -> String:
# Here I'm imagining the annotations added as another field in the existing property metadata dictionary.
# Seems natural to me, but I don't have expertise here.
var properties: Array[Dictionary] = obj._get_property_list()
for property in properties:
var annotations: Annotations = property["annotations"]
var serialize: Serialize = annotations.get(Serialize)
# Proceed to serialize according to the settings PS: For me the main use case for annotations is a custom inspector plugin. Annotations would be the ideal way to enable one for a field and configure it. That's what most of the built-in annotations are doing for the built-in editors. |
Ah, I see, thanks for the demonstration! This seems like a pretty well-fleshed out approach. Perhaps someone like @dalexeev or @vnen with a better understanding of the language internals can comment on how feasible this would be. My only issue that I can think of at the moment is that GDScript, being a relatively to-the-point, beginner-friendly scripting language, currently doesn't use any patterns like this (that I can think of) where the user is required to extend a particular class in order to get certain language functionality. While you are meant to extend nodes to benefit from certain functionality, I feel that is more of an engine feature than a language feature. Maybe the distinction isn't all that important, but I can't help but feel the approach might feel overly complicated for newcomers. Then again, implementing your own annotations isn't exactly "Hello World", so maybe my concern newcomer friendliness isn't super relevant here. Thinking about this some more, I wonder if instead of extends Annotation
class_name Serialize we could just do annotation Serialize |
I could get behind this approach. It gives me what I'd be looking for out of my use cases, and it "feels" like it fits with the rest of GDScript, for the most part.
I think custom Annotations are an intermediate to advanced use case. I'm imagining some libraries providing common ones, and that being most beginner's interactions with them. I would focus on users with experience with meta programming in other languages and a moderate level of Godot experience as the target audience. imho.
I really like |
1. What capabilities should custom annotations have?Or "What are custom annotations?" Standard annotations are more than just metadata. For example, const _METADATA = {
foo = {name = "json_foo"},
bar = {},
}
var foo: int
var bar: String But this is non-standardized and less convenient since the metadata is separated from the members. There is also no static analysis, which is potentially possible for custom annotations. Also, custom annotations as metadata are not relevant to Python decorators. This is a separate language feature. 2. How to declare custom annotations and avoid conflicts with standard ones?One of the reasons we added annotations to the language is to reduce the number of keywords and potential conflicts with user identifiers. Now we can add any annotation and not worry about conflicts, unlike keywords. If we want to add support for custom annotations, then we must make sure that they do not conflict with the standard ones. For example, we can require that all custom annotations begin with The option above does not require any action to declare annotations. Any annotation with a valid name and arbitrary arguments are allowed. GDScript doesn't do any validation, but you can implement this yourself (only in runtime, without static checks). Another option is the ability to explicitly declare a custom annotation: specify valid arguments and targets (what class member types the annotation can be applied to). Something like: annotation @data_serialize(name: String) {targets=var,func} In this case, GDScript will check that the passed arguments match to the declared signature and that the target is correct at compile time. However, once there is a declaration, the question of the scope of the declaration arises. Is the declared annotation available globally in the entire project? Or does it only affect the file? Or does it operate within the class and its descendants? 3. How to use custom annotations?Depending on the previous point. An important limitation of the current implementation: annotation arguments must be constant expressions. 4. How to get information from user annotations at runtime?It is suggested above to use @data("serialize", {name = "json_foo"})
var foo: int
@data("serialize")
var bar: String
func convert_to_json(obj: Object) -> String:
var script = obj.get_script()
if script is not GDScript:
return
var properties: Array[Dictionary] = obj.get_property_list()
var annotations: Dictionary = script.get_annotations()
for property in properties:
if annotations.has(property.name):
var property_annotations: Array = annotations[property.name]
# ... In theory, we could add a Footnotes
|
Thoughts on what is sufficient
Yes, it may be infeasible to add these to the PropertyInfo underlying get_property_list. Adding annotations there would fatten the structure and it is used in so many places that the cost to people not using the feature may be unjustifiable. But I would argue that we specifically want the annotations to exist on the core level, so that other languages can expose them. Consider the use case of annotations as configuration for custom editors. It would be intolerable for C# Script's fields to not be configurable to use the custom editor.
This would be sufficient for the custom editor use case, so I'd be happy with it. 👍 Thoughts on what is elegantBut I'll be ambition's advocate: What about reimplementing the currently hard coded annotations on top of the generic annotation system? It would decrease the coupling between gdscript and the editor. It's a bit silly for something like Also, even though the initial sensible implementation should probably just be simple metadata, modeling the annotations as fully fledged classes would enable future extensions if they are deemed worth it. For example, a virtual
True, but annotations are available at compile time, so the compiler is free to pay attention to them if it wishes. Thereby even the most low-level magical things could be triggered just as well by built-in generic annotations as by hard-coded token names. |
Honestly. This. Even if we never get custom,"build/export time" annotation processing for GDScript it's still a useful API for language support, and I don't see any architectural reason for why the code for the existing annotations could not just use this API as their data source. One could argue that it would put it in a half-finished state, but I think that would be a problem. |
Describe the project you are working on:
Multiplayer game
Describe the problem or limitation you are having in your project:
Automaticly serializing custom data classes
Describe the feature / enhancement and how it helps to overcome the problem or limitation:
In 4.0 GDScript now has annotations. What I propose is two fold:
Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams:
Then I can wrote some code to introspect
MyDataClass
and find it's properties that it wants serialized, and produce something like:If this enhancement will not be used often, can it be worked around with a few lines of script?:
It can be worked around, it will just be less flexible and elegant.
Is there a reason why this should be core and not an add-on in the asset library?:
At least this particular approach would not be possible without support from GDScript it's self. But other approaches are possible, such as just listing all of the properties using the current inspection methods, and serializing all of them.
The text was updated successfully, but these errors were encountered: