Godot: Class (instead of file) based script assignment in editor

Created on 31 Dec 2016  Â·  66Comments  Â·  Source: godotengine/godot

Following some discussion with Karroffel and getting neikeq's GodotSharp module functional, I had an idea to make script/class organization easier, by having Godot automatically parse the project files for all classes/scripts and having a popup that lists them (with search bar and filters) to select a class rather than pointing a given node to a file directly. For example, on a Node2D selecting a subclass would make this window appear:

untitled3

For GDScript/VisualScript, the class path could either be the file path in dot notation, or to further reduce the dependency on the file system's organization, GD/VS could have namespaces added or simply always appear top-level with no namespaces.

XCode operates similarly where the selection menu for a custom subclass on a view/component will list all of the classes in the project that inherit from the required type.

archived feature proposal editor

Most helpful comment

I would still very much like a system like this.

It would be important to not break old code, so what about a system like this?


Registry

A script registry would scan the project for all detected script files and queries the appropriate ScriptingLanguage for any "exported" classes. An "exported" class would be identifyable by name.

There could be a "low level" API for querying and even procedurally inserting exposed classes.

By accessing the ScriptRegistry singleton it would be possible to get Scripts by name for example.

Namespaces

Since "exported" classes would need to have a unique name namespaces might be optionally needed, using a default "empty" namespace would work too.

GDScript integration

With this design it wouldn't be required to be limited by the file-based architecture that's currently implemented in Godot.

To not break old code, the ScriptRegistry integration should be optional and old code would still behave like it does.

What could be done with this registry in place, is that multiple classes (or in the case of GDScript - inner classes) could be put into one file.

example of usage

fruits.gd

# no "extends" here, will default to Reference then

export class Fruit:
    var name
    func _init():
        name = "some fruit, has not been given a name yet"

export class Apple extends Fruit:
    func _init():
        name = "Apple"

export class Pear extends Fruit:
    func _init():
        name = "Pear"


export class FruitBox:
    var fruit

    func set_fruit(f):
        fruit = f

    func get_fruit():
        return fruit

# for namespaces this could be used
# 
# export("fruits") class ...
# or this for nested namespaces maybe?
# export("fruits", "tasty")

some_other_file.gd

extends KinematicBody2D # maybe a Player controller?

func on_hit_by_object(obj):
    if not obj.is_in_group("has_fruit_attachement"):
        return

    if obj.get_fruit() is Pear: # inheritance check via name instead of preload()
        show_dialog_bubble("Why are you throwing pears? Their shape doesn't even hurt me on impact")
    else:
        show_dialog_bubble("Ohhh you hit me with a ", obj.get_fruit().name)

func on_fire_button_pressed():
    var fruit_box = FruitBox.new() # creates class by name, not preload()
    fruit_box.set_fruit(Apple.new())
    shoot_box(fruit_box)

GDScript would check if an identifier exists in the registry, like this ScriptRegistry.get_class("SomeClass", ["fruits", "tasty"]) and if it exists it can be used. This is similar to how GDScriptNativeClass works, it checks if an identifier names a class known by ClassDB, Autoloads work the same way.


TL;DR:

  • add a ScriptRegistry singleton that scans for exported classes
  • No big changes needed for this to work
  • existing code will still function
  • purely optional
  • namespaces to allow duplicated names of classes
  • referring to a class would look like accessing a Godot internal class

WDYT?

All 66 comments

I partly agree -- especially for C#/DLScript where the classes are known beforehand, it would be nifty to have this; but for languages as GDScript and VScript where every file is a class, and there is no global "polling" of all files, it makes no sense at all (even less with the scenes idea, maybe you wanted to have just a "dotified" path there?)

Scenes.MainScene doesn't refer to the main scene in the project. It would be equivalent to a folder structure set as res://Scenes/MainScene/TestScript.gd. The Dot notation is to be graphically consistent with namespaces even if it is technically a filepath. It is mostly for C# though where one file can contain multiple classes.

Two possible issues:

  • While this makes sense for languages like C# where there is totally no file/class relation, it would affect usability for GDScript (and other possible similar languages. Java for example has a file/public class relation). I want to select GDScript scripts with a file dialog not by searching a qualified name. And since we don't know the script type the user will choose in advance, we cannot display different loading forms.
  • How are you going to open the script in the editor? A Godot script MUST be a file (not a qualified name), that's how the API works and I don't think it's worth changing it. This one doesn't apply if you only intend to use the qualified time at load time and instantly get the actual file with a new virtual method in the ScriptLanguage API.

This is one of the solutions I had in mind for solving the namespace detection issue with C#: neikeq/GodotSharp#9, but I soon discarded the idea for the reasons above (we are just going to use a simple parser to find the namespace).

Fair concerns, ideally there would be no path association with script loading, so to assign a script you would simply point it to a script with the correct inheritance for the node it is being attached to, and if the file were moved it would update the reference.

