Service is a broad category encompassing any value, function, or feature that an games needs. A service is typically a class with a narrow, well-defined purpose. It should do something specific and do it well.
Ideally, a scene scripts's job is to enable only the user experience. A scene script should present properties and methods for data binding to mediate between the view and the game logic. The view is what the scene renders and the game logic is what includes the notion of a model.
A scene script should use services for tasks that don't involve the scene or scene script logic. Services are good for tasks such as fetching data from the server, validating user input, or logging directly to the console. By defining such processing tasks in an injectable service class, you make those tasks available to any component. You can also make your game more adaptable by injecting different providers of the same kind of service, as appropriate in different circumstances.
This is a simple dependency injection system for Godot. It is designed to be simple to use and easy to understand.
- Add the
injector.gdscript to your autoloads. - Provide a class or value to the injector.
- Inject the class or value from the injector.
Once you provide a value to a node, it and all of it's children will have access to it!
# A class to hold the stats of a player or enemy.
class_name Stats extends Injectable
var health := 100
var attack := 100
func receive_damage(damage: int):
health -= damage# player.gd
@onready var player_stats: Stats = Injector.inject(Stats)
func _enter_tree():
Injector.provide(Stats)
func _process():
if player_stats.health <= 0:
print("Game Over!")
get_tree().reload_current_scene()# enemy.gd
@exports var attack_box: Area2D
@onready var player_stats: Stats = Injector.inject(Stats)
@onready var enemy_stats: Stats = Injector.inject(Stats, self)
func _enter_tree():
Injector.provide(Stats, self)
func _ready():
attack_box.area_entered.connect(_on_attack_box_area_entered)
func _on_attack_box_area_entered(area: Area2D):
player_stats.receive_damage(enemy_stats.attack)There are two types of dependencies: Injectable and InjectionToken.
- Injectable: A class that will be created by the injector.
- InjectionToken: A token that can be used to inject a value.
To make a class injectable, simply extend the Injectable class.
class_name MyInjectable extends Injectable
var my_value := 0
func _init(v: int):
my_value = vNext, we register the injectable with the injector on a specific node. It is best to do this as a variable or in the _enter_tree function. If you do it in the _ready function child nodes may try to access this too soon.
# Option 1:
var my_injectable := Injectable.provide(MyInjectable, self, 1)
# Option 2:
func _enter_tree():
Injectable.provide(MyInjectable, self, 1)Lastly we get the value on a child node that we want to use this for (this can be at any depth, as it does not have to be a direct child). This can be done in the _ready function or with @onready.
# Option 1:
@onready var my_injectable: MyInjectable = Injectable.inject(MyInjectable, self)
# Option 2:
func _ready():
var my_injectable: MyInjectable = Injectable.inject(MyInjectable, self)
# After using option 1 or 2, we can now do this:
print(my_injectable.my_value)Another way is to use an InjectionToken. This is useful if you want to inject a value that is not a class that extends Injectable. An injection token can be either a string or or an instance of InjectionToken.
A good place to put these tokens is in an Autoload script. However, these could also be put in a node as a static var.
# tokens.gd (Autoload as Tokens)
var MyToken := InjectionToken.new("MyToken")Next, we register the token with the injector on a specific node. It is best to do this as a variable or in the _enter_tree function. If you do it in the _ready function child nodes may try to access this too soon.
# Get a reference to the node that will hold the bullet nodes.
@export var bullets: Node2D
# Option 1:
var my_token := Injectable.provide(Tokens.MyToken, self, bullets)
# Option 2:
func _enter_tree():
Injectable.provide(Tokens.MyToken, self, bullets)Lastly we get the value on a child node that we want to use this for (this can be at any depth, as it does not have to be a direct child). This can be done in the _ready function or with @onready
# Option 1:
@onready var bullets: Node2D = Injectable.inject(Tokens.MyToken, self)
# Option 2:
func _ready():
var bullets: Node2D = Injectable.inject(Tokens.MyToken, self)
# After using option 1 or 2, we can now do this:
var bullet := preload("res://Bullet.tscn").instantiate()
bullets.add_child(bullet)An Injectable is a class that will be created by the injector. It is defined by extending the Injectable class. This class can have an _init function that takes any number of arguments.
class_name MyInjectable extends InjectableThis function registers an Injectable or InjectionToken with the injector.
Returns: The Injectable instance or InjectionToken value that was registered with the injector. If nothing was registered, null is returned (this often happens when the wrong type is passed in).
| Argument | Type | Description | Required |
|---|---|---|---|
type |
Injectable or InjectionToken |
The type of the injectable. | true |
source |
Node or "root" |
The node in which to start the search up the tree. Defaults to "root". |
`false |
parameters |
Array or Variant |
An array of parameters to pass to the Injectable (an array is only needed for 2 or more parameters) or a value for an InjectionToken. Defaults to null. |
false |
# Injectable
Injectable.provide(MyInjectable1, self, [1, 2, 3])
Injectable.provide(MyInjectable2, self, "Example")
# InjectionToken
Injectable.provide(Tokens.MyToken1, self, 1)
Injectable.provide(Tokens.MyToken2, self, node_ref)
# String
Injectable.provide("custom_string", self)
Injectable.provide("custom_string", self, "Example")This function finds the Injectable or InjectionToken that was registered with the injector somewhere up the tree.
Returns: The Injectable or InjectionToken that was registered with the injector. If nothing was found, null is returned.
| Argument | Type | Description | Required |
|---|---|---|---|
type |
Injectable or InjectionToken |
The type of the injectable. | true |
source |
Node or "root" |
The node in which to start the search up the tree. Defaults to "root". |
false |
multi |
bool |
If true, an array of all injectables found will be returned. Defaults to false. |
false |
# Injectable
@onready var my_injectable1: MyInjectable1 = Injectable.inject(MyInjectable1, self)
@onready var array: Array = Injectable.inject(MyInjectable1, self, true)
# InjectionToken
@onready var my_token1: Node2D = Injectable.inject(Tokens.MyToken1, self)
@onready var my_token2: int = Injectable.inject(Tokens.MyToken2, self)
@onready var array: Array = Injectable.inject(Tokens.MyToken1, self, true)
# String
@onready var custom_string = Injectable.inject("custom_string", self)An InjectionToken is a token that can be used to create a value that is not a class that extends Injectable. Such as a Node, int, Array, etc. It is defined by creating a new instance of the InjectionToken class.
| Argument | Type | Description | Required |
|---|---|---|---|
name |
String |
The name of the token. | true |
These tokens can be placed where ever you see fit. An Autoload script is a good place to put them for access throughout the project. They can also be placed in a node as a static var.
Hint: Postfixing Token to the variable name is a good way to keep track of these.
# tokens.gd (Autoload as Tokens)
var MyToken = InjectionToken.new("MyToken")
# MyNode.gd (as a static var)
static var MyToken = InjectionToken.new("MyToken")This service provides a reference to the Window or to the SceneTree depending on what you would like.
@onready var game_ref: GameRef = Injector.inject(GameRef)
func _ready():
print(game_ref.root)
print(game_ref.tree)This service provides a reference to a node in the scene tree.
# enemy.gd
class_name Enemy extends Area2D
func _enter_tree():
Injector.provide(NodeRef, self, self)# player.gd
class_name Player extends Node2D
func _on_enemy_entered(area: Area2D):
var enemy = Injector.inject(NodeRef, area)
self.queue_free()