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

Add Trait System to GDScript #97657

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

SeremTitus
Copy link

@SeremTitus SeremTitus commented Sep 30, 2024


GDScript Trait System

Based on the discussion opened in:

The GDScript trait system allows traits to be declared in separate .gdt files or within a trait SomeTrait block.

Traits facilitate efficient code reuse by enabling classes to share and implement common behaviors, reducing duplication and promoting maintainable design.

Syntax Breakdown

Declaring Traits

  • Traits can be defined globally or within classes.

  • In a .gdt file, declare a global trait using trait_name GlobalTrait at the top. trait used for inner traits.

  • Traits can contain all class members: enums, signals, variables, constants, functions and inner classes.

    Example:

    # SomeTrait.gdt
    trait_name SomeTrait
    
    # Trait types used for typing containers.
    var player: TraitPlayable
    var scene_props: Array[TraitMoveable]
    var collected: Dictionary[String, TraitCollectable]
    
    func _ready():
        pass  # Method with implementation, but can be overridden.
    
    # Method to be implemented by the class using the trait.
    # The return type must be `void`.
    func foo() -> void # Bodyless need to be implemented in class using trait.
    
    # The return type can be overridden, but it's not required to specify one.
    func some_method() # Bodyless need to be implemented in class using trait.
    
    # The return type can be overridden but must be a class that inherits from `Node`.
    func some_other_method() -> Node  # Bodyless need to be implemented in class using trait.
    

Using Traits in Classes

  • Use the uses keyword after the extends block, followed by the path or global name of the trait.

  • Traits can include other traits but do not need to implement their unimplemented functions. The implementation burden falls on the class using the trait.

    Example:

    # SomeClass.gd
    extends Node
    
    uses Shapes, Topology  # Global traits
    uses "res://someOtherTrait.gdt"  # File-based trait
    
    func _ready():
        var my_animals : Array = []
        my_animals.append(FluffyCow.new())
        my_animals.append(FluffyBull.new())
        my_animals.append(Bird.new())
        var count = 1
        for animal in my_animals:
            print("Animal ", count)
            if animal is Shearable:
                animal.shear()
            if animal is Milkable:
                animal.milk()
            count += 1
    
    trait Shearable:
        func shear() -> void:
            print("Shearable ok")
    
    trait Milkable:
        func milk() -> void:
            print("Milkable ok")
    
    class FluffyCow:
        uses Shearable, Milkable
    
    class FluffyBull:
        uses Shearable
    
    class Bird:
        pass

Creating Trait files.

  • In Editor go to FileSystem, left click and select "New Script ...". In the pop up select GDTrait as the preferred Language.
  • Alternatively in script creation pop up instead of selecting GDTrait from 'Language' dropdown menu change 'path' extention to '.gdt' and language will automatic change to GDTrait
    image

How Traits Are Handled

Cases

When a class uses a trait, its handled as follows:

1. Trait and Class Inheritance Compatibility:

The trait's inheritance must be a parent of the class's inheritance (compatible), but not the other way around, else an error occurs. Also note traits are pass down by inheritance, If a class is for instance "SomeTrait" also it here classes will be so.

Example:
    # TraitA.gdt
    trait_name TraitA extends Node

    # ClassA.gd
    extends Control
    uses TraitA  # Allowed since Control inherits from Node

2. Used Traits Cohesion:

When a class uses various traits, some traits' members might shadow other traits members ,hence, an error should occur when on the trait relative on the order it is declared.

3. Enums, Constants, Variables, Signals, Functions and Inner Classes:

These are copied over, or an error occurs if they are shadowed.

4. Extending Named Enums:

Named enums can be redeclared in class and have new enum values.
Note that unnamed enum are just copied over if not shadowing.

5. Overriding Variables:

This is allowed if the type is compatible and the value is changed.
Or only the type further specified. Export, Onready, Static state of trait variables are maintained. Setter and getter is maintained else overridden (setters parameters same and the ).

6. Overriding Signal:

This is allowed if parameter count are maintained and the parameter types is compatible by further specified from parent class type.