Maybe a better alternative is to be able to sort scripts by what they extend, such as having an option to search res:// for scripts that extend KinematicBody2D, with the option to include those that indirectly subclass, such as with extends "MyScript.gd".

@neikeq if you're using a parser to find the namespace, does it mean F# can't be used anymore? I liked that the code just had to live in the assembly and be CLI compliant, with this you kick out all the CLI compliant languages that run on Mono, don't you?

@karroffel The current behaviour is that it iterates through the typedef table until it finds a class that derives from GodotEngine.Object and matches the name of the script file. The problem with this is that you can't have NamespaceOne.Player and NamespaceTwo.Player, making namespaces pointless. This will be the default behaviour for any language that has no parser, including F# (in other words, it will be the same as now). Alternatively you can write a parser for it (see 4613cb7).

On my godot python binding, I've deal with this problem following the zen of python: "Explicit is better than implicit"

So to be available from Godot, a Python class should have been explicitly decorated as exposed. Obviously only one exposed class is allowed per file so it feels just like GDScript.

from godot import exposed

@exposed
class Player(Node):
    def _ready(self):
        pass

While it works great with dynamic language where a lot can be done at load time, I'm not sure such a thing could be achieved on C#...

@neikeq I don't know much about C#, is there a __file__ attribute (like the C one which the preprocessor replaces by the file path at compile time) ?
If so you could implement this in the decorator to end up with some classes flagged as exportable and with there associate file path. From there it would be trivial to run your scan between the compiled class and the source file.

@touilleMan I tried that already. I was close to achieving this by using the CallerFilePath attribute. This was an attempt: https://gist.github.com/neikeq/db685eda63e558d207abf12dececa51b
The problem was that the path provided by CallerFilePath is absolute, and considering it's at compile time... I thought about saving the path to the project somewhere and then using it to fix the script paths when running the game, but I don't think anyone would like the idea of exporting their dev directory path with the game.

@neikeq at least you made me puke my tea with your commit message 😆

I guess the simplest way that remains is to force user too keep namespace and file path the same for the exposed classes. Potentially providing a configuration field the user could overwrite to specify by hand a custom path for the class.

What I think would be a better generalized solution for all resources, would be for each resource whether script or mesh to simply have a unique id that would be stored in an import file. So, each resource would have the unique ID instead of a file path. The unique ID could then be associated with each asset. So if a script of any type, or a material, texture, audio file, etc would no longer be dependent on a specific path.

This would require Godot to scan the project when you load it and create a Map of each asset ID and it's path, and then when a script slot has UniqueID in it, then it will internally know that ID X belongs to path X. If an asset is moved, then it's import file is also moved and the Map is updated with the new path.

This would allow all resources to be moved around and literally nothing would ever break.

edit: I think I'll add this as a feature request.

Now, as far as having to type in the fully qualified script name, I think this is a bad idea that sounds very good on the surface. For instance, what happens when you decide a script that you've used in several places in your project has been poorly named?

You have to either:
A) Go to each and every place where you've used it and correct the name.
or
B) Just leave it as it is.

With my solution in the previous post you:
A) Rename the file
and
B) Rename the class in the file

and then everything is auto-magically updated for you since the Unique ID didn't change.

@NathanWarden That's actually closer to what I had in mind: a system that eliminates the dependence on specific files and allows some degree of automatic refactoring. My original post was more about filtering down the options for script files when selecting them, but in general the class/file system Godot uses is tedious to use in cases like accessing another scripts enums due to the lack of static variables, etc.

@Xydium Ah, I think I understand, like having a search feature that would help narrow it down instead of digging through directories or something like that? I think that would be a great idea!!!

I opened a feature requiest for using unique IDs... https://github.com/godotengine/godot/issues/15673

I would still very much like a system like this.

It would be important to not break old code, so what about a system like this?


Registry

A script registry would scan the project for all detected script files and queries the appropriate ScriptingLanguage for any "exported" classes. An "exported" class would be identifyable by name.

There could be a "low level" API for querying and even procedurally inserting exposed classes.

By accessing the ScriptRegistry singleton it would be possible to get Scripts by name for example.

Namespaces

Since "exported" classes would need to have a unique name namespaces might be optionally needed, using a default "empty" namespace would work too.

GDScript integration

With this design it wouldn't be required to be limited by the file-based architecture that's currently implemented in Godot.

To not break old code, the ScriptRegistry integration should be optional and old code would still behave like it does.

What could be done with this registry in place, is that multiple classes (or in the case of GDScript - inner classes) could be put into one file.

example of usage

fruits.gd

# no "extends" here, will default to Reference then

export class Fruit:
    var name
    func _init():
        name = "some fruit, has not been given a name yet"

export class Apple extends Fruit:
    func _init():
        name = "Apple"

