#include "game.hpp" #include "asset-manager.hpp" #include "audio.hpp" #include "camera.hpp" #include "components.hpp" #include "ecs.hpp" #include "entities.hpp" #include "font.hpp" #include "game-settings.hpp" #include "game-type-defs.hpp" #include "imgui.h" #include "level-types.hpp" #include "level.hpp" #include "physics.hpp" #include "player-input.hpp" #include "plugins.hpp" #include "project.hpp" #include "scene.hpp" #include "serialization.hpp" #include "static-ui.hpp" #include "thread-pool.hpp" #include "window.hpp" #include "pk.h" #include #include #include #include #include #include const char *levelName = "demo-level"; const int64_t consoleBufferCount = 30; const int64_t consoleLineLength = 128; char consoleBuffer[consoleBufferCount][consoleLineLength]; int64_t consoleBufferIndex = 0; void Game_RecordImGui() { static bool scrollToBottom = true; if (!ImGui::Begin("Console", &pkeSettings.editorSettings.isShowingConsole)) { ImGui::End(); return; } ImVec2 region = ImGui::GetContentRegionAvail(); region.y -= 27; if (ImGui::BeginListBox("##ConsoleHistory", region)) { for (int64_t i = consoleBufferIndex + 1; i < consoleBufferCount; ++i) { ImGui::Text("%s", consoleBuffer[i]); } for (int64_t i = 0; i < consoleBufferIndex; ++i) { ImGui::Text("%s", consoleBuffer[i]); } if (scrollToBottom) ImGui::SetScrollHereY(1); scrollToBottom = false; ImGui::EndListBox(); } ImGui::Separator(); if (ImGui::InputText("##ConsoleInput", consoleBuffer[consoleBufferIndex], consoleLineLength, ImGuiInputTextFlags_EnterReturnsTrue)) { // TODO parse and execute. scrollToBottom = true; consoleBufferIndex = (consoleBufferIndex + 1) % consoleBufferCount; memset(consoleBuffer[consoleBufferIndex], '\0', consoleLineLength); } auto focusedFlags = (ImGuiFocusedFlags_ChildWindows); if (ImGui::IsWindowFocused(focusedFlags) && !ImGui::IsAnyItemFocused() && !ImGui::IsAnyItemHovered() && !ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGui::IsMouseClicked(ImGuiMouseButton_Left, true)) { ImGui::SetKeyboardFocusHere(-1); } ImGui::End(); } void Game_Tick(double delta) { pk_mem_bucket_reset(pkeSettings.mem_bkt.game_transient); if (pkeSettings.rt.nextLevel != nullptr) { // TODO async this for (uint32_t i = 0; i < pkeSettings.rt.nextLevel->scene_instances.next; ++i) { // TODO can't instantiate a scene that hasn't been loaded yet /* struct pke_scene *scene = pke_scene_get_by_handle(lvl->scene_instances[i].scene_handle); if (scene != nullptr) { Game_LoadSceneFile(scene->name); } */ } pkeSettings.rt.previousLevel = pkeSettings.rt.activeLevel; pkeSettings.rt.activeLevel = pkeSettings.rt.nextLevel; pkeSettings.rt.nextLevel = nullptr; if (pkeSettings.rt.activeLevel->pke_cb_spinup.func != nullptr) { pkeSettings.rt.activeLevel->pke_cb_spinup.func(); } } if (pkeSettings.rt.previousLevel != nullptr) { if (pkeSettings.rt.previousLevel->pke_cb_teardown.func != nullptr) { pkeSettings.rt.previousLevel->pke_cb_teardown.func(); } pke_level_teardown(pkeSettings.rt.previousLevel); pkeSettings.rt.previousLevel = nullptr; } /* * ECS_Tick_Early() gets called first for GC */ ECS_Tick_Early(delta); pke_input_tick(delta); EntityType_Tick(delta); pke_level_tick(delta); pke_ui_tick(delta); pke_scene_tick(delta); PkeCamera_Tick(delta); for (long i = 0; i < LoadedPkePlugins.next; ++i) { if (LoadedPkePlugins[i].OnTick != nullptr) { LoadedPkePlugins[i].OnTick(delta); } } if (pkeSettings.rt.activeLevel->pke_cb_tick.func != nullptr) { reinterpret_cast(pkeSettings.rt.activeLevel->pke_cb_tick.func)(delta); } FontType_Tick(delta); ECS_Tick(delta); pke_audio_tick(delta); EntityType_Tick_Late(delta); ECS_Tick_Late(delta); window_tick_late(delta); } void Game_Main(PKEWindowProperties windowProps, const char *executablePath) { pkeSettings.executablePath = executablePath; fprintf(stdout, "Game_Main Entering\n"); try { Game_Init(); pk_ev_init(pkeSettings.mem_bkt.game); PkeThreads_Init(); AM_Init(); ECS_Init(); Physics_Init(); pke_audio_init(); PkeCamera_Init(); pke_level_init(); pke_scene_master_init(); CreateWindow(windowProps); EntityType_Init(); pke_input_init(); FontType_Init(); pke_ui_init(); pke_ui_init_bindings(); PkeProject_Load(pkeSettings.args.projectPath); if (pkeSettings.args.pluginPath != nullptr) { PkePlugin_Load(pkeSettings.args.pluginPath); } for (long i = 0; i < LoadedPkePlugins.next; ++i) { if (LoadedPkePlugins[i].OnInit != nullptr) { LoadedPkePlugins[i].OnInit(); } } // // at this point, everything is loaded or initialized. // // if we were passed only a scene name, create a faux level. if (!pkeSettings.args.levelName) { // TODO uuids pke_level *lvl = pke_level_create("faux-level", pk_uuid_zed, pk_uuid_zed); fprintf(stdout, "[Game_Main] Creating faux level.\n"); pkeSettings.rt.activeLevel = lvl; } if (pkeSettings.args.levelName == nullptr && pkeSettings.args.sceneName != nullptr) { scene_instance si{}; pke_scene *scene; std::ifstream f(pkeSettings.args.sceneName); if (!f.is_open()) { fprintf(stdout, "[Game_Main] Did not find scene by name specified in arg: '%s'\n", pkeSettings.args.sceneName); goto GAME_SHUTDOWN; } fprintf(stdout, "[Game_Main] loading scene from arg (expecting path): %s\n", pkeSettings.args.sceneName); srlztn_deserialize_helper *h = pke_deserialize_init(pkeSettings.rt.activeLevel, pkeSettings.mem_bkt.game_transient); // 2025-09-09 JCB Scenes no longer contain anything so I'm not sure there's a reason to create one here. // spit-balling here, maybe "scene" files should be assets and not much more. scene = pke_scene_create(pkeSettings.args.sceneName); pke_deserialize_scene_from_stream(f, h); pke_deserialize_scene(h); pke_deserialize_teardown(h); si.scene_handle = scene->scene_handle; pk_arr_append_t(&pkeSettings.rt.activeLevel->scene_instances, si); } GameTimePoint lastTimePoint = pkeSettings.steadyClock.now(); double deltaTillNextRender = pkeSettings.deltaPerFrame; GameTimePoint lastLogTimePoint = pkeSettings.steadyClock.now(); int64_t tickCount = 0; int64_t renderCount = 0; int64_t nsAhead = 0.0; while (pkeSettings.isGameRunning) { glfwPollEvents(); int64_t nsAheadHolder = 0.0; if (nsAhead > 0) { nsAheadHolder = nsAhead; std::this_thread::sleep_for(GameTimeDuration(nsAhead)); nsAhead = 0; } if (vidMode.refreshRate != pkeSettings.targetFPS) { pkeSettings.targetFPS = vidMode.refreshRate; pkeSettings.deltaPerFrame = 1 / double(pkeSettings.targetFPS); } GameTimePoint currentTimePoint = pkeSettings.steadyClock.now(); double deltaThisTick = ((currentTimePoint - lastTimePoint).count() - nsAheadHolder) / NANO_DENOM_DOUBLE; deltaThisTick = std::min(deltaThisTick, pkeSettings.minimumDeltaPerFrame); lastTimePoint = currentTimePoint; deltaTillNextRender -= deltaThisTick; bool shouldRender = pkeSettings.graphicsSettings.isFramerateUnlocked || pkeSettings.graphicsSettings.isWaitingForVsync || deltaTillNextRender <= 0.0; if (shouldRender == false && (deltaTillNextRender > 0.0 && deltaTillNextRender - (deltaThisTick * 2.0) <= 0.0)) { /* * We are ahead of the render schedule * && the current tick's speed would put us behind schedule next tick. * Simulate the extra time we are ahead and prepare to sleep the difference * before the next tick. */ nsAhead = std::floor(deltaTillNextRender * NANO_DENOM_DOUBLE); deltaThisTick += deltaTillNextRender; shouldRender = true; } tickCount += 1; Game_Tick(deltaThisTick); pkeSettings.rt.was_framebuffer_resized = false; if (shouldRender) { Render(); renderCount += 1; double msBehind = deltaTillNextRender * -1000; int64_t behindCount = 0; while (deltaTillNextRender < pkeSettings.deltaPerFrame) { behindCount += 1; deltaTillNextRender += pkeSettings.deltaPerFrame; } if (behindCount > 2) { fprintf(stderr, "[PKE::main] late render - simulated ahead: %fms - delta behind: %fms - missed frames:%ld\n", nsAheadHolder / (NANO_DENOM_DOUBLE / 1000), msBehind, behindCount - 1); fflush(stderr); } } pkeSettings.stats.last_deltas[0] = pkeSettings.stats.last_deltas[1]; pkeSettings.stats.last_deltas[1] = pkeSettings.stats.last_deltas[2]; pkeSettings.stats.last_deltas[2] = deltaThisTick; pkeSettings.stats.tick_rate = 3.L / (pkeSettings.stats.last_deltas[0] + pkeSettings.stats.last_deltas[1] + pkeSettings.stats.last_deltas[2]); if ((currentTimePoint - lastLogTimePoint).count() > std::chrono::nanoseconds::period::den) { lastLogTimePoint = currentTimePoint; fprintf(stdout, "TPS: ~%.03f - actual:%ld - presents:%ld\n", pkeSettings.stats.tick_rate, tickCount, renderCount); fflush(stdout); tickCount = 0; renderCount = 0; } pkeSettings.isGameRunning = !glfwWindowShouldClose(window); } vkDeviceWaitIdle(vkDevice); } catch (const std::exception &exc) { fprintf(stderr, "Game_Main EXCEPTION: %s\n", exc.what()); } catch (const char *err) { fprintf(stderr, "Game_Main UNHANDLED EXCEPTION: %s\n", err); } catch (...) { fprintf(stderr, "Game_Main UNHANDLED EXCEPTION\n"); } GAME_SHUTDOWN: fprintf(stdout, "Game_Main SHUTDOWN INITIATED\n"); #ifndef NDEBUG // TODO debug print buckets before shutdown #endif for (long i = 0; i < LoadedPkePlugins.next; ++i) { if (LoadedPkePlugins[i].OnTeardown) { LoadedPkePlugins[i].OnTeardown(); } } PkePlugin_Teardown(); EntityType_Teardown(); pke_ui_teardown(); FontType_Teardown(); pke_input_teardown(); pke_scene_master_teardown(); pke_level_teardown(); PkeCamera_Teardown(); pke_audio_teardown(); Physics_Teardown(); ECS_Teardown(); DestroyWindow(); AM_DebugPrint(); AM_Teardown(); PkeThreads_Teardown(); pk_ev_teardown(); Game_Teardown(); // TODO debug print buckets after shutdown fprintf(stdout, "Game_Main Exiting\n"); } void Game_Init() { pkeSettings.mem_bkt.game = pk_mem_bucket_create("game", 1UL << 26, PK_MEMBUCKET_FLAG_NONE); pkeSettings.mem_bkt.game_transient = pk_mem_bucket_create("game-transient", 1UL << 26, PK_MEMBUCKET_FLAG_TRANSIENT); pk_mem_bucket_set_client_mem_bucket(pkeSettings.mem_bkt.game); for (uint64_t i = 0; i < consoleBufferCount; ++i) { memset(consoleBuffer[i], '\0', consoleLineLength); } } void Game_Teardown() { pk_mem_bucket_destroy(pkeSettings.mem_bkt.game_transient); pk_mem_bucket_destroy(pkeSettings.mem_bkt.game); pkeSettings.mem_bkt.game_transient = NULL; pkeSettings.mem_bkt.game = NULL; }