Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Moduli personalizzati in C++

Moduli

Godot consente di estendere il motore in modo modulare. È possibile creare nuovi moduli e poi abilitarli/disabilitarli. Ciò consente di aggiungere nuove funzionalità al motore a ogni livello senza modificarne il core, che può essere suddiviso per l'utilizzo e il riutilizzo in moduli diversi.

I moduli si trovano nella sottocartella modules/ del sistema di build. Come predefinito, sono abilitati decine di moduli, come GDScript (che, sì, non fa parte del motore base), il supporto di GridMap, un modulo per le espressioni regolari e altri. È possibile creare e combinare tutti i nuovi moduli desiderati. Il sistema di build di SCons se ne occuperà in modo trasparente.

What for?

Sebbene sia raccomandato scrivere la maggior parte di un gioco in tramite script (perché fa risparmiare un sacco di tempo), è perfettamente possibile usare C++. Aggiungere moduli C++ può essere utile nei seguenti scenari:

  • Associare una libreria esterna a Godot (come PhysX, FMOD, ecc.).

  • Ottimizza le parti critiche di un gioco.

  • Aggiungere nuove funzionalità al motore e/o all'editor.

  • Convertire un gioco esistente in Godot.

  • Scrivere un nuovo gioco interamente in C++ perché non puoi vivere senza C++.

Nota

Sebbene sia possibile utilizzare moduli per la logica di gioco personalizzata, GDExtension è generalmente più adatto in quanto non richiede di ricompilare il motore dopo ogni modifica al codice.

I moduli C++ sono necessari principalmente quando GDExtension non è sufficiente ed è richiesta una integrazione più profonda nel motore.

Creare un nuovo modulo

Prima di creare un modulo, assicurarsi di scaricare il codice sorgente di Godot e compilarlo.

To create a new module, the first step is creating a directory inside modules/. If you want to maintain the module separately, you can checkout a different VCS into modules and use it.

Il modulo di esempio si chiamerà "summator" (godot/modules/summator). Al suo interno creeremo una classe sommator:

godot/modules/summator/summator.h
#pragma once

#include "core/object/ref_counted.h"

class Summator : public RefCounted {
    GDCLASS(Summator, RefCounted);

    int count;

protected:
    static void _bind_methods();

public:
    void add(int p_value);
    void reset();
    int get_total() const;

    Summator();
};

E poi il file cpp.

godot/modules/summator/summator.cpp
#include "summator.h"

void Summator::add(int p_value) {
    count += p_value;
}

void Summator::reset() {
    count = 0;
}

int Summator::get_total() const {
    return count;
}

void Summator::_bind_methods() {
    ClassDB::bind_method(D_METHOD("add", "value"), &Summator::add);
    ClassDB::bind_method(D_METHOD("reset"), &Summator::reset);
    ClassDB::bind_method(D_METHOD("get_total"), &Summator::get_total);
}

Summator::Summator() {
    count = 0;
}

La nuova classe si deve registrare in qualche modo, quindi è necessario creare altri due file:

register_types.h
register_types.cpp

Importante

Questi file devono trovarsi nella cartella di livello superiore del modulo (accanto ai file SCsub e config.py) affinché il modulo sia registrato correttamente.

Questi file dovrebbero contenere quanto segue:

godot/modules/summator/register_types.h
#include "modules/register_module_types.h"

void initialize_summator_module(ModuleInitializationLevel p_level);
void uninitialize_summator_module(ModuleInitializationLevel p_level);
/* yes, the word in the middle must be the same as the module folder name */
godot/modules/summator/register_types.cpp
#include "register_types.h"

#include "core/object/class_db.h"
#include "summator.h"

void initialize_summator_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }
    ClassDB::register_class<Summator>();
}

void uninitialize_summator_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }
   // Nothing to do here in this example.
}

Successivamente, dobbiamo creare un file SCsub affinché il sistema di compilazione compili questo modulo:

godot/modules/summator/SCsub
# SCsub

Import('env')

env.add_source_files(env.modules_sources, "*.cpp") # Add all cpp files to the build

Con più sorgenti, è anche possibile aggiungere ogni file singolarmente a un elenco di stringhe di Python:

src_list = ["summator.cpp", "other.cpp", "etc.cpp"]
env.add_source_files(env.modules_sources, src_list)

Ciò offre potenti possibilità, utilizzando Python per costruire l'elenco dei file tramite cicli e istruzioni logiche. Si vedano alcuni moduli forniti di norma con Godot per alcuni esempi.

