Refactor engine main loop to allow external stepping in Web

This commit is contained in:
Wojtek Figat
2026-02-16 22:32:02 +01:00
parent 0835a6559c
commit 2a9c6bbd1d
3 changed files with 134 additions and 79 deletions

View File

@@ -74,7 +74,7 @@ int32 Engine::ExitCode = 0;
Window* Engine::MainWindow = nullptr;
double EngineIdleTime = 0;
int32 Engine::Main(const Char* cmdLine)
int32 Engine::OnInit(const Char* cmdLine)
{
#if COMPILE_WITH_PROFILER
extern void InitProfilerMemory(const Char* cmdLine, int32 stage);
@@ -151,7 +151,7 @@ int32 Engine::Main(const Char* cmdLine)
#if LOG_ENABLE
Log::Logger::Dispose();
#endif
return 0;
return 1;
}
#endif
@@ -171,88 +171,96 @@ int32 Engine::Main(const Char* cmdLine)
EngineImpl::IsReady = true;
PROFILE_MEM_END();
return 0;
}
void Engine::OnLoop()
{
// Reduce CPU usage by introducing idle time if the engine is running very fast and has enough time to spend
const bool useSleep = !PLATFORM_WEB; // TODO: this should probably be a platform setting
if ((useSleep && Time::UpdateFPS > ZeroTolerance) || !Platform::GetHasFocus())
{
double nextTick = Time::GetNextTick();
double timeToTick = nextTick - Platform::GetTimeSeconds();
// Sleep less than needed, some platforms may sleep slightly more than requested
if (timeToTick > 0.002)
{
PROFILE_CPU_NAMED("Idle");
auto sleepStart = Platform::GetTimeSeconds();
Platform::Sleep(1);
auto sleepEnd = Platform::GetTimeSeconds();
EngineIdleTime += sleepEnd - sleepStart;
}
}
// App paused logic
if (Platform::GetIsPaused())
{
OnPause();
do
{
Platform::Sleep(10);
Platform::Tick();
} while (Platform::GetIsPaused() && !ShouldExit());
if (ShouldExit())
return;
OnUnpause();
}
// Use the same time for all ticks to improve synchronization
const double time = Platform::GetTimeSeconds();
// Update application (will gather data and other platform related events)
{
PROFILE_CPU_NAMED("Platform.Tick");
Platform::Tick();
#if COMPILE_WITH_PROFILER
extern void TickProfilerMemory();
TickProfilerMemory();
#endif
}
// Update game logic
if (Time::OnBeginUpdate(time))
{
OnUpdate();
OnLateUpdate();
Time::OnEndUpdate();
EngineIdleTime = 0;
}
// Start physics simulation
if (Time::OnBeginPhysics(time))
{
OnFixedUpdate();
OnLateFixedUpdate();
Time::OnEndPhysics();
}
// Draw frame
if (Time::OnBeginDraw(time))
{
OnDraw();
Time::OnEndDraw();
}
}
int32 Engine::Main(const Char* cmdLine)
{
// Initialize
ExitCode = OnInit(cmdLine);
if (ExitCode != 0)
return ExitCode;
// Main engine loop
const bool useSleep = true; // TODO: this should probably be a platform setting
while (!ShouldExit())
{
// Reduce CPU usage by introducing idle time if the engine is running very fast and has enough time to spend
if ((useSleep && Time::UpdateFPS > ZeroTolerance) || !Platform::GetHasFocus())
{
double nextTick = Time::GetNextTick();
double timeToTick = nextTick - Platform::GetTimeSeconds();
// Sleep less than needed, some platforms may sleep slightly more than requested
if (timeToTick > 0.002)
{
PROFILE_CPU_NAMED("Idle");
auto sleepStart = Platform::GetTimeSeconds();
Platform::Sleep(1);
auto sleepEnd = Platform::GetTimeSeconds();
EngineIdleTime += sleepEnd - sleepStart;
}
}
// App paused logic
if (Platform::GetIsPaused())
{
OnPause();
do
{
Platform::Sleep(10);
Platform::Tick();
} while (Platform::GetIsPaused() && !ShouldExit());
if (ShouldExit())
break;
OnUnpause();
}
// Use the same time for all ticks to improve synchronization
const double time = Platform::GetTimeSeconds();
// Update application (will gather data and other platform related events)
{
PROFILE_CPU_NAMED("Platform.Tick");
Platform::Tick();
#if COMPILE_WITH_PROFILER
extern void TickProfilerMemory();
TickProfilerMemory();
#endif
}
// Update game logic
if (Time::OnBeginUpdate(time))
{
OnUpdate();
OnLateUpdate();
Time::OnEndUpdate();
EngineIdleTime = 0;
}
// Start physics simulation
if (Time::OnBeginPhysics(time))
{
OnFixedUpdate();
OnLateFixedUpdate();
Time::OnEndPhysics();
}
// Draw frame
if (Time::OnBeginDraw(time))
{
OnDraw();
Time::OnEndDraw();
}
OnLoop();
}
// Call on exit event
// Shutdown
OnExit();
// Delete temporary directory only if Engine is closing normally (after crash user/developer can restore some data)
if (FileSystem::DirectoryExists(Globals::TemporaryFolder))
{
FileSystem::DeleteDirectory(Globals::TemporaryFolder);
}
return ExitCode;
}
@@ -556,6 +564,12 @@ void Engine::OnExit()
#endif
Platform::Exit();
// Delete temporary directory only if Engine is closing normally (after crash user/developer can restore some data)
if (ExitCode == 0 && FileSystem::DirectoryExists(Globals::TemporaryFolder))
{
FileSystem::DeleteDirectory(Globals::TemporaryFolder);
}
}
void EngineImpl::InitLog()

View File

@@ -14,6 +14,7 @@ class JsonAsset;
/// </summary>
API_CLASS(Static) class FLAXENGINE_API Engine
{
friend class PlatformMain;
DECLARE_SCRIPTING_TYPE_NO_SPAWN(Engine);
public:
@@ -215,6 +216,8 @@ public:
API_PROPERTY() static bool HasGameViewportFocus();
private:
static int32 OnInit(const Char* cmdLine);
static void OnLoop();
static void OnPause();
static void OnUnpause();
};

View File

@@ -3,10 +3,48 @@
#if PLATFORM_WEB
#include "Engine/Engine/Engine.h"
#include <emscripten/emscripten.h>
class PlatformMain
{
int32 State = 0;
static void Loop()
{
// Tick engine
Engine::OnLoop();
if (Engine::ShouldExit())
{
// Exit engine
Engine::OnExit();
emscripten_cancel_main_loop();
emscripten_force_exit(Engine::ExitCode);
return;
}
}
public:
static int32 Main()
{
// Initialize engine
int32 result = Engine::OnInit(TEXT(""));
if (result != 0)
return result;
// Setup main loop to be called by Emscripten
emscripten_set_main_loop(Loop, -1, false);
// Run the first loop
Loop();
return 0;
}
};
int main()
{
return Engine::Main(TEXT(""));
return PlatformMain::Main();
}
#endif