export class Pear extends Fruit:
    func _init():
        name = "Pear"


export class FruitBox:
    var fruit

    func set_fruit(f):
        fruit = f

    func get_fruit():
        return fruit

# for namespaces this could be used
# 
# export("fruits") class ...
# or this for nested namespaces maybe?
# export("fruits", "tasty")

some_other_file.gd

extends KinematicBody2D # maybe a Player controller?

func on_hit_by_object(obj):
    if not obj.is_in_group("has_fruit_attachement"):
        return

    if obj.get_fruit() is Pear: # inheritance check via name instead of preload()
        show_dialog_bubble("Why are you throwing pears? Their shape doesn't even hurt me on impact")
    else:
        show_dialog_bubble("Ohhh you hit me with a ", obj.get_fruit().name)

func on_fire_button_pressed():
    var fruit_box = FruitBox.new() # creates class by name, not preload()
    fruit_box.set_fruit(Apple.new())
    shoot_box(fruit_box)

GDScript would check if an identifier exists in the registry, like this ScriptRegistry.get_class("SomeClass", ["fruits", "tasty"]) and if it exists it can be used. This is similar to how GDScriptNativeClass works, it checks if an identifier names a class known by ClassDB, Autoloads work the same way.


TL;DR:

  • add a ScriptRegistry singleton that scans for exported classes
  • No big changes needed for this to work
  • existing code will still function
  • purely optional
  • namespaces to allow duplicated names of classes
  • referring to a class would look like accessing a Godot internal class

WDYT?

@karroffel That would be much more convenient, especially if it works with autocompletion. Static variables would be a nice inclusion too, especially for enums.

Just discovered that I've been working on a similar project. Hopefully somebody else hasn't already been working on this too. XD Gotta figure out how we're gonna coordinate on this mess, cause I'm trying to create one branch that handles this Issue and another.

I think gdscript needs no changes for this. Entire system does not need go
become more complex for differences with other systems, or an apparent ease
of use that is not.

On Mar 9, 2018 19:35, "Will Nations" notifications@github.com wrote:

Just discovered that I've been working on a similar project. Hopefully
somebody else hasn't already been working on this too. XD Gotta figure out
how we're gonna coordinate on this mess, cause I'm trying to create one
branch that handles this Issue and another
https://github.com/godotengine/godot/issues/6067.

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/godotengine/godot/issues/7402#issuecomment-371964960,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AF-Z20W_kL-5IcDwgujmNV3BuESUIPn9ks5tcwOzgaJpZM4LYlq8
.

@reduz So then, if we wanted a feature of this sort, it would have to be a feature that is exclusive to and part of the /modules/gdscript/ module? Or you just don't want GDScript to support this at all because of the added complexity?

Also selecting class from a global list is imho a terrible idea. You still know better where your file is than where your class is on a large project.

For C#, I still think the best that can be done is marking the main class in the file with some tag, I fail to see why doing this is not possible.

@willnationsdev I see this pointless in GDScript too. I don't think it's needed.

In other words, going from script oriented to class oriented is a definitive no from me.

@reduz

Also selecting class from a global list is imho a terrible idea. You still know better where your file is than where your class is on a large project.
...
In other words, going from script oriented to class oriented is a definitive no from me.
I beg to differ on this one, at least when it involves C# support.

I can understand how GDScript might not need any such changes, as it is a special purpose language which means anything written in that language would presuppose a Godot specific workflow.

But we can't assume the same with C#, or potentially other similar general purpose languages that are not used as scripts. Such a language like C# is never meant to be used by referencing the sources.

It was simply an unfortunate side effect of introducing the C# binding after GDScript - which is more suitable for such a script oriented approach - rather than being a conspicuous choice to provide an optimal workflow for C# users as well.

C# already has a well established convention and mechanism to resolve and manage dependencies which is all assembly/class based rather than script based, and it's the way what most of its tools and libraries presupposes and what the developers are accustomed to.

And unlike GDScript, there exist a vast amount of third party libraries that can be referenced from a C# project in their binary format. Godot currently supports referencing those dlls from the main *.csproj file, but when it comes to addons we still need to find a way to make it possible for a C# project to reference C# addons with its own binary dependencies both within Godot and external IDEs.

If we are to adhere to a workflow that is alien to a typical C# development process, it would make the development set up more difficult and common practices like building your addon from a popular IDE and distributing it as a NuGet package would provide only a limited usage in Godot, because any classes contained in such an assembly cannot be referecend within the editor.

And I disagree that providing a global class list to be a terrible idea. It's the way most of the IDEs supports a similar functionality (i.e. selecting a target class for a unit test), and with auto completion or namespace support, it's pretty easy to find a wanted class that way.

Actually, in most cases, it's much easier than referencing them by their source locations. Developing a C# project can involve dealing with multiple libraries and classes can be located in a deep namespace hierarchy. So, doing a quick search by their names is often much more convenient than having to navigate between different file system locations.