Per aggiungere cartelle di inclusione che il compilatore può esaminare, è possibile aggiungerle ai percorsi dell'ambiente:

env.Append(CPPPATH=["mylib/include"]) # this is a relative path
env.Append(CPPPATH=["#myotherlib/include"]) # this is an 'absolute' path

Se si desidera aggiungere flag personalizzati al compilatore durante la compilazione del modulo, è necessario prima clonare env, in maniera che non aggiunga quei flag all'intera compilazione di Godot (il che potrebbe causare errori). SCsub di esempio con flag personalizzati:

godot/modules/summator/SCsub
Import('env')

module_env = env.Clone()
module_env.add_source_files(env.modules_sources, "*.cpp")
# Append CCFLAGS flags for both C and C++ code.
module_env.Append(CCFLAGS=['-O2'])
# If you need to, you can:
# - Append CFLAGS for C code only.
# - Append CXXFLAGS for C++ code only.

E infine, il file di configurazione per il modulo, il quale è uno script Python che deve avere il nome config.py:

godot/modules/summator/config.py
# config.py

def can_build(env, platform):
    return True

def configure(env):
    pass

Al modulo viene chiesto se è possibile compilarlo per la piattaforma specifica (in questo caso, True significa che verrà compilato per ogni piattaforma).

E questo è tutto. Speriamo non sia stato troppo complesso! Il modulo finale dovrebbe apparire così:

godot/modules/summator/config.py
godot/modules/summator/summator.h
godot/modules/summator/summator.cpp
godot/modules/summator/register_types.h
godot/modules/summator/register_types.cpp
godot/modules/summator/SCsub

È possibile quindi comprimerlo e condividerlo con tutti gli altri. Quando si compila per ogni piattaforma (istruzioni nelle sezioni precedenti), il proprio modulo sarà incluso.

Utilizzare il modulo

Ora è possibile utilizzare il modulo appena creato da qualsiasi script:

var s = Summator.new()
s.add(10)
s.add(20)
s.add(30)
print(s.get_total())
s.reset()

Il risultato sarà 60.

Vedi anche

L'esempio precedente di Summator è ottimo per piccoli moduli personalizzati, ma cosa fare se si desidera utilizzare una libreria esterna più grande? Consultare Binding to external libraries per i dettagli sulle associazioni alle librerie esterne.

Avvertimento

