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

GDScript 2.0 (old) #39093

Closed
wants to merge 1 commit into from
Closed

Conversation

vnen
Copy link
Member

@vnen vnen commented May 27, 2020

EDIT: The discussion here is quite lengthy now and a lot of cross-talk. I can answer small questions here (about the current PR, not future plans) but if you have a proposal to change something I'd prefer that you open a new issue and link it here, so the discussion can be contained in a thread. If you have a feature request you need to follow the GIP procedure.


This is a basic version of what we call "GDScript 2.0" (not an official name since GDScript changes with each engine version). The point is that this breaks compatibility much more than the previous updates.

The reason is that we can take the opportunity to clean up both implementation and design, allowing for a less-confusing language in which we can simplify the more complex constructs into a cleaner appearance. Also the implementation of GDScript has been patched so much that it became super difficult to understand (I can get some blame for introducing static types, which increase the complexity much more).

Most of the things remain the same, so it's not a completely new language. But people will have to relearn some stuff. I will write a new documentation page with the new syntax and everything so the reference will be complete (EDIT: already done it in godotengine/godot-docs#3623). Here I'll highlight most of what is change and what is still missing (which I'll be working on next).

This is only a partial version, mostly to show users what to expect sooner rather than later, and also to show what I'm working on.

Changes

Annotations

Some keywords have been replaced by annotations, as outlined in the proposal: godotengine/godot-proposals#828.

@tool, @onready, and the RPC keywords (@remote, @master, @puppet, @remotesync, @mastersync, @puppetsync) are now annotations. @tool must be the first thing in the file (apart from comments) otherwise it doesn't work.

Exporting now uses the annotations listed in the proposal: godotengine/godot-proposals#828 (comment)

Ignoring warnings will also be done with annotations. But there's no warnings yet, so it's not added for now.

yield is now await

The yield keyword is removed in favor of await. You can use await some_signal which works the same as yield did before. You can also do await my_func() which is the same as yield(my_func(), "completed") but it also works if the function isn't a coroutine: it'll just run synchronously and get the returned value. Note that if my_func() returns a signal, then it will wait for that signal to be emitted.

_init is now new

The _init constructor was declared in object but only really called in GDScript. Since you do MyClass.new() to create a new instance, it makes more sense to use the same name as constructor. This also simplifies type checking as it doesn't need to rename the method name in some peculiar cases.

I reverted this change for the reason outlined here: #39093 (comment)

super keyword for super calls

The previous syntax for super calls was to use .func_name() which can be confused as a continuation of the previous line. Now you to to explicitly use super.func_name(). If you want to call the same function you are in, then super() is enough. This is also valid for constructors, so the old _init().() syntax isn't valid anymore. There's no validation yet, but when type-checks are added back it will enforce a super() call in the constructor if the super class also have a custom constructor.

First-class functions and signals

Now that we have Signal and Callable as Variant types, they are used to represent first-class object. So instead of calling $Button.connect("button_up", self, "on_button_up") you can get the signal and connect to the function name: $Button.button_up.connect(on_button_up). This avoids the use of strings.

Note that Callable has a call() function. So if you want to pass functions around you need to call them like this: var x = my_func; x.call(). Using x() won't work.

String types

Now we have StringName in Variant so a notation for it was added: &"This is StringName". Those are interned strings which are better to use when equality tests are frequent. Since @ is now used for annotations, writing NodePaths need the ^ prefix instead: ^"This/Is/NodePath".

Broken things

Since this is an initial pull request (as I wanted to have it partially working and possible to test ASAP), a bunch of things are still not working at all, as they need to be re-implemented with the new parser code.

Type checks

This will require quite some more time to add back. But in the bright side it should work much better than before. Including a fix to the dreaded issues with cycles.

You can use types in your code, but they won't do anything. This includes casting.

Code completion

Since this relies partially on type checks, it will also be done later.

Warnings

Similarly to the previous point, this also heavily relies on type-checking.

Language server

This relied on the parser code. It will be updated to use the new parser.

setget

My intention is to replace this with properties (see godotengine/godot-proposals#844). I haven't implemented yet to wait the discussion to show possible things that I could be missing. Still, setget as it was will be replaced, so I didn't implement the old way.

Some optimizations

The old code already had some optimizations in place like reducing constant expression on compilation, and replacing range in for loop to not allocate an array. This is undone for now but will be added back soon. Main reason is that the new code will be able to do a better job reducing those expressions by waiting until more information is available.

Probably something else

I tried my best to test the code with some weird scenarios, but users never fail to surprise me. Eventually I'll go back to my project to automate GDScript testing which will avoid regressions from being introduced.

If something is broken, you can open an issue. However, unless it's something really bad I'll refrain doing anything before the next phase is completed.

I also haven't tested in release yet, so no guarantee it's fully working (note that nobody should be using the master branch in production, so this shouldn't be a big deal).

What about traits, lambdas, etc.?

My plan is to first make GDScript work back in the way it was, apart from the actual syntax changes. Then I'll work on new features. Even though there's quite some time before Godot 4.0 enters feature freeze, I prefer to delay working on new features. If we need to release 4.0 without those, it's not the end of the world.

@vnen vnen added this to the 4.0 milestone May 27, 2020
@dalexeev
Copy link
Member

Note that Callable has a call() function. So if you want to pass functions around you need to call them like this: var x = my_func; x.call(). Using x() won't work.

I understood correctly:

func my_func():
    print("Hello!")

func _ready():
    my_func() # It works.
    my_func.call() # It works.
    
    var x = my_func
    x.call() # It works.
    x() # This does not work.

?

@Zylann
Copy link
Contributor

Zylann commented May 27, 2020

@onready

Given the frequency at which onready is used I wonder if it's not better to leave it as a keyword?

Now we have StringName in Variant so a notation for it was added: &"This is StringName"

This is a huge thing. To the point it is the default for every string in C#, Java, Lua, Javascript and... maybe Python? It may have a use far more than we think, and then requiring to use & a lot.
Basically, everything that is not straight up text. Config properties, dictionary entries, shader parameters... which brings the question of making existing dictionary-based APIs use more StringName, like raycast results for example. So to me, it almost sounds like it's text strings that should have a special syntax instead, but it might sound extreme compared to our current GDScript.

About dictionaries, they can be used a lot in GDScript because apart from classes, that's the only other way to represent named associative data. Considering we can declare them this way:

var d = {
	one = "Hello",
	two = "World"
}

print(d.one, d.two)

At the moment this still uses String all the way.
Should one and two be StringName when written in this way? Or should we always have to go through the burden of writing:

var d = {
	&"one": "Hello",
	&"two": "World"
}

print(d[&"one"], d[&"two"])

Sadly I can see why it may not be simple to do such a choice, at least with dictionaries...

Since @ is now used for annotations, writing NodePaths need the ^ prefix instead: ^"This/Is/NodePath"

I didn't know this was even a thing :O
So now we have $, ^ and & to prefix strings in order to get a node, make a NodePath, or make a StringName.

@hilfazer
Copy link
Contributor

Will enums be 2nd class citizens or still 3rd?

@clayjohn clayjohn mentioned this pull request May 27, 2020
@vnen
Copy link
Member Author

vnen commented May 27, 2020

@dalexeev yes, that is correct.

@vnen
Copy link
Member Author

vnen commented May 27, 2020

@onready

Given the frequency at which onready is used I wonder if it's not better to leave it as a keyword?

Well, I'm not sure if "frequency" is the defining point. It's more like it's tangent to execution. onready is a bit in the fence since it's delayed initialization. But also: it's just one character more.

Now we have StringName in Variant so a notation for it was added: &"This is StringName"

This is a huge thing. To the point it is the default for every string in C#, Java, Lua, Javascript and... maybe Python? It may have a use far more than we think, and then requiring to use & a lot.
Basically, everything that is not straight up text. Config properties, dictionary entries, shader parameters... which brings the question of making existing dictionary-based APIs use more StringName, like raycast results for example. So to me, it almost sounds like it's text strings that should have a special syntax instead, but it might sound extreme compared to our current GDScript.

In a way, yes, maybe making StringName by default is more useful. OTOH, many of this can be done behind the user's knowing. If you are passing as parameter to a function that accepts a StringName, it'll be converted automatically. So it's more of a question on when do you need to intern the string, which many times don't make much difference.

It might be something to consider. I probably wouldn't make multiline strings a StringName though.

About dictionaries, they can be used a lot in GDScript because apart from classes, that's the only other way to represent named associative data. Considering we can declare them this way:

var d = {
	one = "Hello",
	two = "World"
}

print(d.one, d.two)

[...]

My idea is to make that use StringName by default. This still need to be fixed in core though, because Variant::get_named() coerces to a String.

Main problem is that there's a difference between attribute and subscript (that is, doing d.one vs doing d["one"]). Well, if you do it directly it uses the same code, but if the index is a variable it just uses that value and a regular get(). There's also a difference between Lua style (one = "Hello") and Python style ("one": "Hello). The first I can make StringName the others not really. Maybe making StringName default solves this...

I have to think about it and also consult with reduz.

Since @ is now used for annotations, writing NodePaths need the ^ prefix instead: ^"This/Is/NodePath"

I didn't know this was even a thing :O

Against all odds this is actually a documented feature.

So now we have $, ^ and & to prefix strings in order to get a node, make a NodePath, or make a StringName.

Well, $ doesn't need a string, since you can do $VBoxContainer/Button just fine. But yes, we have the three prefixes.

@vnen vnen force-pushed the gdscript-rewrite-squash branch from d23b565 to 733f73f Compare May 27, 2020 22:07
@Sslaxx
Copy link

Sslaxx commented May 27, 2020

Any way of preventing confusion between ^ and $ in particular? Warnings or errors if they're used in an inappropriate place?

@neikeq
Copy link
Contributor

neikeq commented May 28, 2020

Not against that syntax. but it would be great if it could also be omitted when it can be inferred, like these cases:

var name: StringName = "foo"
call("bar") # expected parameter is of type StringName

@norlowski
Copy link

I'm a pretty new Godot user, but I thought you might want to hear from a 'common folk'. The first class signals sound awesome. However, I am confused about the need for StringName. Are there optimizations that can be had by using such a class?

Also, The removal of typed parameters seems like a regression. I've found them immensely valuable as I learn Godot and GDscript. Maybe I misunderstood and the final PR will have them back?

It seems like the readability of GDScript will decrease if we add more symbols, especially to strings as in the example from @Zylann . Maybe there are optimizations I am not aware of that would come with such changes, but I am not excited about StringName.

This is a huge thing. To the point it is the default for every string in C#, Java, Lua, Javascript and... maybe Python? It may have a use far more than we think, and then requiring to use & a lot.

var d = {
	&"one": "Hello",
	&"two": "World"
}

print(d[&"one"], d[&"two"])

@Reneator
Copy link

Type checks
This will require quite some more time to add back. But in the bright side it should work much better than before. Including a fix to the dreaded issues with cycles.

Thats fantastic! I was often struggling with circular references, which would then create a cascade of script-errors, when i am using class_name + static typing in a lot of classes.

@Calinou
Copy link
Member

Calinou commented May 28, 2020

Are there optimizations that can be had by using such a class?

Yes, StringNames are much cheaper to compare to each other compared to regular Strings. This kind of optimization is also found in many other languages.

Also, The removal of typed parameters seems like a regression. I've found them immensely valuable as I learn Godot and GDscript. Maybe I misunderstood and the final PR will have them back?

They haven't been implemented yet, but they will be re-added before the final release.

Maybe there are optimizations I am not aware of that would come with such changes, but I am not excited about StringName.

To solve the usability issues, we could make StringName the default (or automatically infer its use when required).

@Myran
Copy link

Myran commented May 28, 2020

This is the standard way for me to reference nodes

export (NodePath) onready var energy_label = get_node(energy_label)

Not using this will mean that any change to the tree will break paths with sometimes hard to find bugs. While using this ensures it will always work if I can run the scene.

Will this line still be possible after the update and how would it look?

(Its a bit awkward to write and I wish I could write somthing like this instead)

NodeVar energy_label

@fab918
Copy link

fab918 commented May 28, 2020

Sound really exciting!

In a way, yes, maybe making StringName by default is more useful. OTOH, many of this can be done behind the user's knowing. If you are passing as parameter to a function that accepts a StringName, it'll be converted automatically. So it's more of a question on when do you need to intern the string, which many times don't make much difference.

I think this should be transparent for the users to keep the readability and simplicity of GDScript.

@Reneator better to be patient and let the team have a clean restart, this PR is a big change, this is also the time to make or give your feedback on proposals. They are some about typing, i.e., make a fully typed workflow possible (any type, nullabe & way to force the typing) and many others.

@bojidar-bg
Copy link
Contributor

Random suggestion, but what if instead of making normal (") string literals into StringNames and mutliline (""") string literals into Strings, we made double-quoted (" and """) string literals into Strings and single-quoted (' and ''') string literals into StringNames?

Alternatively, can we somehow use the same sigil for nodepaths and string names? (Gut feeling says "no" on this one, but maybe...)

@dalexeev
Copy link
Member

_init is now new

#16863 (comment), #16863 (comment):

200528-2

I think @Zylann's remark is pretty significant. Conceptually, new() is a static function of the class, while self is available inside _init().

yes, that is correct.

I understand that this is for performance, but this behavior is rather unusual and can be confusing. Do you plan to allow the use of x() in the future?

@nathanfranke
Copy link
Contributor

Now that we have Signal and Callable as Variant types, they are used to represent first-class object. So instead of calling $Button.connect("button_up", self, "on_button_up") you can get the signal and connect to the function name: $Button.button_up.connect(on_button_up). This avoids the use of strings.

What about the target parameter of connect (Where we put self)? Is it now an optional second parameter of Signal.connect()?

If so, that sounds like good behaviour to me.

$Button.button_up.connect(on_button_up), but optionally $Button.button_up.connect(on_button_up, $ListeningNode).

@aaronfranke aaronfranke changed the title GDScript 2.0 [WIP] GDScript 2.0 May 28, 2020
@aaronfranke aaronfranke marked this pull request as draft May 28, 2020 10:57
@katoneko
Copy link

@nathanfranke I think path to the function already has target baked in.
$Button.button_up.connect(on_button_up) # func is here
$Button.button_up.connect(some_object.on_button_up) # func is in some_object

@nathanfranke
Copy link
Contributor

@katoneko Thanks, makes a lot more sense now 😄

@dalexeev
Copy link
Member

@nathanfranke

$Button.button_up.connect($ListeningNode.on_button_up)

@Zylann
Copy link
Contributor

Zylann commented May 28, 2020

I think @Zylann's remark is pretty significant. Conceptually, new() is a static function of the class, while self is available inside _init().

Yeah, I remember that... and it makes me wonder how could a script/class could create a new instance of itself then, without having to rely on stunts like get_script() (not viable in static context) or circular reference to itself which would then require the inconvenience to write load(fullpath).new() or being forced to give it a global class_name. If new was a static function, you could just call new() from inside the script, both in a method or a static method, and it would just work. But if new is also the non-static constructor function, it won't. Maybe the constructor could be named _new, or just constructor(), like Squirrel?
Btw, would be nice to have destructor too.

@dalexeev
Copy link
Member

it makes me wonder how could a script/class could create a new instance of itself

Something like this, but this makes little sense:

static func new(a: int) -> MyClass:
    var instance = MyClass.__new__()
    instance.a = a
    return instance

So, there are 3 reasonable options:

  1. Leave _init as it is.
  2. Rename _init to _new.
  3. constructor().

@vnen
Copy link
Member Author

vnen commented May 28, 2020

I think @Zylann's remark is pretty significant. Conceptually, new() is a static function of the class, while self is available inside _init().

In practice I don't feel there's much of a difference. Having the constructor called new() and instancing as MyClass.new() makes it more clear that it's calling the new() function. You can't have a static new() in your script anyway (since MyClass.new() creates a new instance instead of calling your function) so the name was unusable. Now it has the same meaning.

Python is different because you create instances with MyClass(). So there's not a "function that you are calling". Even though in GDScript the new() function is in the GDScript class itself, having new in the script makes it more cohese.

It also simplify type checking, since MyClass.new() can be checked with the new() function in MyClass without having to rename under the hood...

Yeah, I remember that... and it makes me wonder how could a script/class could create a new instance of itself then, without having to rely on stunts like get_script() (not viable in static context) or circular reference to itself which would then require the inconvenience to write load(fullpath).new() or being forced to give it a global class_name. If new was a static function, you could just call new() from inside the script, both in a method or a static method, and it would just work. But if new is also the non-static constructor function, it won't. Maybe the constructor could be named _new, or just constructor(), like Squirrel?
Btw, would be nice to have destructor too.

AFAIK it wasn't possible before to have a naked new() to create a new instance. If it's possible, you also you apply a higher meaning to what new() is, as an implicit static method.

I believe this can be simply solved by adding a static reference to the same script, like __script__ or something. So you can do __script__.new() instead.

@aaronfranke
Copy link
Member

aaronfranke commented Jun 5, 2020

@Geequlim The reason GDScript is being rewritten is because of Epic megagrant money that specifically must be used for either rendering or scripting. EDIT: Sorry, this statement is misleading, as akien said below. The truth is that Godot asked for megagrant money and Epic granted it specifically for rendering or scripting, so scripting is being redone because Godot devs wanted to and the grant provided an opportunity, not just because Godot wants to spend money or do what Epic says.

https://www.patreon.com/posts/funding-and-up-36771862

As many know, we got an Epic Megagrant two months ago which basically has been used so both I (Juan) and George Marques can work full time paid by it (the Megagrant was specifically requested and granted for working on rendering and scripting, can't be used for anything else).

@Zireael07
Copy link
Contributor

@yuyubibibobibo: For a language with an ecosystem, use Python bindings.

@revanj
Copy link
Contributor

revanj commented Jun 5, 2020

I just read the entire proposal thread on annotations and it seems to be a dope feature. I never actually encountered @"Nodepath" syntax before so I'm assuming NameString& and Nodepath^ wouldn't affect too many people. GD Script 2.0 really is an awesome upgrade.
Still, sorry I have to be the one guy that says this, but....
The change would mean that every single current plugin needs to be rewritten, more or less. This can easily trigger some people, knowing their work suddenly just got deprecated overnight. I'm quite new so I don't know if this happened with Godot 2.0 -> 3.0 transition before. I wonder if anyone who writes Godot plugins can share his or her view on the change...?

@akien-mga
Copy link
Member

The reason GDScript is being rewritten is because of Epic megagrant money that specifically must be used for either rendering or scripting.

Please avoid making such statements. You make it sound like we have to change GDScript because we got money for it, while in reality, we asked for funding because we wanted to improve GDScript... There's a difference between doing work for the sake of using money (what your statement infers) and doing work that we planned to do, funding it with money that we requested for it.

@vnen
Copy link
Member Author

vnen commented Jun 5, 2020

  • Why we need a new script language to replace the current one ?
    Why the new one is still the way to build from zero ?
  • We have to maintain the parser,runtime,LSP,code editor,debugger etc
  • We have to pay a lot of time to make it better then we might need another new one

My reasoning is similar to rewrite of the renderer, audio engine, and other code that has been redone since Godot went open source: the code is stale, full of patches, using older non-optimized techniques, hard to maintain (be adding enhancements or fixing bugs).

I took the opportunity to improve in the areas that caused confusing and had non-intuitive behavior: like yield, super calls, and setget. Those are things that I've seen multiple users having difficult understanding. Besides that, annotations were introduced to reduce the load on keywords which were added to GDScript without much thought (onready, remote and other RPC modifiers). And that's pretty much it for what changed.

First-class functions and signals is more a change in the engine API than GDScript itself.

  • For user who using this language have to write everything from zero too this is really a problem

GDScript is changing and breaking compatibility, but not by that much. I will work on a translating guide and possibly an automated tool. Won't be a long guide, because there isn't a lot to say. There's no reason to "write from zero". There will be things to adapt but not that much.

In fact, I believe that GDScript changes in themselves will be less of a problem than translating the API changes from 3.x to 4.0.

Therefore, I suggest not to continue to be devoted to investing in self-developed languages. Instead, choose a scripting language with a good ecosystem, so that we can be free from the tedious tool chain development work and bug fixes. The most important thing is that game developers have also reduced the burden, they do not need to learn a new programming language that never been heard. They can use godot to develop their dream games faster with their existing programming skills.

We also lose stuff by adopting another language. The fact that we can add keywords (or now annotations) to change behavior specific to the engine is a great way to improve fast prototyping, which is one of the reasons GDScript exist. Like onready or using $ as shorthand for get_node is something hard or impossible to do with a general purpose third-party language.

It's also a fallacy that "we can be free from the tedious tool chain development work and bug fixes". Maintaining a third-party language requires updating the engine and the bindings from the third-party, making sure the new additions still work, that it didn't introduce new bugs, etc. We also need debugger integration because we need to pause the game on a breakpoints.

If you look at the C# bindings, you can see that there's a bunch of work to maintain that.

Having an integrated code editor is something that a lot of people like. Even without now mainstream features, it's something that could be improved over time. I do believe this would be case even if a established language were introduced. The editor could be a LSP client that interact with language servers to provide the common completion and error-checking features. That could happen for GDScript, C#, or another language.

And the thing is, if we were to drop GDScript, C# is already there and working, so that would be the new main language. If the point is using a established language with a vast ecosystem, you are free to use C#.

@vnen
Copy link
Member Author

vnen commented Jun 5, 2020

Still, sorry I have to be the one guy that says this, but....
The change would mean that every single current plugin needs to be rewritten, more or less. This can easily trigger some people, knowing their work suddenly just got deprecated overnight. I'm quite new so I don't know if this happened with Godot 2.0 -> 3.0 transition before. I wonder if anyone who writes Godot plugins can share his or her view on the change...?

Again: API changes will be a bigger problem than GDScript syntax changes. I had a plugin to convert from 2.1 to 3.0 and wasn't that much of a deal to rewrite (I also took the opportunity to clean up code, so it was a plus).

@dalexeev
Copy link
Member

dalexeev commented Jun 6, 2020

Is &name syntax (without quotes) support planned for simple StringNames (like $Node)?

# Before:
tween.interpolate_property(self, "variable", variable, new_value, duration)

# After:
tween.interpolate_property(&variable, variable, new_value, duration)
# (The color of "&" and "variable" in the code highlight should be the same.)

# Or only:
tween.interpolate_property(&"variable", variable, new_value, duration)
# ?

@Zylann
Copy link
Contributor

Zylann commented Jun 6, 2020

@dalexeev &name would be ambiguous with the existing & operator I think

@dalexeev
Copy link
Member

dalexeev commented Jun 6, 2020

@Zylann With the bitwise AND operator? But it is binary, not unary. And code highlighting greatly affects perception. I think the choice of the & character for StringNames is due to the similarity with the syntax of pointers.

@hilfazer
Copy link
Contributor

hilfazer commented Jun 8, 2020

@Zylann With the bitwise AND operator? But it is binary, not unary. And code highlighting greatly affects perception. I think the choice of the & character for StringNames is due to the similarity with the syntax of pointers.

&"quoted_string_with_ampersand" can have single colour as well.

@me2beats
Copy link

me2beats commented Jun 15, 2020

The previous syntax for super calls was to use .func_name() which can be confused as a continuation of the previous line. Now you to to explicitly use super.func_name()

will it now be possible to store superclass of A in a variable
var super_a = super
and call its method from another class (for example B)?
super_a.func(args)

something like what Python can do

class Z:
    def x(self):
        print ('Z x')

class A(Z):
    def x(self):
        print ('A x')

class B:
    a = A()
    def y(self):
        
        super(A, self.a).x()

B().y()

# prints `Z x`

@vnen
Copy link
Member Author

vnen commented Jul 22, 2020

Superseded by #40598.

@chucklepie
Copy link

Can yield(my_func(), "completed") be expanded upon? as in, is the only way to use await my_func() be if a 'completed' signal is raised, or has another mechanism been put in for a 'default' class signal. For example, "animation_finished"...

@YuriSizov
Copy link
Contributor

Can yield(my_func(), "completed") be expanded upon? as in, is the only way to use await my_func() be if a 'completed' signal is raised, or has another mechanism been put in for a 'default' class signal. For example, "animation_finished"...

The completed example is for coroutines. If you want to await for a signal you just await object.signal. It's explained in the same paragraph in the OP.

@chucklepie
Copy link

Can yield(my_func(), "completed") be expanded upon? as in, is the only way to use await my_func() be if a 'completed' signal is raised, or has another mechanism been put in for a 'default' class signal. For example, "animation_finished"...

The completed example is for coroutines. If you want to await for a signal you just await object.signal. It's explained in the same paragraph in the OP.

Yeah, my bad, I was just reading it quickly and it's late. I like that connect is becoming first-class too. If all the gdscript "string" mechanisms are wiped out completely it'll be a glorious day.

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

Successfully merging this pull request may close these issues.