And it's also how Unity supports its C# binding as well, so I think it'd be better if we also support such a workflow that is more common to typical C# development projects including those involving Unity, rather than shoehorning a binary based language into a workflow designed for a scripting language.

With optional typing, something like syntatic sugar but for classes can be useful to make better code.

Writing

func a_function(load("res://some_directory/sub_directory/character_weapon.gd"): weapon)
  var load("res://some_directory/sub_directory/character.gd"):character = load("res://some_directory/sub_directory/character.gd").new()
...

may look ugly, probably :thinking:

Idk if the static typing was meant to include scripted types (not privy to any of that debate). If it does though, I'm guessing that in that case we'd have to use something without identifiers of any kind, like...

const MyType = preload("res://dir/dir/another_dir/MyType.gd")

func my_func(p_obj: MyType):
    # do something with p_obj where (p_obj is MyType) == true

@willnationsdev I know, also the whole thing an be made by a plugin that registers every new GD file in a singleton (writing consts in the script) for global access, but something integrated may be better.

@eon-s I thought of that too. It's basically a hack, but it'd be MUCH better than nothing. I already set it up so that EditorPlugins can add/remove singletons. Now, if I can submit a PR for GDScript so that it can extend from a Script object instance and not just a string literal filepath, then that'd be something. The GDScript module would have to be able to see into the Editor somehow to look them up though (or have them added to the global map).

@reduz I agree that my original idea at the top of this thread is mostly pointless, and only a convenience feature to avoid clicking through folders. However, with the suggestion from @karroffel, the discussion shifted to file-versus-class as the basis for GDScript. Since his recommendation was an entirely optional feature, a 'definitive no' is slightly heavy-handed. One of the main issues I've had with GDScript is the way scripts are handled as loaded files, and thus require explicit load statements at the top of other scripts for all use cases. Changing the location of a script file means changing all the scripts that reference its path. While that is a similar issue as with import statements in Java, most Java IDEs offer a refactor tool that automatically updates such references, and an auto-import tool that removes the need to remember class locations. As far as I know, Godot's file-mover does not modify paths written in scripts, and while autocomplete does provide script paths, the suggestions aren't filtered down to a specific class name. Loading and storing countless different scripts to do instance-of checks or instantiation is far less elegant than an import namespace.* statement. GDScript is already quick and easy to learn, and an optional feature that resembles other popular languages would assist those coming from a Java/C# background.

Static-typed GDScript classes could instead be a different 'script type', like C# Script, NativeScript, and VisualScript. GDScript (.gd) would be for the existing file-based system, and GDClass (.gc) could be for a registered class with a namespace. The format for such a file might then resemble:

namespace enemies.ranged
import enemies.*

class Tank extends RangedEnemy:
    const MOVE_SPEED = 100 #All consts are static by default

    static var count = 0 #Static vars would also be a nice feature

    var name

    func _init(name):
        count += 1
        self.name = name

    func _process(delta):
        position.x += 100 * delta

If necessary, file scripts could still reference a class script either with import statements or by loading the file, and class scripts could reference file scripts by loading them as well. This ability likely would not be commonly used though, as most developers would select and stick with one paradigm for a project.

First, It's not heavy handed, this is about making a game engine, so the priority is that it's simple and performant.

Second, the use case is rare at most. It's only for assigning scripts to nodes. I'v worked on pretty large projects and only did this once.. twice maybe? 99% of the time you create new scripts, rarely assign them.

So, bloat for something rarely used is difficult to justify for me.

Sorry, did not mean to close.

If we are going to separate the 'file vs class' aspect from this issue and treat it purely as a usability problem for GDScript, maybe we should reopen #15661 and continue the discussion there?

While I might agree that it could be less of a problem for GDScript, I believe it's not simply a matter of convenience for C# projects though.

I don't really see it as a problem for C# either, the rationale is the same.

1) 99% of the time you create scripts, not assign them.
2) Class based makes 1) more complex

I just don't see any justification for the use case.

@reduz I don't believe that @karroffel's idea would sabotage simplicity or performance. Godot now has four scripting languages, and a binding API for almost any other language. Contrast that with Unity's choice to drop support for anything but C#. Giving users options may increase the length of a drop-down menu, but allowing users to stick with what they already know makes everything else simpler.

However, unofficial languages will never be as convenient or well-suited to Godot as GDScript. Even the well-implemented C#/Mono binding has additional implementation and compilation challenges. Providing more ways to use GDScript, instead of bailing to another language, would simplify the user experience.

The issue is no longer just about assigning scripts from paths versus from qualified names. It's about writing scripts too. With my background in Java, const Class = preload("res://script.gd") is annoying, especially if I need to load multiple scripts:

const enemy_types_path = "res://scripts/enemies"
const enemy_type_names = ["tank", "soldier", "aircraft"]
var enemy_types = load_enemy_types()

