Files
game-cards/addons/gut/documentation/docs/Doubling-Singletons.md
2026-05-29 09:16:10 +08:00

142 lines
7.4 KiB
Markdown

# Doubling-Singletons
You can create pseudo-doubles of Engine Singletons (not to be confused with Autoloads) using `double_singleton` or `partial_double_singleton`. Godot Engine Singletons are single instance classes that are created by Godot. `Input`, `OS`, and `Time` are Engine Singletons that are commonly used. A full list of supported Engine Singletons can be found below.
All Engine Singletons extend `Object` but their doubles extend `RefCounted`. This was done so that they would be freed automatically.
__These doubles do not replace the existing Engine Singleton__, so they must be injected into a variable in your script. You must then use this local singleton reference in your script instead of directly referencing the Singleton. If you use `:=` you still get all autocomplete features in the editor.
```gdscript
class_name Player
var my_local_input_singleton_ref := Input
func _physics_process(delta):
if(my_local_input_singleton_ref.is_action_just_pressed("jump")):
....
```
```gdscript
extends GutTest
func test_player_does_something_with_input():
var dbl_input = partial_double_singleton(Input).new()
var p = Player.new()
p.my_local_input_singleton_ref = dbl_input
stub(dbl_input.is_action_just_pressed)\
.to_return(true)\
.when_passed("jump")
...
```
## Differences to a normal Double
Engine Singleton doubles are different from normal doubles in the following way:
* Singleton doubles wrap around a an Engine Singleton, they do not extend it.
* Properties are copied from the source Singleton when an instance of the double is created. This means the intial values will change (on calls to `.new()`) if the Singleton's properties change.
* `double_singleton` and `partial_double_singleton` parameters are checked against a list of known-valid Engine Singletons.
* Inherit from `RefCounted`, not the source Engine Singleton or `Object`.
* Parial doubles of singletons, or stubbing `to_call_super`, calls methods on the source Engine Singleton.
* The properties/methods of `Object` are never included in the double, regardless of the Double Strategy.
* Ignoring a method on an Engine Singleton means it will not exist in the double, whereas normal doubles just don't get overrides for the ignored method.
```gdscript
ignore_method_when_doubling(Time, 'get_ticks_msec')
var inst = double_singleton(Time).new()
assert_false(inst.has_method('get_ticks_msec'))
```
## Example
This example has a class that uses the `Time` singleton. We make a double of `Time` in the tests and "inject" it into the instance of `UsesTime` we are testing. We then stub the double to return values that allow us to verify `UsesTime` is correctly using `Time`.
``` gdscript
class_name UsesTime
# Must have a reference to Engine Singleton that we can
# inject our double into.
var t := Time
var _start_time = -1
func start():
_start_time = t.get_ticks_msec()
func end():
var monday_extra = 0
if(t.get_date_dict_from_system().weekday == t.WEEKDAY_MONDAY):
monday_extra = 10
return t.get_ticks_msec() - _start_time + monday_extra
```
``` gdscript
extends GutTest
# Fun fact, this test will fail if ran on any Monday. I wrote this on a
# Wednesday, so it passes. This is a doozy of a flakey test. Don't make
# tests
func test_calling_end_returns_elapsed_time_using_msecs():
var dbl_time = partial_double_singleton(Time).new()
var inst = UsesTime.new()
inst.t = dbl_time
stub(dbl_time.get_ticks_msec).to_return(0)
inst.start()
stub(dbl_time.get_ticks_msec).to_return(10)
assert_eq(inst.end(), 10)
# Illustrate that enums are included in singleton doubles.
func test_on_mondays_elapsed_time_is_longer_because_time_moves_slower_on_mondays():
var dbl_time = double_singleton(Time).new()
var inst = UsesTime.new()
inst.t = dbl_time
stub(dbl_time.get_date_dict_from_system)\
.to_return({
"year": 2025,
"month": 1,
"day": 1,
"weekday": Time.WEEKDAY_MONDAY})
stub(dbl_time.get_ticks_msec).to_return(0)
inst.start()
stub(dbl_time.get_ticks_msec).to_return(10)
assert_eq(inst.end(), 20)
```
## Eligible Singletons
I have verified that a double of these can be created and instantiated. All the ways they could be used has not been explored. Your mileage may vary. Please open an issue if you encounter a problem when doubling any of these Engine Singletons.
* [AudioServer](https://docs.godotengine.org/en/stable/classes/class_audioserver.html)
* [CameraServer](https://docs.godotengine.org/en/stable/classes/class_cameraserver.html)
* [ClassDB](https://docs.godotengine.org/en/stable/classes/class_classdb.html)
* [DisplayServer](https://docs.godotengine.org/en/stable/classes/class_displayserver.html)
* [Engine](https://docs.godotengine.org/en/stable/classes/class_engine.html)
* [EngineDebugger](https://docs.godotengine.org/en/stable/classes/class_enginedebugger.html)
* [GDExtensionManager](https://docs.godotengine.org/en/stable/classes/class_gdextensionmanager.html)
* [Geometry2D](https://docs.godotengine.org/en/stable/classes/class_geometry2d.html)
* [Geometry3D](https://docs.godotengine.org/en/stable/classes/class_geometry3d.html)
* [GodotNavigationServer2D](https://docs.godotengine.org/en/stable/classes/class_godotnavigationserver2d.html)
* [IP](https://docs.godotengine.org/en/stable/classes/class_ip.html)
* [Input](https://docs.godotengine.org/en/stable/classes/class_input.html)
* [InputMap](https://docs.godotengine.org/en/stable/classes/class_inputmap.html)
* [JavaClassWrapper](https://docs.godotengine.org/en/stable/classes/class_javaclasswrapper.html)
* [JavaScriptBridge](https://docs.godotengine.org/en/stable/classes/class_javascriptbridge.html)
* [Marshalls](https://docs.godotengine.org/en/stable/classes/class_marshalls.html)
* [NativeMenuMacOS](https://docs.godotengine.org/en/stable/classes/class_nativemenumacos.html)
* [NavigationMeshGenerator](https://docs.godotengine.org/en/stable/classes/class_navigationmeshgenerator.html)
* [NavigationServer3D](https://docs.godotengine.org/en/stable/classes/class_navigationserver3d.html)
* [OS](https://docs.godotengine.org/en/stable/classes/class_os.html)
* [Performance](https://docs.godotengine.org/en/stable/classes/class_performance.html)
* [PhysicsServer2D](https://docs.godotengine.org/en/stable/classes/class_physicsserver2d.html)
* [PhysicsServer2DManager](https://docs.godotengine.org/en/stable/classes/class_physicsserver2dmanager.html)
* [PhysicsServer3D](https://docs.godotengine.org/en/stable/classes/class_physicsserver3d.html)
* [PhysicsServer3DManager](https://docs.godotengine.org/en/stable/classes/class_physicsserver3dmanager.html)
* [ProjectSettings](https://docs.godotengine.org/en/stable/classes/class_projectsettings.html)
* [RenderingServer](https://docs.godotengine.org/en/stable/classes/class_renderingserver.html)
* [ResourceLoader](https://docs.godotengine.org/en/stable/classes/class_resourceloader.html)
* [ResourceSaver](https://docs.godotengine.org/en/stable/classes/class_resourcesaver.html)
* [ResourceUID](https://docs.godotengine.org/en/stable/classes/class_resourceuid.html)
* [TextServerManager](https://docs.godotengine.org/en/stable/classes/class_textservermanager.html)
* [ThemeDB](https://docs.godotengine.org/en/stable/classes/class_themedb.html)
* [Time](https://docs.godotengine.org/en/stable/classes/class_time.html)
* [TranslationServer](https://docs.godotengine.org/en/stable/classes/class_translationserver.html)
* [WorkerThreadPool](https://docs.godotengine.org/en/stable/classes/class_workerthreadpool.html)
* [XRServer](https://docs.godotengine.org/en/stable/classes/class_xrserver.html)