Se si prevede che il modulo sia accessibile dal progetto in esecuzione (non solo dall'editor), è necessario ricompilare anche ogni modello di esportazione che si intende utilizzare, poi specificare il percorso del modello personalizzato in ogni preimpostazione di esportazione. Altrimenti, si verificheranno errori durante l'esecuzione del progetto, poiché il modulo non è compilato nel modello di esportazione. Consultare le pagine Compiling per ulteriori informazioni.

Compilare un modulo esternamente

Compilare un modulo implica spostare i sorgenti del modulo direttamente nella cartella modules/ del motore. Sebbene ciò sia il modo più semplice per compilare un modulo, ci sono un paio di ragioni per cui potrebbe non essere pratico:

  1. È necessario copiare manualmente i sorgenti dei moduli ogni volta che si desidera compilare il motore, con o senza il modulo, oppure effettuare passaggi in più per disabilitare manualmente un modulo durante la compilazione con un'opzione di compilazione simile a module_summator_enabled=no. La creazione di collegamenti simbolici potrebbe anche essere una soluzione, ma potrebbe essere necessario superare le restrizioni del sistema operativo, come aver bisogno del privilegio dei collegamenti simbolici se si effettua il tutto tramite uno script.

  2. Depending on whether you have to work with the engine's source code, the module files added directly to modules/ changes the working tree to the point where using a VCS (like git) proves to be cumbersome as you need to make sure that only the engine-related code is committed by filtering changes.

Quindi, se si ritiene che la struttura indipendente dei moduli personalizzati sia necessaria, prendiamo il nostro modulo "summator" e spostiamolo nella cartella padre del motore:

mkdir ../modules
mv modules/summator ../modules

Compilare il motore con il nostro modulo fornendo l'opzione di compilazione custom_modules che accetta un elenco separato da virgole di percorsi di cartelle contenenti moduli C++ personalizzati, simile al seguente:

scons custom_modules=../modules

Il sistema di compilazione rileverà tutti i moduli nella cartella ../modules e li compilerà di conseguenza, incluso il nostro modulo "summator".

Avvertimento

Qualsiasi percorso passato a custom_modules verrà convertito internamente in un percorso assoluto per distinguere tra moduli personalizzati e predefiniti. Ciò significa che operazioni come la generazione della documentazione dei moduli potrebbero basarsi su una struttura di percorsi specifica sul computer.

Personalizzare l'inizializzazione dei tipi di modulo

Modules can interact with other built-in engine classes during runtime and even affect the way core types are initialized. So far, we've been using register_summator_types as a way to bring in module classes to be available within the engine.

A crude order of the engine setup can be summarized as a list of the following type registration methods:

preregister_module_types();
preregister_server_types();
register_core_singletons();
register_server_types();
register_scene_types();
EditorNode::register_editor_types();
register_platform_apis();
register_module_types();
initialize_physics();
initialize_navigation_server();
register_server_singletons();
register_driver_types();
ScriptServer::init_languages();

La nostra classe Summator viene inizializzata durante la chiamata di register_module_types(). Immaginiamo di dover soddisfare una dipendenza comune all'esecuzione del modulo (come i singleton) o di dover sovrascrivere i callback dei metodi del motore esistenti prima che vengano assegnati dal motore stesso. In tal caso, vogliamo assicurarci che le classi del nostro modulo vengano registrate prima di qualsiasi altro tipo predefinito.

Qui possiamo definire un metodo preregister_summator_types() facoltativo che verrà chiamato prima di qualsiasi altra cosa durante la fase di configurazione preregister_module_types() del motore.

Ora dobbiamo aggiungere questo metodo ai file di intestazione e di sorgente register_types:

godot/modules/summator/register_types.h
#define MODULE_SUMMATOR_HAS_PREREGISTER
void preregister_summator_types();

void register_summator_types();
void unregister_summator_types();

Nota

A differenza di altri metodi di registrazione, dobbiamo definire esplicitamente MODULE_SUMMATOR_HAS_PREREGISTER per far sapere al sistema di compilazione quali chiamate di metodo rilevanti includere in fase di compilazione. È necessario anche convertire il nome del modulo in maiuscolo.

godot/modules/summator/register_types.cpp
#include "register_types.h"

#include "core/object/class_db.h"
#include "summator.h"

void preregister_summator_types() {
    // Called before any other core types are registered.
    // Nothing to do here in this example.
}

void register_summator_types() {
    ClassDB::register_class<Summator>();
}

void unregister_summator_types() {
   // Nothing to do here in this example.
}

Scrivere documentazione personalizzata

Scrivere la documentazione può sembrare un compito noioso, ma è altamente consigliato documentare il modulo appena creato per renderlo più facile per gli utenti. Senza contare che il codice scritto un anno fa potrebbe diventare indistinguibile da quello scritto da qualcun altro, quindi sia gentile con te stesso del futuro!

Per configurare documentazione personalizzata per il modulo sono necessari diversi passaggi:

  1. Crea una nuova cartella nella radice del modulo. Il nome della cartella può essere qualsiasi cosa, ma in questa sezione useremo il nome doc_classes.

  2. Ora dobbiamo modificare config.py e aggiungere il seguente codice:

    def get_doc_path():
        return "doc_classes"
    
    def get_doc_classes():
        return [
            "Summator",
        ]
    

La funzione get_doc_path() è utilizzata dal sistema di compilazione per determinare la posizione della documentazione. In questo caso, si troverà nella cartella modules/summator/doc_classes. Se non si definisce questa funzione, il percorso della documentazione per il modulo verrà automaticamente considerato la cartella principale doc/classes.

Il metodo get_doc_classes() è necessario affinché il sistema di compilazione sappia quali classi registrate appartengono al modulo. È necessario elencare tutte le classi qui. Le classi che non sono elencate finiranno nella cartella principale doc/classes.

Suggerimento

Si può usare Git per verificare se sono state saltate alcune classi controllando i file non tracciati con git status. Ad esempio:

git status

Esempio di output:

Untracked files:
    (use "git add <file>..." to include in what will be committed)

    doc/classes/MyClass2D.xml
    doc/classes/MyClass4D.xml
    doc/classes/MyClass5D.xml
    doc/classes/MyClass6D.xml
    ...
  1. Ora possiamo generare la documentazione:

Possiamo farlo eseguendo il doctool di Godot, ovvero godot --doctool <path>, che scaricherà il riferimento dell'API del motore nel <path> specificato in formato XML.

Nel nostro caso, lo punteremo alla radice del repository clonato. È possibile anche puntarlo a un'altra cartella e copiare solo i file che servono.

Eseguire il comando:

bin/<godot_binary> --doctool .

Ora se si va alla cartella godot/modules/summator/doc_classes, si noterà che contiene un file Summator.xml, o qualsiasi altra classe, a cui è stato fatto riferimento nella funzione get_doc_classes.

Modificare i file seguendo il class reference primer e ricompilare il motore.

Una volta completato il processo di compilazione, la documentazione sarà accessibile all'interno del sistema di documentazione integrato nel motore.

Per mantenere aggiornata la documentazione, d'ora in poi basterà solo modificare uno dei file XML e ricompilare il motore.

Se si cambia l'API del modulo, si può anche estrare nuovamente la documentazione: conterrà gli elementi aggiunti in precedenza. Naturalmente, se si punta alla cartella di Godot, assicurarsi di non perdere lavoro, estraendo la documentazione precedente da una build del motore precedente sopra quella più recente.

Si noti che se non si hanno diritti di accesso in scrittura al percorso <path> fornito, si potrebbe riscontrare un errore simile al seguente:

ERROR: Can't write doc file: docs/doc/classes/@GDScript.xml
   At: editor/doc/doc_data.cpp:956

Scrivere test unitari personalizzati

È possibile scrivere test unitari autonomi come parte di un modulo C++. Se non si ha ancora familiarità con il processo di test unitario in Godot, consultate Test unitari.

La procedura è la seguente:

  1. Creare una nuova cartella con il nome tests/ nella radice del modulo:

cd modules/summator
mkdir tests
cd tests
  1. Create a new test suite: test_summator.h. The header must be prefixed with test_ so that the build system can collect it and include it as part of the tests/test_main.cpp where the tests are run.

  2. Scrivere alcuni casi di test. Ecco un esempio:

godot/modules/summator/tests/test_summator.h
#pragma once

#include "tests/test_macros.h"

#include "modules/summator/summator.h"

namespace TestSummator {

TEST_CASE("[Modules][Summator] Adding numbers") {
    Ref<Summator> s = memnew(Summator);
    CHECK(s->get_total() == 0);

    s->add(10);
    CHECK(s->get_total() == 10);

    s->add(20);
    CHECK(s->get_total() == 30);

    s->add(30);
    CHECK(s->get_total() == 60);

    s->reset();
    CHECK(s->get_total() == 0);
}

} // namespace TestSummator
  1. Compilare il motore con scons tests=yes ed eseguire i test con il seguente comando:

./bin/<godot_binary> --test --source-file="*test_summator*" --success

You should see the passing assertions now.

Aggiunta di icone personalizzato nell'editor

Similmente a come si può scrivere una documentazione autonoma all'interno di un modulo, è possibile anche creare le proprie icone personalizzate da mostrare per le classi nell'editor.

Per il processo effettivo creare le icone dell'editor da integrare nel motore, fare riferimento prima a Icone dell'editor.

Dopo aver creato le icone, procedere con i seguenti passaggi:

  1. Creare una nuova cartella nella radice del modulo con il nome icons. Questo è il percorso predefinito in cui il motore cerca le icone del modulo per l'editor.

  2. Spostare le icone svg appena create (ottimizzate o meno) in tale cartella.

  3. Ricompilare il motore ed eseguire l'editor. Ora le icone appariranno nell'interfaccia dell'editor dove appropriato.

Se si desidera memorizzare le proprie icone da qualche altra parte all'interno del modulo, aggiungere il seguente frammento di codice a config.py per sovrascrivere il percorso predefinito:

def get_icons_path():
    return "path/to/icons"

Riassumendo

Ricordati di:

  • Utilizzare la macro GDCLASS per l'ereditarietà, affinché Godot possa incapsularla.

  • Utilizzare _bind_methods per associare le funzioni allo scripting e consentire loro di funzionare come callback per i segnali.

  • Evitare l'ereditarietà multipla per le classi esposte a Godot, poiché GDCLASS non la supporta. È comunque possibile utilizzare l'ereditarietà multipla nelle proprie classi, purché non siano esposte all'API di scripting di Godot.

Ma non è tutto: a seconda di cosa si farà, si incontreranno delle sorprese (si spera positive).

  • Se si eredita da Node (o da qualsiasi tipo di nodo derivato, come Sprite2D), la nuova classe apparirà nell'editor, nell'albero di ereditarietà nella finestra di dialogo "Aggiungi un nodo".

  • Se si eredita da Resource, questa apparirà nell'elenco delle risorse e tutte le proprietà esposte potranno essere serializzate quando si salvano/caricano.

  • Con la stessa logica è possibile estendere l'Editor e quasi tutte le aree del motore.