func load_enemy_types():
    var types = []
    for name in enemy_type_names:
        types.append(load(enemy_types_path + name + ".gd"))
    return types

Versus:

import enemies.*

Registered classes can still behave like script objects, in the sense of being able to store them as variables and in arrays:

namespace spawners

import enemies.*

export class EnemySpawner extends Spawner:
    var enemy_types = [Tank, Soldier, Aircraft]

    func _ready():
        var random_enemy = enemy_types[randi() % 3].new()

@mysticfall, yes this issue has now seperated from #15661.

@reduz Let me elaborate then. Let's assume I have MyProject which references 3rdPartyAddon both of which are C# projects.

In a typical C# development, all I need to do is adding the reference of 3rdPartyAddon to MyProject within an IDE, preferrably by using something like NuGet.

However, such a use case is currently impossible with Godot, if it is needed to reference a class from 3rdPartyAddon within the editor. So, I also need the source bundle of the 3rdPartyAddon project and extract it under addons and reference them by source files.

But not only that, I also need to edit MyProject.csproj to include every source files from 3rdPartyAddon to make it compile within the IDE, and also find any external assembly references 3rdPartyAddon.csproj it may contain and manually add them to the main project as well.

This is far from how a typical C# developer would want to manage their projects. It's inconvenient enough for a single addon, but it'd be quite a problem when you have to use large number of addons and have to keep the source/library references in MyProject.csproj every time you add/remove/update them.

And I strongly disagree that people would very rarely assign scripts to existing nodes. Godot currently don't have tons of 3rd party addons like Unity has, but I believe (and hope) we will get there eventually.

And when I used Unity, I've used many framework or template type assets which provide scripts that you can attach to your own nodes. Even in Godot, I assign different classes to a node all the time, at least while I'm developing. So I believe it's not something we can assume to be true for all developers and all their projects.

If we ever become popular and have a growing ecosystem like Unity current has with their Asset Store, it'd be much more common to see projects that rely on many different types of addons and in large numbers.

In that case, navigating into a few different deep hierarchies (i.e. Addons/3rdPartyAddon/Scripts/Com/Acme/Controller/PlayerControl.cs) every time you assign a node can quickly become cumbersome.

At the very least, I believe it is best to support a workflow that is well established and optimized for each individual bindings, rather than simply assuming every binding languges for Godot would work like GDScript does when forcing them when they don't.

@Xydium Again, I apologize, but will not add more complexity to GDScript to make it work like Java. It may bother you but this is still not a common case scenario and, for most users, GDScript is much simpler than Java.

I think there are more chances to get Java to work in Godot rather than doing this. Also If you are doing this in C#, I don't quite get why it wouldn't work.

@mysticfall This is clearly a problem for the case of a third party add-on that comes compiled, but i can imagine much simpler work arounds to this that do not require changing how Godot works. Also again, no matter how it works, that add-on will most likely register the custom types for you, so you may not need to assign them anyway.

Again, I would like to discuss regular use cases that are not corner cases. I don't want to discuss implementation complexity.

To me, the way it currently works, is fine in 99% of use cases, and usability is better than using classes. Rare use cases like the ones described should be workaroundable. Throwing away the system that has the best usability for something not often used really does not make sense to me. This is why I'm completely against it.

@reduz That's fair, though I'm not looking for GDScript to become like Java, nor would I find Java preferable in any way to GDScript.

I consider this proposed feature more like the Built-In Script option. Built-In Script is an additional setting, thus, adds complexity, however it also simplifies things greatly by eliminating the requirement for a separate file when only implementing a handful of basic functions. There are projects which may never make use of it, but the feature exists for those that need or prefer it.

Similarly, class-based scripting can significantly simplify writing scripts (as shown above). Instantiating objects from scripts is not a corner case. It's fundamental to object-oriented programming. The above example of loading multiple scripts and storing them in an array has appeared in almost every project I have worked on in Godot, from platformers to space-shooters to real-time-strategy games. There are more situations than just that where classes would simplify script-writing. It should not replace the current file-based system. The two can coexist, and users can choose whichever they find more convenient for the use cases they encounter.

To further prevent any confusion for end-users, the class system could be disabled by default, and require explicit enabling from the project settings.

For GDScript at least, one way that this could be faked a bit is by declaring all of the scripts as constants in a singleton. The only thing you would need in order to make it work is allow people to inherit from a Script object instance declared in a Singleton's context.

@Xydium Actually, I did my best to avoid built-in from being used, because it generates some problems that need workarounds. It exists "for free" due to how Godot works, there is zero extra code to make it happen. Originally it was not possible to create a built-in script , but the option was added because users still really wanted it and it was pretty much a line of code (in the new script dialog). If they have problems with it, it's up to them.

