A 3D game engine from scratch.
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}