Hot-Reloading
In this chapter you will learn about Jenova Framework Hot-Reloading System.
What is Hot-Reloading?
C++ is a pre-compiled language, meaning it runs from pre-built bytecode. When you build your C++ code, it is compiled into machine code and the linker packs it into a module. Once a module is loaded, it cannot be modified unless the process it's running in is closed. This means that any changes to C++ code require a rebuild while the application is not running, followed by a restart.
On the other hand, scripting languages allow for quicker iteration, letting you apply changes and immediately see their effects without restarting the game engine. This makes development more efficient but also highlights a bottleneck when using C/C++ for game development.
Hot-reloading allows developers to modify code without restarting the application. In Jenova Framework, you can Hot-Reload your C++ code just like a scripting language thanks to the powerful hot-reloading system called Sakura.
Jenova Hot-Reloading is supported on all Godot forks. However, Hot-Reloading Nested Extensions requires a compatible distribution. Currently, Jenova Hot-Reloading system with Nested Extensions is supported only on the following engine distributions :
Sakura System
Sakura Hot-Reloading System (SHS) allows you to edit C++ scripts and see changes on-the-fly. This powerful system brings real scripting flexibility to your pipeline, accelerating development workflows. Sakura offers hot-reloading for both Active and Passive C++ Scripts across all engine distributions. However, hot-reloading Nested Extensions requires a compatible build.
Sakura provides multiple hot-reloading methods :
- Script Change Trigger Mode : In this mode, every change to C++ scripts from any external or internal editor triggers a system-wide build and hot-reload. This mode is the best choice for Visual Studio Code and Neovim workflow.
- Script Change Reload Mode : Similar to the first mode, but only gets triggered when the engine issues a reload on script files.
- Watchdog Invoke/Bootstrap Mode : Designed for custom build systems such as Visual Studio IDE. In this mode, scripts are compiled outside the Jenova build system. After a successful compilation, the compiled binary is parsed and only a hot-reload is issued.
📖 Learn about External Changes Trigger Modes
Enabling Hot-Reload
By default, Hot-Reloading is disabled. To enable it, navigate to Editor > Editor Settings... > Jenova
In Jenova settings, change External Changes Trigger Mode to one of the hot-reloading methods explained earlier.
Additionally, to enable hot-reloading at runtime activate the Use Hot-Reload at Runtime setting.
📝 Note : If you're planning to use Visual Studio for builds, set the trigger mode to Bootstrap Project on Watchdog Invoke.
Using Hot-Reload
To use the hot-reloading feature in C++ scripts, simply run your game in debug mode using the Play Button, make changes to the scripts and save them. You will see the updates instantly. To use hot-reloading in Nested Extensions, you need to use Sakura functions from JenovaSDK.
This topic is covered in the Nested Extensions chapter.
- If you're developing Nested Extensions that only work at runtime without hot-reloading, avoid using Sakura functions.
- If you're working on Nested Resource Extensions assigned to a Nested Node Extension, close scenes, build, and reopen them.
Global Memory/Variables
While using the Sakura Hot-Reload System in C++ Scripts, all allocated objects and variables defined in C++ scripts will be lost. To solve this issue, JenovaSDK provides a persistent cross-reload storage system that allows allocation of global pointers, memory regions, and variables.
Using global memory/variables is possible through the following JenovaSDK functions :
// Global Memory API
void* GetGlobalPointer(MemoryID id);
void* SetGlobalPointer(MemoryID id, void* ptr);
void DeleteGlobalPointer(MemoryID id);
void* AllocateGlobalMemory(MemoryID id, size_t size);
void FreeGlobalMemory(MemoryID id);
template <typename T> T* GlobalPointer(MemoryID id);
template <typename T> T GlobalGet(MemoryID id);
template <typename T> void GlobalSet(MemoryID id, const T& newValue);
// Global Variables API
Variant GetGlobalVariable(VariableID id);
void SetGlobalVariable(VariableID id, Variant var);
template <typename T> T GlobalVariable(VariableID id);
Cross-Reload API
- Global Memory storage uses a
MemoryID
unique identifier to manage global pointers and memory regions. - Global Variable storage uses a
VariableID
unique identifier to store global variables. - Global Variables only support standard engine types compatible with
Variant
. - Functions
GetGlobalPointer
,SetGlobalPointer
, andDeleteGlobalPointer
allow storing, retrieving, and deleting object pointers in global storage. When usingSetGlobalPointer
, the previous stored pointer is returned, allowing it to be freed. - Functions
AllocateGlobalMemory
andFreeGlobalMemory
provide global memory allocation and deallocation for raw data. - Template functions
GlobalPointer
,GlobalGet
, andGlobalSet
simplify memory access.GlobalPointer
retrieves a global pointer and casts it to a specified type.GlobalGet
andGlobalSet
directly access and modify objects by dereferencing global pointers. - Functions
GetGlobalVariable
andSetGlobalVariable
store and retrieve aVariant
in global storage. - Template function
GlobalVariable
enables quick access to a global variable by automatically casting it to a specified type.
Using Global Storage
Here's an example demonstrating usage of the Global Storage feature :
// Define Static Global Storage IDs
constexpr const char* playerHandleID = "player-handle";
constexpr const char* isControllerActiveID = "is-controller-active";
// Global Pointers
lobby::PlayerHandle* playerHandle = nullptr;
// Script Block
JENOVA_SCRIPT_BEGIN
// Node Events
void OnAwake()
{
// Create/Allocate Global Player Handle
playerHandle = GlobalPointer<lobby::PlayerHandle>(playerHandleID);
if (playerHandle == nullptr)
{
playerHandle = CreateNewPlayer();
SetGlobalPointer(playerHandleID, playerHandle);
}
// Create Global Variables
SetGlobalVariable(isControllerActiveID, false);
}
void OnDestroy(Caller* instance)
{
// Release Global Player Handle
if (playerHandle)
{
if (!playerHandle->Release()) Alert("Error : Failed to Release Player Handle.");
delete playerHandle;
DeleteGlobalPointer(playerHandle);
}
}
void OnReady()
{
// Retrive Global Player Handle
playerHandle = GlobalPointer<lobby::PlayerHandle>(playerHandleID);
// Initialize Player
if (playerHandle->Initialize(lobby::GetPlayeName()))
{
SetGlobalVariable(isControllerActiveID, true);
};
}
void OnProcess(double delta)
{
// Handle Player Data If It's Valid
if (GlobalVariable<bool>(isControllerActiveID))
{
...
}
}
JENOVA_SCRIPT_END