I really don't see how it can simplify writing scripts, I only see a LOT more code complexity required for something that will _definitely_ not be used often. I just can't see it being worth it.

@reduz Adding custom types would be nice, but I'd much like it to be a feature rather than something required only for C# (or other similar non-scripting languages) when GDScript doesn't suffer the same limitation.

And even if we can let users to use custom types as a workaround, there still remains an issue of forcing users to manually merge and maintain source lists for every addons referenced in MyProject.csproj.

Unity does this by automatically parsing and generating .csproj file whenever something is changed in the project. I don't think this to be the best way to go as it's error prone and not really the idiomatic way of doing things in C#, and I'm not sure it'd be much easier either.

@willnationsdev if you want a script as a global class in GDScript, this is really easy to do and I thought about it many times, but it would be GDScript-only. I'm sure it would aid in usability for more complex projects.

@mysticfall it would work exactly as GDScript, the custom types are a C++ Node/Resource + Script, custom nodes/resources don't magically become part of the engine in GDScript either. This is on purpose.

In fact, it's easier to do this in C# than in GDScript at the moment.

@reduz So there is a precedent of popular support overriding simplicity?

Here's full details on the use case I explained above:

I need a node to spawn different types of enemies. These enemies exist as scripts, and are a subclass of some generic Enemy which inherits from Node2D. In order to instantiate a script, I need a reference of some kind to it. In the current file-only system, I need to explicitly load every enemy subtype from each file path, and store it in an array. Each enemy file path consists of the directory, the enemy name, and the file extension. Consider the following hypothetical directory tree:

res://
    enemies
        ranged
            trebuchet.gd
            soldier.gd
            tank.gd
        melee
            mech.gd
            swordsman.gd
        support
            medic.gd
            transport.gd

As stated, I need to load each of these scripts from their paths. This is made worse by the fact that I organized enemies in folders by type, to keep things _simple_.

So, I could just write out explicit constants for each, and then put them in an array. Each time I make a new enemy type, I need another constant, and another load statement:

const TREBUCHET = preload("res://enemies/ranged/trebuchet.gd")
const SOLDIER = preload("res://enemies/ranged/soldier.gd")
#I was going to type these all out, but the fact that I don't want to proves my point
var enemy_types = [TREBUCHET, SOLDIER, ...]

To be clever, and to make adding new enemies slightly simpler, I can store the different directories and enemy types as strings in arrays, and then load them with a loop.

const ENEMY_TYPES = [
    "ranged", ["trebuchet, soldier, tank"],
    "melee", ["mech", "swordsman"],
    "support", ["medic", "transport"]
]

var enemy_types = load_enemy_types()

func load_enemy_types():
    var enemy_types = []
    for dir in range(0, 6, 2):
        for type in ENEMY_TYPES[dir + 1]:
            enemy_types.append(load("res://enemies/" + ENEMY_TYPES[dir] + "/" + type + ".gd"))
    return enemy_types

Contrast either of those techniques with:

import enemies.ranged.*
import enemies.melee.*
import enemies.support.*

var enemy_types = [Trebuchet, Soldier, Tank, Mech, Swordsman, Medic, Transport]

#Or, if class objects need to be explicit:

var enemy_types = [Trebuchet.class, Soldier.class, Tank.class, ...]

If this class-system existed, I would use it almost exclusively, not just in convenient use cases. I've heard rumors that statically-typed GDScript may also be implemented in the future, which I would use almost-exclusively too. The two together would make a valuable addition to the engine.

@reduz Thanks for the explanation. But in that case, probably it's less relevant to the problem that I initially mentioned.

All I want to do is a way to write a Godot addon in C#, and allow other people to either extend its functionality by extending some classes to create their own, or assign them to nodes in their projects.

In order to achieve that, I need to tell them something like "Download the source bundle and extract it somewhere, then open MyAddon.csproj to copy all Compile and Reference tags to merge them into your own .csproj file, and you'll need to do it again whenever you upgrade it or you add/remove any other addon you may use", which is obviously not an optimal workflow for anyone.

And if I'm not mistaken, Godot's C# binding already tries to translate file paths into class names before asking Mono to resolve them. Currently, it seems that it's broken for addons because it doesn't use a proper namespace/domain for them.

In other words, we still need to find some way to map file paths to class names properly anyway unless we want all addon writers to follow some weird namespace convention like Addon.MyAddon that exactly matches the file path.

I haven't looked into the C++ side much yet, but if I recall correctly, we already made it each binding's responsibility to resolve types from script references. Then why can't we also ask each bindings to present suitable list or hierarchy of types, so that C# can implement it using Mono's API that queries exposed types in each assembly, while GDScript can simply list all *.gd in the project path?

I don't know if it would be a much more difficult work, compared to writing some parser/generator for *.csproj that detects every file system changes like Unity does.

