diff --git a/README.md b/README.md index edaf618..41188dc 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ Given that Godot Engine lacks an official interface system, many developers reso ## 🗺️ Roadmap - [x] Core trait system -- [ ] Automatic multi trait dependencies injection -- [ ] Automatic dependent trait declaration and creation +- [x] Automatic multi trait dependencies injection +- [x] Automatic dependent trait declaration and creation - [x] Generation of an helper script to provide strong typed features and code completion in editor - [ ] Inline traits into scripts by using the `@inline_trait(TheTraitName)` annotation - [x] Helper methods to invoke code if object _is a [something]_ or else invoke a _fallback method_ @@ -278,11 +278,11 @@ func _process(delta:float) -> void: GTraits.unset_damageable(crate) ``` -#### 🔑 Automatic trait dependency injection +#### 🔑 Automatic trait dependencies injection -Traits may depend on each other to function, or they may require a _receiver object_ (the trait carrier) to implement a specific behavior. For instance, a _Damageable_ trait would likely need a _Healthable_ object to deduct health from when damage is taken. +Traits may depend on each other to function, or they may require a _receiver object_ (the trait carrier) to implement a specific behavior. For instance, a _Damageable_ trait would likely need a _Healthable_ object to deduct health from when damage is taken. It may also requires a _Loggable_ trait to do some debug prints. -_Godot Traits_ provides automatic injection of trait dependencies into trait constructors. If a trait constructor requests an object of the same type as the _trait receiver_ (or no specific type), the _receiver_ is automatically injected into the trait. +_Godot Traits_ provides automatic injection of trait dependencies into trait constructors. If a trait constructor requests an object of the same type as the _trait receiver_ (or no specific type), the _receiver_ is automatically injected into the trait. If the trait constructor requires other traits, thoses traits will be retrieved from the _trait receiver_ itself. If some traits can not be resolved in the _receiver_, they are automatically (recursively) instantiated, registered into the _trait receiver_ for future usage, and injected into the instantiating trait. ```gdscript ##### @@ -324,15 +324,20 @@ If the trait constructor requests an object of a different type than the _receiv class_name Damageable var _healthable:Healthable - -# This trait needs a Healthable to work (an object to remove health from) -# Healthable is also a trait. GTraits will check if the receiver object owns this traits, and automatically -# inject the Healthable trait into this trait constructor -func _init(the_healthable:Healthable) -> void: +var _loggable:Loggable + +# This trait needs both Healthable (an object to remove health from) and Loggable (an object that is +# able to print debug logs) to work. Healthable is also a trait. GTraits will check if the receiver +# object owns those traits, and automatically inject them intothis trait constructor. +# If the receiver does not have the required traits, they are automatically instantiated, registered into +# the receiver and injected into this trait. +func _init(the_healthable:Healthable, the_loggable:Loggable) -> void: _healthable = the_healthable + _loggable = the_loggable func take_damage(damage:int) -> void: _healthable.health -= damage + _loggable.log("Took %d damage!" % damage) ##### # File world.gd @@ -341,21 +346,27 @@ extends Node2D func _init() -> void: var crate:Node2D = preload("crate.tscn").instantiate() - GTraits.set_healthable(crate) - # This will automatically make Damageable trait to be construct using the Healthable trait declared above. - # Order is important here, since Damageable trait needs the Healthable trait, so Healthable trait should be - # declared BEFORE the Damageable trait. + # Only the Damageable trait is set initially + # Now, when the Damageable trait is constructed, it automatically declares, creates, + # and injects the required Healthable and Loggable traits into the crate GTraits.set_damageable(crate) + assert(GTraits.is_damageable(crate), "It is Damageable !") + assert(GTraits.is_loggable(crate), "It is Loggable too !") + assert(GTraits.is_healthable(crate), "It is Healthable too !") ``` -##### 📜 Automatic trait dependency injection rules +##### 📜 Automatic trait dependencies injection rules + +- When automatically instantiating traits, developers need to be mindful of cyclic dependencies—cases where traits depend on each other. _Godot Traits_ cannot construct such traits due to the inherent cyclic structure. If encountered, an assertion error is raised, providing details about the cyclic dependency, + +![image](addons/godot-traits/documentation/assets/gtraits_cyclic_dep_detection.png) + +- The _auto-instantiation_ feature in _Godot Traits_ is limited to handling trait instances. If a trait's constructor demands an instance of a type that is not a trait, an assertion error will be raised. This limitation is essential as generic types may not adhere to trait rules and cannot be treated as such, -- ⚠️ For now, only constructor with zero or one argument are handled by _Godot Traits_. See ___Roadmap___ for evolution, -- If `GTraits` can not find the required trait to inject into another trait constructor, an assertion error is raised, +![image](addons/godot-traits/documentation/assets/gtraits_not_a_trait_error.png) -![image](addons/godot-traits/documentation/assets/gtraits_assertion_error_can_not_instantiate.png) +- Default arguments in trait constructors are not considered. -- Trait declaration order is important since a trait must exist into an object to be injectable into another trait constructor. #### 🔑 Traits inheritance diff --git a/addons/godot-traits/core/builder/gtraits_trait_builder.gd b/addons/godot-traits/core/builder/gtraits_trait_builder.gd new file mode 100644 index 0000000..ad1538c --- /dev/null +++ b/addons/godot-traits/core/builder/gtraits_trait_builder.gd @@ -0,0 +1,148 @@ +extends RefCounted +class_name GTraitsTraitBuilder + +## +## Trait builder for [GTraits]. +## +## [color=red]This is an internal API.[/color] +#------------------------------------------ +# Constants +#------------------------------------------ + +#------------------------------------------ +# Signals +#------------------------------------------ + +#------------------------------------------ +# Exports +#------------------------------------------ + +#------------------------------------------ +# Public variables +#------------------------------------------ + +#------------------------------------------ +# Private variables +#------------------------------------------ + +# All known traits, for dep injection. As a dictionary to check trait in o(1) +var _known_traits:Dictionary +var _traits_storage:GTraitsStorage = GTraitsStorage.new() +var _type_oracle:GTraitsTypeOracle = GTraitsTypeOracle.new() + +#------------------------------------------ +# Godot override functions +#------------------------------------------ + +#------------------------------------------ +# Public functions +#------------------------------------------ + +## Declare a class as a trait, making it available for dependency injection +func register_trait(a_trait:Script) -> void: + _known_traits[a_trait] = true + +## Retuns the trait for the given receiver. If the trait already exists, it is just returned. Otherwise, +## it is instantiated, registered into the receiver and returned. +func instantiate_trait(a_trait:Script, receiver:Object) -> Object: + return _instantiate_trait(a_trait, receiver) + +#------------------------------------------ +# Private functions +#------------------------------------------ + +func _instantiate_trait(a_trait:Script, receiver:Object, encoutered_traits:Array[Script] = []) -> Object: + # Check if this is an actual trait + if not _known_traits.get(a_trait, false): + assert(false, "Type '%s' is not a trait and can not be automatically instantiated" % _type_oracle.get_script_class_name(a_trait)) + return null + + # Check there is no cyclic dependencies in progress + if encoutered_traits.has(a_trait): + var cyclic_dependency_string:String = encoutered_traits \ + .map(_type_oracle.get_script_class_name) \ + .reduce(func(accum, name): return "%s -> %s" % [accum, name]) + cyclic_dependency_string = "%s -> %s" % [cyclic_dependency_string, _type_oracle.get_script_class_name(a_trait)] + assert(false, "⚠️ Cyclic dependency detected during trait instantiation: %s" % cyclic_dependency_string) + return null + # Register this trait to be encountered + encoutered_traits.append(a_trait) + + # If receiver already has the given trait, return it immediatly, else try to instantiate it + var trait_instance:Object = _traits_storage.get_trait_instance(receiver, a_trait) + if not is_instance_valid(trait_instance): + trait_instance = _instantiate_trait_for_receiver(a_trait, receiver, encoutered_traits) + + # This trait has been handled, so we can pop it out + encoutered_traits.pop_back() + + return trait_instance + +func _instantiate_trait_for_receiver(a_trait:Script, receiver:Object, encoutered_traits:Array[Script]) -> Object: + assert(a_trait.can_instantiate(), "Trait '%s' can not be instantiated" % _traits_storage.get_trait_class_name(a_trait)) + + # Trait constructor ('_init' method) can take 0 or multiple parameters. + # If it takes parameters, it can either be: + # - the object itself, since trait may need contextual usage to work + # - a trait of the object itself + var constructor_parameters:Array[Object] = [] + # To print a warning if the receiver is injected multiple times in the same constructor + # Maybe something is wrong in that case... + var receiver_object_already_injected:bool = false + # Tells if there was a fatal error during trait instantiation + var error_encountered:bool = false + + # Look for _init method to check if it takes parameters or not + for method in a_trait.get_script_method_list(): + if method.name == "_init": + # Find/construct required arguments + for arg in method.args: + # Is it the receiver itself, or a trait ? + var constructor_argument_class_name:String = arg.class_name + if constructor_argument_class_name.is_empty(): + # Argument is not strongly typed. Just pass the receiver itself as parameter + # Hope for the best ! + if receiver_object_already_injected: + printerr("⚠️ Injecting at least twice the trait receiver into trait '%s' constructor" % _traits_storage.get_trait_class_name(a_trait)) + receiver_object_already_injected = true + constructor_parameters.append(receiver) + else: + # Two possibilities : + # - parameter is an instance of the receiver itself : the receiver is the expected parameter + # - else, parameter is an instance of a trait, so try to get it or instantiate it + if _type_oracle.is_object_instance_of(constructor_argument_class_name, receiver): + constructor_parameters.append(receiver) + else: + var needed_trait:Script = _type_oracle.get_script_from_class_name(constructor_argument_class_name) + if not is_instance_valid(needed_trait): + assert(false, "Trait '%s' can not be found in project." % constructor_argument_class_name) + error_encountered = true + break + + var trait_instance:Object = _instantiate_trait(needed_trait, receiver, encoutered_traits) + if not is_instance_valid(trait_instance): + assert(false, "Unable to instantiate trait '%s'." % constructor_argument_class_name) + error_encountered = true + break + + constructor_parameters.append(trait_instance) + + # Ugly but efficient: there is only one _init method in a script ! + break + + # Something went wrong, can not instantiate objects... + if error_encountered: + return null + + # Instantiate trait and save it into the receiver trait instances storage + var trait_instance:Object = a_trait.new.callv(constructor_parameters) + _traits_storage.store_trait_instance(receiver, trait_instance) + + # If trait has parent classes, to prevent to create new trait instance if parent classes are asked for this + # receiver, register this trait instance has the one to be returned when a parent class is asked (POO style) + var parent_script:Script = a_trait.get_base_script() + while(parent_script != null): + _traits_storage.store_trait_instance(receiver, trait_instance, parent_script) + parent_script = parent_script.get_base_script() + + return trait_instance diff --git a/addons/godot-traits/core/gtraits_core.gd b/addons/godot-traits/core/gtraits_core.gd index 27d4c0a..b4b943b 100644 --- a/addons/godot-traits/core/gtraits_core.gd +++ b/addons/godot-traits/core/gtraits_core.gd @@ -58,8 +58,8 @@ class_name GTraitsCore # Private variables #------------------------------------------ -static var _type_oracle:GTraitsTypeOracle = GTraitsTypeOracle.new() static var _traits_storage:GTraitsStorage = GTraitsStorage.new() +static var _trait_builder:GTraitsTraitBuilder = GTraitsTraitBuilder.new() #------------------------------------------ # Godot override functions @@ -69,6 +69,10 @@ static var _traits_storage:GTraitsStorage = GTraitsStorage.new() # Public functions #------------------------------------------ +## Declare a class as a trait. It then becomes available for several operations on trait. +static func register_trait(a_trait:Script) -> void: + _trait_builder.register_trait(a_trait) + ## Returns [code]true[/code] if an object has a given trait, [code]false[/code] otherwise. static func is_a(a_trait:Script, object:Object) -> bool: if not is_instance_valid(object): @@ -93,7 +97,7 @@ static func add_trait_to(a_trait:Script, object:Object) -> Object: else: # Register trait into object, and instantiate it object_traits.push_back(a_trait) - trait_instance = _instantiate_trait_for_object(a_trait, object) + trait_instance = _trait_builder.instantiate_trait(a_trait, object) return trait_instance @@ -155,56 +159,3 @@ static func if_is_a_or_else(a_trait:Script, object:Object, if_callable:Callable, # Private functions #------------------------------------------ -static func _instantiate_trait_for_object(a_trait:Script, object:Object) -> Object: - assert(a_trait.can_instantiate(), "Trait '%s' can not be instantiated" % _traits_storage.get_trait_class_name(a_trait)) - - # Trait constructor ('_init' method) can take 0 or 1 parameter. - # If it takes one parameter, it can either be: - # - the object itself, since trait may need contextual usage to work - # - a trait of the object itself - var constructor_parameter:Object = null - var constructor_has_argument:bool = false - - # Look for _init method to check if it takes parameters or not - for method in a_trait.get_script_method_list(): - if method.name == "_init": - if method.args.is_empty(): - # No parameter in trait constructor, simplest use case ! - pass - elif method.args.size() == 1: - # Trait takes one parameter for sure - constructor_has_argument = true - # But, is it the object itself, or one of its already declared traits ? - var constructor_argument_class_name:String = method.args[0].class_name - if constructor_argument_class_name.is_empty(): - # Argument is not strongly typed. Just pass the object itself as parameter - # Hope for the best ! - constructor_parameter = object - else: - # Two possibilities : - # - parameter is an instance of the object itself : the object is the expected parameter - # - else, parameter is an instance of the object traits, so try to get it - if _type_oracle.is_object_instance_of(constructor_argument_class_name, object): - constructor_parameter = object - else: - var needed_trait:Script = _type_oracle.get_script_from_class_name(constructor_argument_class_name) - assert(is_instance_valid(needed_trait), "Trait '%s' can not be found in project." % constructor_argument_class_name) - constructor_parameter = _traits_storage.get_trait_instance(object, needed_trait, true) - else: - assert(false, "Trait constructor can not be called") - - # Ugly but efficient: there is only one _init method in a script ! - break - - # Instantiate trait and save it into the object trait instances storage - var trait_instance:Object = a_trait.new(constructor_parameter) if constructor_has_argument else a_trait.new() - _traits_storage.store_trait_instance(object, trait_instance) - - # If trait has parent classes, to prevent to create new trait instance if parent classes are asked for this - # object, register this trait instance has the one to be returned when a parent class is asked (POO style) - var parent_script:Script = a_trait.get_base_script() - while(parent_script != null): - _traits_storage.store_trait_instance(object, trait_instance, parent_script) - parent_script = parent_script.get_base_script() - - return trait_instance diff --git a/addons/godot-traits/documentation/assets/gtraits_assertion_error_can_not_instantiate.png b/addons/godot-traits/documentation/assets/gtraits_assertion_error_can_not_instantiate.png deleted file mode 100644 index b5d233f..0000000 Binary files a/addons/godot-traits/documentation/assets/gtraits_assertion_error_can_not_instantiate.png and /dev/null differ diff --git a/addons/godot-traits/documentation/assets/gtraits_assertion_error_not_moveable.png b/addons/godot-traits/documentation/assets/gtraits_assertion_error_not_moveable.png index 253b6f2..eff2b68 100644 Binary files a/addons/godot-traits/documentation/assets/gtraits_assertion_error_not_moveable.png and b/addons/godot-traits/documentation/assets/gtraits_assertion_error_not_moveable.png differ diff --git a/addons/godot-traits/documentation/assets/gtraits_code_completion.png b/addons/godot-traits/documentation/assets/gtraits_code_completion.png index 814f693..0f683cb 100644 Binary files a/addons/godot-traits/documentation/assets/gtraits_code_completion.png and b/addons/godot-traits/documentation/assets/gtraits_code_completion.png differ diff --git a/addons/godot-traits/documentation/assets/gtraits_code_navigation.gif b/addons/godot-traits/documentation/assets/gtraits_code_navigation.gif index d387224..24e7cb0 100644 Binary files a/addons/godot-traits/documentation/assets/gtraits_code_navigation.gif and b/addons/godot-traits/documentation/assets/gtraits_code_navigation.gif differ diff --git a/addons/godot-traits/documentation/assets/gtraits_cyclic_dep_detection.png b/addons/godot-traits/documentation/assets/gtraits_cyclic_dep_detection.png new file mode 100644 index 0000000..b6406e0 Binary files /dev/null and b/addons/godot-traits/documentation/assets/gtraits_cyclic_dep_detection.png differ diff --git a/addons/godot-traits/documentation/assets/gtraits_not_a_trait_error.png b/addons/godot-traits/documentation/assets/gtraits_not_a_trait_error.png new file mode 100644 index 0000000..a3be681 Binary files /dev/null and b/addons/godot-traits/documentation/assets/gtraits_not_a_trait_error.png differ diff --git a/addons/godot-traits/documentation/assets/gtraits_settings.png b/addons/godot-traits/documentation/assets/gtraits_settings.png index 42983a4..fd93e88 100644 Binary files a/addons/godot-traits/documentation/assets/gtraits_settings.png and b/addons/godot-traits/documentation/assets/gtraits_settings.png differ diff --git a/addons/godot-traits/examples/cyclic-dependency-detection/README.md b/addons/godot-traits/examples/cyclic-dependency-detection/README.md new file mode 100644 index 0000000..f1f6f04 --- /dev/null +++ b/addons/godot-traits/examples/cyclic-dependency-detection/README.md @@ -0,0 +1,12 @@ +# Cyclic dependencies detection + +This example demonstrates how cyclic dependencies are detected when instantiating traits. + +Trait dependencies (constructor parameters) can be auto-instantiate. There exists a corner case where a cyclic +dependency can exist between required dependencies. As a consequence, it's not mpossible to create the object graph. +_Godot Traits_ can detect such cyclic dependencies and warn the developer about them. + +## Technical elements + +- `main.tscn` : scene to run. The main scene try to add `Cyclic1` trait, but this trait can not be instantiated due to +cyclic depdencies. An assertion error will be raised. diff --git a/addons/godot-traits/examples/cyclic-dependency-detection/main.gd b/addons/godot-traits/examples/cyclic-dependency-detection/main.gd new file mode 100644 index 0000000..aa46003 --- /dev/null +++ b/addons/godot-traits/examples/cyclic-dependency-detection/main.gd @@ -0,0 +1,40 @@ +extends Node2D + +#------------------------------------------ +# Constants +#------------------------------------------ + +#------------------------------------------ +# Signals +#------------------------------------------ + +#------------------------------------------ +# Exports +#------------------------------------------ + +#------------------------------------------ +# Public variables +#------------------------------------------ + +#------------------------------------------ +# Private variables +#------------------------------------------ + +#------------------------------------------ +# Godot override functions +#------------------------------------------ + +func _ready() -> void: + # By setting this object as a Cyclic1, Gtraits will auto instantiate all its dependencies: + # so Cyclic2, Cyclic3, then Cyclic4. But Cyclic4 requires a Cyclic2 to be instantiate. It's a cyclic + # dependencies and it will raise an assertion error + GTraits.set_cyclic_1(self) + +#------------------------------------------ +# Public functions +#------------------------------------------ + +#------------------------------------------ +# Private functions +#------------------------------------------ + diff --git a/addons/godot-traits/examples/cyclic-dependency-detection/main.tscn b/addons/godot-traits/examples/cyclic-dependency-detection/main.tscn new file mode 100644 index 0000000..a177d0c --- /dev/null +++ b/addons/godot-traits/examples/cyclic-dependency-detection/main.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://ckdj32ggju5dr"] + +[ext_resource type="Script" path="res://addons/godot-traits/examples/cyclic-dependency-detection/main.gd" id="1_ayw7t"] + +[node name="Main" type="Node2D"] +script = ExtResource("1_ayw7t") diff --git a/addons/godot-traits/examples/dynamic-add-remove-trait/npc.gd b/addons/godot-traits/examples/dynamic-add-remove-trait/npc.gd index 888a1cc..7a335c3 100644 --- a/addons/godot-traits/examples/dynamic-add-remove-trait/npc.gd +++ b/addons/godot-traits/examples/dynamic-add-remove-trait/npc.gd @@ -25,8 +25,12 @@ extends Object #------------------------------------------ func _init() -> void: - GTraits.set_killable(self) + # Healthable trait depends on Killable trait, so by setting this NPC Healthable, it will + # also be Killable ! Healthable and Killable requires a Loggable to work, so the NPC will + # became a Loggable too GTraits.set_healthable(self) + assert(GTraits.is_killable(self), "Should be killable !") + assert(GTraits.is_loggable(self), "Should be loggable !") GTraits.set_critical_damageable(self) #------------------------------------------ diff --git a/addons/godot-traits/examples/not-a-trait-detection/README.md b/addons/godot-traits/examples/not-a-trait-detection/README.md new file mode 100644 index 0000000..0f62879 --- /dev/null +++ b/addons/godot-traits/examples/not-a-trait-detection/README.md @@ -0,0 +1,8 @@ +# Detecting usage of types that are not traits + +This example demonstrates how _Godot Traits_ detect usage of normal types (not traits) as traits. + +## Technical elements + +- `main.tscn` : scene to run. The main scene try to declare the node as a `NotATrait`, which is not +a trait type. This will raise an assertion error. diff --git a/addons/godot-traits/examples/not-a-trait-detection/main.gd b/addons/godot-traits/examples/not-a-trait-detection/main.gd new file mode 100644 index 0000000..c675614 --- /dev/null +++ b/addons/godot-traits/examples/not-a-trait-detection/main.gd @@ -0,0 +1,40 @@ +extends Node2D + +#------------------------------------------ +# Constants +#------------------------------------------ + +#------------------------------------------ +# Signals +#------------------------------------------ + +#------------------------------------------ +# Exports +#------------------------------------------ + +#------------------------------------------ +# Public variables +#------------------------------------------ + +#------------------------------------------ +# Private variables +#------------------------------------------ + +#------------------------------------------ +# Godot override functions +#------------------------------------------ + +func _ready() -> void: + # This will raise an assertion error since type NotATrait is not a trait. So it can not be used + # as a trait. There is no helper methods in the GTraits class for this type, and Godot Traits will + # not allow its usage as a trait in dependency injection. + GTraits.add_trait_to(NotATrait, self) + +#------------------------------------------ +# Public functions +#------------------------------------------ + +#------------------------------------------ +# Private functions +#------------------------------------------ + diff --git a/addons/godot-traits/examples/not-a-trait-detection/main.tscn b/addons/godot-traits/examples/not-a-trait-detection/main.tscn new file mode 100644 index 0000000..4895fd6 --- /dev/null +++ b/addons/godot-traits/examples/not-a-trait-detection/main.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://dhhvq847mnhk"] + +[ext_resource type="Script" path="res://addons/godot-traits/examples/not-a-trait-detection/main.gd" id="1_gfy5d"] + +[node name="Main" type="Node2D"] +script = ExtResource("1_gfy5d") diff --git a/addons/godot-traits/examples/trait-inheritance/npc.gd b/addons/godot-traits/examples/trait-inheritance/npc.gd index fca2574..8d52167 100644 --- a/addons/godot-traits/examples/trait-inheritance/npc.gd +++ b/addons/godot-traits/examples/trait-inheritance/npc.gd @@ -25,8 +25,12 @@ extends Node2D #------------------------------------------ func _init() -> void: - GTraits.set_killable(self) + # Healthable trait depends on Killable trait, so by setting this NPC Healthable, it will + # also be Killable ! Healthable and Killable requires a Loggable to work, so the NPC will + # became a Loggable too GTraits.set_healthable(self) + assert(GTraits.is_killable(self), "Should be killable !") + assert(GTraits.is_loggable(self), "Should be loggable !") GTraits.set_critical_damageable(self) #------------------------------------------ diff --git a/addons/godot-traits/examples/traits/critical_damageable.gd b/addons/godot-traits/examples/traits/critical_damageable.gd index cef4dc2..958dc49 100644 --- a/addons/godot-traits/examples/traits/critical_damageable.gd +++ b/addons/godot-traits/examples/traits/critical_damageable.gd @@ -2,7 +2,6 @@ class_name CriticalDamageable extends Damageable - #------------------------------------------ # Constants #------------------------------------------ @@ -29,7 +28,9 @@ extends Damageable func _notification(what: int) -> void: if what == NOTIFICATION_PREDELETE: - print("GTraitsCoreExampleCriticalDamageable : I'm beeing freed !") + # The Loggable trait is inherited from the Damageable trait ! + # So it's accessible here too ! + _loggable.log("(crit) I'm beeing freed !") #------------------------------------------ # Public functions diff --git a/addons/godot-traits/examples/traits/cyclic_1.gd b/addons/godot-traits/examples/traits/cyclic_1.gd new file mode 100644 index 0000000..27874d5 --- /dev/null +++ b/addons/godot-traits/examples/traits/cyclic_1.gd @@ -0,0 +1,39 @@ +extends Node +# @trait +class_name Cyclic1 + +#------------------------------------------ +# Constants +#------------------------------------------ + +#------------------------------------------ +# Signals +#------------------------------------------ + +#------------------------------------------ +# Exports +#------------------------------------------ + +#------------------------------------------ +# Public variables +#------------------------------------------ + +#------------------------------------------ +# Private variables +#------------------------------------------ + +#------------------------------------------ +# Godot override functions +#------------------------------------------ + +func _init(cyclic2: Cyclic2) -> void: + pass + +#------------------------------------------ +# Public functions +#------------------------------------------ + +#------------------------------------------ +# Private functions +#------------------------------------------ + diff --git a/addons/godot-traits/examples/traits/cyclic_2.gd b/addons/godot-traits/examples/traits/cyclic_2.gd new file mode 100644 index 0000000..83e6033 --- /dev/null +++ b/addons/godot-traits/examples/traits/cyclic_2.gd @@ -0,0 +1,39 @@ +extends Node +# @trait +class_name Cyclic2 + +#------------------------------------------ +# Constants +#------------------------------------------ + +#------------------------------------------ +# Signals +#------------------------------------------ + +#------------------------------------------ +# Exports +#------------------------------------------ + +#------------------------------------------ +# Public variables +#------------------------------------------ + +#------------------------------------------ +# Private variables +#------------------------------------------ + +#------------------------------------------ +# Godot override functions +#------------------------------------------ + +func _init(cyclic3: Cyclic3) -> void: + pass + +#------------------------------------------ +# Public functions +#------------------------------------------ + +#------------------------------------------ +# Private functions +#------------------------------------------ + diff --git a/addons/godot-traits/examples/traits/cyclic_3.gd b/addons/godot-traits/examples/traits/cyclic_3.gd new file mode 100644 index 0000000..6d76f19 --- /dev/null +++ b/addons/godot-traits/examples/traits/cyclic_3.gd @@ -0,0 +1,39 @@ +extends Node +# @trait +class_name Cyclic3 + +#------------------------------------------ +# Constants +#------------------------------------------ + +#------------------------------------------ +# Signals +#------------------------------------------ + +#------------------------------------------ +# Exports +#------------------------------------------ + +#------------------------------------------ +# Public variables +#------------------------------------------ + +#------------------------------------------ +# Private variables +#------------------------------------------ + +#------------------------------------------ +# Godot override functions +#------------------------------------------ + +func _init(cyclic4: Cyclic4) -> void: + pass + +#------------------------------------------ +# Public functions +#------------------------------------------ + +#------------------------------------------ +# Private functions +#------------------------------------------ + diff --git a/addons/godot-traits/examples/traits/cyclic_4.gd b/addons/godot-traits/examples/traits/cyclic_4.gd new file mode 100644 index 0000000..a14a00b --- /dev/null +++ b/addons/godot-traits/examples/traits/cyclic_4.gd @@ -0,0 +1,42 @@ +extends Node +# @trait +class_name Cyclic4 + +#------------------------------------------ +# Constants +#------------------------------------------ + +#------------------------------------------ +# Signals +#------------------------------------------ + +#------------------------------------------ +# Exports +#------------------------------------------ + +#------------------------------------------ +# Public variables +#------------------------------------------ + +#------------------------------------------ +# Private variables +#------------------------------------------ + +#------------------------------------------ +# Godot override functions +#------------------------------------------ + +# Explicitly requires Cyclic2 trait, but Cyclic2 requires Cyclic3 trait +# that requires Cycle4 trait +# It's a cyclic dependency, Godot Traits will throw an assertion error +func _init(cyclic2: Cyclic2) -> void: + pass + +#------------------------------------------ +# Public functions +#------------------------------------------ + +#------------------------------------------ +# Private functions +#------------------------------------------ + diff --git a/addons/godot-traits/examples/traits/damageable.gd b/addons/godot-traits/examples/traits/damageable.gd index e3f009d..a8adc3c 100644 --- a/addons/godot-traits/examples/traits/damageable.gd +++ b/addons/godot-traits/examples/traits/damageable.gd @@ -24,18 +24,23 @@ class_name Damageable # The object that has this trait var _receiver:Object +# The Loggable trait of the receiver, can be injected into _init method +# or it can be retrieved using GTraits.as_loggable(_receiver) +var _loggable:Loggable #------------------------------------------ # Godot override functions #------------------------------------------ -# Automatically requires the trait owner as a dependency, to use it for some logic in this trait -func _init(receiver:Object) -> void: +# Automatically requires the trait receiver as a dependency, to use it for some logic in this trait +# Also required the receiver Loggable trait. If it's not Loggable, it will automatically become Loggable +func _init(receiver:Object, loggable:Loggable) -> void: _receiver = receiver + _loggable = loggable func _notification(what: int) -> void: if what == NOTIFICATION_PREDELETE: - print("Damageable : I'm beeing freed !") + _loggable.log("I'm beeing freed !") #------------------------------------------ # Public functions @@ -46,11 +51,7 @@ func take_damage(amount:int) -> void: # Checks if the object owning this trait is also a Healthable # If so, apply damages - if GTraits.is_a(Healthable, _receiver): - var healthable:Healthable = GTraits.as_a(Healthable, _receiver) - if healthable.is_alive(): - print('I took %s damages !' % effective_damage) - GTraits.as_a(Healthable, _receiver).health -= effective_damage + GTraits.if_is_healthable(_receiver, _apply_damages.bind(effective_damage)) #------------------------------------------ # Private functions @@ -58,3 +59,7 @@ func take_damage(amount:int) -> void: func _compute_amount_of_damage(initial_amount:int) -> int: return initial_amount + +func _apply_damages(healthable:Healthable, effective_damage:int) -> void: + if healthable.is_alive(): + healthable.health -= effective_damage diff --git a/addons/godot-traits/examples/traits/healthable.gd b/addons/godot-traits/examples/traits/healthable.gd index a80b314..5c972ad 100644 --- a/addons/godot-traits/examples/traits/healthable.gd +++ b/addons/godot-traits/examples/traits/healthable.gd @@ -22,14 +22,19 @@ extends RefCounted var max_health:int = 100 var health:int = max_health: set(value): + var old_health:int = health health = clamp(value, 0, max_health) - if value <= 0: - _killable.kill() + if old_health != health: + _loggable.log("HP : %s/%s" % [health, max_health]) + if value <= 0: + _killable.kill() #------------------------------------------ # Private variables #------------------------------------------ +# Reference to the Loggable, needed for this trait to print messages +var _loggable:Loggable # Reference to the Killable, needed for this trait to handle death ! var _killable:Killable @@ -37,10 +42,12 @@ var _killable:Killable # Godot override functions #------------------------------------------ -# Automatically requires that the trait owner is also a Killable object -# Killable trait must be declared before this trait since it's a dependency -func _init(killable:Killable) -> void: +# Automatically requires that the trait receiver is also a Killable and a Loggable object +# If the receiver does not have those traits yet, they will be automatically instantiated and +# registered into the receiver for future usages. +func _init(killable:Killable, loggable:Loggable) -> void: _killable = killable + _loggable = loggable #------------------------------------------ # Public functions diff --git a/addons/godot-traits/examples/traits/inner_traits.gd b/addons/godot-traits/examples/traits/inner_traits.gd new file mode 100644 index 0000000..ce2ab6f --- /dev/null +++ b/addons/godot-traits/examples/traits/inner_traits.gd @@ -0,0 +1,49 @@ +class_name InnerTraits + +# Demonstrate inner class trait declaration + +# @trait(alias=Moveable) +class Moveable extends RefCounted: + + # This is the receiver as a CharacterBody2D + var _character:CharacterBody2D + + func _init(character:CharacterBody2D) -> void: + _character = character + + func move(dir:Vector2) -> void: + _character.velocity += dir * 300 + _character.move_and_slide() + +#------------------------------------------ +# Constants +#------------------------------------------ + +#------------------------------------------ +# Signals +#------------------------------------------ + +#------------------------------------------ +# Exports +#------------------------------------------ + +#------------------------------------------ +# Public variables +#------------------------------------------ + +#------------------------------------------ +# Private variables +#------------------------------------------ + +#------------------------------------------ +# Godot override functions +#------------------------------------------ + +#------------------------------------------ +# Public functions +#------------------------------------------ + +#------------------------------------------ +# Private functions +#------------------------------------------ + diff --git a/addons/godot-traits/examples/traits/killable.gd b/addons/godot-traits/examples/traits/killable.gd index 26b6f52..b832bea 100644 --- a/addons/godot-traits/examples/traits/killable.gd +++ b/addons/godot-traits/examples/traits/killable.gd @@ -24,10 +24,18 @@ var is_killed:bool = false # Private variables #------------------------------------------ +var _loggable:Loggable + #------------------------------------------ # Godot override functions #------------------------------------------ +# Loggable trait will automatically be constructed, registered into receiver and injected into +# this constructor, unless receiver already has a Loggable trait, in this case the existing trait +# will be directly injected +func _init(loggable:Loggable) -> void: + _loggable = loggable + #------------------------------------------ # Public functions #------------------------------------------ @@ -35,7 +43,7 @@ var is_killed:bool = false func kill() -> void: if not is_killed: is_killed = true - print("I've been killed !") + _loggable.log("I've been killed !") #------------------------------------------ # Private functions diff --git a/addons/godot-traits/examples/traits/loggable.gd b/addons/godot-traits/examples/traits/loggable.gd new file mode 100644 index 0000000..ca70de4 --- /dev/null +++ b/addons/godot-traits/examples/traits/loggable.gd @@ -0,0 +1,45 @@ +# @trait +class_name Loggable +extends RefCounted + +#------------------------------------------ +# Constants +#------------------------------------------ + +#------------------------------------------ +# Signals +#------------------------------------------ + +#------------------------------------------ +# Exports +#------------------------------------------ + +#------------------------------------------ +# Public variables +#------------------------------------------ + +#------------------------------------------ +# Private variables +#------------------------------------------ + +var _log_context:String + +#------------------------------------------ +# Godot override functions +#------------------------------------------ + +# Automatically requires the trait receiver as a dependency +func _init(receiver) -> void: + _log_context = str(receiver.get_instance_id()) + +#------------------------------------------ +# Public functions +#------------------------------------------ + +func log(message:String) -> void: + print("%s| %s" % [_log_context, message]) + +#------------------------------------------ +# Private functions +#------------------------------------------ + diff --git a/addons/godot-traits/examples/traits/not_a_trait.gd b/addons/godot-traits/examples/traits/not_a_trait.gd new file mode 100644 index 0000000..40eef73 --- /dev/null +++ b/addons/godot-traits/examples/traits/not_a_trait.gd @@ -0,0 +1,37 @@ +extends RefCounted +class_name NotATrait + +# Here to demonstrate how Godot Traits detect wrong trait usage + +#------------------------------------------ +# Constants +#------------------------------------------ + +#------------------------------------------ +# Signals +#------------------------------------------ + +#------------------------------------------ +# Exports +#------------------------------------------ + +#------------------------------------------ +# Public variables +#------------------------------------------ + +#------------------------------------------ +# Private variables +#------------------------------------------ + +#------------------------------------------ +# Godot override functions +#------------------------------------------ + +#------------------------------------------ +# Public functions +#------------------------------------------ + +#------------------------------------------ +# Private functions +#------------------------------------------ + diff --git a/addons/godot-traits/examples/use-trait-auto-injection/npc.gd b/addons/godot-traits/examples/use-trait-auto-injection/npc.gd index ce59514..794632b 100644 --- a/addons/godot-traits/examples/use-trait-auto-injection/npc.gd +++ b/addons/godot-traits/examples/use-trait-auto-injection/npc.gd @@ -25,8 +25,12 @@ extends Node2D #------------------------------------------ func _init() -> void: - GTraits.set_killable(self) + # Healthable trait depends on Killable trait, so by setting this NPC Healthable, it will + # also be Killable ! Healthable and Killable requires a Loggable to work, so the NPC will + # became a Loggable too GTraits.set_healthable(self) + assert(GTraits.is_killable(self), "Should be killable !") + assert(GTraits.is_loggable(self), "Should be loggable !") GTraits.set_damageable(self) #------------------------------------------ diff --git a/addons/godot-traits/helper/gtraits_helper_generator.gd b/addons/godot-traits/helper/gtraits_helper_generator.gd index d0b0d65..789b991 100644 --- a/addons/godot-traits/helper/gtraits_helper_generator.gd +++ b/addons/godot-traits/helper/gtraits_helper_generator.gd @@ -219,12 +219,22 @@ func _get_script_traits(script:Script) -> Dictionary: func _generate_gtraits_helper() -> void: # Before generating GTraits script, ensure that all references scripts are still available # some may have been deleted from outside Godot Editor + # Do it in 2 passes since it's not supported to erase values from a dictionary while iterating + var scripts_to_remove:PackedStringArray = [] for script_path in _traits_by_scripts: if not FileAccess.file_exists(script_path): - _traits_by_scripts.erase(script_path) + scripts_to_remove.append(script_path) + for script_path in scripts_to_remove: + _traits_by_scripts.erase(script_path) # Then proceed to generation var indent_string:String = _get_indent_string() + var ordered_trait_qualified_names:Array = _traits_by_scripts.values() \ + .map(func(traits:Dictionary): return traits.keys()) \ + .reduce(func(accu:Array, trait_names:Array): return accu + trait_names, []) + ordered_trait_qualified_names.sort() + + # Be predictable for trait order : do not depend on parse order var content:String = '' content += "# ##########################################################################\n" @@ -238,8 +248,14 @@ func _generate_gtraits_helper() -> void: content += "## Auto-generated utility to handle traits in Godot.\n" content += "## \n" content += "\n" - content += "#region Core methods\n" + content += "#region Trait declaration\n\n" + content += "static func _static_init() -> void:\n" + for a_trait in ordered_trait_qualified_names: + content += indent_string + "GTraitsCore.register_trait(%s)\n" % a_trait + content += "\n" + content += "#endregion\n" content += "\n" + content += "#region Core methods\n\n" content += "## Shortcut for [method GTraitsCore.as_a]\n" content += "static func as_a(a_trait:Script, object:Object) -> Object:\n" content += indent_string + "return GTraitsCore.as_a(a_trait, object)\n" diff --git a/addons/godot-traits/plugin.cfg b/addons/godot-traits/plugin.cfg index 63f37a4..f36ccbd 100644 --- a/addons/godot-traits/plugin.cfg +++ b/addons/godot-traits/plugin.cfg @@ -3,5 +3,5 @@ name="Godot Traits" description="Traits made easy in Godot." author="Adrien Quillet" -version="0.2.0" +version="0.3.0" script="godot-traits.gd"