Nested Extensions
In this chapter you will learn the steps to develop Nested Extensions in Jenova Ecosystem.
Why Nested Extensions?
GDExtension enables developers to write high-performance C++ code that integrates with Godot without requiring engine recompilation. It allows the addition of new features, classes and resources while maintaining flexibility. Jenova Runtime is itself a GDExtension, but developing GDExtensions can be notoriously frustrating.
A standout feature of the Jenova Framework is its ability to let developers define new classes, resources and features directly within their C++ scripts—eliminating extra setup steps. On top of that, it supports Hot-Reloading, allowing instant updates without restarting the engine.
Nested Extensions (NE) work in both the editor and runtime, Allowing you to add Objects, Menus and Plugins to your workflow on-the-fly.
Using Nested Extensions
To start developing your Nested Extensions, You need to follow a simple routine, Everything else will be just same as GDExtension, Additonally you can use JenovaSDK features in Nested Extensions as well. Remember unlike C++ Scripts, You don't need to use Script Block and all signals must be overriden inside your extension class.
The Anatomy
Every Nested Extension requires three parts. Extension Class, Register Function and Unregister Function.
Here's the most basic Nested Extension Code :
// Godot SDK
#include <Godot/godot.hpp>
#include <Godot/classes/node.hpp>
// Jenova SDK
#include <JenovaSDK.h>
// Namespaces
using namespace godot;
using namespace jenova::sdk;
// Extension Class
class MyNode : public Node
{
GDCLASS(MyNode, Node);
protected:
static void _bind_methods() {};
}
// Register Function
void RegisterMyNode()
{
ClassDB::register_class<MyNode>();
sakura::FinishReload("MyNode");
}
// Unregister Function
void UnregisterMyNode()
{
sakura::PrepareReload("MyNode");
sakura::Dispose("MyNode");
}
Register and Unregister functions must be called from Boot Script. Module Events provide the only reliable Start/Shutdown process.
// Jenova SDK
#include <JenovaSDK.h>
// Import Register/Unregister Functions
extern void RegisterMyNode();
extern void UnregisterMyNode();
// Jenova Module Is Loading
JENOVA_EXPORT bool JenovaBoot()
{
RegisterMyNode();
return true;
}
// Jenova Module Is Unloading
JENOVA_EXPORT bool JenovaShutdown()
{
UnregisterMyNode();
return true;
}
⌬ Self Activation Macro ⌬ Version 0.3.8.0+
Nested Extensions can also be registered and unregistered without a boot script by usingJENOVA_ACTIVATOR
macro.
This feature is designed to simplify and accelerate development. However, if the order of module registration is important, it is recommended to use the boot script method. Note that both methods can be used simultaneously without causing any conflicts.
// Register Function
void RegisterMyNode()
{
ClassDB::register_class<MyNode>();
sakura::FinishReload("MyNode");
}
// Unregister Function
void UnregisterMyNode()
{
sakura::PrepareReload("MyNode");
sakura::Dispose("MyNode");
}
// Activator
JENOVA_ACTIVATOR(MyNode, RegisterMyNode, UnregisterMyNode)
📝 Note: Remember, If you need to use your nested extensions from another script, make sure to use a Header Script
The order of registration and unregistration nested extensions is important. If one Nested Extension depends on another in its structure, the second one must be registered first.
Usage Examples
That's it! Now that you know how to create Nested Extensions, let's go through some usage examples.
Nested Nodes
The following example demonstrates a simple NoiseNode3D applying movement to itself at runtime.
// Godot SDK
#include <Godot/godot.hpp>
#include <Godot/classes/time.hpp>
#include <Godot/classes/node.hpp>
#include <Godot/classes/node3d.hpp>
// Jenova SDK
#include <JenovaSDK.h>
// Namespaces
using namespace godot;
using namespace jenova::sdk;
// NoiseNode3D Implementation
class NoiseNode3D : public Node3D
{
GDCLASS(NoiseNode3D, Node3D);
private:
bool isEnabled = true;
bool ExecuteInEditor = true;
protected:
static void _bind_methods()
{
ClassDB::bind_method(D_METHOD("GetEnabledState"), &NoiseNode3D::GetEnabledState);
ClassDB::bind_method(D_METHOD("SetEnabledState", "value"), &NoiseNode3D::SetEnabledState);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "Enabled"), "SetEnabledState", "GetEnabledState");
ClassDB::bind_method(D_METHOD("GetEditorState"), &NoiseNode3D::GetEditorState);
ClassDB::bind_method(D_METHOD("SetEditorState", "value"), &NoiseNode3D::SetEditorState);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "Execute In Editor"), "SetEditorState", "GetEditorState");
}
public:
// Setters/Getters Implementation
bool GetEnabledState() const
{
return this->isEnabled;
}
void SetEnabledState(bool value)
{
this->isEnabled = value;
}
bool GetEditorState() const
{
return this->ExecuteInEditor;
}
void SetEditorState(bool value)
{
this->ExecuteInEditor = value;
}
public:
// Event Signals
void OnChildEnteredTree(const Node* node)
{
Output("Child (%s) Entered Tree", GetCStr(node->get_name()));
}
public:
// Event Methods
void _enter_tree() override
{
if (!this->is_connected("child_entered_tree", callable_mp(this, &NoiseNode3D::OnChildEnteredTree)))
{
this->connect("child_entered_tree", callable_mp(this, &NoiseNode3D::OnChildEnteredTree));
}
Output("Node (%s) Entered Tree.", GetCStr(this->get_name()));
}
void _ready() override
{
this->set_position(Vector3(0, 0, 0));
this->set_rotation(Vector3(0, 0, 0));
}
void _process(double p_delta) override
{
// Check for Editor
if (IsEditor() && !ExecuteInEditor) return;
// Check for Properties
if (!isEnabled) return;
// Get Time
int64_t time_msec = Time::get_singleton()->get_ticks_msec();
float time = static_cast<float>(time_msec) / 1000.0f;
// Generate junky movements
float junky_x = sin(time * 2.0f) * 0.8f;
float junky_y = cos(time * 1.5f) * 0.6f;
float junky_z = sin(time * 3.0f) * 0.7f;
this->set_position(Vector3(junky_x, junky_y, junky_z));
float junky_rot_x = sin(time * 1.0f) * 3.5f;
float junky_rot_y = cos(time * 0.5f) * 2.9f;
float junky_rot_z = sin(time * 0.7f) * 4.3f;
this->set_rotation(Vector3(junky_rot_x, junky_rot_y, junky_rot_z));
}
};
// Register/Unregister
void RegisterNoiseNode3D()
{
// Register Class
ClassDB::register_class<NoiseNode3D>();
// Finish Reload
sakura::FinishReload("NoiseNode3D");
}
void UnregisterNoiseNode3D()
{
// Prepare for Reload
sakura::PrepareReload("NoiseNode3D");
// Release Class
sakura::Dispose("NoiseNode3D");
}
After compiling, Head to scene tree and open the Create New Node dialog and you'll find NoiseNode3D
under the Node
section.
Add it to your scene and assign a MeshInstance3D
node to it. Enjoy your dancing custom node!
NoiseNode3D
applies rotational movement using noise values in the viewport.Nested Resources
The following example demonstrates how to create a new resource PlayerData storing basic player information.
// Godot SDK
#include <Godot/godot.hpp>
#include <Godot/classes/resource.hpp>
#include <Godot/classes/texture2d.hpp>
// Jenova SDK
#include <JenovaSDK.h>
// Namespaces
using namespace godot;
using namespace jenova::sdk;
// PlayerData Implementation
class PlayerData : public Resource
{
GDCLASS(PlayerData, Resource);
private:
// Resources
Ref<Texture2D> player_avatar;
// Configuration
String player_name;
Color player_name_color = Color(1.0f, 1.0f, 1.0f);
float player_score = 1.0f;
protected:
static void _bind_methods()
{
// Bind Resources
ClassDB::bind_method(D_METHOD("set_player_avatar", "texture"), &PlayerData::set_player_avatar);
ClassDB::bind_method(D_METHOD("get_player_avatar"), &PlayerData::get_player_avatar);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "player_avatar", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_player_avatar", "get_player_avatar");
// Bind Configuration
ClassDB::bind_method(D_METHOD("set_player_name", "name"), &PlayerData::set_player_name);
ClassDB::bind_method(D_METHOD("get_player_name"), &PlayerData::get_player_name);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "player_name"), "set_player_name", "get_player_name");
ClassDB::bind_method(D_METHOD("set_player_name_color", "color"), &PlayerData::set_player_name_color);
ClassDB::bind_method(D_METHOD("get_player_name_color"), &PlayerData::get_player_name_color);
ADD_PROPERTY(PropertyInfo(Variant::COLOR, "player_name_color"), "set_player_name_color", "get_player_name_color");
ClassDB::bind_method(D_METHOD("set_player_score", "value"), &PlayerData::set_player_score);
ClassDB::bind_method(D_METHOD("get_player_score"), &PlayerData::get_player_score);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "player_score", PROPERTY_HINT_RANGE, "0,1"), "set_player_score", "get_player_score");
}
public:
// Resource Setters/Getters
void set_player_avatar(const Ref<Texture2D> &texture) { player_avatar = texture; }
Ref<Texture2D> get_player_avatar() const { return player_avatar; }
// Configuration Setters/Getters
void set_player_name(const String &name) { player_name = name; }
String get_player_name() const { return player_name; }
void set_player_name_color(const Color &color) { player_name_color = color; }
Color get_player_name_color() const { return player_name_color; }
void set_player_score(float value) { player_score = CLAMP(value, 0.0f, 1.0f); }
float get_player_score() const { return player_score; }
};
// Register/Unregister
void RegisterPlayerData()
{
ClassDB::register_class<PlayerData>();
sakura::FinishReload("PlayerData");
}
void UnregisterPlayerData()
{
sakura::PrepareReload("PlayerData");
sakura::Dispose("PlayerData");
}
After compiling, create a new resource in the File System. You'll find PlayerData
under the Resource
section. Open it and modify the values.
PlayerData
stores values and integrates into the engine ecosystem.Additionally, if you want to use your nested resource in a Nested Node or another Nested Resource, you can define it in the following form :
// Bind Nested Resource
ClassDB::bind_method(D_METHOD("set_player_data", "material"), &OnlinePlayer::set_player_data);
ClassDB::bind_method(D_METHOD("get_player_data"), &OnlinePlayer::get_player_data);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "_playerdata", PROPERTY_HINT_RESOURCE_TYPE, "PlayerData"), "set_player_data", "get_player_data");
Nested Singletons
The following example demonstrates creation of a simple nested Singleton class that exposes various APIs to the engine. It can be accessed from GDScript, C# or any other third-party language supported by the engine.
// Godot SDK
#include <Godot/godot.hpp>
#include <Godot/classes/engine.hpp>
// Jenova SDK
#include <JenovaSDK.h>
// Namespaces
using namespace godot;
using namespace jenova::sdk;
// GameManager Implementation
class GameManager : public Object
{
GDCLASS(GameManager, Object);
protected:
static void _bind_methods()
{
ClassDB::bind_method(D_METHOD("create_new_player"), &GameManager::CreateNewPlayer);
}
public:
// Exposed Methods
void CreateNewPlayer()
{
print_line("Jenova C++ Nested Extension Method Executed.");
}
};
// Singleton Instance
GameManager* singleton_instance = nullptr;
// Register/Unregister
void RegisterGameManager()
{
// Register Class
ClassDB::register_class<GameManager>();
// Create a Global Singleton Instance
singleton_instance = memnew(GameManager);
// Register Singleton
Engine::get_singleton()->register_singleton("GameManager", singleton_instance);
}
void UnregisterGameManager()
{
// Unregister Singleton
Engine::get_singleton()->unregister_singleton("GameManager");
// Release Global Singleton Instance
if (singleton_instance) memdelete(singleton_instance);
// Release Class
sakura::Dispose("GameManager");
}
After compiling, create a new GDScript and assign it to a node. Open the script in the editor and you'll see that the class appears in the autocomplete suggestions. You can then directly use the GameManager.create_new_player
function within your script.
Nested Plugins
Nested Plugins allow dynamic creation of Editor Plugins for the engine editor. In the following example, we add icons to a Nested Node and Nested Resource. Additionally, We add a new Tool Menu that displays a "Hello World" message.
// Godot SDK
#include <Godot/godot.hpp>
#include <Godot/classes/editor_interface.hpp>
#include <Godot/classes/editor_plugin.hpp>
#include <Godot/classes/editor_plugin_registration.hpp>
#include <Godot/classes/texture2d.hpp>
#include <Godot/classes/resource_loader.hpp>
// Jenova SDK
#include <JenovaSDK.h>
// Namespaces
using namespace godot;
using namespace jenova::sdk;
// EditorTool Implementation
class EditorTool : public EditorPlugin
{
GDCLASS(EditorTool, EditorPlugin);
private:
Ref<Texture2D> nodeNoise3DIcon;
Ref<Texture2D> playerDataIcon;
protected:
static void _bind_methods() {}
public:
// Awake/Destroy
void OnAwake()
{
// Load Icons
nodeNoise3DIcon = ResourceLoader::get_singleton()->load("res://Assets/Icons/NoiseNode3D.svg");
playerDataIcon = ResourceLoader::get_singleton()->load("res://Assets/Icons/PlayerData.svg");
// Register Icons
SetClassIcon("NoiseNode3D", nodeNoise3DIcon);
SetClassIcon("PlayerData", playerDataIcon);
// Register Tool Menu
add_tool_menu_item("Say Hello World!", callable_mp(this, &EditorTool::OnMenuItemClick));
}
void OnDestroy()
{
// Dispose Icons
if (nodeNoise3DIcon.is_valid()) nodeNoise3DIcon.unref();
if (playerDataIcon.is_valid()) playerDataIcon.unref();
}
// Events
void OnMenuItemClick()
{
Alert("Hello World!");
}
};
// Register/Unregister
void RegisterEditorTool()
{
// We Only Register This Extension In Editor
if (GetEngineMode() == EngineMode::Editor)
{
// Register Class
ClassDB::register_internal_class<EditorTool>();
// Register Plugin
EditorPlugins::add_by_type<EditorTool>();
}
}
void UnregisterEditorTool()
{
if (ClassDB::class_exists("EditorTool"))
{
// Unregister Plugin
EditorPlugins::remove_by_type<EditorTool>();
// Release Class
sakura::Dispose("EditorTool");
}
}
After compiling, you will see your Nested Classes with their brand-new icons and there will be a new menu item in Project > Tools!
Congratulations! You've learned how to create Nested Extensions! 🎉