if you want a script as a global class in GDScript, this is really easy to do and I thought about it many times, but it would be GDScript-only. I'm sure it would aid in usability for more complex projects.

@reduz Wait, really? What I suggested would be okay? I don't see how that would be very different from registering names / namespaces in a HashMap for each language though. The entire concept of a TypeDB that could coalesce names and namespaces for all types of scripts and/or scenes is basically doing the same thing as using singletons, only the singletons route is LESS performant, not more (as far as I'm aware):

  1. The editor creates a singleton node that has the same name as your project name.
  2. If you add a plugin, the editor creates a singleton node with a name for the plugin (defined in the plugin.cfg). All of the generated singleton names are UpperCamelCase.
  3. Any time a script file is saved, a new constant with an UpperCamelCase name is automatically added to the script that represents the singleton node. (would use the singleton that is most relevant, i.e. a script file in a plugin directory would go into that plugin's singleton script, etc.).
  4. Enable GDScript and VisualScript to inherit from a Script object instance directly rather than having to supply a string literal filepath to them.

If you followed those steps, then you'd be able to do something like this:

//project my_project
my_project.godot
    addons
        my_plugin
            plugin_type.gd
project_type.gd
test.gd

# test.gd
extends MyPlugin.PluginType

func _ready():
    var obj = MyProject.ProjectType.new()

This would actually require minimal changes to GDScript, and I'm guessing you'd be able to do something similar to make VisualScript be able to access the singleton nodes by name as well.

So, if something like that would be permissible, how is that better than a centralized type / script database in core? I mean, with this version, any time you made changes to a script, the "namespacing" script would have to be completely reloaded, causing you to interpret the file and recreate all of the different constants, etc. Whereas if it's all in a core db of some kind, it's just a simple HashMap insertion in constant time, which is WAY faster than anything else.

@Xydium I understand your problem perfectly.

So, if I I have to choose between adding a lot of code complexity so your tiny bit of code, that you will not even use often, is snaller.

Or..

Leave as is, and you write a bit more code in this particular case.

Put yourself in my shoes. What do you think I should do?

@mysticfalls I think this is a mono specific problem that would need to be discussed with @neikeq. I personally dont understand why the restriction of same file name as class is in place. I imagine it should be possible to do away with it.

@willnationsdev TypeDB? Do you mean something like ClassDB? The singleton database of classes that was renamed and made accessible from scripts three days after I opened this issue in 2016 as per commit ce26eb7?

@Xydium It was a reference to this other issue I recently created. #17387

@willnationsdev I had something much simpler in mind, but I have the feeling this thread is kind of a mess, because each of you have a different idea in mind of what you need..

XD right. Well, @Xydium's issue seems pretty similar to mine in the sense of adding namespaces / typenames to GDScript for usability purposes. Oh, but I see he wants to bring into the current namespace a subset of types with a single line statement...gotcha.

@reduz Ok, then I'll ask the admin (anyone knows whom I can ask this?) to reopen #15661 so that we can discuss it from the Mono's side first. Thanks!

@reduz What was the "something much simpler" you had in mind? I wouldn't mind getting started on an implementation since I've already wasted a few months of my own time (my fault), lol. XD

@reduz I agree that it shouldn't be implemented if I'm the only one who would want such a feature. @karroffel's explanation of standard use cases is more complete, and again, the idea connects more with a separate, statically-typed GDScript than a mere enhancement of the current GDScript. The only way to know if it's a desired feature by the community would be to poll the community.

@willnationsdev Working within the file-based GDScript, your idea from #17387 would also solve the issue in my example use case.

@Xydium Awesome. Well, the idea from there is functionally identical to the concept of adding tons of singletons for top-level namespaces. And if he's alright with doing that, then there's hope. Just need to hear what his idea is! excitement building~

Edit:

I suppose the simpler idea may involve just directly adding the items to the static global_map in the GDScript class? That truly would be something that only works for GDScript though, whereas my idea would work with any scripting language, technically speaking. get_node("/root/MyPlugin").PluginType.new() or something like that for non-GDScript languages. Or create syntax sugar of some sort for other languages too...?

@willnationsdev I'd still prefer to see optional statically-typed variables (for better code completion) and optional class-based scripts (so that scripts can be static types for variables and thus receive better code completion), which such a ScriptDB would assist in creating. However I understand @reduz's concern regarding simplicity, and mixing two paradigms in one language _could_ be detrimental. At the same time, there are situations where dynamic types are better and vice versa, so the ability to employ static types only when they provide benefits would be nice.

Versus:
import enemies.*

Actually, I don't think this is any better tbh, in fact, it could even be a detriment. Because that doesn't give us the power to manually preload what we want, based on a certain map / scene that a player is loading. Having more control over what we preload and reference in dictionaries/array is a good thing! I feel like your example is over-exaggerating a problem that's easily solvable by GDScript already. (Hence your example :P)

@girng I don't see why you would want control over loading code. I understand with scenes (and that wouldn't change) but code?

