Notifiche di Godot
Ogni oggetto in Godot implementa un metodo _notification. Il suo scopo è consentire all'oggetto di rispondere a una varietà di callback a livello di motore che potrebbero riguardarlo. Ad esempio, se il motore indica a un CanvasItem di "disegnare", chiamerà _notification(NOTIFICATION_DRAW).
Alcune di queste notifiche, come "draw", sono utili da sovrascrivere negli script. Tanto che Godot ne espone molte con funzioni dedicate:
_ready():NOTIFICATION_READY_enter_tree():NOTIFICATION_ENTER_TREE_exit_tree():NOTIFICATION_EXIT_TREE_process(delta):NOTIFICATION_PROCESS_physics_process(delta):NOTIFICATION_PHYSICS_PROCESS_draw():NOTIFICATION_DRAW
Ciò che gli utenti potrebbero non intuire è che le notifiche esistono anche per i tipi diversi da Node, ad esempio:
Object::NOTIFICATION_POSTINITIALIZE: una callback che viene attivato durante l'inizializzazione dell'oggetto. Non accessibile agli script.
Object::NOTIFICATION_PREDELETE: un callback che viene attivato prima che il motore elimini un oggetto, ovvero un "distruttore".
E inoltre molti dei callback che esistono nei nodi non hanno metodi dedicati, ma sono comunque molto utili.
Node::NOTIFICATION_PARENTED: un callback che viene attivato ogni volta che viene aggiunto un nodo figlio a un altro nodo.
Node::NOTIFICATION_UNPARENTED: un callback che viene attivato ogni volta che viene rimosso un nodo figlio da un altro nodo.
È possibile accedere a tutte queste notifiche personalizzate tramite il metodo universale _notification().
Nota
Anche i metodi nella documentazione etichettati come "virtuali" sono concepiti per essere sovrascritti dagli script.
Un esempio classico è il metodo _init in Object. Sebbene non abbia una NOTIFICATION_* equivalente, il motore lo chiama comunque. La maggior parte dei linguaggi (tranne C#) lo usano come costruttore.
Allora, in quale situazione si dovrebbe utilizzare ciascuna di queste notifiche o funzioni virtuali?
_process vs _physics_process vs *_input
Utilizza _process() quando sono necessari intervalli di tempo dipendenti dal frame rate. Se il codice che aggiorna i dati degli oggetti deve essere aggiornato il più spesso possibile, questo è il posto giusto. Qui vengono spesso eseguiti controlli logici ricorrenti e memorizzazione dei dati nella cache, ma tutto dipende dalla frequenza con cui è necessario aggiornarli. Se non è necessario eseguirli ogni frame, un'altra opzione è implementare un ciclo Timer-timeout.
# Allows for recurring operations that don't trigger script logic
# every frame (or even every fixed frame).
func _ready():
var timer = Timer.new()
timer.autostart = true
timer.wait_time = 0.5
add_child(timer)
timer.timeout.connect(func():
print("This block runs every 0.5 seconds")
)
using Godot;
public partial class MyNode : Node
{
// Allows for recurring operations that don't trigger script logic
// every frame (or even every fixed frame).
public override void _Ready()
{
var timer = new Timer();
timer.Autostart = true;
timer.WaitTime = 0.5;
AddChild(timer);
timer.Timeout += () => GD.Print("This block runs every 0.5 seconds");
}
}
using namespace godot;
class MyNode : public Node {
GDCLASS(MyNode, Node)
public:
// Allows for recurring operations that don't trigger script logic
// every frame (or even every fixed frame).
virtual void _ready() override {
Timer *timer = memnew(Timer);
timer->set_autostart(true);
timer->set_wait_time(0.5);
add_child(timer);
timer->connect("timeout", callable_mp(this, &MyNode::run));
}
void run() {
UtilityFunctions::print("This block runs every 0.5 seconds.");
}
};
Utilizza _physics_process() quando sono necessari intervalli di tempo indipendenti dal frame rate. Se il codice necessita di aggiornamenti costanti nel tempo, a prescindere dalla velocità con cui il tempo avanza, questo è il posto giusto. Le operazioni cinematiche e di trasformazione degli oggetti ricorrenti si dovrebbero eseguire qui.
Sebbene sia possibile, per ottenere le migliori prestazioni, si dovrebbe evitare di effettuare controlli di input durante questi callback. _process() e _physics_process() verranno attivati a ogni occasione (non "riposano" come predefinito). Al contrario, i callback *_input() verranno attivati solo sui frame in cui il motore ha effettivamente rilevato l'input.
È possibile verificare le azioni di input all'interno dei callback di input allo stesso modo. Se si desidera utilizzare un intervallo di tempo (delta), è possibile recuperarlo dai metodi corrispondenti, se necessario.
# Called every frame, even when the engine detects no input.
func _process(delta):
if Input.is_action_just_pressed("ui_select"):
print(delta)
# Called during every input event.
func _unhandled_input(event):
match event.get_class():
"InputEventKey":
if Input.is_action_just_pressed("ui_accept"):
print(get_process_delta_time())
using Godot;
public partial class MyNode : Node
{
// Called every frame, even when the engine detects no input.
public void _Process(double delta)
{
if (Input.IsActionJustPressed("ui_select"))
GD.Print(delta);
}
// Called during every input event. Equally true for _input().
public void _UnhandledInput(InputEvent @event)
{
switch (@event)
{
case InputEventKey:
if (Input.IsActionJustPressed("ui_accept"))
GD.Print(GetProcessDeltaTime());
break;
}
}
}
using namespace godot;
class MyNode : public Node {
GDCLASS(MyNode, Node)
public:
// Called every frame, even when the engine detects no input.
virtual void _process(double p_delta) override {
if (Input::get_singleton->is_action_just_pressed("ui_select")) {
UtilityFunctions::print(p_delta);
}
}
// Called during every input event. Equally true for _input().
virtual void _unhandled_input(const Ref<InputEvent> &p_event) override {
Ref<InputEventKey> key_event = event;
if (key_event.is_valid() && Input::get_singleton->is_action_just_pressed("ui_accept")) {
UtilityFunctions::print(get_process_delta_time());
}
}
};
_init vs inizializzazione vs esportazione
Se lo script inizializza il proprio sotto-albero di nodi, senza una scena, il codice si dovrebbe eseguire in _init(). Anche altre inizializzazioni indipendenti dallo SceneTree o da proprietà si dovrebbero eseguire qui.
Nota
L'equivalente in C# al metodo _init() di GDScript è il costruttore.
_init() viene attivato prima di _enter_tree() o _ready(), ma dopo che uno script ha creato e inizializzato le sue proprietà. Quando si istanzia una scena, i valori delle proprietà vengono assegnati secondo la seguente sequenza:
Assegnazione del valore iniziale: alla proprietà viene assegnato il suo valore di inizializzazione, oppure il suo valore predefinito se non ne è specificato uno. Se esiste un setter, non viene utilizzato.
Assegnazione in
_init(): il valore della proprietà viene sostituito da qualsiasi assegnazione effettuata in_init(), attivando il setter.Assegnazione del valore esportato: il valore di una proprietà esportata viene nuovamente sostituito da qualsiasi valore impostato nell'Ispettore, attivando il setter.
# test is initialized to "one", without triggering the setter.
@export var test: String = "one":
set(value):
test = value + "!"
func _init():
# Triggers the setter, changing test's value from "one" to "two!".
test = "two"
# If someone sets test to "three" from the Inspector, it would trigger
# the setter, changing test's value from "two!" to "three!".
using Godot;
public partial class MyNode : Node
{
private string _test = "one";
[Export]
public string Test
{
get { return _test; }
set { _test = $"{value}!"; }
}
public MyNode()
{
// Triggers the setter, changing _test's value from "one" to "two!".
Test = "two";
}
// If someone sets Test to "three" in the Inspector, it would trigger
// the setter, changing _test's value from "two!" to "three!".
}
using namespace godot;
class MyNode : public Node {
GDCLASS(MyNode, Node)
String test = "one";
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("get_test"), &MyNode::get_test);
ClassDB::bind_method(D_METHOD("set_test", "test"), &MyNode::set_test);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "test"), "set_test", "get_test");
}
public:
String get_test() { return test; }
void set_test(String p_test) { return test = p_test; }
MyNode() {
// Triggers the setter, changing _test's value from "one" to "two!".
set_test("two");
}
// If someone sets test to "three" in the Inspector, it would trigger
// the setter, changing test's value from "two!" to "three!".
};
Come risultato, l'istanziazione di uno script anziché di una scena può influire sia sull'inizializzazione, sia sul numero di volte in cui il motore chiama il setter.
_ready vs _enter_tree vs NOTIFICATION_PARENTED
Quando viene istanziata una scena connessa alla prima scena eseguita, Godot istanzia i nodi lungo l'albero (effettuando chiamate a _init()) e costruisce l'albero verso il basso, partendo dalla radice. Ciò assicura che le chiamate a _enter_tree() vengano eseguite a cascata lungo l'albero. Una volta completato l'albero, i nodi foglia chiamano _ready. Un nodo chiamerà questo metodo una volta che tutti i nodi figlio avranno finito di chiamare il proprio. Ciò provocherà quindi una cascata inversa che risale fino alla radice dell'albero.
Quando viene istanziato uno script o una scena autonoma, i nodi non vengono aggiunti allo SceneTree al momento della creazione, quindi non vengono attivati i callback _enter_tree(). Viene invece eseguita solo la chiamata a _init(). Quando la scena viene aggiunta allo SceneTree, vengono eseguite le chiamate _enter_tree() e _ready().
Se è necessario attivare un comportamento che accade quando i nodi diventano padri di un altri, a prescindere che accada o meno come parte della scena principale o attiva, è possibile utilizzare la notifica PARENTED. Ad esempio, ecco uno frammento di codice che collega un metodo di un nodo a un segnale personalizzato sul nodo padre senza errori. Utile per i nodi incentrati sui dati che si potrebbero creare in fase di esecuzione.
extends Node
var parent_cache
func connection_check():
return parent_cache.has_user_signal("interacted_with")
func _notification(what):
match what:
NOTIFICATION_PARENTED:
parent_cache = get_parent()
if connection_check():
parent_cache.interacted_with.connect(_on_parent_interacted_with)
NOTIFICATION_UNPARENTED:
if connection_check():
parent_cache.interacted_with.disconnect(_on_parent_interacted_with)
func _on_parent_interacted_with():
print("I'm reacting to my parent's interaction!")
using Godot;
public partial class MyNode : Node
{
private Node _parentCache;
public bool ConnectionCheck()
{
return _parentCache.HasUserSignal("InteractedWith");
}
public void _Notification(int what)
{
switch (what)
{
case NotificationParented:
_parentCache = GetParent();
if (ConnectionCheck())
{
_parentCache.Connect("InteractedWith", Callable.From(OnParentInteractedWith));
}
break;
case NotificationUnparented:
if (ConnectionCheck())
{
_parentCache.Disconnect("InteractedWith", Callable.From(OnParentInteractedWith));
}
break;
}
}
private void OnParentInteractedWith()
{
GD.Print("I'm reacting to my parent's interaction!");
}
}
using namespace godot;
class MyNode : public Node {
GDCLASS(MyNode, Node)
Node *parent_cache = nullptr;
void on_parent_interacted_with() {
UtilityFunctions::print("I'm reacting to my parent's interaction!");
}
public:
void connection_check() {
return parent_cache->has_user_signal("interacted_with");
}
void _notification(int p_what) {
switch (p_what) {
case NOTIFICATION_PARENTED:
parent_cache = get_parent();
if (connection_check()) {
parent_cache->connect("interacted_with", callable_mp(this, &MyNode::on_parent_interacted_with));
}
break;
case NOTIFICATION_UNPARENTED:
if (connection_check()) {
parent_cache->disconnect("interacted_with", callable_mp(this, &MyNode::on_parent_interacted_with));
}
break;
}
}
};