Example:

    # TraitA.gdt
    trait_name TraitA
    signal someSignal(out: Node)

    # ClassA.gd
    uses TraitA
    signal someSignal(out: Node2D) # Overridden signal

7. Overriding Functions:

Allowed if parameter count are maintained, return types and parameter types are compatible, but the function body can be changed. Static and rpc state of trait functions are maintained.

8. Unimplemented (Bodyless) Functions:

The class must provide an implementation. If a bodyless function remains unimplemented, an error occurs. Static and rpc state of trait functions are maintained.

9. Extending Inner Classes:

Inner classes defined in used trait can be redeclared in class and have new members provide not shadow members declared inner class declared in trait. Allow Member overrides for variables, Signals and function while extending Enum and its' Inner Classes.

Example:

    # Shapes.gdt
    trait_name Shapes
    class triangle: # Declared
        var edges:int = 3
        var face:int = 1
        func print_faces():
            print(face)


    # Draw.gd
    uses Shapes
    class triangle: # Redeclared
        var verticies:int = 3 # Add a new member
        var face:int =  2 # Overriding Variable
        func print_faces(): # Overriding Function
            print(face-1)

Special Trait Features

10. Trait can use other Traits:

A trait is allows to use another trait except it does not alter members of the trait it is using by overriding or extending.
However, cyclic use of traits (TraitA uses TraitB and TraitB uses TraitA) is not permitted and will result in error.

11. Tool Trait:

if one trait containing the @tool keyword is used it converts classes (except inner classes) and traits using it into tool scripts.

12. File-Level Documentation:

Member documentation is copied over from trait else overridden.


System Implementation Progress

  • Implement and verify How Traits Are Handled
  • Debugger Integration
  • Trait typed Assignable (variable, array, dictionary)
  • Trait type as method & signal parameters' type
  • Trait type as method return type
  • Trait type casting (as)
  • Class is Trait type compatibility check (is)
  • Make .gdt files unattachable to objects/nodes
  • Hot reloadable Classes using traits when trait Changes (for Editor and script documentation)
  • Making Traits not directly accessible member/calls/instancing
  • Write Tests
  • Write Documentation (user manual) docs#10393
  • Override editor gutter icons
  • "ctrl + click" integration with trait functions

Bugsquad edit:

@DaloLorn
Copy link

Is there a specific reason you specified a void return type for foo()? Do abstract methods always need a strictly defined return type?

If not, might I recommend a different return type for clarity?

@AdriaandeJongh
Copy link
Contributor

AdriaandeJongh commented Oct 1, 2024

(Edited because I missed a part in the OP description)

Fantastic start on this feature. Thank you!

One comment: please use implements over uses, as per the original proposal.

@DaloLorn
Copy link

DaloLorn commented Oct 1, 2024

I'm not sure your reasoning lines up with your conclusion there, but I can't say I have much of a preference, what with it being a strictly cosmetic affair.

@btarg
Copy link

btarg commented Oct 1, 2024

This system seems very similar to the abstract keyword, which has already been suggested.. what makes this approach better than abstract classes?
I already use a lot of base classes with empty functions that are then overridden in my game currently. Will this improve the workflow comparatively?

@DaloLorn
Copy link

DaloLorn commented Oct 1, 2024

Abstract classes are still beholden to the class hierarchy: No class can inherit from two classes at a time.

There is some value in having both of these, I suppose, but traits are far more powerful.

@dalexeev
Copy link
Member

dalexeev commented Oct 1, 2024

This system seems very similar to the abstract keyword, which has already been suggested.. what makes this approach better than abstract classes?

See:

Also, as DaloLorn said, these are independent features that can coexist together. Traits offer capabilities that classic inheritance cannot provide.

@rrenna
Copy link

rrenna commented Oct 1, 2024

This looks great, will traits be able to constrain typed collections (ie. Array[Punchable], Dictionary[String, Kickable]) ?

@Dynamic-Pistol
Copy link

Amazing that somebody cared to make this,but since the original proposal is shut down,here is some feedback

  1. use impl instead of uses,this makes more sense and also is short too
  2. don't have a seperate file extension for traits,have .gd files be able to support different object types
  3. no trait_name, name it instead type_name
  4. traits should only have functions (abstract and virtual),no other stuff

