On scripting in the Godot game engine, directly referencing other nodes is convenient via the node path references. However, it creates strong dependencies among scripts. These dependencies makes testing and reusing the script more difficult. In this article, I will demonstrate that there are at least two alternative solutions to make a GDScript more loosely coupled. The two solutions are Dependency Injection and Applying Signal. The solutions may put the code a little more complicate, but it makes the scripts more loosely coupled. It should help promote reusability and testability.
All of the implementations are on Github as below:
https://github.com/nthana/TightlyCoupled
https://github.com/nthana/LooselyCoupled
https://github.com/nthana/LooselyCoupledUsingSignal
The Original Example Code
Source code : https://github.com/nthana/TightlyCoupled
The example contains two scripts: Score.gd (figure 2) and Player.gd (figure 3) composing side-by-side in the main scene: MyGame.tscn (figure 1). The score object’s responsibilities are to keep and to show the score number. The player object’s responsibilities are to check scoring conditions and to inform the score object to increase the score number. For demonstration purpose, the scoring condition is simplified to : increasing one score each time the process() method is called.
Considering player.gd, there is a direct reference to the score object location: /root/MyGame/Score. This reference makes Player.gd inflexible because it required the score object to always be at the specific location. Testing and reusing will rely on this specific score object location.



Method 1: Dependency Injection
Source code : https://github.com/nthana/LooselyCoupled
If the path to the score object should not be hard-coded on the player, then how the player will know which score object to use? The solution is to “pass” the score object from outside of the player script. This technique has a formal name called Dependency Injection.
The name “Dependency Injection” may sound intimidate, but the concept is not too hard. Dependency Injection is the “passing” of any objects from outsider to the script in need [see *]. To apply this technique, we modified our code in two steps. First, declare an object variable “_score”. Second, let other object init the _score object (by a constructor, by a setter method or by a directly setting the _score variable). In Godot, the easiest way is to allow directly setting the _score variable by exporting that variable. The first attempt shown in figure 4.
[* I adapted this definition from Wikipedia. This definition is for the dynamic-typed language such as GDScript. For strongly-typed languages, it may require to extract an interface and change concrete class references into the more general interface references. ]

Unfortunately in the current version of Godot (v.3.1.2), we cannot export variables of type “Node” or “GDScript”. For this reason, we export a NodePath instead, as shown in figure 5. And then convert the node path into the score object. Noted that the code will look a bit more complicate. The assert statement is just for checking if someone forgot to set the score_path before using this script; it help catching bug early.
Now, the player does not contain any hard-coded path. Then, who will have responsible to pass the score path to the player? The answer is the third object: the main game scene. In figure 6, on the inspector window, the main game scene connects the score object to the player object by setting the script variable: store_path. Because the main game scene is the client of both the player and the score objects, it is the natural candidate to do this task. That is the finish of this technique; now everything should work the same as the original code.


Method 2: Applying Signal
Source code : https://github.com/nthana/LooselyCoupledUsingSignal
Another way to decouple the score script from the player script is by redesigning the player responsibilities. The player may not need to be responsible for informing the score object of the score number. To put it another way, the player may not need to know the score object at all. The player only need to inform the outsider “whenever” a score should be increased. Then it is the outsider responsibility to increase the score. This concept could be implemented using an event (or Signal in Godot).
Implemented in figure 7, player defined its own signal called “increase_score(number)”. And whenever the score should be increased, the signal would be emitted. The next question is who will be responsible to wire the signal to the score object? The answer is the main scene.
In figure 8 and 9, the main scene connect the event (signal) to a method. Normally we need an event handler method such as _on_increase_score() on some object, and let that object calls the Score.increase(number). But in this case, the Score.increase(number) has the same method signature with the signal increase_score(number). For this reason, we can connect the signal directly to Score.increase() method. And now, everything should work as expected.



Discussion
Please note that even through decoupled scripts bring more flexibility, it might not always need to decouple everything. One reason is that decoupling techniques also increase code complexities and require our precious time. It is the design trade-off to decide the appropriate decoupling degree. Some scripts which may not need decoupling are: not-plan-to-reuse scripts, short-project scripts, short tutorial scripts, or small scripts. The costs of decoupling might not justify the benefits on these scripts.
Keywords: Godot Game Programming, Object-Oriented Design, Program Decomposition