@QbieShay having control in what you preload in Godot is extremely powerful for smooth level transitions, low memory usage (only load what you need, instead of * ), and more but just off the top of my head

@reduz I don't think that adding the classDB would make the code that much more complicated.

I've been using Godot for some months now and i already made some games with it. What i noticed, when the project started to be more and more complex, is that i felt:

  1. More and more scared to refactor
  2. Annoyed by having to search for some scripts of which i didn't remember the position

If classDB was there, I imagine it keeping track of where the scripts are and, when you need to move a script, classDB will update. So far, the only way to keep track of that is either having an external tool or run the game to see where it breaks.

What if the game becomes medium sized (no need for GDNative, still using GDScript)? How much time do you need to spend playing the game before noticing that something broke?

About the script searching, I tried to respect the guidelines on where to put resources and scripts, but sometimes scripts just don't fit anywhere.

I don't even want to start the static typing discussion here, but class DB would be a nice step forward for that. Two words about that here

I think that if you look around in open source Godot projects, you will soon find a lot of people that implemented something like classDB in their own projects. IIRC, @Faless did.

I already commented on the other issue, but thought I'd chime in here too:

  • Type based scripting isn't more difficult than file based scripting. It could literally be the exact same UI to drag and drop project scripts (engine fills in type reference instead of path reference), PLUS a new script search window, from which you could also drag/drop scripts. Maybe I'm missing something, but I don't understand why working with types instead of files is _more_ difficult. Actually seems like it would be easier, and certainly more flexible w.r.t. script library dependency management.
  • For refactoring/renaming types: If at any point a type reference becomes broken (type no longer exists, was renamed, different namespace, etc...), just help the user fix it. List all the broken type references in the project, let them pick a new type (from a searchable/filterable list). Fixes the broken reference wherever it's referenced. Simple. For types in the project, add a "Rename type" command to the context menu for .cs files in the project, that automates the process. Just like Visual Studio, if you rename the file first, ask the user if they want to rename the type as well, then update all the references to that type in the scene.

Renaming support is the responsibility of coder and IDE not the game engine IMO.

@hubbyist I think a game engine is a kind of IDE in a way.

Godot editor is a godot engine game so the IDE like part is not that much coupled with the engine core. And it is a good way of separating concerns I think. Renaming support may be even an official editor plugin but not a core feature. As well multi class in a file is a language specific functionality it seems and must be handled by either editor or the language binding. If PHP bindings would be added, having multiple classes in a file may help as well. But a language binding must not effect the core functionalities and lock engine to the methodologies of a specific language in any way. So binding must conform to engine not the other way around IMO.

Godot editor is a godot engine game so the IDE like part is not that much coupled with the engine core. And it is a good way of separating concerns I think. Renaming support may be even an official editor plugin but not a core feature. As well multi class in a file is a language specific functionality it seems and must be handled by either editor or the language binding. If PHP bindings would be added, having multiple classes in a file may help as well. But a language binding must not effect the core functionalities and lock engine to the methodologies of a specific language in any way. So binding must conform to engine not the other way around IMO.

It's not so much a language binding imposing methodologies of a specific language on the engine so much as the engine imposing the methodologies of a specific language and its binding on _other_ languages and their bindings.

Maybe a little context and history would be useful here.

Historically, most game engines used a proper "scripting" language for game scripting, such as Lua or a custom language built for the engine. These were almost always FILE based scripts - one script per file. And scripts were expected to be small and self contained, essentially "content", and not making use of libraries of functionality beyond that file. The notion was, if something more complicated was needed it would get added to the engine code itself (because in the past engines were custom written on a game-by-game basis).

But in modern times, game engines are far more general purpose, and "scripting" has become less little bits of customized logic in content to pretty much all of the game logic of the game itself. As such, "scripts" are expected to do much more sophisticated things, and thus it makes sense to support the kind of abstractions you would use in traditional "programming" to scale up to more sophisticated code bases - such as using third party libraries, separating code into modules, code-reuse, and so on. But, the file-based methodology that worked well in the past hinders this, IMO.

In my view, the engine should abstract over the specifics of how a language binding handles scripts physically on disk and simply expect the binding to provide it a list of attachable scripts, which the user can then attach to objects in the scene, and let the language binding handle the best way to implement specifics beyond that. For some language bindings, that might be file paths (lua, etc...) or modules/bundles/etc... (python, javascript), or built assemblies (C#, Java, Rust, etc...).

@reduz Some months ago we were discussing this on IRC and IINW you were going to implement this. Is it still planned?

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!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mefihl picture mefihl  Â·  3Comments

Zylann picture Zylann  Â·  3Comments

Spooner picture Spooner  Â·  3Comments

n-pigeon picture n-pigeon  Â·  3Comments

testman42 picture testman42  Â·  3Comments