From 29ec0d2d62ade90e7b4b1a44f9b9891db89fd0c0 Mon Sep 17 00:00:00 2001 From: Malcolm Anderson Date: Sun, 8 Dec 2024 12:25:21 -0800 Subject: [PATCH 1/3] Add documentation for traits to relevant pages Add trait-related keywords to syntax highlighting Update style guide and static typing pages Currently, the style guide uses what seem like good reasonable policies to me (notably, PascalCase adjectives for trait names, and `uses` after `extends`). We'll need to make sure this is agreed-upon as the good way to do it before merging, though Document behavior of static variables in traits [no ci] Document behavior of casting to traits --- _extensions/gdscript.py | 4 +- .../scripting/gdscript/gdscript_basics.rst | 212 ++++++++++++++++-- .../gdscript/gdscript_styleguide.rst | 53 +++-- .../scripting/gdscript/static_typing.rst | 22 +- 4 files changed, 254 insertions(+), 37 deletions(-) diff --git a/_extensions/gdscript.py b/_extensions/gdscript.py index 59c99cc3f3a..664e8416cd6 100644 --- a/_extensions/gdscript.py +++ b/_extensions/gdscript.py @@ -151,7 +151,9 @@ def innerstring_rules(ttype): "namespace", # Reserved for potential future use. "signal", "static", - "trait", # Reserved for potential future use. + "trait", + "trait_name", + "uses", "var", # Other keywords. "await", diff --git a/tutorials/scripting/gdscript/gdscript_basics.rst b/tutorials/scripting/gdscript/gdscript_basics.rst index bd6ca5d9fc7..46f54e82a0d 100644 --- a/tutorials/scripting/gdscript/gdscript_basics.rst +++ b/tutorials/scripting/gdscript/gdscript_basics.rst @@ -42,6 +42,9 @@ here's an example of how GDScript looks. # Inheritance: extends BaseClass + # Trait usage: + uses Talkable, Flammable + # Member variables. var a = 5 @@ -103,6 +106,19 @@ here's an example of how GDScript looks. super.something(p1, p2) + # When traits are used, they may have to implement abstract + # methods defined by the trait: + func talk_to(): + print("Hi, you just talked to me!") + + + # Traits can also implement methods that the concrete class + # can use without defining itself: + func yet_another_something(): + print("I'm going to light on fire!") + light_on_fire() + + # Inner class class Something: var a = 10 @@ -174,7 +190,13 @@ in case you want to take a look under the hood. +------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ | extends | Defines what class to extend with the current class. | +------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ -| is | Tests whether a variable extends a given class, or is of a given built-in type. | +| trait | Defines an inner trait. See `Inner traits`_. | ++------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ +| trait_name | Defines the script as a globally accessible trait with the specified name. See `Registering named traits`_. | ++------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ +| uses | Defines what trait(s) the current class should use. | ++------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ +| is | Tests whether a variable extends a given class, uses a given trait, or is of a given built-in type. | +------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ | in | Tests whether a value is within a string, array, range, dictionary, or node. When used with ``for``, it iterates through them instead of testing. | +------------+---------------------------------------------------------------------------------------------------------------------------------------------------+ @@ -853,7 +875,7 @@ The GDScript static analyzer takes typed arrays into account, however array meth ``front()`` and ``back()`` still have the ``Variant`` return type. Typed arrays have the syntax ``Array[Type]``, where ``Type`` can be any ``Variant`` type, -native or user class, or enum. Nested array types (like ``Array[Array[int]]``) are not supported. +native or user class, trait, or enum. Nested array types (like ``Array[Array[int]]``) are not supported. :: @@ -861,7 +883,8 @@ native or user class, or enum. Nested array types (like ``Array[Array[int]]``) a var b: Array[Node] var c: Array[MyClass] var d: Array[MyEnum] - var e: Array[Variant] + var e: Array[MyTrait] + var f: Array[Variant] ``Array`` and ``Array[Variant]`` are the same thing. @@ -1036,9 +1059,10 @@ Valid types are: - Built-in types (Array, Vector2, int, String, etc.). - Engine classes (Node, Resource, RefCounted, etc.). -- Constant names if they contain a script resource (``MyScript`` if you declared ``const MyScript = preload("res://my_script.gd")``). -- Other classes in the same script, respecting scope (``InnerClass.NestedClass`` if you declared ``class NestedClass`` inside the ``class InnerClass`` in the same scope). +- Constant names if they contain a script or trait resource (``MyScript`` if you declared ``const MyScript = preload("res://my_script.gd")``). +- Other classes or traits in the same file, respecting scope (``InnerClass.NestedClass`` if you declared ``class NestedClass`` inside the ``class InnerClass`` in the same scope). - Script classes declared with the ``class_name`` keyword. +- Traits declared with the ``trait_name`` keyword. - Autoloads registered as singletons. .. note:: @@ -1104,11 +1128,11 @@ A class member variable can be declared static:: static var a -Static variables belong to the class, not instances. This means that static variables +Static variables belong to the class or trait, not instances. This means that static variables share values between multiple instances, unlike regular member variables. -From inside a class, you can access static variables from any function, both static and non-static. -From outside the class, you can access static variables using the class or an instance +From inside a class/trait, you can access static variables from any function, both static and non-static. +From outside the class/trait, you can access static variables using the class/trait or an instance (the second is not recommended as it is less readable). .. note:: @@ -1179,6 +1203,24 @@ A base class static variable can also be accessed via a child class:: B.x = 3 prints(A.x, B.x) # 3 3 +.. warning:: + + Static variables declared on traits belong to the trait *itself*, and are *not* + inherited by classes that use the trait:: + + trait HasStatic: + static var static_var = 3 + + class UsingClass: + uses HasStatic + + func _ready(): + # Will work + print(HasStatic.static_var) + + # Will NOT work, as UsingClass does not get static_var from HasStatic + print(UsingClass.static_var) + ``@static_unload`` annotation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1208,15 +1250,15 @@ Values assigned to typed variables must have a compatible type. If it's needed t coerce a value to be of a certain type, in particular for object types, you can use the casting operator ``as``. -Casting between object types results in the same object if the value is of the -same type or a subtype of the cast type. +Casting between object types or traits results in the same object if the value is of the +same type or a subtype of the cast type, or if the value uses the trait. :: var my_node2D: Node2D my_node2D = $Sprite2D as Node2D # Works since Sprite2D is a subtype of Node2D. -If the value is not a subtype, the casting operation will result in a ``null`` value. +If the value is not a subtype or does not use the trait, the casting operation will result in a ``null`` value. :: @@ -1324,7 +1366,7 @@ or ``0`` if it is the first entry in the enum. Multiple keys with the same value Functions --------- -Functions always belong to a `class `_. The scope priority for +Functions always belong to a `class `_ or a `trait `_. The scope priority for variable look-up is: local → class member → global. The ``self`` variable is always available and is provided as an option for accessing class members, but is not always required (and should *not* be sent as the function's first @@ -1941,6 +1983,11 @@ A class (stored as a file) can inherit from: Multiple inheritance is not allowed. +.. note:: + Godot 4.x introduces `traits `_ to GDScript, which may cover many of + the use cases for multiple inheritance. See their section for more information + about how they work. + Inheritance uses the ``extends`` keyword:: # Inherit/extend a globally available class. @@ -1957,7 +2004,7 @@ Inheritance uses the ``extends`` keyword:: If inheritance is not explicitly defined, the class will default to inheriting :ref:`class_RefCounted`. -To check if a given instance inherits from a given class, +To check if a given instance inherits from a given class or uses a given trait, the ``is`` keyword can be used:: # Cache the enemy class. @@ -1969,6 +2016,15 @@ the ``is`` keyword can be used:: if entity is Enemy: entity.apply_damage() + # Cache the Flammable trait. + const Flammable = preload("flammable.gdt") + + # [...] + + # Use 'is' to check usage of the trait. + if entity is Flammable: + entity.light_on_fire() + To call a function in a *super class* (i.e. one ``extend``-ed in your current class), use the ``super`` keyword:: @@ -2121,6 +2177,136 @@ class resource is done by calling the ``new`` function on the class object:: var a = MyClass.new() a.some_function() +Traits +------ + +Traits are collections of behaviors and attributes that classes can use to guarantee +functionality to themselves and other objects that may be attempting to use them. + +Like classes, by default all ``.gdt`` files are unnamed traits, and you must reference +them using a relative or absolute path. +:: + # Use the trait 'interactable.gdt'. + uses "res://path/to/interactable.gdt" + +Note that traits on their own *cannot* be instantiated the same way that classes can. + +.. _doc_gdscript_basics_trait_name: + +Registering named traits +~~~~~~~~~~~~~~~~~~~~~~~~ + +Traits can be given a global name by using the ``trait_name`` keyword. +:: + trait_name MyTrait + +Using traits in a class +~~~~~~~~~~~~~~~~~~~~~~~ + +For a class to use a trait, use the ``uses`` keyword: +:: + class_name MyScript + uses MyTrait + + +Traits may also extend classes. If a trait extends a class, then any class +that uses that trait must also have that class as an ancestor. +:: + # movable.gdt + trait_name Movable + extends PhysicsBody2D + + # character.gd + class_name Character + extends CharacterBody2D + uses Movable # Allowed, since CharacterBody2D inherits from PhysicsBody2D. + +The ``is`` keyword can be used to determine if a given instance uses a particular trait. +:: + if entity is Movable: + entity.move() + +If a trait provides a method signature, but no body, then the using class must implement +a body for the method. +:: + # explosive.gdt + trait_name Explosive + + func explode() # Body is not defined here, so it must be defined in each class that uses it. + + + # exploding_barrel.gd + class_name ExplodingBarrel + extends Sprite2D + uses Explosive + + func explode(): # If this definition of Explosive.explode isn't provided, we will get an error. + print("Kaboom!") + queue_free() + +If a trait provides a method signature *and* a body, then the using class inherits it by default +and doesn't need to provide its own implementation. It still can override the trait's +implementation if desired, but the parameter count must stay the same, and the parameter and return +types must be compatible. +:: + # damageable.gdt + trait_name Damageable + + func take_damage(): + print("Ouch!") + + + # invincible_npc.gd + class_name InvincibleNPC + extends Sprite2D + uses Damageable + + # Allowed, and will run instead of Damageable's original take_damage method. + func take_damage(): + print("You can't hurt me!") + +.. + TODO: Confirm these behaviors + +Other class members have similar rules: + +- Variables and constants can be overridden, as long as the type is compatible and the value is changed. +- Signals can be overriden, as long as the parameter count is maintained and the parameter types are compatible. +- Named enums can be overriden and have new enum values. + +.. _doc_gdscript_basics_inner_traits: + +Inner traits +~~~~~~~~~~~~ + +Like inner classes, a class or trait file may contain inner traits, defined with the ``trait`` +keyword. Unlike inner classes, they cannot be instantiated directly, but their name can be +referenced for using or checking use of themselves. +:: + # An inner trait in this class file. + trait SomeInnerTrait: + func do_something(): + print("I did something!") + + + # An inner class in this class file, which uses the inner trait. + class SomeInnerClass: + uses SomeInnerTrait + + + func _init(): + var c = SomeInnerClass.new() + if c is SomeInnerTrait: + c.do_something() + +.. _doc_gdscript_basics_traits_as_resources: + +Traits as resources +~~~~~~~~~~~~~~~~~~~ + +Traits stored as files are treated as :ref:`GDTraits `, and must +be loaded similarly to classes (see `Classes as resources`_). + Exports ------- diff --git a/tutorials/scripting/gdscript/gdscript_styleguide.rst b/tutorials/scripting/gdscript/gdscript_styleguide.rst index 9dd84ad05da..1f791b8820c 100644 --- a/tutorials/scripting/gdscript/gdscript_styleguide.rst +++ b/tutorials/scripting/gdscript/gdscript_styleguide.rst @@ -30,6 +30,7 @@ Here is a complete class example based on these guidelines: class_name StateMachine extends Node + uses Activatable ## Hierarchical State machine for the player. ## ## Initializes states and delegates engine callbacks ([method Node._physics_process], @@ -629,6 +630,8 @@ code. As a summary table: +---------------+----------------+----------------------------------------------------+ | Class names | PascalCase | ``class_name YAMLParser`` | +---------------+----------------+----------------------------------------------------+ +| Trait names | PascalCase | ``trait_name Interactable`` | ++---------------+----------------+----------------------------------------------------+ | Node names | PascalCase | ``Camera3D``, ``Player`` | +---------------+----------------+----------------------------------------------------+ | Functions | snake_case | ``func load_level():`` | @@ -647,7 +650,7 @@ code. As a summary table: File names ~~~~~~~~~~ -Use snake_case for file names. For named classes, convert the PascalCase class +Use snake_case for file names. For named classes and traits, convert the PascalCase class or trait name to snake_case:: # This file should be saved as `weapon.gd`. @@ -660,6 +663,12 @@ name to snake_case:: class_name YAMLParser extends Object +:: + + # This file should be saved as `interactable.gdt`. + trait_name Interactable + extends Node + This is consistent with how C++ files are named in Godot's source code. This also avoids case sensitivity issues that can crop up when exporting a project from Windows to other platforms. @@ -667,13 +676,13 @@ from Windows to other platforms. Classes and nodes ~~~~~~~~~~~~~~~~~ -Use PascalCase for class and node names: +Use PascalCase for class, trait, and node names: :: extends CharacterBody3D -Also use PascalCase when loading a class into a constant or a variable: +Also use PascalCase when loading a class or trait into a constant or a variable: :: @@ -766,23 +775,24 @@ We suggest to organize GDScript code this way: 01. @tool 02. class_name 03. extends - 04. ## docstring - - 05. signals - 06. enums - 07. constants - 08. @export variables - 09. public variables - 10. private variables - 11. @onready variables - - 12. optional built-in virtual _init method - 13. optional built-in virtual _enter_tree() method - 14. built-in virtual _ready method - 15. remaining built-in virtual methods - 16. public methods - 17. private methods - 18. subclasses + 04. uses + 05. ## docstring + + 06. signals + 07. enums + 08. constants + 09. @export variables + 10. public variables + 11. private variables + 12. @onready variables + + 13. optional built-in virtual _init method + 14. optional built-in virtual _enter_tree() method + 15. built-in virtual _ready method + 16. remaining built-in virtual methods + 17. public methods + 18. private methods + 19. subclasses We optimized the order to make it easy to read the code from top to bottom, to help developers reading the code for the first time understand how it works, and @@ -808,6 +818,8 @@ global type in your project using this feature. For more information, see :ref:`doc_gdscript`. Then, add the ``extends`` keyword if the class extends a built-in type. +If the class uses any traits, add the ``uses`` keyword along with all of the trait +names (or filepaths, if the traits are unnamed) of the traits it uses. Following that, you should have the class's optional :ref:`documentation comments `. @@ -818,6 +830,7 @@ and how other developers should use it, for example. class_name MyNode extends Node + uses MyTrait ## A brief description of the class's role and functionality. ## ## The description of the script, what it can do, diff --git a/tutorials/scripting/gdscript/static_typing.rst b/tutorials/scripting/gdscript/static_typing.rst index b7065ddd058..a3d62ca2e43 100644 --- a/tutorials/scripting/gdscript/static_typing.rst +++ b/tutorials/scripting/gdscript/static_typing.rst @@ -116,8 +116,10 @@ Here is a complete list of what can be used as a type hint: 7. Global, native and custom named enums. Note that an enum type is just an ``int``, there is no guarantee that the value belongs to the set of enum values. 8. Constants (including local ones) if they contain a preloaded class or enum. +9. :ref:`Global traits `. +10. :ref:`Inner traits <_doc_gdscript_basics_inner_traits>`. -You can use any class, including your custom classes, as types. There are two ways +You can use any class or trait, including your custom classes and traits, as types. There are two ways to use them in scripts. The first method is to preload the script you want to use as a type in a constant:: @@ -135,6 +137,20 @@ and you can use it anywhere, without having to preload it into a constant:: var my_rifle: Rifle +These methods also work with traits, except using ``trait_name`` and ``uses`` in place +of ``class_name`` and ``extends``:: + + const Shootable = preload("res://player/weapons/shootable.gdt") + var something_shootable: Shootable + +:: + + class_name Rifle + uses Shootable + + # Somewhere else... + var my_shootable_thing: Shootable + Specify the return type of a function with the arrow ``->`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -151,7 +167,7 @@ as with variables:: health_points -= damage return health_points <= 0 -You can also use your own classes as return types:: +You can also use your own classes or traits as return types:: # Adds an item to the inventory and returns it. func add(reference: Item, amount: int) -> Item: @@ -199,7 +215,7 @@ To define the type of an ``Array``, enclose the type name in ``[]``. An array's type applies to ``for`` loop variables, as well as some operators like ``[]``, ``[]=``, and ``+``. Array methods (such as ``push_back``) and other operators -(such as ``==``) are still untyped. Built-in types, native and custom classes, +(such as ``==``) are still untyped. Built-in types, native and custom classes and traits, and enums may be used as element types. Nested array types (like ``Array[Array[int]]``) are not supported. From be35be0c8c8c71f828a0e80907dd9b025f99876e Mon Sep 17 00:00:00 2001 From: Malcolm Anderson Date: Wed, 11 Dec 2024 17:26:23 -0800 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: tetrapod <145553014+tetrapod00@users.noreply.github.com> --- tutorials/scripting/gdscript/gdscript_basics.rst | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tutorials/scripting/gdscript/gdscript_basics.rst b/tutorials/scripting/gdscript/gdscript_basics.rst index 46f54e82a0d..59d9dba668f 100644 --- a/tutorials/scripting/gdscript/gdscript_basics.rst +++ b/tutorials/scripting/gdscript/gdscript_basics.rst @@ -1131,8 +1131,8 @@ A class member variable can be declared static:: Static variables belong to the class or trait, not instances. This means that static variables share values between multiple instances, unlike regular member variables. -From inside a class/trait, you can access static variables from any function, both static and non-static. -From outside the class/trait, you can access static variables using the class/trait or an instance +From inside a class or trait, you can access static variables from any function, both static and non-static. +From outside the class or trait, you can access static variables using the class/trait or an instance (the second is not recommended as it is less readable). .. note:: @@ -1983,11 +1983,7 @@ A class (stored as a file) can inherit from: Multiple inheritance is not allowed. -.. note:: - Godot 4.x introduces `traits `_ to GDScript, which may cover many of - the use cases for multiple inheritance. See their section for more information - about how they work. - +Multiple inheritance is not allowed, but Godot does support `traits `_, which cover many of the same use cases. Inheritance uses the ``extends`` keyword:: # Inherit/extend a globally available class. @@ -2180,7 +2176,8 @@ class resource is done by calling the ``new`` function on the class object:: Traits ------ -Traits are collections of behaviors and attributes that classes can use to guarantee +Since Godot 4.x, GDScript supports traits, which are collections of behaviors and attributes +that classes can use to guarantee functionality to themselves and other objects that may be attempting to use them. Like classes, by default all ``.gdt`` files are unnamed traits, and you must reference From 4b74581f92f1798e6bb25cc9e98b6ecbc98f5863 Mon Sep 17 00:00:00 2001 From: Malcolm Anderson Date: Wed, 11 Dec 2024 17:42:10 -0800 Subject: [PATCH 3/3] Updated description of static variables for traits --- .../scripting/gdscript/gdscript_basics.rst | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/tutorials/scripting/gdscript/gdscript_basics.rst b/tutorials/scripting/gdscript/gdscript_basics.rst index 59d9dba668f..6aa7c8bc7ab 100644 --- a/tutorials/scripting/gdscript/gdscript_basics.rst +++ b/tutorials/scripting/gdscript/gdscript_basics.rst @@ -1132,7 +1132,7 @@ Static variables belong to the class or trait, not instances. This means that st share values between multiple instances, unlike regular member variables. From inside a class or trait, you can access static variables from any function, both static and non-static. -From outside the class or trait, you can access static variables using the class/trait or an instance +From outside the class or trait, you can access static variables using the class or an instance (the second is not recommended as it is less readable). .. note:: @@ -1203,23 +1203,17 @@ A base class static variable can also be accessed via a child class:: B.x = 3 prints(A.x, B.x) # 3 3 -.. warning:: - - Static variables declared on traits belong to the trait *itself*, and are *not* - inherited by classes that use the trait:: +Likewise, if a class uses a trait with static variables, it will inherit those +variables:: - trait HasStatic: - static var static_var = 3 + trait HasStatic: + static var static_var = 3 - class UsingClass: - uses HasStatic + class UsingClass: + uses HasStatic - func _ready(): - # Will work - print(HasStatic.static_var) - - # Will NOT work, as UsingClass does not get static_var from HasStatic - print(UsingClass.static_var) + func _ready(): + print(UsingClass.static_var) ``@static_unload`` annotation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~