Add Trait System to GDScript
GDScript Trait System
Based on the discussion opened in:
- https://github.com/godotengine/godot-proposals/issues/6416
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
.gdtfile, declare a global trait usingtrait_name GlobalTraitat the top.traitused 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
useskeyword after theextendsblock, 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 GDTrait will automatic change to GDTrait
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.
Example:
# TraitA.gdt
trait_name TraitA extends Node
# ClassA.gd
extends Control
uses TraitA # Allowed si nce 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.
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.
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
- [x] Implement and verify How Traits Are Handled
- [ ] Debugger Integration
- [ ] Trait typed Assignable (variable, array, dictionary) - variable is done
- [ ] Trait type as method & signal parameters' type
- [x] Trait type as method return type
- [x] Trait type casting (
as) - [x] Class is Trait type compatibility check (
is) - [x] Make
.gdtfiles unattachable to objects/nodes - [x] Hot reloadable Classes using traits when trait Changes (for Editor and script documentation)
- [ ] Write Tests
- [ ] Write Documentation
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?
(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.
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.
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?
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.
This system seems very similar to the
abstractkeyword, which has already been suggested.. what makes this approach better than abstract classes?
See:
- #67777
- #82987
Also, as DaloLorn said, these are independent features that can coexist together. Traits offer capabilities that classic inheritance cannot provide.
This looks great, will traits be able to constrain typed collections (ie. Array[Punchable], Dictionary[String, Kickable]) ?
Amazing that somebody cared to make this,but since the original proposal is shut down,here is some feedback
- use
implinstead ofuses,this makes more sense and also is short too - don't have a seperate file extension for traits,have
.gdfiles be able to support different object types - no
trait_name, name it insteadtype_name - traits should only have functions (abstract and virtual),no other stuff
- 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)
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.
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
implis inconsistently abbreviated next to the written-outextendsandclass_namekeywords that will almost always be just above or below it.- for non-native English speakers,
implis harder to understand at first glance, and un-google-able for translation. - If contributors want Godot to be beginner friendly, writing out
implementsis 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,
implcould 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
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
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.
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.
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.
As various others have suggested to use
implinstead ofimplements, I wanted to make a case againstimpl. Take this example:class_name SomeClass extends BaseClass impl TraitA
implis inconsistently abbreviated next to the written-outextendsandclass_namekeywords that will almost always be just above or below it.- for non-native English speakers,
implis harder to understand at first glance, and un-google-able for translation.- If contributors want Godot to be beginner friendly, writing out
implementsis 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,
implcould imply "implies", "impels", "implant", "implode".The 6 characters
implsaves overimplementsis 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.
I agree on "impl" been a very confusing keyword.
To resolve the class reference error: Compile godot, then run ./bin/godot<TAB> --doctool, then push the result.
I'm not sure if this is planned, but from a usability standpoint, it would be really nice to be able to check if something uses a trait. For example, if I have an Animal class with various animals that implement traits such as IShearable and IMilkable, like so:
sheep.gd
class_name Sheep
extends Animal
uses IShearable
func shear() -> void:
print("Sheep sheared!")
fluffy_cow.gd
class_name FluffyCow
extends Animal
uses IMilkable, IShearable
func milk() -> void:
print("Milked fluffy cow!")
func shear() -> void:
print("Sheared fluffy cow!")
Then, elsewhere I could have a list of whatever Animal I want and check if they have traits like so:
func _ready():
var my_animals : Array[Animal] = []
my_animals.append(Sheep.new())
my_animals.append(FluffyCow.new())
for animal in my_animals:
if animal is IShearable:
animal.shear()
if animal is IMilkable:
animal.milk()
Other than that, the traits feel very usable even in this incomplete state. I've been playing around with them and they feel very intuitive and unbelievably useful.
I should hope your suggestion is redundant! Strong typing with traits would be impossible without these kinds of things. :joy:
(On another note, I'm pretty sure you can milk sheep too. Maybe a fluffy bull would have been better? 😛)
... Well what do you know. It wasn't redundant. 😮
Great catch!
I would be in favor of not using a custom extension. Having to click a dropdown menu to select the type in the script creation menu is more annoying than a keyword in the script and adding a prefix to the filename if you want to have it easier to find. Less UI stuff to deal with and more consistent with the rest of godot
... Come to think of it, a checkbox might be easier to work with than a dropdown here. 🤔
Not a huge deal, though, in my opinion.
I would be in favor of not using a custom extension. Having to click a dropdown menu to select the type in the script creation menu is more annoying than a keyword in the script and adding a prefix to the filename if you want to have it easier to find. Less UI stuff to deal with and more consistent with the rest of godot @GuyUnger
I have add the ability to switch between by GDScript and GDTrait by editing the path's extension hopefully it will make it easier and comfortable. (Note: This will apply to all languages you have available, if extension matches existing language it will switch to it)
Also using ".gdt" file is optional, traits can be declared in normal class files.
@SeremTitus could you confirm that if you implement a trait into a class you are required to add the trait's functions to that class or it will produce an error? I'm hoping to use this functionality like an interface so would appreciate the confirmation that this is the intended behavior!
@SeremTitus could you confirm that if you implement a trait into a class you are required to add the trait's functions to that class or it will produce an error? I'm hoping to use this functionality like an interface so would appreciate the confirmation that this is the intended behavior!
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.
The class must provide an implementation. If a bodyless function remains unimplemented, an error occurs. Static and rpc state of trait functions are maintained.
Got it thanks! I wasn't sure if that only meant you needed to provide a body to the function but glad to hear it works as I hoped.
Radiant pointed out today that, according to the "System Implementation Progress" checklist, tests and documentation have yet to be done. Is this something that other contributors could help with to move the PR along, or is it best left to the original authors of the PR?
Documentation-wise, there isn't much to be done. The new @GDTrait class reference files copies everything from @GDScript. Frankly, it shouldn't even exist. So all of that is worth reconsidering by the original author.
Do user guides and tutorials fall outside of the scope of "documentation" for the purposes of this PR? It sounds like most, if not all, of the syntax has at least been locked down, so perhaps we could work on producing manual pages that describe how and why to use traits, so they're ready for when traits themselves are merged?