How Godot SceneTree Initialization and the Main Loop Work: A Deep Dive into the Engine Source
Godot's SceneTree initialization begins in Main::start() which creates a MainLoop instance (typically SceneTree) and registers it with the OS singleton; the platform-specific OS run-loop then drives the engine by calling physics_process() for fixed-step physics and process() for frame updates until the application quits.
The SceneTree serves as the central nervous system of every Godot application, managing the node hierarchy,viewport rendering, and the execution flow that powers both gameplay and editor functionality. Understanding how the godotengine/godot repository initializes this system and maintains its main loop is essential for engine contributors and advanced users building custom runtime behavior. This article examines the exact source code paths that bootstrap the SceneTree and the architectural patterns that govern the per-frame execution cycle.
The Startup Sequence: From Main to SceneTree
The engine boot process follows a strict three-phase initialization defined in main/main.cpp. This sequence establishes the foundation upon which the SceneTree is constructed and activated.
Entry Points in main/main.cpp
Execution begins with static methods in the Main class. First, Main::setup() parses command-line arguments and initializes low-level subsystems. Next, Main::setup2() completes resource loading and server initialization. Finally, Main::start() determines which MainLoop implementation to instantiate based on the execution context (editor, project manager, or standalone game).
During Main::start(), the engine evaluates whether to launch the editor or runtime:
// main/main.cpp (simplified)
MainLoop *main_loop = nullptr;
if (editor) { // --editor flag or project manager
main_loop = memnew(SceneTree); // default scene-tree implementation
}
...
OS::get_singleton()->set_main_loop(main_loop); // registers it with OS
The memnew(SceneTree) allocation creates the concrete MainLoop object, while OS::set_main_loop() stores this pointer within the OS singleton for later retrieval by the platform layer.
Registering with the OS Layer
The OS abstraction (core/os/os.h) acts as the bridge between platform-specific code and engine logic. When set_main_loop() receives the SceneTree pointer, it prepares the system to delegate lifecycle events to this object. This registration must complete before OS::run() begins, as the run-loop assumes a valid MainLoop instance exists.
The MainLoop Interface and OS Abstraction
Godot decouples the platform run-loop from game logic through the abstract MainLoop class defined in core/os/main_loop.h. This interface declares the virtual methods that every main loop implementation—including SceneTree—must override.
The MainLoop Virtual Interface
The MainLoop base class establishes four critical lifecycle hooks:
class MainLoop : public Object {
GDCLASS(MainLoop, Object);
protected:
static void _bind_methods();
GDVIRTUAL0(_initialize)
GDVIRTUAL1R(bool, _physics_process, double) // physics tick
GDVIRTUAL1R(bool, _process, double) // per-frame tick
GDVIRTUAL0(_finalize)
public:
virtual void initialize(); // called once at startup
virtual bool physics_process(double p_time); // fixed timestep
virtual bool process(double p_time); // variable frame time
virtual void finalize(); // called on shutdown
virtual ~MainLoop() {}
};
The boolean return values from physics_process() and process() serve as exit signals; returning true requests immediate engine termination.
The OS Run-Loop Implementation
Platform-specific OS classes (such as OS_Unix or OS_Windows) implement the run() method declared in core/os/os.h. This method contains the actual while-loop that drives the engine until _quit becomes true:
// core/os/os.cpp (conceptual)
int OS::run() {
while (!_quit) {
// handle OS events (window messages, input, etc.)
if (main_loop) {
// fixed-step physics
if (main_loop->physics_process(delta)) break;
// idle processing and rendering
if (main_loop->process(delta)) break;
}
}
return exit_code;
}
This architecture ensures that physics processing executes at a fixed timestep independent of frame rate, while process ticks occur as frequently as the display refresh allows.
SceneTree Implementation Details
SceneTree (scene/main/scene_tree.h and scene/main/scene_tree.cpp) inherits from MainLoop and provides the concrete implementation used by default in Godot projects. It extends the basic interface with node management, group processing, and signal emission.
Initialization Phase
When OS::run() first invokes the main loop, it calls SceneTree::initialize(). This method performs several critical setup operations:
- Configures the root Viewport and Window nodes
- Registers the tree with the
Enginesingleton - Establishes default input handling and rendering contexts
- Emits the
"tree_changed"signal to notify listeners that the hierarchy is ready
This initialization occurs exactly once per application lifetime, before any physics or rendering occurs.
Physics and Process Ticks
The SceneTree implements the dual-update strategy required for stable game simulation:
physics_process(double p_time) executes at a fixed physics tick rate (default 60 Hz). It processes:
- Physics server synchronization
- Timer and tween updates tied to physics
- Node groups registered for physics callbacks
- Emission of the
"physics_frame"signal
process(double p_time) runs once per rendered frame. It handles:
- Idle-time node group processing
- Input event propagation
- Rendering server updates and draw calls
- Emission of the
"process_frame"signal
Both methods return false by default to continue execution, or true to trigger immediate shutdown.
Frame Lifecycle Hooks
Between the physics and process phases, SceneTree utilizes iteration_prepare() and iteration_end() (declared in scene/main/scene_tree.h) to manage interpolation states and synchronization points. These hooks enable features like physics interpolation and ensure the rendering server receives consistent data during multi-threaded scenarios.
Practical Code Examples
Creating a Custom MainLoop in C++
You can bypass SceneTree entirely by implementing a custom MainLoop subclass:
class MyLoop : public MainLoop {
GDCLASS(MyLoop, MainLoop);
public:
void initialize() override {
print_line("MyLoop init");
}
bool physics_process(double delta) override {
// Fixed-step physics code here
return false; // return true to request quit
}
bool process(double delta) override {
// Per-frame rendering logic here
return false;
}
void finalize() override {
print_line("MyLoop cleanup");
}
};
// Registration:
OS::get_singleton()->set_main_loop(memnew(MyLoop));
Accessing the SceneTree in GDScript
From any node in the hierarchy, access the active SceneTree using:
var tree = get_tree() # Returns the current SceneTree
print(tree.get_frame()) # Current frame counter
tree.quit() # Request engine shutdown
Connecting to Engine Signals
Monitor structural changes and frame events:
func _ready():
get_tree().connect("tree_changed", self, "_on_tree_changed")
get_tree().connect("physics_frame", self, "_on_physics_frame")
func _on_tree_changed():
print("SceneTree structure updated")
func _on_physics_frame():
print("Physics tick occurred")
Summary
- Initialization Flow: Godot bootstraps through
Main::setup()→Main::setup2()→Main::start(), which instantiatesSceneTreeand registers it viaOS::set_main_loop()inmain/main.cpp. - Abstraction Layer: The
MainLoopinterface incore/os/main_loop.hdecouples platform code from game logic, declaringinitialize(),physics_process(),process(), andfinalize()hooks. - Execution Loop: Platform-specific OS implementations call the virtual methods in a tight loop, separating fixed-timestep physics from variable-time rendering.
- SceneTree Extensions:
scene/main/scene_tree.cppadds node management, group processing, and viewport rendering atop the base interface. - Customization: Developers can replace the default SceneTree with custom
MainLoopimplementations by subclassing and registering with the OS singleton.
Frequently Asked Questions
What is the difference between MainLoop and SceneTree?
MainLoop is an abstract interface defined in core/os/main_loop.h that specifies the minimum contract required to drive the engine—initialization, physics processing, frame processing, and finalization. SceneTree is a concrete implementation located in scene/main/scene_tree.h that inherits from MainLoop and adds node hierarchy management, viewport handling, group processing, and GDScript signal emission. While all Godot games use a MainLoop, most use the SceneTree specialization unless implementing a custom runtime that does not require nodes.
How do I access the SceneTree from a Node?
Call get_tree() from any class inheriting from Node. This method returns a pointer to the active SceneTree instance registered with the OS singleton. From GDScript, this provides access to methods like change_scene_to_file(), create_timer(), and quit(). In C++ module development, you can also retrieve it via SceneTree::get_singleton() after initialization completes.
Can I replace the default SceneTree with a custom MainLoop?
Yes. Create a C++ class inheriting from MainLoop, override the virtual methods (initialize(), physics_process(), process(), finalize()), and register it with OS::get_singleton()->set_main_loop() before OS::run() begins. This technique is used for specialized applications like headless servers, custom rendering engines, or non-game tools built on Godot's core infrastructure that do not require the node system.
How does the engine handle physics vs. process timing?
The physics_process() method executes at a fixed timestep defined by ProjectSettings.physics/common/physics_ticks_per_second (default 60 Hz), ensuring deterministic simulation regardless of frame rate. The process() method executes once per rendered frame, passing the actual delta time since the previous frame. The OS run-loop in core/os/os.cpp calls both methods sequentially each iteration, allowing SceneTree to update physics servers before processing input and rendering.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →