A 3D game engine from scratch.
at main 543 lines 17 kB view raw
1// (c) 2020 Vlad-Stefan Harbuz <vlad@vladh.net> 2 3#include <chrono> 4namespace chrono = std::chrono; 5#include <thread> 6#include "../src_external/pstr.h" 7#include "util.hpp" 8#include "engine.hpp" 9#include "logs.hpp" 10#include "debug.hpp" 11#include "peony_parser.hpp" 12#include "peony_parser_utils.hpp" 13#include "models.hpp" 14#include "constants.hpp" 15#include "internals.hpp" 16#include "renderer.hpp" 17#include "intrinsics.hpp" 18 19 20engine::State *engine::state = nullptr; 21 22 23// This should only be used for printing things out for debugging 24engine::State * 25engine::debug_get_engine_state() 26{ 27 return engine::state; 28} 29 30 31models::EntityLoader * 32engine::get_entity_loader(entities::Handle entity_handle) 33{ 34 return engine::state->entity_loaders[entity_handle]; 35} 36 37 38models::ModelLoader * 39engine::push_model_loader() 40{ 41 return engine::state->model_loaders.push(); 42} 43 44 45f64 46engine::get_t() 47{ 48 return engine::state->t; 49} 50 51 52f64 53engine::get_dt() 54{ 55 return engine::state->dt; 56} 57 58 59u32 engine::get_frame_number() 60{ 61 return engine::state->timing_info.n_frames_since_start; 62} 63 64 65void 66engine::run_main_loop(GLFWwindow *window) 67{ 68 while (!engine::state->should_stop) { 69 glfwPollEvents(); 70 process_input(window); 71 72 if ( 73 !engine::state->is_manual_frame_advance_enabled || 74 engine::state->should_manually_advance_to_next_frame 75 ) { 76 update_timing_info(&engine::state->perf_counters.last_fps); 77 78 // If we should pause, stop time-based events. 79 if (!engine::state->should_pause) { 80 update_dt_and_perf_counters(); 81 } 82 83 update(); 84 renderer::render(window); 85 86 if (engine::state->is_manual_frame_advance_enabled) { 87 engine::state->should_manually_advance_to_next_frame = false; 88 } 89 90 input::reset_n_mouse_button_state_changes_this_frame(); 91 input::reset_n_key_state_changes_this_frame(); 92 93 if (engine::state->should_limit_fps) { 94 std::this_thread::sleep_until( 95 engine::state->timing_info.time_frame_should_end); 96 } 97 98 if (SETTINGS.print_fps_on) { 99 logs::info("%u fps", engine::state->perf_counters.last_fps); 100 } 101 } 102 103 104 if (glfwWindowShouldClose(window)) { 105 engine::state->should_stop = true; 106 } 107 } 108} 109 110 111void engine::init(engine::State *engine_state, memory::Pool *asset_memory_pool) { 112 engine::state = engine_state; 113 engine::state->model_loaders = Array<models::ModelLoader>( 114 asset_memory_pool, MAX_N_MODELS, "model_loaders"); 115 engine::state->entity_loaders = Array<models::EntityLoader>( 116 asset_memory_pool, MAX_N_ENTITIES, "entity_loaders", true, 1); 117 engine::state->timing_info = init_timing_info(165); 118 119} 120 121 122void 123engine::destroy_model_loaders() 124{ 125 engine::state->model_loaders.clear(); 126} 127 128 129void 130engine::destroy_scene() 131{ 132 // If the current scene has not finished loading, we can neither 133 // unload it nor load a new one. 134 if (!engine::state->is_world_loaded) { 135 logs::info("Cannot load or unload scene while loading is already in progress."); 136 return; 137 } 138 139 // TODO: Also reclaim texture names from TextureNamePool, otherwise we'll 140 // end up overflowing. 141 destroy_model_loaders(); 142 mats::destroy_non_internal_materials(); 143 144 entities::destroy_non_internal_entities(); 145 engine::state->entity_loaders.delete_elements_after_index( 146 entities::get_first_non_internal_handle()); 147} 148 149 150bool 151engine::load_scene(const char *scene_name) 152{ 153 // If the current scene has not finished loading, we can neither 154 // unload it nor load a new one. 155 if (!engine::state->is_world_loaded) { 156 gui::log("Cannot load or unload scene while loading is already in progress."); 157 return false; 158 } 159 160 // Get some memory for everything we need 161 memory::Pool temp_memory_pool = {}; 162 defer { memory::destroy_memory_pool(&temp_memory_pool); }; 163 164 // Load scene file 165 char scene_path[MAX_PATH] = {}; 166 pstr_vcat(scene_path, MAX_PATH, SCENE_DIR, scene_name, SCENE_EXTENSION, NULL); 167 gui::log("Loading scene: %s", scene_path); 168 169 peony_parser::PeonyFile *scene_file = MEMORY_PUSH(&temp_memory_pool, 170 peony_parser::PeonyFile, "scene_file"); 171 if (!peony_parser::parse_file(scene_file, scene_path)) { 172 gui::log("Could not load scene: %s", scene_path); 173 return false; 174 } 175 176 // Destroy our current scene after we've confirmed we could load the new scene. 177 destroy_scene(); 178 pstr_copy(engine::state->current_scene_name, MAX_COMMON_NAME_LENGTH, scene_name); 179 180 // Get only the unique used materials 181 Array<char[MAX_COMMON_NAME_LENGTH]> used_materials( 182 &temp_memory_pool, MAX_N_MATERIALS, "used_materials"); 183 peony_parser_utils::get_unique_string_values_for_prop_name( 184 scene_file, &used_materials, "materials"); 185 186 // Create Materials 187 peony_parser::PeonyFile *material_file = MEMORY_PUSH(&temp_memory_pool, 188 peony_parser::PeonyFile, "material_file"); 189 each (used_material, used_materials) { 190 memset(material_file, 0, sizeof(peony_parser::PeonyFile)); 191 char material_file_path[MAX_PATH] = {}; 192 pstr_vcat(material_file_path, MAX_PATH, 193 MATERIAL_FILE_DIRECTORY, *used_material, MATERIAL_FILE_EXTENSION, nullptr); 194 if (!peony_parser::parse_file(material_file, material_file_path)) { 195 gui::log("Could not load material: %s", material_file_path); 196 break; 197 } 198 assert(material_file->n_entries > 0); 199 peony_parser_utils::create_material_from_peony_file_entry( 200 mats::push_material(), 201 &material_file->entries[0], 202 &temp_memory_pool 203 ); 204 } 205 206 range (0, scene_file->n_entries) { 207 peony_parser::Entry *entry = &scene_file->entries[idx]; 208 209 // Create entities::Entity 210 entities::Entity *entity = entities::add_entity_to_set(entry->name); 211 212 // Create models::ModelLoader 213 char const *model_path = peony_parser_utils::get_string( 214 peony_parser_utils::find_prop(entry, "model_path") 215 ); 216 // NOTE: We only want to make a models::ModelLoader from this peony_parser::Entry if we haven't 217 // already encountered this model in a previous entry. If two entities 218 // have the same `model_path`, we just make one model and use it in both. 219 models::ModelLoader *found_model_loader = engine::state->model_loaders.find( 220 [model_path](models::ModelLoader *candidate_model_loader) -> bool { 221 return pstr_eq(model_path, candidate_model_loader->model_path); 222 } 223 ); 224 if (found_model_loader) { 225 logs::info("Skipping already-loaded model %s", model_path); 226 } else { 227 peony_parser_utils::create_model_loader_from_peony_file_entry( 228 entry, entity->handle, engine::state->model_loaders.push()); 229 } 230 231 // Create models::EntityLoader 232 peony_parser_utils::create_entity_loader_from_peony_file_entry( 233 entry, entity->handle, 234 engine::state->entity_loaders[entity->handle]); 235 } 236 237 return true; 238} 239 240 241void 242engine::handle_console_command() 243{ 244 char *text_input = input::get_text_input(); 245 gui::log("%s%s", gui::CONSOLE_SYMBOL, text_input); 246 247 char command[input::MAX_TEXT_INPUT_COMMAND_LENGTH] = {}; 248 char arguments[input::MAX_TEXT_INPUT_ARGUMENTS_LENGTH] = {}; 249 250 pstr_split_on_first_occurrence(text_input, 251 command, input::MAX_TEXT_INPUT_COMMAND_LENGTH, 252 arguments, input::MAX_TEXT_INPUT_ARGUMENTS_LENGTH, 253 ' '); 254 255 if (pstr_eq(command, "help")) { 256 gui::log( 257 "Some useful commands\n" 258 "--------------------\n" 259 "loadscene <scene_name>: Load a scene\n" 260 "renderdebug <internal_texture_name>: Display an internal texture. " 261 "Use texture \"none\" to disable.\n" 262 "help: show help" 263 ); 264 } else if (pstr_eq(command, "loadscene")) { 265 load_scene(arguments); 266 } else if (pstr_eq(command, "renderdebug")) { 267 renderer::set_renderdebug_displayed_texture_type( 268 mats::texture_type_from_string(arguments)); 269 } else { 270 gui::log("Unknown command: %s", command); 271 } 272 273 pstr_clear(text_input); 274} 275 276 277void 278engine::update_light_position(f32 amount) 279{ 280 each (light_component, *lights::get_components()) { 281 if (light_component->type == lights::LightType::directional) { 282 lights::adjust_dir_light_angle(amount); 283 break; 284 } 285 } 286} 287 288 289void 290engine::process_input(GLFWwindow *window) 291{ 292 // NOTE: We need to enable text input one frame after the actual key 293 // is pressed to enable it, so that text starts being processed another 294 // frame after that. This is because, on Linux, the backtick character 295 // pressed to enable text input also then immediately gets processed as 296 // text, which is not what we want. 297 if (engine::state->should_enable_text_input) { 298 input::enable_text_input(); 299 engine::state->should_enable_text_input = false; 300 } 301 302 if (input::is_key_now_down(GLFW_KEY_GRAVE_ACCENT)) { 303 if (gui::is_console_enabled()) { 304 gui::set_console_enabled(false); 305 input::disable_text_input(); 306 } else { 307 gui::set_console_enabled(true); 308 engine::state->should_enable_text_input = true; 309 } 310 } 311 312 if (input::is_key_now_down(GLFW_KEY_ENTER)) { 313 handle_console_command(); 314 } 315 316 if (input::is_key_now_down(GLFW_KEY_BACKSPACE)) { 317 input::do_text_input_backspace(); 318 } 319 320 if (input::is_key_now_down(GLFW_KEY_ESCAPE)) { 321 input::clear_text_input(); 322 } 323 324 if (input::is_text_input_enabled()) { 325 // If we're typing text in, don't run any of the following stuff. 326 return; 327 } 328 329 // Continuous 330 if (input::is_key_down(GLFW_KEY_W)) { 331 cameras::move_front_back(cameras::get_main(), 1, engine::get_dt()); 332 } 333 334 if (input::is_key_down(GLFW_KEY_S)) { 335 cameras::move_front_back(cameras::get_main(), -1, engine::get_dt()); 336 } 337 338 if (input::is_key_down(GLFW_KEY_A)) { 339 cameras::move_left_right(cameras::get_main(), -1, engine::get_dt()); 340 } 341 342 if (input::is_key_down(GLFW_KEY_D)) { 343 cameras::move_left_right(cameras::get_main(), 1, engine::get_dt()); 344 } 345 346 if (input::is_key_down(GLFW_KEY_Z)) { 347 update_light_position(0.10f * (f32)(engine::get_dt())); 348 } 349 350 if (input::is_key_down(GLFW_KEY_X)) { 351 update_light_position(-0.10f * (f32)(engine::get_dt())); 352 } 353 354 if (input::is_key_down(GLFW_KEY_SPACE)) { 355 cameras::move_up_down(cameras::get_main(), 1, engine::get_dt()); 356 } 357 358 if (input::is_key_down(GLFW_KEY_LEFT_CONTROL)) { 359 cameras::move_up_down(cameras::get_main(), -1, engine::get_dt()); 360 } 361 362 // Transient 363 if (input::is_key_now_down(GLFW_KEY_ESCAPE)) { 364 glfwSetWindowShouldClose(window, true); 365 } 366 367 if (input::is_key_now_down(GLFW_KEY_C)) { 368 input::set_is_cursor_enabled(!input::is_cursor_enabled()); 369 renderer::update_drawing_options(window); 370 } 371 372 if (input::is_key_now_down(GLFW_KEY_R)) { 373 load_scene(engine::state->current_scene_name); 374 } 375 376 if (input::is_key_now_down(GLFW_KEY_TAB)) { 377 engine::state->should_pause = !engine::state->should_pause; 378 } 379 380 if (input::is_key_now_down(GLFW_KEY_MINUS)) { 381 engine::state->timescale_diff -= 0.1f; 382 } 383 384 if (input::is_key_now_down(GLFW_KEY_EQUAL)) { 385 engine::state->timescale_diff += 0.1f; 386 } 387 388 if (input::is_key_now_down(GLFW_KEY_BACKSPACE)) { 389 renderer::set_should_hide_ui(!renderer::should_hide_ui()); 390 } 391 392 if (input::is_key_down(GLFW_KEY_N)) { 393 engine::state->should_manually_advance_to_next_frame = true; 394 } 395 396 if (input::is_key_now_down(GLFW_KEY_0)) { 397 *((volatile unsigned int*)0) = 0xDEAD; 398 } 399} 400 401 402bool 403engine::check_all_entities_loaded() 404{ 405 bool are_all_done_loading = true; 406 407 each (material, *mats::get_materials()) { 408 bool is_done_loading = mats::prepare_material_and_check_if_done(material); 409 if (!is_done_loading) { 410 are_all_done_loading = false; 411 } 412 } 413 414 u32 new_n_valid_model_loaders = 0; 415 each (model_loader, engine::state->model_loaders) { 416 if (!models::is_model_loader_valid(model_loader)) { 417 continue; 418 } 419 new_n_valid_model_loaders++; 420 bool is_done_loading = models::prepare_model_loader_and_check_if_done(model_loader); 421 if (!is_done_loading) { 422 are_all_done_loading = false; 423 } 424 } 425 engine::state->n_valid_model_loaders = new_n_valid_model_loaders; 426 427 u32 new_n_valid_entity_loaders = 0; 428 each (entity_loader, engine::state->entity_loaders) { 429 if (!models::is_entity_loader_valid(entity_loader)) { 430 continue; 431 } 432 new_n_valid_entity_loaders++; 433 434 models::ModelLoader *model_loader = engine::state->model_loaders.find( 435 [entity_loader](models::ModelLoader *candidate_model_loader) -> bool { 436 return pstr_eq(entity_loader->model_path, candidate_model_loader->model_path); 437 } 438 ); 439 if (!model_loader) { 440 logs::fatal("Encountered an models::EntityLoader %d for which we cannot find the models::ModelLoader.", 441 entity_loader->entity_handle); 442 } 443 444 bool is_done_loading = models::prepare_entity_loader_and_check_if_done( 445 entity_loader, 446 model_loader); 447 448 // NOTE: If a certain models::EntityLoader is complete, it's done everything it 449 // needed to and we don't need it anymore. 450 if (is_done_loading) { 451 // TODO: We need to do this in a better way. We should somehow let the 452 // Array know when we delete one of these. Even though it's sparse, 453 // it should have length 0 if we know there's nothing in it. That way 454 // we don't have to iterate over it over and over. 455 memset(entity_loader, 0, sizeof(models::EntityLoader)); 456 } 457 458 if (!is_done_loading) { 459 are_all_done_loading = false; 460 } 461 } 462 engine::state->n_valid_entity_loaders = new_n_valid_entity_loaders; 463 464 return are_all_done_loading; 465} 466 467 468void 469engine::update() 470{ 471 if (engine::state->is_world_loaded && !engine::state->was_world_ever_loaded) { 472 load_scene(DEFAULT_SCENE); 473 engine::state->was_world_ever_loaded = true; 474 } 475 476 cameras::update_matrices(cameras::get_main()); 477 478 engine::state->is_world_loaded = check_all_entities_loaded(); 479 480 lights::update(cameras::get_main()->position); 481 behavior::update(); 482 anim::update(); 483 physics::update(); 484} 485 486 487engine::TimingInfo 488engine::init_timing_info(u32 target_fps) 489{ 490 TimingInfo timing_info = {}; 491 timing_info.second_start = chrono::steady_clock::now(); 492 timing_info.frame_start = chrono::steady_clock::now(); 493 timing_info.last_frame_start = chrono::steady_clock::now(); 494 timing_info.frame_duration = chrono::nanoseconds( 495 (u32)((f64)1.0f / (f64)target_fps) 496 ); 497 return timing_info; 498} 499 500 501void 502engine::update_timing_info(u32 *last_fps) 503{ 504 auto *timing = &engine::state->timing_info; 505 timing->n_frames_since_start++; 506 timing->last_frame_start = timing->frame_start; 507 timing->frame_start = chrono::steady_clock::now(); 508 timing->time_frame_should_end = timing->frame_start + timing->frame_duration; 509 510 timing->n_frames_this_second++; 511 chrono::duration<f64> time_since_second_start = 512 timing->frame_start - timing->second_start; 513 if (time_since_second_start >= chrono::seconds(1)) { 514 timing->second_start = timing->frame_start; 515 *last_fps = timing->n_frames_this_second; 516 timing->n_frames_this_second = 0; 517 } 518} 519 520 521void 522engine::update_dt_and_perf_counters() 523{ 524 engine::state->dt = util::get_us_from_duration( 525 engine::state->timing_info.frame_start - engine::state->timing_info.last_frame_start); 526 if (engine::state->timescale_diff != 0.0f) { 527 engine::state->dt *= max(1.0f + engine::state->timescale_diff, (f64)0.01f); 528 } 529 530 engine::state->perf_counters.dt_hist[engine::state->perf_counters.dt_hist_idx] = 531 engine::state->dt; 532 engine::state->perf_counters.dt_hist_idx++; 533 if (engine::state->perf_counters.dt_hist_idx >= DT_HIST_LENGTH) { 534 engine::state->perf_counters.dt_hist_idx = 0; 535 } 536 f64 dt_hist_sum = 0.0f; 537 for (u32 idx = 0; idx < DT_HIST_LENGTH; idx++) { 538 dt_hist_sum += engine::state->perf_counters.dt_hist[idx]; 539 } 540 engine::state->perf_counters.dt_average = dt_hist_sum / DT_HIST_LENGTH; 541 542 engine::state->t += engine::state->dt; 543}