#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 void Game_Init(); void Game_Tick(double delta); void Game_Teardown(); void Game_RecordImGui(); const char *levelName = "demo-level"; 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 && 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 pke_game_main_init(PKEWindowProperties windowProps, const char *executablePath, PKEPluginInterface *plugins, unsigned int n_plugins) { fprintf(stdout, "[%s] pke_game_main_init Entering.\n", __FILE__); unsigned int u; pkeSettings.executablePath = executablePath; 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 (u = 0; u < n_plugins; ++u) { pk_arr_append_t(&LoadedPkePlugins, plugins[u]); } for (u = 0; u < LoadedPkePlugins.next; ++u) { if (LoadedPkePlugins[u].OnInit != nullptr) { LoadedPkePlugins[u].OnInit(); } } fprintf(stdout, "[%s] pke_game_main_init Exiting.\n", __FILE__); } void pke_game_main_load() { fprintf(stdout, "[%s] pke_game_main_load Entering.\n", __FILE__); // 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, "[%s] pke_game_main_load Creating faux level.\n", __FILE__); 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, "[%s] pke_game_main_load Did not find scene by name specified in arg: '%s'\n", __FILE__, pkeSettings.args.sceneName); exit(1); } fprintf(stdout, "[%s] pke_game_main_load loading scene from arg (expecting path): %s\n", __FILE__, 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); } fprintf(stdout, "[%s] pke_game_main_load Exiting.\n", __FILE__); } void pke_game_main_run() { fprintf(stdout, "[%s] pke_game_main_run Entering.\n", __FILE__); 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); } fprintf(stdout, "[%s] pke_game_main_run Exiting.\n", __FILE__); } void pke_game_main_teardown() { fprintf(stdout, "[%s] pke_game_main_teardown Entering.\n", __FILE__); // tear down any loaded levels if (pkeSettings.rt.nextLevel != nullptr) { if (pkeSettings.rt.nextLevel->pke_cb_teardown.func != nullptr) { pkeSettings.rt.nextLevel->pke_cb_teardown.func(); } pke_level_teardown(pkeSettings.rt.nextLevel); pkeSettings.rt.nextLevel = nullptr; } if (pkeSettings.rt.activeLevel != nullptr) { if (pkeSettings.rt.activeLevel->pke_cb_teardown.func != nullptr) { pkeSettings.rt.activeLevel->pke_cb_teardown.func(); } pke_level_teardown(pkeSettings.rt.activeLevel); pkeSettings.rt.activeLevel = nullptr; } 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; } // Tick until entities have settled. // This allows things like nested entities (static_ui) to flush out. // Without this, plugins will have their OnTeardown called before the engine // has had a chance to free anything active. // Example: player input LIFO error if a loaded level had an active set. pk_bkt_arr *bkt_arr_ents = ECS_GetEntities(); pk_bkt_arr_handle settle_handle_l = pk_bkt_arr_handle_MAX; pk_bkt_arr_handle settle_handle_r = pk_bkt_arr_handle_MAX; pk_bkt_arr_handle settle_handle_ll = bkt_arr_ents->head_l; pk_bkt_arr_handle settle_handle_rr = bkt_arr_ents->head_r; while (settle_handle_ll != settle_handle_l && settle_handle_rr != settle_handle_r) { Game_Tick(0.f); settle_handle_l = settle_handle_ll; settle_handle_r = settle_handle_rr; settle_handle_ll = bkt_arr_ents->head_l; settle_handle_rr = bkt_arr_ents->head_r; } fprintf(stdout, "Game_Main SHUTDOWN INITIATED\n"); #ifndef NDEBUG // TODO debug print buckets before shutdown #endif vkDeviceWaitIdle(vkDevice); 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, "[%s] pke_game_main_teardown Exiting.\n", __FILE__); } void Game_Main(PKEWindowProperties windowProps, const char *executablePath) { pke_game_main_init(windowProps, executablePath, nullptr, 0); pke_game_main_load(); pke_game_main_run(); pke_game_main_teardown(); } 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); } 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; }