@radiantgurl
Copy link
Contributor

radiantgurl commented Oct 2, 2024

  1. traits should only have functions (abstract and virtual),no other stuff

Considering work has already been made for signals, we should get to keep them too. (unless massive performance issues appear)

@Mickeon
Copy link
Contributor

Mickeon commented Oct 2, 2024

I am a bit concerned on the performance of this in general, but that would be something that can be solved over time. I am really, really ecstatic about this.

I agree. There's no reason to exclude signals from traits if the work has been done.

@SeremTitus SeremTitus force-pushed the GDTraits branch 2 times, most recently from 36d7605 to 4088f53 Compare October 3, 2024 14:57
Copy link
Contributor

@radiantgurl radiantgurl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some nitpicks, mostly you should rename uses with impl everywhere.

modules/gdscript/gdscript.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_analyzer.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_analyzer.cpp Show resolved Hide resolved
modules/gdscript/gdscript_analyzer.cpp Show resolved Hide resolved
modules/gdscript/gdscript_analyzer.h Show resolved Hide resolved
modules/gdscript/gdscript_parser.h Show resolved Hide resolved
modules/gdscript/gdscript_parser.h Show resolved Hide resolved
modules/gdscript/gdscript_parser.h Show resolved Hide resolved
modules/gdscript/gdscript_tokenizer.cpp Show resolved Hide resolved
modules/gdscript/gdscript_tokenizer.h Show resolved Hide resolved
@AdriaandeJongh
Copy link
Contributor

AdriaandeJongh commented Oct 4, 2024

As various others have suggested to use impl instead of implements, I wanted to make a case against impl. Take this example:

class_name SomeClass
extends BaseClass
impl TraitA
  • impl is inconsistently abbreviated next to the written-out extends and class_name keywords that will almost always be just above or below it.
  • for non-native English speakers, impl is harder to understand at first glance, and un-google-able for translation.
  • If contributors want Godot to be beginner friendly, writing out implements is the more friendly option here, because it requires less knowledge of the programming language and associated jargon.
  • as rightfully mentioned on mastodon in response to me yapping about engine UX, impl could imply "implies", "impels", "implant", "implode".

The 6 characters impl saves over implements is not worth the confusion and inconsistency. Instead, perhaps you would agree with me that this is much more readable and consistent:

class_name SomeClass
extends BaseClass
implements TraitA

@Dynamic-Pistol
Copy link

impl is inconsistently abbreviated next to the written-out extends and class_name keywords that will almost always be just above or below it.

what would you abbreviate extends and class_name to? only answers i can think of is for class_name, which could be changed to use the script name, and extends could just be extend

for non-native English speakers, impl is harder to understand at first glance, and un-google-able for translation.

Pretty sure non-native speakers are able to understand abbreviations, by your logic int and bool should be Integer and Boolean, this doesn't include all the abbreviated types that exist already

If contributors want Godot to be beginner friendly, writing out implements is the more friendly option here, because it requires less knowledge of the programming language and associated jargon.

traits aren't exactly beginner stuff, when someone starts with a language they learn they might learn traits ,but for gamedev you don't learn certain stuff until you get the basics/become a casual programmer , when i was a unity developer, i didn't learn about interfaces (which are extremely similar to traits) until i had advanced enough and realised i need some other solution to inheritance

as rightfully mentioned on mastodon in response to me yapping about engine UX, impl could imply "implies", "impels", "implant", "implode".

this makes no sense? let's take a look at some example code:

class_name Door
extends AnimatableBody3D
impl Interactable

what would "implies" mean in a progammer context?, "impels" isn't even abbreviated correctly, "implant"? seriously?, "implode" would be a function for gameplay

The 6 characters impl saves over implements is not worth the confusion and inconsistency. Instead, perhaps you would agree with me that this is much more readable and consistent

previous points still matter, also rust uses the impl keyword, and i am pretty sure it popularized the concept of traits and yet still 0 complains from it

@OscarCookeAbbott
Copy link

Is using a separate file extension necessary? And if not, would it be better to stick to .gd? From a UX perspective it seems a lot simpler and easier not to.

