godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.17k stars 98 forks source link

Add a Node query language for getting nodes and arrays of nodes #11221

Open xxxrvn opened 6 days ago

xxxrvn commented 6 days ago

Describe the project you are working on

It is a general proposal idea, for getting nodes or list of nodes.

Describe the problem or limitation you are having in your project

It would be better for a lot of solutions.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Instead of writing Parent.get_node("Child"), it should be possible to use a more concise syntax like Parent.$Child. This would make code cleaner and more intuitive while building on Godot’s existing shorthand for accessing nodes.

The $ operator could be enhanced to include powerful query capabilities: Additional syntax extensions could include:

$: Returns the first matching node.

$$: Returns a list of all matching nodes.

P.$$*: Returns a list of all direct child nodes of P.

P.$$>*: Returns all child nodes of P, recursively.

P.$$>*{.Rocket}: Filters the child nodes to include only those of the class Rocket.

P.$$>*{:Items}: Filters the child nodes to include only those in the group Items.

P.$$>*{.Rocket|Bomb}{@active}: Filters for nodes of class Rocket or Bomb where the active variable is true.

P.$$>*{.Rocket|Bomb}{@fromPlayer!=self}: Filters for nodes of class Rocket or Bomb where the fromPlayer variable is not the caller (self).

P.$$>*{::func(x):(x.val1%3)==0}: Returns nodes where a custom function evaluates as true, such as nodes whose val1 variable is divisible by 3.

P.$<*{$Map}: Finds the closest parent node with the name Map.

A compact syntax could replace verbose code for checking overlapping areas. For example, instead of writing:

var areas = get_overlapping_areas() for area in areas:
  if area is Rocket or area is Bomb:
    if area.fromPlayer != self:
      damage(area.makeDamage())

You could use a more streamlined version like this:


for area in $<*{$Map}.$$>*{.Rocket|Bomb}{::overlapps}{@fromPlayer!=self}:
    damage(area.makeDamage())

This approach eliminates boilerplate code and makes intent clearer while working with physics areas and scene graph queries.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

$: Returns the first matching node.

$$: Returns a list of all matching nodes.

P.$$*: Returns a list of all direct child nodes of P.

P.$$>*: Returns all child nodes of P, recursively.

P.$$>*{.Rocket}: Filters the child nodes to include only those of the class Rocket.

P.$$>*{:Items}: Filters the child nodes to include only those in the group Items.

P.$$>*{.Rocket|Bomb}{@active}: Filters for nodes of class Rocket or Bomb where the active variable is true.

P.$$>*{.Rocket|Bomb}{@fromPlayer!=self}: Filters for nodes of class Rocket or Bomb where the fromPlayer variable is not the caller (self).

P.$$>*{::func(x):(x.val1%3)==0}: Returns nodes where a custom function evaluates as true, such as nodes whose val1 variable is divisible by 3.

P.$<*{$Map}: Finds the closest parent node with the name Map.

If this enhancement will not be used often, can it be worked around with a few lines of script?

It could be a general improvement and main feature.

Is there a reason why this should be core and not an add-on in the asset library?

This improvement would regard core parsing functions, and could be better optimized if it would be a core feature.

dalexeev commented 6 days ago

Instead of writing Parent.get_node("Child"), it should be possible to use a more concise syntax like Parent.$Child.

There are find_child(), find_children() and find_parent() methods that support some filters.

For more complex search rules, you can implement a custom tree traversal/search algorithm, the engine provides everything you need for that. In my opinion, this is more material for a library/addon than for the core.

As for syntactic sugar in GDScript, I think it would be redundant and confusing. Yes, GDScript has $Node shorthand and ^"NodePath" literal, but these are very basic and common scenarios. Syntactic sugar for complex things has the danger of becoming overly complicated over time.

It would also be a waste of tokens that could potentially be used for other things. Yes, GDScript is a domain-specific language. But its domain is scripting game logic in Godot, it is not a highly specialized language related only to tree queries/rules (XPath, CSS, etc.). So even if we add something like that to core, I don't think that means we have to add syntactic sugar to GDScript. It would just be a Node method that takes a string. Like querySelectorAll() in JavaScript.

Meorge commented 6 days ago

I like the idea of having ways to filter and search for nodes, but I admit some of the syntax proposed here is confusing. Between

var areas = get_overlapping_areas()
for area in areas:
  if area is Rocket or area is Bomb:
    if area.fromPlayer != self:
      damage(area.makeDamage())

and

for area in $<*{$Map}.$$>*{.Rocket|Bomb}{::overlaps}{@fromPlayer!=self}:
    damage(area.makeDamage())

I would personally much prefer the former snippet. While it may take a few more lines, each line is very clear and concise, and I can track exactly what it's meaning. The latter snippet feels very dense, and I have to keep looking back and forth at your key to try to understand what each step is doing.

sockeye-d commented 6 days ago

Alternatively, you can use Array.filter:

for area in get_overlapping_areas().filter(func(area): return area is Rocket or area is Bomb and area.fromPlayer != self)
    damage(area.makeDamage())
Mickeon commented 5 days ago

It would be better for a lot of solutions.

In this repository's README, it is stated:

All proposals must be linked to a substantive use-case. In justifying your proposal, it is not enough to say it would be "nice" or "helpful". Use the template to show how Godot is not currently meeting your needs and then explain how your proposal will meet a particular need.

So, this feels like a proposal that is really looking for a problem that doesn't exist. I understand it sounds cool. The suggested syntax is very reminiscent of JQuery to me, but GDScript is not JavaScript. Godot generally discourages this kind syntax because, although it may be faster to write in the moment, it makes the code harder to maintain.