@eon-s
Copy link
Contributor

eon-s commented Oct 4, 2024

Is using a separate file extension necessary? And if not, would it be better to stick to .gd? From a UX perspective it seems a lot simpler and easier not to.

Extensions can be useful for quick search, filter, etc., without the need to check the content of the file nor adding extra prefix/suffix to file names (so it's better in UX terms), also can help other tools like the filesystem to implement custom icons.

@DaloLorn
Copy link

DaloLorn commented Oct 4, 2024

Put me down as another vote in favor of "implements", for what it's worth. I'm indifferent on "implements" versus "uses", but I'm not nearly so indifferent on "impl" versus "implements": The majority of Adriaan's concerns have not been addressed to my satisfaction.

@JJulioTLG
Copy link

JJulioTLG commented Oct 4, 2024

As various others have suggested to use impl instead of implements, I wanted to make a case against impl. Take this example:

class_name SomeClass
extends BaseClass
impl TraitA
  • impl is inconsistently abbreviated next to the written-out extends and class_name keywords that will almost always be just above or below it.
  • for non-native English speakers, impl is harder to understand at first glance, and un-google-able for translation.
  • If contributors want Godot to be beginner friendly, writing out implements is the more friendly option here, because it requires less knowledge of the programming language and associated jargon.
  • as rightfully mentioned on mastodon in response to me yapping about engine UX, impl could imply "implies", "impels", "implant", "implode".

The 6 characters impl saves over implements is not worth the confusion and inconsistency. Instead, perhaps you would agree with me that this is much more readable and consistent:

class_name SomeClass
extends BaseClass
implements TraitA

I think uses is fine.

@antimundo
Copy link

I agree on "impl" been a very confusing keyword.

@radiantgurl
Copy link
Contributor

radiantgurl commented Oct 7, 2024

To resolve the class reference error: Compile godot, then run ./bin/godot<TAB> --doctool, then push the result.

@SeremTitus SeremTitus force-pushed the GDTraits branch 2 times, most recently from f27a5d9 to 8d2b91c Compare October 9, 2024 17:35
@AlyceOsbourne
Copy link

If we didn't use the .gdt extension for traits and instead included it in .gd files, how would we know which one a given file was meant to describe?

We would check to see if trait_name is used, or, we could add an annotation @trait like we do with tool. It seems that previously, trait was already reserved as a keyword, makes sense to use it, or free it for use.

@KoBeWi
Copy link
Member

KoBeWi commented Jan 12, 2025

I don't think it makes sense to have unnamed traits. Unlike regular scripts, you always have to extend them; they can't be attached to nodes and can't be built-in. Since they are that much different, they can follow a different rules and you'd have to always name them. There are always internal traits (i.e. trait instead of trait_name) if you want non-global traits; you can just nest them in unnamed scripts.

@elenakrittik
Copy link
Contributor

Hello! Sorry for asking this here, the original discussion is locked.

I've read through this thread and the original proposal's thread and haven't noticed much reason for the separation between .gd and .gdt files beyond "to avoid confusion". However, the way i see it this only adds to confusion:

  • As has already been noted by someone in this thread, "GDScript Class" and "GDScript Trait" are not different languages (which is implied by the different file extensions), they are just two different things you can do in GDScript "The Language". (The new UI helps to clear this up, though.)

  • It's still possible to write a perfectly valid GDScript "Class" file that only contains a trait:

    trait InnerTrait:
        pass

    Meaning that GDScript "Trait" syntax is actually a subset of GDScript "Class" (beyond, maybe, trait_name), making the distinction between the two.. not so distinct.

    Bonus info

    In fact, if .gdt still makes it into a release, one could do a nice little trick to avoid using it: if you want to make a trait called Interactable, make a .gd file with class_name Interactable and trait Trait: # trait body, i.e. write the trait inline (since it's the same syntax!). Then, you can access it from anywhere as Interactable.Trait! I believe you could then even make the class implement the trait in a generic way so as to allow attaching Interactable.gd directly to nodes, which sounds cursed even to me, but may nevertheless be useful. (Which is not to say that that is a good way to do it, but it's probably the best if you don't want any .gdt in your eyes haha)

  • Another reason (from the original proposal) was to "[make GDScript "Trait" not] usable for things that requires a script". I strongly disagree that adding a separate file extension is the right way to fix an issue with the way script attaching (or file picking in general) works. I also struggle to imagine the "user tried to attach a GDScript "Trait" to a node" scenario being any common, so, assuming this case is indeed "unpopular", it doesn't make sense to optimize for it in favor of other things.

  • Somewhat related to the above bullet-point, there was a valid concern by Meorge about the ambiguity when no class_name nor trait_name is used (or other syntax indicating a specific flavor). While i fully support KoBeWi on the point of requiring trait_name as a way to remove ambiguity, i'd also like to add that in addition to that we can also simply "default" to GDScript "Class" if we don't see a trait_name. Alternatively, if unnamed traits are deemed of high demand, there's also the possibility of requiring a marker annotation instead (like @trait), or even extend the recently-added .uid files to contain "gdscript flavor" metadata as well.

If there's anything i'm missing, please point it out!

@elenakrittik
Copy link
Contributor

elenakrittik commented Jan 12, 2025

The debugger is fix, project export is tested and working.

I have also added support for scripting languages to be able to have multiple scripts. Scripts can now be selected directly in a dropdown or change file path extention:
Recording.2025-01-05.175855.mp4

Note the script dropdown will only be available when there options

This is nice improvement, but, perhaps it's just me, a "Script" selector paired with "Language" selector looks quite confusing, especially given that the "class" flavor of GDScript in the "Script" selector is called just "GDScript", exactly like in the "Language" selector. I think it would be better to:

  • Rename "Script" to something like "Type", "Kind", "Flavor", "Variant", etc., to better communicate that this selector lists variants (kinds, types, flavors, you get it; i'll refer to this as TKFV to save keystrokes) of the language selected through the "Language" selector.

  • I would consider renaming the "GDScript" TKFV to something like "GDClass" to both better explain the difference and better align stylistically with "GDTrait". Better yet, we can instead have "GDScript Class" and "GDScript Trait", or even just "Class" and "Trait" since the "GDScript" part is obvious (?) from the language selector.

@Dynamic-Pistol
Copy link

so for the confusion rn, i have a somewhat idea for a solution

example for explict class

script_type class
extends Node
type_name Door
uses Interactable

example for trait

script_type trait
extends Node
type_name Interactable

this can also be done for enums and structs, but those aren't important rn

@dalexeev
Copy link
Member

so for the confusion rn, i have a somewhat idea for a solution

It's an interesting idea, but the need for a separate extension is due to the fact that Godot relies on them to determine the resource type. A GDScript file (*.gd) is a class (which can be either named or unnamed) and is a GDScript resource. Unlike most other languages, where a single file can contain many top-level classes and other definitions (global functions, variables, etc.)

The difference between a script/class and a trait for Godot is that a class can be attached as a script, while a trait cannot. So not only GDScript, but also ResourceLoader needs to understand whether a particular file is a script/class or a trait.

Note that file extension, resource type and resource loader are different things, not necessarily in a one-to-one relationship. So perhaps we could apply your suggestion and use the same gd extension for multiple resource types, see ResourceFormatLoader._get_resource_type().

The key point is that GDTrait (or GDScriptTrait) should be a resource file, just like we have Shader and ShaderInclude resources (but they use different extensions, .gdshader and .gdshaderinc). Or we could use different extensions as originally proposed, for better recognizability and compatibility. In this case *.gdt files do not have to be resource files.

modules/gdscript/doc_classes/GDTrait.xml Outdated Show resolved Hide resolved
@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 1: Could not inheritance from trait, "TraitA".
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GDScript analyzer and runtime tests now support tracking multiple errors in a single file. Please group similar errors into single files instead of creating many small files. Same goes for feature tests. Yes, you shouldn't put everything in one file, but I think there are too many of them now.

Copy link
Author

@SeremTitus SeremTitus Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try to refactor tests

@elenakrittik
Copy link
Contributor

It's an interesting idea, but the need for a separate extension is due to the fact that Godot relies on them to determine the resource type. A GDScript file (*.gd) is a class (which can be either named or unnamed) and is a GDScript resource. Unlike most other languages, where a single file can contain many top-level classes and other definitions (global functions, variables, etc.)

And having a "main" class in every script is essential for Godot, yeah, understandable.

Note that file extension, resource type and resource loader are different things, not necessarily in a one-to-one relationship. So perhaps we could apply your suggestion and use the same gd extension for multiple resource types, see ResourceFormatLoader._get_resource_type().

👀

The key point is that GDTrait (or GDScriptTrait) should be a resource file, just like we have Shader and ShaderInclude resources (but they use different extensions, .gdshader and .gdshaderinc). Or we could use different extensions as originally proposed, for better recognizability and compatibility. In this case *.gdt files do not have to be resource files.

On a second thought, do you think it will be possible/feasible to allow traits-inside-.gd to be registered globally? Maybe via an annotation like @global next to trait <name> ...? (And would that even be in scope for this PR?) If so, that would actually solve my biggest issue with .gdt (namely, forcing it's usage if you want to avoid workarounds like the one i outlined in the "Bonus info" section to get global traits) and allow those who don't like the distinction to avoid using it

@SeremTitus
Copy link
Author

SeremTitus commented Jan 18, 2025

@export_group and @export annotation fixed, script GDTrait is now renamed to GDScriptTrait

Since trait typed variables store classes using the trait type, when @export annotation is used on a trait typed variable we expect to assign in editor a node (& not a resource) that has an attached script using the trait type.

@DaloLorn
Copy link

Is there some reason we couldn't attach a trait-typed resource? 😕

@Lazy-Rabbit-2001
Copy link
Contributor

Is there some reason we couldn't attach a trait-typed resource? 😕

Though I haven't looked through all posts in this pr, I can simply imagine that if you assign an exported resource with a trait type, e.g., Traitable, all resources using the trait will be available in the inspector, regardless of the difference of their base types. For example, it could be required to use a Texture-based resource, in which case if you use a trait for this, it's hard to guarantee whether the resource you assign in the inspector is derived from Texture.

@SeremTitus
Copy link
Author

SeremTitus commented Jan 19, 2025

Is there some reason we couldn't attach a trait-typed resource? 😕

resources files stored in files such as ".res", ".tres". I thought there no way to identify the constructing gdscript class but after taking a second look at the format it is possible therefore I can check if the class uses the trait type when editor is assigning... I will fix that issue

@DaloLorn
Copy link

Is there some reason we couldn't attach a trait-typed resource? 😕

Though I haven't looked through all posts in this pr, I can simply imagine that if you assign an exported resource with a trait type, e.g., Traitable, all resources using the trait will be available in the inspector, regardless of the difference of their base types. For example, it could be required to use a Texture-based resource, in which case if you use a trait for this, it's hard to guarantee whether the resource you assign in the inspector is derived from Texture.

Hmm. If you have a trait Ancestor that extends Resource, could you not make an intermediary Descendant trait that uses Ancestor but extends Texture? And then when you needed an Ancestor variable derived from Texture, you'd type it to Descendant instead of Ancestor.

Your original concern is valid, though, and makes me wonder if it could make sense to define variables as belonging to multiple compatible types at once, without defining an intermediary type for it. For instance, using TypeScript syntax: var sillyThing: Ancestor & Texture.

@elenakrittik
Copy link
Contributor

Also, is it too early to make a final decision on the uses vs implements question?

@ajreckof
Copy link
Member

ajreckof commented Jan 25, 2025

  1. Unimplemented (Bodyless) Functions:

The class must provide an implementation. If a bodyless function remains unimplemented, an error occurs. Static and rpc state of trait functions are maintained.

I feel this is usefull but would be really similar to making it abstract. The only difference I can see between the two would be that this implementation wouldn't let abstract class not implement the functions, which imo isn't usefull and doesn't warrant the redudndancy.

@Grublady
Copy link
Contributor

I feel this is usefull but would be really similar to making it abstract. The only difference I can see between the two would be that this implementation wouldn't let abstract class not implement the functions, which imo isn't usefull and doesn't warrant the redudndancy.

I think this has been brought up already—a couple of replies seem to agree that abstract classes are a distinct feature as they are based on an single-inheritance hierarchy.

Also, in my opinion, the whole point of having traits is to statically guarantee the availability of required properties/methods, so allowing them to be unimplemented would defeat the purpose.

@DaloLorn
Copy link

Well, abstract classes can't be instantiated anyway, can they? So it's fine if an abstract class doesn't satisfy the contract specified by an abstract trait.

@ajreckof
Copy link
Member

I feel this is usefull but would be really similar to making it abstract. The only difference I can see between the two would be that this implementation wouldn't let abstract class not implement the functions, which imo isn't usefull and doesn't warrant the redudndancy.

I think this has been brought up already—a couple of replies seem to agree that abstract classes are a distinct feature as they are based on an single-inheritance hierarchy.

Also, in my opinion, the whole point of having traits is to statically guarantee the availability of required properties/methods, so allowing them to be unimplemented would defeat the purpose.

Here I was not saying " because abstract class exist, traits shouldn't exist" just that there is no reason in my opinion to give the possibility to create body-less functions in traits in addition to abstract functions. Abstract functions could be put inside traits which would have the same effect and wouldn't be tied to the single-inheritance hierarchy, hence why this a different subject to concern which had been previously brought. For the part of leaving functions unimplemented, personally I agree with DaloLorn but would gladly change my mind if someone points out some good reason to have both of them.

@KoBeWi
Copy link
Member

KoBeWi commented Jan 26, 2025

I've been testing this briefly and the bugs I mentioned before are fixed. Traits work without problems as types, as export variables and support debugging. One minor problem is that I had to reload scene after adding another trait to script, because it wasn't picked as supported Node type; it's rather minor though, I'm surprised that node exporting even works with multiple traits in a script.

There might be some bugs left here and there, but overall the feature works fine right now.

@KoBeWi
Copy link
Member

KoBeWi commented Jan 27, 2025

Actually there is one rather obvious and major problem - you can't export trait Resources. Export only works with nodes, for Resources you get

Export type if is Trait must inherit from node.

Judging from the error it looks intended? Or is it oversight?

@SeremTitus
Copy link
Author

Export type if is Trait must inherit from node.

Initially intended, however I will rework it

@KoBeWi
Copy link
Member

KoBeWi commented Jan 28, 2025

There's also a bug with exported Node traits. You can assign them by clicking the Assign the button and selecting, but not by drag and drop.

@SeremTitus
Copy link
Author

In the recent push, exported trait-typed variables now align with the expected export behavior. Additionally, traits extending Nodes and Resources have been removed from various selection and creation windows.

@Dynamic-Pistol
Copy link

Made an interaction system with this, very pleased

have other issues tho

Issues

Variables

trait variables override the class variables and the only way to access the shadowed variables is to cast self to the base class and get the property

this also works even if the trait variable's type is different from the class variable

also untested, but i think the overriding variables with the same name and type count as 2 different variables, not sure so check

Functions

trait's functions work very weird with existing methods when the trait extends CollisionObject2D and has some methods of CharacterBody2D and a class extending CharacterBody2D implements that trait

the trait and the class will fight over the function definition and if you somehow settle it, the engine will give you a warning for overriding a class method that the trait demands (in this case the move_and_xxx twins)

Possible solutions

Variables

while the best solution is to simply not have variables in traits, i doubt it will happen so here is another possible solution

if the variable has the same name and type as a class's property, it will refer to that property (basically an alias/ptr if you will)

for the different type, i was going to suggest an alt to super for traits, but not sure how it would scale with multiple traits, a possible solution is to error if a variable with the same name but a different type exists in a class, but this feels wrong

btw did anyone check what happens if traits share a variable names?

Functions

if the function definition matches an existing function in a base class, you won't need to override it, this also solves my case

if a function's name but return_type/parameters don't match, there is a hacky solution i found

func MyTrait.my_class_func(same: Parameter) -> AndReturnType:
    pass

func other():
   #`tbase` stands for trait base
    tbase[MyTrait].my_class_func(blah_blah)

source:

Bad test.zip

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.

Add a Trait system for GDScript