From 56d53cb5092e8c02de498ce2a43228966241d7b7 Mon Sep 17 00:00:00 2001 From: doyle Date: Sun, 17 Sep 2023 18:14:51 +1000 Subject: [PATCH] Initial commit --- .gitattributes | 1 + .gitignore | 1 + .gitmodules | 3 + External/tely | 1 + build.bat | 107 +++++ playground.cpp | 994 +++++++++++++++++++++++++++++++++++++++++++ playground_game.cpp | 423 ++++++++++++++++++ playground_game.h | 171 ++++++++ playground_unity.cpp | 1 + playground_unity.h | 67 +++ 10 files changed, 1769 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .gitmodules create mode 160000 External/tely create mode 100644 build.bat create mode 100644 playground.cpp create mode 100644 playground_game.cpp create mode 100644 playground_game.h create mode 100644 playground_unity.cpp create mode 100644 playground_unity.h diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c5edd1d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +Data filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..527b85c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +Build/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..caff89a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "External/tely"] + path = External/tely + url = ssh://gitea@git.doylet.dev:8110/doylet/tely.git diff --git a/External/tely b/External/tely new file mode 160000 index 0000000..502c8dc --- /dev/null +++ b/External/tely @@ -0,0 +1 @@ +Subproject commit 502c8dca126566cd603b83f047851787144b0b8f diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..6ae6ac7 --- /dev/null +++ b/build.bat @@ -0,0 +1,107 @@ +@echo off +setlocal + +set script_dir_backslash=%~dp0 +set script_dir=%script_dir_backslash:~0,-1% +set build_dir=%script_dir%\Build +set code_dir=%script_dir% +set tely_dir=%code_dir%\External\tely + +REM ================================================================================================ + +mkdir %build_dir% 2>nul +pushd %build_dir% +mkdir %build_dir%\Data 2>nul + +REM ================================================================================================ + +set robocopy_cmd=robocopy /MIR /NJH /NJS /NDL /NP %code_dir%\Data %build_dir%\Data +call powershell -Command "$duration = Measure-Command {%robocopy_cmd% | Out-Default}; Write-Host 'robocopy:' $duration.TotalSeconds 'seconds'" + +REM ================================================================================================ + +REM TODO: Raylib seems to have problems shutting down fsanitize=address? +set common_compile_flags=/W4 /Z7 /MT /EHsc /nologo /I %tely_dir% +set common_link_flags=/link /incremental:no + +REM raylib ========================================================================================= + +set raylib_dir=%tely_dir%\external\raylib +if not exist %build_dir%\rcore.obj ( + cl %common_compile_flags% ^ + /w ^ + /c ^ + /D _DEFAULT_SOURCE ^ + /D PLATFORM_DESKTOP ^ + /I %raylib_dir% ^ + /I %raylib_dir%\external\glfw\include ^ + /I %raylib_dir%\glfw\deps\mingw ^ + %raylib_dir%\rcore.c ^ + %raylib_dir%\utils.c ^ + %raylib_dir%\raudio.c ^ + %raylib_dir%\rmodels.c ^ + %raylib_dir%\rtext.c ^ + %raylib_dir%\rtextures.c ^ + %raylib_dir%\rshapes.c ^ + %raylib_dir%\rglfw.c +) + +REM raylib flags ========================================================================================= + +set raylib_compile_flags=%common_compile_flags% ^ +/Tp %tely_dir%\tely_platform_raylib_unity.h ^ +/I "%raylib_dir%" + +set raylib_link_flags=%common_link_flags% ^ +%build_dir%\rcore.obj ^ +%build_dir%\utils.obj ^ +%build_dir%\raudio.obj ^ +%build_dir%\rmodels.obj ^ +%build_dir%\rtext.obj ^ +%build_dir%\rtextures.obj ^ +%build_dir%\rshapes.obj ^ +%build_dir%\rglfw.obj ^ +gdi32.lib opengl32.lib winmm.lib user32.lib shell32.lib + +REM DLL flags ====================================================================================== + +set dll_compile_flags=%common_compile_flags% /LD /Tp %code_dir%\playground_unity.h +set dll_link_flags=%common_link_flags% + +REM MSVC commands ================================================================================== + +set msvc_exe_name=playground_msvc +set msvc_cmd=cl %raylib_compile_flags% /Fo%msvc_exe_name% /Fe%msvc_exe_name% %raylib_link_flags% +set msvc_dll_cmd=cl %dll_compile_flags% /Fotely_dll_msvc /Fetely_dll_msvc %dll_link_flags% + +REM CLANG commands ================================================================================= + +set clang_exe_name_sdl=playground_clang +set clang_cmd_sdl=clang-cl %compile_flags% /Fo%clang_exe_name_sdl% /Fe%clang_exe_name_sdl% %link_flags% +set clang_dll_cmd=clang-cl %dll_compile_flags% /Fotely_dll_clang /Fetely_dll_clang %dll_link_flags% + +REM MSVC build ===================================================================================== + +set msvc_build_platform=$platform_time = Measure-Command {%msvc_cmd% ^| Out-Default}; +set msvc_build_dll=$dll_time = Measure-Command {%msvc_dll_cmd% ^| Out-Default}; +set msvc_print_dll_time=Write-Host 'msvc dll:'$dll_time.TotalSeconds's' +set msvc_print_platform_and_dll_time=Write-Host 'msvc (platform+dll):'($platform_time.TotalSeconds + $dll_time.TotalSeconds)'s ('$platform_time.TotalSeconds + $dll_time.TotalSeconds')' +set msvc_check_if_exe_locked=$File = [System.IO.File]::Open('%build_dir%\%msvc_exe_name%.exe', 'Open', 'Write'); $File.Close(); $File.Dispose() + +set msvc_exe_is_locked=0 +if exist %build_dir%\%msvc_exe_name%.exe ( + powershell -Command "%msvc_check_if_exe_locked%" 2>nul && set msvc_exe_is_locked=0 || set msvc_exe_is_locked=1 +) + +echo foo> %msvc_exe_name%.lock +if %msvc_exe_is_locked% == 1 ( + powershell -Command "%msvc_build_dll% %msvc_print_dll_time%" +) else ( + powershell -Command "%msvc_build_platform% %msvc_build_dll% %msvc_print_platform_and_dll_time%" +) +del %msvc_exe_name%.lock + +REM CLANG build ==================================================================================== + +popd +exit /B 1 diff --git a/playground.cpp b/playground.cpp new file mode 100644 index 0000000..2c0e1eb --- /dev/null +++ b/playground.cpp @@ -0,0 +1,994 @@ +#if defined(__clang__) +#pragma once +#include "tely_dll_unity.h" +#endif + +extern "C" __declspec(dllexport) +void TELY_DLL_Reload(void *user_data) +{ + TELY_Platform *platform = DQN_CAST(TELY_Platform *)user_data; + Dqn_Library_SetPointer(platform->core.dqn_lib); +} + +extern "C" __declspec(dllexport) +void TELY_DLL_Init(void *user_data) +{ + TELY_Platform *platform = DQN_CAST(TELY_Platform *)user_data; + TELY_DLL_Reload(user_data); + + { + Dqn_Arena_TempMemoryScope(&platform->arena); + TELY_ChunkPool pool = {}; + pool.arena = &platform->arena; + + void *bytes16 = TELY_ChunkPool_Alloc(&pool, 16); + TELY_ChunkPool_Dealloc(&pool, bytes16); + DQN_ASSERT(pool.slots[TELY_ChunkPoolSlotSize_16b] == DQN_CAST(void *)(DQN_CAST(char *)bytes16 - sizeof(TELY_ChunkPoolSlot))); + DQN_ASSERT(pool.slots[TELY_ChunkPoolSlotSize_16b]->next == nullptr); + + void *bytes17 = TELY_ChunkPool_Alloc(&pool, 17); + TELY_ChunkPool_Dealloc(&pool, bytes17); + DQN_ASSERT(pool.slots[TELY_ChunkPoolSlotSize_32b] == DQN_CAST(void *)(DQN_CAST(char *)bytes17 - sizeof(TELY_ChunkPoolSlot))); + DQN_ASSERT(pool.slots[TELY_ChunkPoolSlotSize_32b]->next == nullptr); + + void *bytes1 = TELY_ChunkPool_Alloc(&pool, 1); + void *bytes2 = TELY_ChunkPool_Alloc(&pool, 1); + TELY_ChunkPool_Dealloc(&pool, bytes1); + TELY_ChunkPool_Dealloc(&pool, bytes2); + DQN_ASSERT(pool.slots[TELY_ChunkPoolSlotSize_16b] == DQN_CAST(void *)(DQN_CAST(char *)bytes2 - sizeof(TELY_ChunkPoolSlot))); + DQN_ASSERT(pool.slots[TELY_ChunkPoolSlotSize_16b]->next == DQN_CAST(void *)(DQN_CAST(char *)bytes1 - sizeof(TELY_ChunkPoolSlot))); + + void *bytes128k = TELY_ChunkPool_Alloc(&pool, DQN_KILOBYTES(128)); + TELY_ChunkPool_Dealloc(&pool, bytes128k); + DQN_ASSERT(pool.slots[TELY_ChunkPoolSlotSize_128k] == DQN_CAST(void *)(DQN_CAST(char *)bytes128k - sizeof(TELY_ChunkPoolSlot))); + DQN_ASSERT(pool.slots[TELY_ChunkPoolSlotSize_128k]->next == nullptr); + } + + // NOTE: TELY Game ============================================================================= + + TELY_Assets *assets = &platform->assets; + TELY_Game *game = Dqn_Arena_New(&platform->arena, TELY_Game, Dqn_ZeroMem_Yes); + game->chunk_pool.arena = &platform->arena; + platform->user_data = game; + { + TELY_AssetSpriteSheet *sheet = &game->hero_sprite_sheet; + Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(nullptr); + Dqn_String8 sheet_path = Dqn_FsPath_ConvertF(scratch.arena, "%.*s/adventurer-v1.5-sheet.png", DQN_STRING_FMT(assets->textures_dir)); + sheet->tex_handle = platform->func_load_texture(assets, DQN_STRING8("Hero"), sheet_path); + sheet->sprite_count = 109; + sheet->sprites_per_row = 7; + sheet->sprite_size = Dqn_V2I_InitNx2(50, 37); + + TELY_AssetSpriteAnimation hero_anims[] = { + {DQN_STRING8("Everything"), /*index*/ 0, /*count*/ sheet->sprite_count}, + {DQN_STRING8("Idle"), /*index*/ 0, /*count*/ 3}, + {DQN_STRING8("Run"), /*index*/ 8, /*count*/ 6}, + {DQN_STRING8("Jump"), /*index*/ 14, /*count*/ 10}, + {DQN_STRING8("Floor slide"), /*index*/ 24, /*count*/ 5}, + {DQN_STRING8("Unknown"), /*index*/ 29, /*count*/ 9}, + {DQN_STRING8("Attack A"), /*index*/ 38, /*count*/ 11}, + {DQN_STRING8("Attack B"), /*index*/ 49, /*count*/ 4}, + {DQN_STRING8("Attack C"), /*index*/ 53, /*count*/ 6}, + {DQN_STRING8("Hurt A"), /*index*/ 59, /*count*/ 5}, + {DQN_STRING8("Hurt B"), /*index*/ 64, /*count*/ 5}, + {DQN_STRING8("Unsheath sword"), /*index*/ 69, /*count*/ 4}, + {DQN_STRING8("Sheath sword"), /*index*/ 73, /*count*/ 4}, + {DQN_STRING8("Air drift"), /*index*/ 77, /*count*/ 2}, + {DQN_STRING8("Air drop"), /*index*/ 79, /*count*/ 2}, + {DQN_STRING8("Ladder climb"), /*index*/ 81, /*count*/ 4}, + {DQN_STRING8("Chi push"), /*index*/ 85, /*count*/ 8}, + {DQN_STRING8("Leap slice A"), /*index*/ 93, /*count*/ 7}, + {DQN_STRING8("Leap slice B"), /*index*/ 100, /*count*/ 3}, + {DQN_STRING8("Leap slice C"), /*index*/ 103, /*count*/ 6}, + }; + + game->hero_sprite_anims = Dqn_Slice_Alloc(&platform->arena, DQN_ARRAY_UCOUNT(hero_anims), Dqn_ZeroMem_No); + DQN_MEMCPY(game->hero_sprite_anims.data, &hero_anims, sizeof(hero_anims[0]) * DQN_ARRAY_UCOUNT(hero_anims)); + } + + game->entities = Dqn_VArray_Init(&platform->arena, 1024 * 8); + game->root_entity = Dqn_VArray_Make(&game->entities, 1, Dqn_ZeroMem_No); + Dqn_FArray_Add(&game->parent_entity_stack, game->root_entity->handle); + + // NOTE: Unit test DFS pre-order and post-order walk + { + // NOTE: Setup entity-tree ================================================================= + + TELY_GameEntity *f = TELY_Game_MakeEntityPointerF(game, "F"); + TELY_Game_PushParentEntity(game, f->handle); + TELY_GameEntity *b = TELY_Game_MakeEntityPointerF(game, "B"); + TELY_GameEntity *g = TELY_Game_MakeEntityPointerF(game, "G"); + TELY_Game_PushParentEntity(game, b->handle); + TELY_GameEntity *a = TELY_Game_MakeEntityPointerF(game, "A"); + TELY_GameEntity *d = TELY_Game_MakeEntityPointerF(game, "D"); + TELY_Game_PushParentEntity(game, d->handle); + TELY_GameEntity *c = TELY_Game_MakeEntityPointerF(game, "C"); + TELY_GameEntity *e = TELY_Game_MakeEntityPointerF(game, "E"); + TELY_Game_PopParentEntity(game); + TELY_Game_PopParentEntity(game); + + TELY_Game_PushParentEntity(game, g->handle); + TELY_GameEntity *i = TELY_Game_MakeEntityPointerF(game, "I"); + TELY_Game_PushParentEntity(game, i->handle); + TELY_GameEntity *h = TELY_Game_MakeEntityPointerF(game, "H"); + TELY_Game_PopParentEntity(game); + TELY_Game_PopParentEntity(game); + TELY_Game_PopParentEntity(game); + + // NOTE: Pre order test ==================================================================== + + TELY_GameEntity *pre_order_walk[9] = {}; + Dqn_usize pre_order_walk_count = 0; + for (TELY_GameEntityIterator it = {}; TELY_Game_DFSPreOrderWalkEntityTree(game, &it, game->root_entity);) { + DQN_ASSERT(pre_order_walk_count < DQN_ARRAY_UCOUNT(pre_order_walk)); + pre_order_walk[pre_order_walk_count++] = it.entity; + } + + DQN_ASSERT(pre_order_walk_count == DQN_ARRAY_UCOUNT(pre_order_walk)); + DQN_ASSERT(pre_order_walk[0] == f); + DQN_ASSERT(pre_order_walk[1] == b); + DQN_ASSERT(pre_order_walk[2] == a); + DQN_ASSERT(pre_order_walk[3] == d); + DQN_ASSERT(pre_order_walk[4] == c); + DQN_ASSERT(pre_order_walk[5] == e); + DQN_ASSERT(pre_order_walk[6] == g); + DQN_ASSERT(pre_order_walk[7] == i); + DQN_ASSERT(pre_order_walk[8] == h); + + // NOTE: Post order test =================================================================== + + TELY_GameEntity *post_order_walk[9] = {}; + Dqn_usize post_order_walk_count = 0; + for (TELY_GameEntityIterator it = {}; TELY_Game_DFSPostOrderWalkEntityTree(game, &it, game->root_entity);) { + DQN_ASSERT(post_order_walk_count < DQN_ARRAY_UCOUNT(post_order_walk)); + post_order_walk[post_order_walk_count++] = it.entity; + } + + DQN_ASSERT(post_order_walk_count == DQN_ARRAY_UCOUNT(post_order_walk)); + DQN_ASSERT(post_order_walk[0] == a); + DQN_ASSERT(post_order_walk[1] == c); + DQN_ASSERT(post_order_walk[2] == e); + DQN_ASSERT(post_order_walk[3] == d); + DQN_ASSERT(post_order_walk[4] == b); + DQN_ASSERT(post_order_walk[5] == h); + DQN_ASSERT(post_order_walk[6] == i); + DQN_ASSERT(post_order_walk[7] == g); + DQN_ASSERT(post_order_walk[8] == f); + + // NOTE: Cleanup =========================================================================== + TELY_Game_DeleteEntity(game, game->root_entity->handle); + DQN_ASSERT(game->root_entity->first_child == nullptr); + DQN_ASSERT(game->root_entity->last_child == nullptr); + DQN_ASSERT(game->root_entity->next == nullptr); + DQN_ASSERT(game->root_entity->prev == nullptr); + DQN_ASSERT(game->root_entity->parent == nullptr); + } + + // NOTE: Test sprite animation entity + { + TELY_GameEntity *first_entity = TELY_Game_MakeEntityPointerF(game, "Hero"); + first_entity->local_pos = Dqn_V2_InitNx2(100.f, 100.f); + first_entity->size_scale = Dqn_V2_InitNx1(4); + first_entity->sprite_sheet = &game->hero_sprite_sheet; + first_entity->sprite_anims = game->hero_sprite_anims; + first_entity->local_hit_box_size = Dqn_V2_InitV2I(game->hero_sprite_sheet.sprite_size); + first_entity->flags |= TELY_EntityFlag_Clickable; + first_entity->flags |= TELY_EntityFlag_MoveByKeyboard; + first_entity->flags |= TELY_EntityFlag_MoveByMouse; + } + + // NOTE: Freya's Game Dev Math Lecture 01 + TELY_FreyaGameMath *freya_gm = &game->freya_game_math; + { + TELY_GameEntity *group_box = TELY_Game_MakeEntityPointerF(game, "Lecture 01 Group Box"); + TELY_Game_PushParentEntity(game, group_box->handle); + DQN_DEFER { TELY_Game_PopParentEntity(game); }; + { + group_box->local_pos = Dqn_V2_InitNx2(70, 609); + group_box->flags |= TELY_EntityFlag_DrawHitBox; + group_box->flags |= TELY_EntityFlag_DeriveHitBoxFromChildrenBoundingBox; + group_box->flags |= TELY_EntityFlag_Clickable; + group_box->flags |= TELY_EntityFlag_MoveByKeyboard; + group_box->flags |= TELY_EntityFlag_MoveByMouse; + freya_gm->lec01_group_box = group_box->handle; + } + + TELY_GameEntity *coordinate_axis = TELY_Game_MakeEntityPointerF(game, "Coordinate Axis"); + DQN_ASSERT(coordinate_axis->parent == group_box); + { + coordinate_axis->local_hit_box_size = Dqn_V2_InitNx1(24); + coordinate_axis->flags |= TELY_EntityFlag_Clickable; + coordinate_axis->flags |= TELY_EntityFlag_MoveByKeyboard; + coordinate_axis->flags |= TELY_EntityFlag_MoveByMouse; + + Dqn_f32 const axis_length = platform->core.window_size.y * 0.25f; + + TELY_GameShape *axis_y = Dqn_FArray_Make(&coordinate_axis->shapes, Dqn_ZeroMem_Yes); + axis_y->type = TELY_GameShapeType_Line; + axis_y->p2 = Dqn_V2_InitNx2(0, -axis_length); + axis_y->line_thickness = 2.f; + axis_y->colour = TELY_COLOUR_RED_TOMATO_V4; + + TELY_GameShape *axis_x = Dqn_FArray_Make(&coordinate_axis->shapes, Dqn_ZeroMem_Yes); + axis_x->type = TELY_GameShapeType_Line; + axis_x->p2 = Dqn_V2_InitNx2(axis_length, 0); + axis_x->line_thickness = 2.f; + axis_x->colour = TELY_COLOUR_GREEN_DARK_KHAKI_V4; + + freya_gm->lec01_axis = coordinate_axis->handle; + } + + // ========================================================================================= + Dqn_f32 const point_radius = 8.f; + Dqn_f32 const point_diameter = point_radius * 2.f; + + TELY_GameEntity *point_a = TELY_Game_MakeEntityPointerF(game, "A"); + { + point_a->local_pos = Dqn_V2_InitNx2(400, -100); + point_a->local_hit_box_size = Dqn_V2_InitNx1(point_diameter); + point_a->flags |= TELY_EntityFlag_Clickable; + point_a->flags |= TELY_EntityFlag_MoveByKeyboard; + point_a->flags |= TELY_EntityFlag_MoveByMouse; + point_a->parent = coordinate_axis; + + TELY_GameShape *circle = Dqn_FArray_Make(&point_a->shapes, Dqn_ZeroMem_Yes); + circle->type = TELY_GameShapeType_Circle; + circle->circle_radius = point_radius; + circle->colour = TELY_COLOUR_RED_PALE_VIOLET_V4; + freya_gm->lec01_point_a = point_a->handle; + } + + TELY_GameEntity *point_b = TELY_Game_MakeEntityPointerF(game, "B"); + { + point_b->local_pos = Dqn_V2_InitNx2(300, -300); + point_b->local_hit_box_size = Dqn_V2_InitNx1(point_diameter); + point_b->flags |= TELY_EntityFlag_Clickable; + point_b->flags |= TELY_EntityFlag_MoveByKeyboard; + point_b->flags |= TELY_EntityFlag_MoveByMouse; + + TELY_GameShape *circle = Dqn_FArray_Make(&point_b->shapes, Dqn_ZeroMem_Yes); + circle->type = TELY_GameShapeType_Circle; + circle->circle_radius = point_radius; + circle->colour = TELY_COLOUR_BLUE_CADET_V4; + freya_gm->lec01_point_b = point_b->handle; + } + } + + uint16_t font_size = 18; + // NOTE: Freya's Game Dev Math Lecture 01 Assignment 01 + { + TELY_GameEntity *group_box = TELY_Game_MakeEntityPointerF(game, "Assignment 01 Group Box"); + TELY_Game_PushParentEntity(game, group_box->handle); + DQN_DEFER { TELY_Game_PopParentEntity(game); }; + { + group_box->local_pos = Dqn_V2_InitNx2(659, 404); + group_box->flags |= TELY_EntityFlag_DrawHitBox; + group_box->flags |= TELY_EntityFlag_DeriveHitBoxFromChildrenBoundingBox; + group_box->flags |= TELY_EntityFlag_Clickable; + group_box->flags |= TELY_EntityFlag_MoveByKeyboard; + group_box->flags |= TELY_EntityFlag_MoveByMouse; + freya_gm->lec01_task01_group_box = group_box->handle; + } + + // NOTE: Radial trigger + { + TELY_GameEntity *radial_trigger = TELY_Game_MakeEntityPointerF(game, "Radial trigger"); + freya_gm->lec01_task01_radial_trigger = radial_trigger->handle; + freya_gm->lec01_task01_radial_trigger_radius = 100.f; + radial_trigger->local_hit_box_size = Dqn_V2_InitNx1(font_size); + radial_trigger->flags |= TELY_EntityFlag_Clickable; + radial_trigger->flags |= TELY_EntityFlag_MoveByKeyboard; + radial_trigger->flags |= TELY_EntityFlag_MoveByMouse; + + // NOTE: Draw the "sensor" + TELY_GameShape *circle = Dqn_FArray_Make(&radial_trigger->shapes, Dqn_ZeroMem_Yes); + circle->type = TELY_GameShapeType_Circle; + circle->circle_radius = radial_trigger->local_hit_box_size.w * .5f; + circle->colour = TELY_COLOUR_BLUE_CADET_V4; + + // NOTE: Draw the trigger range + TELY_GameShape *trigger_circle = Dqn_FArray_Make(&radial_trigger->shapes, Dqn_ZeroMem_Yes); + trigger_circle->type = TELY_GameShapeType_Circle; + trigger_circle->circle_radius = freya_gm->lec01_task01_radial_trigger_radius; + trigger_circle->colour = TELY_COLOUR_BLUE_CADET_V4; + } + + // NOTE: Movable dot that will trigger the radial trigger + { + TELY_GameEntity *player = TELY_Game_MakeEntityPointerF(game, "Player"); + freya_gm->lec01_task01_player = player->handle; + + player->local_pos = Dqn_V2_InitNx2(118, 0); + player->local_hit_box_size = Dqn_V2_InitNx1(font_size * 2.f); + player->flags |= TELY_EntityFlag_Clickable; + player->flags |= TELY_EntityFlag_MoveByKeyboard; + player->flags |= TELY_EntityFlag_MoveByMouse; + + // NOTE: Draw the player + TELY_GameShape *rect = Dqn_FArray_Make(&player->shapes, Dqn_ZeroMem_Yes); + rect->type = TELY_GameShapeType_Rect; + rect->p2 = player->local_hit_box_size; + rect->colour = TELY_COLOUR_GREEN_DARK_SEA_V4; + } + } + + { + TELY_GameEntity *group_box = TELY_Game_MakeEntityPointerF(game, "Assignment 02 Group Box"); + TELY_Game_PushParentEntity(game, group_box->handle); + DQN_DEFER { TELY_Game_PopParentEntity(game); }; + { + group_box->local_pos = Dqn_V2_InitNx2(761, 590); + group_box->flags |= TELY_EntityFlag_DrawHitBox; + group_box->flags |= TELY_EntityFlag_DeriveHitBoxFromChildrenBoundingBox; + group_box->flags |= TELY_EntityFlag_Clickable; + group_box->flags |= TELY_EntityFlag_MoveByKeyboard; + group_box->flags |= TELY_EntityFlag_MoveByMouse; + freya_gm->lec01_task02_group_box = group_box->handle; + } + + TELY_GameEntity *ray_begin = TELY_Game_MakeEntityPointerF(game, "Ray Begin"); + { + ray_begin->local_pos = Dqn_V2_InitNx2(0, 0); + ray_begin->local_hit_box_size = Dqn_V2_InitNx1(font_size); + ray_begin->flags |= TELY_EntityFlag_Clickable; + ray_begin->flags |= TELY_EntityFlag_MoveByKeyboard; + ray_begin->flags |= TELY_EntityFlag_MoveByMouse; + freya_gm->lec01_task02_ray_begin = ray_begin->handle; + + // NOTE: Draw the ray origin + TELY_GameShape *circle = Dqn_FArray_Make(&ray_begin->shapes, Dqn_ZeroMem_Yes); + circle->type = TELY_GameShapeType_Circle; + circle->circle_radius = ray_begin->local_hit_box_size.w * .5f; + circle->colour = TELY_COLOUR_BLUE_CADET_V4; + } + + TELY_GameEntity *ray_end = TELY_Game_MakeEntityPointerF(game, "Ray End"); + { + ray_end->local_pos = Dqn_V2_InitNx2(100, 220); + ray_end->local_hit_box_size = Dqn_V2_InitNx1(font_size); + ray_end->flags |= TELY_EntityFlag_Clickable; + ray_end->flags |= TELY_EntityFlag_MoveByKeyboard; + ray_end->flags |= TELY_EntityFlag_MoveByMouse; + freya_gm->lec01_task02_ray_end = ray_end->handle; + + // NOTE: Draw the ray origin + TELY_GameShape *circle = Dqn_FArray_Make(&ray_end->shapes, Dqn_ZeroMem_Yes); + circle->type = TELY_GameShapeType_Circle; + circle->circle_radius = ray_end->local_hit_box_size.w * .5f; + circle->colour = TELY_COLOUR_BLUE_CADET_V4; + } + + TELY_GameEntity *surface = TELY_Game_MakeEntityPointerF(game, "Surface"); + { + surface->local_pos = Dqn_V2_InitNx2(0, 200); + surface->local_hit_box_size = Dqn_V2_InitNx2(400, 25); + surface->flags |= TELY_EntityFlag_Clickable; + surface->flags |= TELY_EntityFlag_MoveByKeyboard; + surface->flags |= TELY_EntityFlag_MoveByMouse; + freya_gm->lec01_task02_surface = surface->handle; + + TELY_GameShape *rect = Dqn_FArray_Make(&surface->shapes, Dqn_ZeroMem_Yes); + rect->type = TELY_GameShapeType_Rect; + rect->p2 = surface->local_hit_box_size; + rect->colour = TELY_COLOUR_BLUE_CADET_V4; + } + } + + game->camera.scale = Dqn_V2_InitNx1(1); + game->inter_regular_font = platform->func_load_font(assets, DQN_STRING8("Inter (Regular)"), DQN_STRING8("Data/Inter-Regular.otf"), font_size); + game->inter_italic_font = platform->func_load_font(assets, DQN_STRING8("Inter (Italic)"), DQN_STRING8("Data/Inter-Italic.otf"), font_size); + game->jetbrains_mono_font = platform->func_load_font(assets, DQN_STRING8("JetBrains Mono NL (Regular)"), DQN_STRING8("Data/JetBrainsMonoNL-Regular.ttf"), font_size); + game->test_audio = platform->func_load_audio(assets, DQN_STRING8("Test Audio"), DQN_STRING8("Data/Audio/Purrple Cat - Moonwinds.qoa")); + + // NOTE: TELY audio ============================================================================ + + TELY_Audio *audio = &platform->audio; + audio->chunk_pool = &game->chunk_pool; + + // NOTE: TELY ui =============================================================================== + + TELY_UI *ui = &game->ui; + ui->arena.allocs_are_allowed_to_leak = true; +} + +static void TELY_DLL_FreyaMathForGameDevs(TELY_Platform *platform) +{ + TELY_Assets *assets = &platform->assets; + TELY_Renderer *renderer = &platform->renderer; + TELY_Game *game = DQN_CAST(TELY_Game *) platform->user_data; + TELY_AssetFont *font = TELY_Asset_GetFont(assets, game->jetbrains_mono_font); + Dqn_f32 line_thickness = 2.f; + + TELY_Render_PushFont(renderer, font->handle); + TELY_Render_PushColourV4(renderer, TELY_COLOUR_WHITE_PALE_GOLDENROD_V4); + DQN_DEFER { + TELY_Render_PopFont(renderer); + TELY_Render_PopColourV4(renderer); + }; + + TELY_FreyaGameMath *freya_gm = &game->freya_game_math; + // NOTE: Lecture 01: Numbers, Vectors & Dot Product ============================================ + { + { + // NOTE: Render label onto the group box + TELY_GameEntity const *group_box = TELY_Game_GetEntity(game, freya_gm->lec01_group_box); + Dqn_Rect group_box_world_hit_box = TELY_Game_CalcEntityWorldHitBox(game, group_box->handle); + TELY_Render_Text(renderer, group_box_world_hit_box.pos, Dqn_V2_InitNx2(0.f, 1.f), DQN_STRING8("Numbers, Vectors & Dot Product")); + } + + TELY_GameEntity const *axis = TELY_Game_GetEntity(game, freya_gm->lec01_axis); + TELY_GameEntity const *point_a = TELY_Game_GetEntity(game, freya_gm->lec01_point_a); + TELY_GameEntity const *point_b = TELY_Game_GetEntity(game, freya_gm->lec01_point_b); + + Dqn_V2 const axis_world_pos = TELY_Game_CalcEntityWorldPos(game, axis->handle); + Dqn_V2 const point_a_world_pos = TELY_Game_CalcEntityWorldPos(game, point_a->handle); + Dqn_V2 const point_b_world_pos = TELY_Game_CalcEntityWorldPos(game, point_b->handle); + + // NOTE: Render line from axis -> 'A' + TELY_Render_LineColourV4(renderer, axis_world_pos, point_a_world_pos, TELY_COLOUR_WHITE_PALE_GOLDENROD_V4, line_thickness); + TELY_Render_Text(renderer, point_a_world_pos, Dqn_V2_InitNx1(0.f), DQN_STRING8("A")); + + // NOTE: Render line from axis -> 'B' + TELY_Render_LineColourV4(renderer, axis_world_pos, point_b_world_pos, TELY_COLOUR_BLUE_CADET_V4, line_thickness); + TELY_Render_Text(renderer, point_b_world_pos, Dqn_V2_InitNx1(0.f), DQN_STRING8("B")); + + // NOTE: Normalise point 'A' + Dqn_V2 point_a_norm = Dqn_V2_Normalise(point_a_world_pos - axis_world_pos); + Dqn_V2 point_a_norm_to_axis = axis_world_pos + (point_a_norm * font->pixel_height * 5.f); + + // NOTE: Draw the normalised vector of 'A' + TELY_Render_CircleColourV4(renderer, point_a_norm_to_axis, (point_a->local_hit_box_size.w * .5f), TELY_RenderShapeMode_Fill, TELY_COLOUR_GREEN_DARK_SEA_V4); + TELY_Render_Text(renderer, point_a_norm_to_axis, Dqn_V2_InitNx1(0.5f), DQN_STRING8("A normalised")); + + // NOTE: Draw the scalar projection of 'B' onto 'A' + Dqn_f32 scalar_proj = Dqn_V2_Dot(point_a_norm, point_b_world_pos - axis_world_pos); + TELY_Render_TextF(renderer, + Dqn_V2_InitNx2(axis_world_pos.x, axis_world_pos.y), + Dqn_V2_InitNx1(0.f), + "Scalar Projection: %.2f", scalar_proj); + + // NOTE: Draw the vector projection of 'B' onto 'A' + Dqn_V2 vector_proj = axis_world_pos + (point_a_norm * scalar_proj); + TELY_Render_CircleColourV4(renderer, vector_proj, (point_a->local_hit_box_size.w * .5f), TELY_RenderShapeMode_Fill, TELY_COLOUR_GREEN_DARK_SEA_V4); + TELY_Render_Text(renderer, vector_proj, Dqn_V2_InitNx1(0.5f), DQN_STRING8("Vector Projection")); + + // NOTE: Draw the line projection the vector 'B' onto 'A' + TELY_Render_LineColourV4(renderer, point_b_world_pos, vector_proj, TELY_COLOUR_WHITE_PALE_GOLDENROD_V4, line_thickness); + } + + // NOTE: Lecture 01: Assignment 01 - Radial Trigger ============================================ + { + // NOTE: Render label onto the group box + TELY_GameEntity const *group_box = TELY_Game_GetEntity(game, freya_gm->lec01_task01_group_box); + Dqn_Rect group_box_world_hit_box = TELY_Game_CalcEntityWorldHitBox(game, group_box->handle); + TELY_Render_Text(renderer, group_box_world_hit_box.pos, Dqn_V2_InitNx2(0.f, 1.f), DQN_STRING8("Assignment 01: Radial Trigger")); + + // NOTE: Grab the radial trigger and player + TELY_GameEntity *radial_trigger = TELY_Game_GetEntity(game, freya_gm->lec01_task01_radial_trigger); + TELY_GameEntity *player = TELY_Game_GetEntity(game, freya_gm->lec01_task01_player); + + Dqn_V2 player_world_pos = TELY_Game_CalcEntityWorldPos(game, player->handle); + Dqn_V2 radial_trigger_world_pos = TELY_Game_CalcEntityWorldPos(game, radial_trigger->handle); + + Dqn_f32 player_to_radial_trigger_dist_sq = Dqn_V2_LengthSq_V2x2(player_world_pos, radial_trigger_world_pos); + Dqn_Rect player_bbox = TELY_Game_CalcEntityWorldBoundingBox(game, player->handle); + + // NOTE: Sweep radius with player size (e.g. minkowski sum) + Dqn_f32 trigger_radius = freya_gm->lec01_task01_radial_trigger_radius + (player->local_hit_box_size.w * .5f); + + // NOTE: Check if we trigger the radial trigger + if (player_to_radial_trigger_dist_sq < DQN_SQUARED(trigger_radius)) { + TELY_Render_Text(renderer, + Dqn_V2_InitNx2(player_bbox.pos.x + player_bbox.size.w * .5f, player_bbox.pos.y), + Dqn_V2_InitNx2(0.5f, 1), + DQN_STRING8("!")); + } + } + + // NOTE: Lecture 01: Assignment 02 - Reflect =================================================== + { + // NOTE: Render label onto the group box + TELY_GameEntity const *group_box = TELY_Game_GetEntity(game, freya_gm->lec01_task02_group_box); + Dqn_Rect group_box_world_hit_box = TELY_Game_CalcEntityWorldHitBox(game, group_box->handle); + TELY_Render_Text(renderer, group_box_world_hit_box.pos, Dqn_V2_InitNx2(0.f, 1.f), DQN_STRING8("Assignment 02: Reflect")); + + TELY_GameEntity *ray_begin = TELY_Game_GetEntity(game, freya_gm->lec01_task02_ray_begin); + TELY_GameEntity *ray_end = TELY_Game_GetEntity(game, freya_gm->lec01_task02_ray_end); + TELY_GameEntity *surface = TELY_Game_GetEntity(game, freya_gm->lec01_task02_surface); + + Dqn_V2 ray_begin_world_pos = TELY_Game_CalcEntityWorldPos(game, ray_begin->handle); + Dqn_V2 ray_end_world_pos = TELY_Game_CalcEntityWorldPos(game, ray_end->handle); + Dqn_Rect surface_rect = TELY_Game_CalcEntityWorldBoundingBox(game, surface->handle); + Dqn_V2 rect_x0y0 = Dqn_V2_InitNx2(surface_rect.pos.x, surface_rect.pos.y); + Dqn_V2 rect_x1y0 = Dqn_V2_InitNx2(surface_rect.pos.x + surface_rect.size.w, surface_rect.pos.y); + Dqn_V2 rect_x0y1 = Dqn_V2_InitNx2(surface_rect.pos.x, surface_rect.pos.y + surface_rect.size.h); + Dqn_V2 rect_x1y1 = Dqn_V2_InitNx2(surface_rect.pos.x + surface_rect.size.h, surface_rect.pos.y + surface_rect.size.h); + + Dqn_V2 rect_x0y0_dir = rect_x1y0 - rect_x0y0; + Dqn_V2 ray_dir = ray_end_world_pos - ray_begin_world_pos; + Dqn_RaycastLineIntersectV2Result rect_top_raycast = Dqn_Raycast_LineIntersectV2(ray_begin_world_pos, ray_dir, rect_x0y0, rect_x0y0_dir); + if (rect_top_raycast.hit && rect_top_raycast.t_b >= 0 && rect_top_raycast.t_b <= 1.f && rect_top_raycast.t_a >= 0 && rect_top_raycast.t_a <= 1.f) { + Dqn_V2 intersect_p = rect_x0y0 + (rect_x0y0_dir * rect_top_raycast.t_b); + Dqn_V2 in = intersect_p - ray_begin_world_pos; + Dqn_V2 reflect = Dqn_V2_Reflect(in, rect_x0y0_dir); + TELY_Render_LineColourV4(renderer, ray_begin_world_pos, ray_end_world_pos, TELY_COLOUR_BLUE_CADET_V4, 2.f); + TELY_Render_LineColourV4(renderer, intersect_p, intersect_p + reflect, TELY_COLOUR_BLUE_CADET_V4, 2.f); + } + } +} + +extern "C" __declspec(dllexport) +void TELY_DLL_FrameUpdate(void *user_data) +{ + TELY_Platform *platform = DQN_CAST(TELY_Platform *) user_data; + TELY_PlatformInput *input = &platform->input; + TELY_Assets *assets = &platform->assets; + TELY_Renderer *renderer = &platform->renderer; + TELY_Game *game = DQN_CAST(TELY_Game *) platform->user_data; + TELY_UI *ui = &game->ui; + + TELY_UI_FrameSetup(ui, assets, &platform->frame_arena); + TELY_UI_PushFont(ui, game->jetbrains_mono_font); + + TELY_Render_ClearColourV3(renderer, TELY_COLOUR_BLACK_MIDNIGHT_V4.rgb); + TELY_Render_PushFont(renderer, game->jetbrains_mono_font); + { + Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(nullptr); + Dqn_String8Builder builder = {}; + builder.allocator = scratch.allocator; + + Dqn_String8Builder_AppendF(&builder, "TELY"); + if (Dqn_String8_IsValid(platform->core.os_name)) + Dqn_String8Builder_AppendF(&builder, " | %.*s", DQN_STRING_FMT(platform->core.os_name)); + + Dqn_String8Builder_AppendF(&builder, + " | %dx%d %.1fHz | TSC %.1f GHz", + platform->core.display.size.w, + platform->core.display.size.h, + platform->core.display.refresh_rate, + platform->core.tsc_per_second / 1'000'000'000.0); + + if (platform->core.ram_mb) + Dqn_String8Builder_AppendF(&builder, " | RAM %.1fGB", platform->core.ram_mb / 1024.0); + + Dqn_String8Builder_AppendF(&builder, + " | Work %04.1fms/f (%04.1f%%) | %05.1f FPS | Frame %'I64u | Timer %.1fs", + input->work_ms, + input->work_ms * 100.0 / input->delta_ms, + 1000.0 / input->delta_ms, + input->frame_counter, + input->timer_s); + + + Dqn_String8 info_label = Dqn_String8Builder_Build(&builder, scratch.allocator); + TELY_Render_Text(renderer, /*position*/ Dqn_V2_InitNx1(10), /*align*/ Dqn_V2_InitNx1(0), info_label); + } + + // ============================================================================================= + + game->prev_clicked_entity = game->clicked_entity; + game->prev_hot_entity = game->hot_entity; + game->prev_active_entity = game->active_entity; + game->hot_entity = {}; + game->active_entity = {}; + Dqn_FArray_Clear(&game->parent_entity_stack); + Dqn_FArray_Add(&game->parent_entity_stack, game->root_entity->handle); + + Dqn_M2x3 model_view = {}; + { + Dqn_V2 rotate_origin = game->camera.world_pos - (Dqn_V2_InitV2I(platform->core.window_size) * .5f); + model_view = Dqn_M2x3_Identity(); + model_view = Dqn_M2x3_Mul(model_view, Dqn_M2x3_Translate(rotate_origin)); + model_view = Dqn_M2x3_Mul(model_view, Dqn_M2x3_Rotate(game->camera.rotate_rads)); + model_view = Dqn_M2x3_Mul(model_view, Dqn_M2x3_Scale(game->camera.scale)); + model_view = Dqn_M2x3_Mul(model_view, Dqn_M2x3_Translate((rotate_origin * -1) + game->camera.world_pos)); + TELY_Render_PushTransform(renderer, model_view); + } + + Dqn_V2 world_mouse_p = Dqn_M2x3_MulV2(model_view, input->mouse_p); + + // ============================================================================================= + + TELY_Audio *audio = &platform->audio; + if (audio->playback_size == 0) { + TELY_Audio_Play(audio, game->test_audio, 1.f /*volume*/); + } + + // ============================================================================================= + + if (TELY_Platform_InputKeyWasDown(input->mouse_left) && TELY_Platform_InputKeyIsDown(input->mouse_left)) { + if (game->prev_active_entity.id) + game->active_entity = game->prev_active_entity; + } else { + for (TELY_GameEntityIterator it = {}; TELY_Game_DFSPreOrderWalkEntityTree(game, &it, game->root_entity); ) { + TELY_GameEntity *entity = it.entity; + if (entity->local_hit_box_size.x <= 0 || entity->local_hit_box_size.y <= 0) + continue; + + if ((entity->flags & TELY_EntityFlag_Clickable) == 0) + continue; + + Dqn_Rect world_hit_box = TELY_Game_CalcEntityWorldHitBox(game, entity->handle); + if (!Dqn_Rect_ContainsPoint(world_hit_box, world_mouse_p)) + continue; + + game->hot_entity = entity->handle; + if (TELY_Platform_InputKeyIsPressed(input->mouse_left)) { + game->active_entity = entity->handle; + game->clicked_entity = entity->handle; + } + } + } + + if (TELY_Platform_InputKeyIsReleased(input->mouse_left)) + game->clicked_entity = game->prev_active_entity; + + Dqn_V2 dir_vector = {}; + if (TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_W)) + dir_vector.y = -1.f; + if (TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_A)) + dir_vector.x = -1.f; + if (TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_S)) + dir_vector.y = +1.f; + if (TELY_Platform_InputScanCodeIsDown(input, TELY_PlatformInputScanCode_D)) + dir_vector.x = +1.f; + + if (game->clicked_entity.id) { + if (TELY_Platform_InputScanCodeIsPressed(input, TELY_PlatformInputScanCode_Delete)) + TELY_Game_DeleteEntity(game, game->clicked_entity); + } else { + game->camera.world_pos += dir_vector * 5.f; + } + + for (TELY_GameEntityIterator it = {}; TELY_Game_DFSPostOrderWalkEntityTree(game, &it, game->root_entity); ) { + TELY_GameEntity *entity = it.entity; + entity->alive_time_s += input->delta_s; + + // NOTE: Move entity by keyboard =========================================================== + if (game->clicked_entity == entity->handle) { + if (entity->flags & TELY_EntityFlag_MoveByKeyboard) { + entity->local_pos += dir_vector * DQN_CAST(Dqn_f32)(300.f * input->delta_s); + if (dir_vector.x) + entity->facing_left = dir_vector.x < 0.f; + } + } + + // NOTE: Move entity by mouse ============================================================== + if (game->active_entity == entity->handle && entity->flags & TELY_EntityFlag_MoveByMouse) { + if (entity->flags & TELY_EntityFlag_MoveByMouse) + entity->local_pos += input->mouse_p_delta; + } + + if (entity->flags & TELY_EntityFlag_DeriveHitBoxFromChildrenBoundingBox) { + Dqn_Rect children_bbox = {}; + + // TODO(doyle): Is the hit box supposed to include the containing + // entity itself? Not sure + children_bbox.pos = TELY_Game_CalcEntityWorldPos(game, entity->handle); + + for (TELY_GameEntityIterator child_it = {}; TELY_Game_DFSPreOrderWalkEntityTree(game, &child_it, entity);) { + TELY_GameEntity *child = child_it.entity; + DQN_ASSERT(child != entity); + + Dqn_Rect bbox = TELY_Game_CalcEntityWorldBoundingBox(game, child->handle); + children_bbox = Dqn_Rect_Union(children_bbox, bbox); + } + + Dqn_Rect padded_bbox = Dqn_Rect_Expand(children_bbox, 16.f); + entity->local_hit_box_offset = padded_bbox.pos - entity->local_pos + (padded_bbox.size * .5f); + entity->local_hit_box_size = padded_bbox.size; + } + + // NOTE: Render shapes in entity =========================================================== + Dqn_V2 world_pos = TELY_Game_CalcEntityWorldPos(game, entity->handle); + for (TELY_GameShape const &shape_ : entity->shapes) { + TELY_GameShape const *shape = &shape_; + Dqn_V2 local_to_world_p1 = world_pos + shape->p1; + Dqn_V2 local_to_world_p2 = world_pos + shape->p2; + switch (shape->type) { + case TELY_GameShapeType_None: { + } break; + + case TELY_GameShapeType_Circle: { + TELY_Render_CircleColourV4(renderer, local_to_world_p1, shape->circle_radius, shape->render_mode, shape->colour); + } break; + + case TELY_GameShapeType_Rect: { + Dqn_Rect rect = Dqn_Rect_InitV2x2(local_to_world_p1, local_to_world_p2 - local_to_world_p1); + rect.pos -= rect.size * .5f; + TELY_Render_RectColourV4(renderer, rect, shape->render_mode, shape->colour); + } break; + + case TELY_GameShapeType_Line: { + TELY_Render_LineColourV4(renderer, local_to_world_p1, local_to_world_p2, shape->colour, shape->line_thickness); + } break; + } + } + + // NOTE: Render entity sprites ============================================================= + if (entity->sprite_sheet && entity->sprite_anims.size) { + TELY_AssetSpriteSheet const *sprite_sheet = entity->sprite_sheet; + TELY_AssetSpriteAnimation const *sprite_anim = entity->sprite_anims.data + entity->anim.index; + + Dqn_usize sprite_index = (sprite_anim->index + (entity->anim.frame % sprite_anim->count)) % sprite_sheet->sprite_count; + Dqn_usize sprite_sheet_row = sprite_index / sprite_sheet->sprites_per_row; + Dqn_usize sprite_sheet_column = sprite_index % sprite_sheet->sprites_per_row; + + Dqn_Rect src_rect = {}; + src_rect.pos.x = DQN_CAST(Dqn_f32)(sprite_sheet_column * sprite_sheet->sprite_size.w); + src_rect.pos.y = DQN_CAST(Dqn_f32)(sprite_sheet_row * sprite_sheet->sprite_size.y); + src_rect.size.w = DQN_CAST(Dqn_f32)sprite_sheet->sprite_size.w; + src_rect.size.h = DQN_CAST(Dqn_f32)sprite_sheet->sprite_size.h; + + Dqn_Rect dest_rect = {}; + dest_rect.size = src_rect.size * entity->size_scale; + dest_rect.pos = world_pos - (dest_rect.size * .5f); + + if (entity->facing_left) + dest_rect.size.w *= -1.f; // NOTE: Flip the texture horizontally + + TELY_Render_TextureColourV4(renderer, sprite_sheet->tex_handle, src_rect, dest_rect, TELY_COLOUR_WHITE_V4); + } + + // NOTE: Render world position ============================================================= + TELY_Render_CircleColourV4(renderer, world_pos, 4.f, TELY_RenderShapeMode_Fill, TELY_COLOUR_RED_TOMATO_V4); + + // NOTE: Render hot/active entity ========================================================== + Dqn_Rect world_hit_box = TELY_Game_CalcEntityWorldHitBox(game, entity->handle); + if (game->clicked_entity == entity->handle) { + TELY_Render_RectColourV4(renderer, world_hit_box, TELY_RenderShapeMode_Line, TELY_COLOUR_WHITE_PALE_GOLDENROD_V4); + } else if (game->hot_entity == entity->handle || (entity->flags & TELY_EntityFlag_DrawHitBox)) { + Dqn_V4 hot_colour = game->hot_entity == entity->handle ? TELY_COLOUR_RED_TOMATO_V4 : TELY_Colour_V4Alpha(TELY_COLOUR_YELLOW_SANDY_V4, .5f); + TELY_Render_RectColourV4(renderer, world_hit_box, TELY_RenderShapeMode_Line, hot_colour); + + if (game->hot_entity == entity->handle && (entity->name.size)) { + Dqn_V2 entity_world_pos = TELY_Game_CalcEntityWorldPos(game, entity->handle); + Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(nullptr); + Dqn_String8 label = Dqn_String8_InitF(scratch.allocator, + "%.*s (%.1f, %.1f)", + DQN_STRING_FMT(entity->name), + entity_world_pos.x, + entity_world_pos.y); + TELY_Render_Text(renderer, world_mouse_p, Dqn_V2_InitNx2(0.f, 1), label); + } + } + } + + // ============================================================================================= + + #if 0 + Dqn_Rect entity_ui_layout = Dqn_Rect_InitNx4(game->entity.pos.x, game->entity.pos.y, 0, 0); + TELY_UI_LayoutScope(ui, entity_ui_layout) { + TELY_UI_RectCutSideScope(ui, Dqn_RectCutSide_Bottom) { + TELY_UI_TextF(ui, TELY_UI_GenerateID(), "Anim: %.*s [%I32u, %I32u], Frame %I64u", DQN_STRING_FMT(hero_anim_label), hero_frame_offset, hero_frame_count, hero_frame); + + TELY_UICommand *ticks_per_anim_frame_label = TELY_UI_TextF(ui, TELY_UI_GenerateID(), "Ticks Per Anim Frame: %I32u", hero_ticks_per_anim_frame); + TELY_UI_LayoutScope(ui, ticks_per_anim_frame_label->offcut_rect) { + TELY_UI_RectCutSideScope(ui, Dqn_RectCutSide_Left) { + if (TELY_UI_Button(ui, TELY_UI_GenerateID(), DQN_STRING8("-"))->interacted) { + hero_ticks_per_anim_frame--; + } + if (TELY_UI_Button(ui, TELY_UI_GenerateID(), DQN_STRING8("+"))->interacted) { + hero_ticks_per_anim_frame++; + } + } + } + + TELY_UICommand *button = TELY_UI_Button(ui, TELY_UI_GenerateID(), DQN_STRING8("Apply")); + static char count_hero_frame[3] = {}; + static Dqn_usize count_hero_frame_size = 0; + TELY_UICommand *count_input_box = TELY_UI_InputBox(ui, TELY_UI_GenerateID(), count_hero_frame, sizeof(count_hero_frame), &count_hero_frame_size); + TELY_UI_LayoutScope(ui, count_input_box->offcut_rect) { + TELY_UI_Text(ui, TELY_UI_GenerateID(), DQN_STRING8("Count")); + } + + static char start_hero_frame[3] = {}; + static Dqn_usize start_hero_frame_size = 0; + TELY_UICommand *start_sprite_input_box = TELY_UI_InputBox(ui, TELY_UI_GenerateID(), start_hero_frame, sizeof(start_hero_frame), &start_hero_frame_size); + TELY_UI_LayoutScope(ui, start_sprite_input_box->offcut_rect) { + TELY_UI_Text(ui, TELY_UI_GenerateID(), DQN_STRING8("Start Frame")); + } + + if (button->interacted) { + Dqn_String8ToU64Result start_hero_frame_u64 = Dqn_String8_ToU64(Dqn_String8_Init(start_hero_frame, start_hero_frame_size), /*separator*/ 0); + Dqn_String8ToU64Result count_hero_frame_u64 = Dqn_String8_ToU64(Dqn_String8_Init(count_hero_frame, count_hero_frame_size), /*separator*/ 0); + if (start_hero_frame_u64.success && count_hero_frame_u64.success) { + hero_frame_offset = DQN_CAST(uint32_t)start_hero_frame_u64.value; + hero_frame_count = DQN_CAST(uint32_t)count_hero_frame_u64.value; + } + } + + for (Dqn_usize anim_index = 0; anim_index < DQN_ARRAY_UCOUNT(hero_anims); anim_index++) { + TELY_AssetSpriteAnimation *anim = hero_anims + anim_index; + if (TELY_UI_Button(ui, TELY_UI_ID(__COUNTER__ + 1, DQN_CAST(uint16_t)anim_index), anim->label)->interacted) { + hero_frame_offset = anim->index; + hero_frame_count = anim->count; + hero_anim_label = anim->label; + } + } + } + } + #endif + + + // ============================================================================================= + + // NOTE: Overlay UI + Dqn_ProfilerZone profile_build_ui = Dqn_Profiler_BeginZoneWithIndex(DQN_STRING8("Build UI"), TELY_ProfileZone_BuildUI); + + #if 0 + Dqn_Rect layout = Dqn_Rect_InitNx4(100, 100, DQN_CAST(Dqn_f32)platform->window_size.w, DQN_CAST(Dqn_f32)platform->window_size.h); + TELY_UI_PushRectCutSide(ui, Dqn_RectCutSide_Top); + TELY_UI_PushLayout(ui, layout); + + TELY_UI_StringColourV4Scope(ui, Dqn_V4_InitNx4(0.8f, 0.8f, 0.8f, 1.f)) { + TELY_UI_Text(ui, TELY_UI_GenerateID(), info_label); + TELY_UI_Text(ui, TELY_UI_GenerateID(), info_label); + } + + TELY_UI_InnerPaddingScope(ui, Dqn_V2_InitNx1(0)) { + TELY_UI_OuterPaddingScope(ui, Dqn_V2_InitNx1(0)) { + TELY_UIID container_id = TELY_UI_GenerateID(); + TELY_UI_BoxScope(ui, container_id) { + for (size_t anchor_index = 1; anchor_index < TELY_ProfileZone_Count; anchor_index++) { + Dqn_ProfilerAnchor *anchor = Dqn_Profiler_AnchorBuffer(Dqn_ProfilerAnchorBuffer_Front) + anchor_index; + uint64_t tsc_exclusive = anchor->tsc_exclusive; + uint64_t tsc_inclusive = anchor->tsc_inclusive; + Dqn_f64 tsc_exclusive_percentage_of_frame = tsc_exclusive * 100 / DQN_CAST(Dqn_f64)input->delta_time_tsc; + Dqn_f64 tsc_exclusive_milliseconds = tsc_exclusive * 1000 / DQN_CAST(Dqn_f64)platform->tsc_per_second; + if (tsc_exclusive == tsc_inclusive) { + TELY_UI_TextF(ui, TELY_UI_GenerateID(), "%.*s[%u]: %.1fms (%.1f%%)", + DQN_STRING_FMT(g_tely_profile_zone_names[anchor_index]), + anchor->hit_count, + tsc_exclusive_milliseconds, + tsc_exclusive_percentage_of_frame); + } else { + Dqn_f64 tsc_inclusive_percentage_of_frame = tsc_inclusive * 100 / DQN_CAST(Dqn_f64)input->delta_time_tsc; + Dqn_f64 tsc_inclusive_milliseconds = tsc_inclusive * 1000 / DQN_CAST(Dqn_f64)platform->tsc_per_second; + + TELY_UI_TextF(ui, TELY_UI_GenerateID(), "%.*s[%u]: %.1f/%.1fms (%.1f/%.1f%%)", + DQN_STRING_FMT(g_tely_profile_zone_names[anchor_index]), + anchor->hit_count, + tsc_exclusive_milliseconds, + tsc_inclusive_milliseconds, + tsc_exclusive_percentage_of_frame, + tsc_inclusive_percentage_of_frame); + } + } + } + + TELY_UI_TextF(ui, TELY_UI_GenerateID(), "mouse_p={%.2f, %.2f}", input->mouse_p.x, input->mouse_p.y); + TELY_UI_TextF(ui, TELY_UI_GenerateID(), "mouse_p_delta={%.2f, %.2f}", input->mouse_p_delta.x, input->mouse_p_delta.y); + TELY_UI_TextF(ui, TELY_UI_GenerateID(), "mouse_wheel={%.2f, %.2f}", input->mouse_wheel.x, input->mouse_wheel.y); + + TELY_PlatformInputKey *left = input->scan_codes + TELY_PlatformInputScanCode_Left; + TELY_UI_TextF(ui, TELY_UI_GenerateID(), "left={transition_count=%u}", left->transition_count); + } + } + + if (TELY_UI_Button(ui, TELY_UI_GenerateID(), DQN_STRING8("Hello world, long button"))->interacted) { + Dqn_Log_DebugF("Button 2 clicked!"); + } + + // TODO(doyle): There's some padding issues here + static Dqn_f32 f32_val = {}; + static Dqn_f64 f64_val = {}; + TELY_UI_BeginBox(ui, TELY_UI_GenerateID()); + TELY_UI_RectCutSideScope(ui, Dqn_RectCutSide_Left) { + TELY_UI_DragBoxF32(ui, TELY_UI_GenerateID(), &f32_val); + TELY_UI_TextF(ui, TELY_UI_GenerateID(), "F32: %.1f", f32_val); + } + TELY_UI_EndBox(ui, TELY_UI_GenerateID()); + + TELY_UI_BeginBox(ui, TELY_UI_GenerateID()); + TELY_UI_RectCutSideScope(ui, Dqn_RectCutSide_Left) { + TELY_UI_DragBoxF64(ui, TELY_UI_GenerateID(), &f64_val); + TELY_UI_TextF(ui, TELY_UI_GenerateID(), "F64: %.1f", f64_val); + } + TELY_UI_EndBox(ui, TELY_UI_GenerateID()); + + TELY_UI_Text(ui, TELY_UI_GenerateID(), DQN_STRING8("Draw")); + TELY_UI_BeginBox(ui, TELY_UI_GenerateID()); + TELY_UI_RectCutSideScope(ui, Dqn_RectCutSide_Left) { + TELY_UI_Checkbox(ui, TELY_UI_GenerateID(), &ui->debug_draw_bounding_rect); + TELY_UI_Text(ui, TELY_UI_GenerateID(), DQN_STRING8("Bounding Rect")); + } + TELY_UI_EndBox(ui, TELY_UI_GenerateID()); + + TELY_UI_BeginBox(ui, TELY_UI_GenerateID()); + TELY_UI_RectCutSideScope(ui, Dqn_RectCutSide_Left) { + TELY_UI_Checkbox(ui, TELY_UI_GenerateID(), &ui->debug_draw_clip_rect); + TELY_UI_Text(ui, TELY_UI_GenerateID(), DQN_STRING8("Clip Rect")); + } + TELY_UI_EndBox(ui, TELY_UI_GenerateID()); + + TELY_UI_BeginBox(ui, TELY_UI_GenerateID()); + TELY_UI_RectCutSideScope(ui, Dqn_RectCutSide_Left) { + TELY_UI_Checkbox(ui, TELY_UI_GenerateID(), &ui->debug_draw_content_rect); + TELY_UI_Text(ui, TELY_UI_GenerateID(), DQN_STRING8("Content Rect")); + } + TELY_UI_EndBox(ui, TELY_UI_GenerateID()); + + TELY_UI_BeginBox(ui, TELY_UI_GenerateID()); + TELY_UI_RectCutSideScope(ui, Dqn_RectCutSide_Left) { + TELY_UI_Checkbox(ui, TELY_UI_GenerateID(), &ui->debug_draw_client_rect); + TELY_UI_Text(ui,TELY_UI_GenerateID(), DQN_STRING8("Client Rect")); + } + TELY_UI_EndBox(ui, TELY_UI_GenerateID()); + + static char buffer[16] = "012345678"; + static Dqn_usize buffer_text_size = 9; + TELY_UI_InputBox(ui, TELY_UI_GenerateID(), buffer, sizeof(buffer), &buffer_text_size); + #endif + + #if 0 + // TODO(doyle): Drawing widgets after this window will draw it relative + // to the window's origin. + static Dqn_Rect window_layout = Dqn_Rect_InitNx4(100, 100, 300, 300); + TELY_UI_BeginWindow(ui, TELY_UI_GenerateID(), DQN_STRING8("TSC Profiler"), &window_layout); + // TELY_UI_PushInnerPadding(ui, Dqn_V2_InitNx1(0)); + // TELY_UI_PushOuterPadding(ui, Dqn_V2_InitNx1(0)); + for (size_t anchor_index = 1; anchor_index < TELY_ProfileZone_Count; anchor_index++) { + Dqn_ProfilerAnchor *anchor = Dqn_Profiler_AnchorBuffer(Dqn_ProfilerAnchorBuffer_Front) + anchor_index; + uint64_t tsc_exclusive = anchor->tsc_exclusive; + uint64_t tsc_inclusive = anchor->tsc_inclusive; + Dqn_f64 tsc_exclusive_percentage_of_frame = tsc_exclusive * 100 / DQN_CAST(Dqn_f64)input->delta_time_tsc; + Dqn_f64 tsc_exclusive_milliseconds = tsc_exclusive * 1000 / DQN_CAST(Dqn_f64)platform->tsc_per_second; + Dqn_String8 zone_name = g_tely_profile_zone_names[anchor_index]; + if (tsc_exclusive == tsc_inclusive) { + TELY_UI_TextF(ui, TELY_UI_GenerateID(), "%.*s[%u]: %.1fms (%.1f%%)", + DQN_STRING_FMT(zone_name), + anchor->hit_count, + tsc_exclusive_milliseconds, + tsc_exclusive_percentage_of_frame); + } else { + Dqn_f64 tsc_inclusive_percentage_of_frame = tsc_inclusive * 100 / DQN_CAST(Dqn_f64)input->delta_time_tsc; + Dqn_f64 tsc_inclusive_milliseconds = tsc_inclusive * 1000 / DQN_CAST(Dqn_f64)platform->tsc_per_second; + + TELY_UI_TextF(ui, TELY_UI_GenerateID(), "%.*s[%u]: %.1f/%.1fms (%.1f/%.1f%%)", + DQN_STRING_FMT(zone_name), + anchor->hit_count, + tsc_exclusive_milliseconds, + tsc_inclusive_milliseconds, + tsc_exclusive_percentage_of_frame, + tsc_inclusive_percentage_of_frame); + } + } + // TELY_UI_PopInnerPadding(ui); + // TELY_UI_PopOuterPadding(ui); + TELY_UI_EndWindow(ui, TELY_UI_GenerateID()); + TELY_UI_Flush(ui, platform, renderer, frame_time); + #endif + + // ============================================================================================= + + TELY_RFui *rfui = &game->rfui; + TELY_RFui_FrameSetup(rfui, &platform->frame_arena); + TELY_RFui_PushFont(rfui, game->jetbrains_mono_font); + #if 0 + + TELY_RFuiResult row = TELY_RFui_Row(rfui, DQN_STRING8("Row ID")); + TELY_RFui_PushParent(rfui, row.widget); + if (TELY_RFui_Button(rfui, DQN_STRING8("File")).clicked) { + Dqn_Log_DebugF("Hello Seaman"); + } + if (TELY_RFui_Button(rfui, DQN_STRING8("Window")).clicked) { + Dqn_Log_DebugF("Hello Seaman"); + } + if (TELY_RFui_Button(rfui, DQN_STRING8("Panel")).clicked) { + Dqn_Log_DebugF("Hello Seaman"); + } + TELY_RFui_PopParent(rfui); + #endif + + // ============================================================================================= + + TELY_DLL_FreyaMathForGameDevs(platform); + + // ============================================================================================= + + TELY_Audio_MixPlaybackSamples(audio, assets); + + // TELY_RFui_Flush(rfui, renderer, input, assets); + Dqn_Profiler_EndZone(profile_build_ui); +} diff --git a/playground_game.cpp b/playground_game.cpp new file mode 100644 index 0000000..37e2900 --- /dev/null +++ b/playground_game.cpp @@ -0,0 +1,423 @@ +#if defined(__clang__) +#pragma once +#include "playground_unity.h" +#endif + +static bool operator==(TELY_GameEntityHandle const &lhs, TELY_GameEntityHandle const &rhs) +{ + bool result = lhs.id == rhs.id; + return result; +} + +static bool operator!=(TELY_GameEntityHandle const &lhs, TELY_GameEntityHandle const &rhs) +{ + bool result = !(lhs == rhs); + return result; +} + +static Dqn_M2x3 TELY_Game_CameraModelViewM2x3(TELY_GameCamera camera, TELY_Platform *platform) +{ + Dqn_M2x3 result = Dqn_M2x3_Identity(); + if (platform) { + Dqn_V2 rotate_origin = camera.world_pos - (Dqn_V2_InitV2I(platform->core.window_size) * .5f); + result = Dqn_M2x3_Mul(result, Dqn_M2x3_Translate(rotate_origin)); + result = Dqn_M2x3_Mul(result, Dqn_M2x3_Rotate(camera.rotate_rads)); + result = Dqn_M2x3_Mul(result, Dqn_M2x3_Scale(camera.scale)); + result = Dqn_M2x3_Mul(result, Dqn_M2x3_Translate((rotate_origin * -1) + camera.world_pos)); + } + return result; +} + +static TELY_GameEntity *TELY_Game_GetEntity(TELY_Game *game, TELY_GameEntityHandle handle) +{ + TELY_GameEntity *result = nullptr; + if (!game) + return result; + + result = game->entities.data; + uint64_t index_from_handle = handle.id & TELY_GAME_ENTITY_HANDLE_INDEX_MASK; + if (index_from_handle >= game->entities.size) + return result; + + TELY_GameEntity *candidate = game->entities.data + index_from_handle; + if (candidate->handle == handle) + result = candidate; + return result; +} + +static bool TELY_Game_DFSPreOrderWalkEntityTree(TELY_Game *game, TELY_GameEntityIterator *it, TELY_GameEntity *root) +{ + if (!game || !it || !root) + return false; + + it->last_visited = it->entity; + if (it->init) { + it->iteration_count++; + } else { + it->init = true; + it->entity = root; + it->entity_parent = it->entity->parent; + it->entity_next = it->entity->next; + it->entity_first_child = it->entity->first_child; + } + + if (it->entity_first_child) { + it->entity = it->entity_first_child; + it->entity_parent = it->entity->parent; + it->entity_next = it->entity->next; + it->entity_first_child = it->entity->first_child; + } else { + while (it->entity->handle != root->handle) { + if (it->entity_next) { + it->entity = it->entity_next; + it->entity_parent = it->entity->parent; + it->entity_next = it->entity->next; + it->entity_first_child = it->entity->first_child; + break; + } else { + if (!it->entity_parent) + break; + it->entity = it->entity_parent; + it->entity_parent = it->entity->parent; + it->entity_next = it->entity->next; + it->entity_first_child = it->entity->first_child; + } + } + } + + return it->entity->handle != root->handle; +} + +static bool TELY_Game_DFSPostOrderWalkEntityTree(TELY_Game *game, TELY_GameEntityIterator *it, TELY_GameEntity *root) +{ + if (!game || !it || !root) + return false; + + bool ascending_tree = it->entity ? (it->last_visited == it->entity->last_child) : false; + it->last_visited = it->entity; + + if (it->init) { + it->iteration_count++; + } else { + it->init = true; + it->entity = root; + it->entity_parent = it->entity->parent; + it->entity_next = it->entity->next; + it->entity_first_child = it->entity->first_child; + } + + // NOTE: Descend to deepest leaf node + if (it->entity_first_child && !ascending_tree) { + while (it->entity_first_child) { + it->entity = it->entity_first_child; + it->entity_parent = it->entity->parent; + it->entity_next = it->entity->next; + it->entity_first_child = it->entity->first_child; + } + } else { + // NOTE: We are at the leaf node, try going across + if (it->entity != root && it->entity_next) { + it->entity = it->entity_next; + it->entity_parent = it->entity->parent; + it->entity_next = it->entity->next; + it->entity_first_child = it->entity->first_child; + ascending_tree = false; + } + + // NOTE: Try descend again + if (it->entity_first_child && !ascending_tree) { + while (it->entity_first_child) { + it->entity = it->entity_first_child; + it->entity_parent = it->entity->parent; + it->entity_next = it->entity->next; + it->entity_first_child = it->entity->first_child; + } + } + + // NOTE: If we could not move further across or down then we've + // exhausted the tree, start moving up. + if (it->last_visited == it->entity) { + it->entity = it->entity_parent; + it->entity_parent = it->entity->parent; + it->entity_next = it->entity->next; + it->entity_first_child = it->entity->first_child; + } + } + + return it->entity->handle != root->handle; +} + + +// NOTE: Parent entity +static void TELY_Game_PushParentEntity(TELY_Game *game, TELY_GameEntityHandle handle) +{ + DQN_ASSERTF(game->parent_entity_stack.size >= 1, "Sentinel/nil entity has not been assigned as the 0th slot yet"); + if (game) + Dqn_FArray_Add(&game->parent_entity_stack, handle); +} + +static void TELY_Game_PopParentEntity(TELY_Game *game) +{ + // NOTE: 0th slot is reserved for the nil entity + if (game && game->parent_entity_stack.size > 1) + Dqn_FArray_PopBack(&game->parent_entity_stack, 1); +} + +static TELY_GameEntityHandle TELY_Game_ActiveParentEntity(TELY_Game const *game) +{ + TELY_GameEntityHandle result = {}; + if (!game || !game->parent_entity_stack.size) + return result; + result = game->parent_entity_stack.data[game->parent_entity_stack.size - 1]; + return result; +} + +static TELY_GameEntity *TELY_Game_ActiveParentEntityPointer(TELY_Game const *game) +{ + TELY_GameEntityHandle handle = TELY_Game_ActiveParentEntity(game); + TELY_GameEntity *result = TELY_Game_GetEntity(DQN_CAST(TELY_Game *)game, handle); + return result; +} + +static TELY_GameEntity *TELY_Game_MakeEntityPointerFV(TELY_Game *game, DQN_FMT_STRING_ANNOTATE char const *fmt, va_list args) +{ + TELY_GameEntity *result = nullptr; + if (!game) + return result; + + DQN_ASSERTF(game->entities.size > 0, "Sentinel/nil entity has not been initialised yet"); + DQN_ASSERTF(game->root_entity, "Sentinel/nil entity has not been assigned yet"); + + result = game->root_entity; // TODO(doyle): Root entity or ... the nil entity? + if (game->entity_free_list) { + result = game->entity_free_list; + game->entity_free_list = game->entity_free_list->next; + result->next = nullptr; + } else { + if (game->entities.size >= (TELY_GAME_ENTITY_HANDLE_INDEX_MAX + 1)) + return result; + + result = Dqn_VArray_Make(&game->entities, 1, Dqn_ZeroMem_Yes); + if (!result) + return result; + result->handle.id = (game->entities.size - 1) & TELY_GAME_ENTITY_HANDLE_INDEX_MASK; + } + + result->size_scale = Dqn_V2_InitNx1(1); + result->parent = TELY_Game_ActiveParentEntityPointer(game); + result->name = TELY_ChunkPool_AllocFmtFV(&game->chunk_pool, fmt, args); + result->waypoints = TELY_ChunkPool_New(&game->chunk_pool, TELY_GameWaypoint); + result->waypoints->next = result->waypoints; + result->waypoints->prev = result->waypoints; + + // NOTE: Attach entity as a child to the parent + TELY_GameEntity *parent = result->parent; + if (parent->first_child) + parent->last_child->next = result; + else + parent->first_child = result; + result->prev = parent->last_child; + parent->last_child = result; + + DQN_ASSERT(!result->next); + DQN_ASSERT(result->handle.id); + DQN_ASSERT(result->parent->handle == game->parent_entity_stack.data[game->parent_entity_stack.size - 1]); + return result; +} + +static TELY_GameEntity *TELY_Game_MakeEntityPointerF(TELY_Game *game, DQN_FMT_STRING_ANNOTATE char const *fmt, ...) +{ + va_list args; + va_start(args, fmt); + TELY_GameEntity *result = TELY_Game_MakeEntityPointerFV(game, fmt, args); + va_end(args); + return result; +} + +static TELY_GameEntityHandle TELY_Game_MakeEntityF(TELY_Game *game, DQN_FMT_STRING_ANNOTATE char const *fmt, ...) +{ + va_list args; + va_start(args, fmt); + TELY_GameEntity *entity = TELY_Game_MakeEntityPointerF(game, fmt, args); + va_end(args); + + TELY_GameEntityHandle result = {}; + if (entity) + result = entity->handle; + return result; +} + +static bool TELY_Game_IsNilEntity(TELY_GameEntity *entity) +{ + bool result = entity ? ((entity->handle.id & TELY_GAME_ENTITY_HANDLE_INDEX_MASK) == 0) : true; + return result; +} + +static void TELY_Game_DetachEntityIntoFreeList(TELY_Game *game, TELY_GameEntityHandle handle) +{ + TELY_GameEntity *entity = TELY_Game_GetEntity(game, handle); + if (TELY_Game_IsNilEntity(entity)) + return; + + // NOTE: Entities in the entity tree always have a parent (except for the + // nil/root entity). If an entity is passed in to this function and there's + // no parent, it's most likely you passed in an entity already in the free + // list (in which case only the next pointer will be set). This is most + // likely a mistake so we guard against it here. + if (!DQN_CHECK(entity->parent)) + return; + + uint64_t const entity_index_from_handle = entity->handle.id & TELY_GAME_ENTITY_HANDLE_INDEX_MASK; + DQN_ASSERT(entity_index_from_handle < game->entities.size); + + uint64_t const entity_generation_raw = entity->handle.id & TELY_GAME_ENTITY_HANDLE_GENERATION_MASK; + uint64_t const entity_generation = entity_generation_raw >> TELY_GAME_ENTITY_HANDLE_GENERATION_RSHIFT; + uint64_t const new_entity_generation = entity_generation + 1; + + // NOTE: De-attach entity from adjacent children + if (entity->prev) + entity->prev->next = entity->next; + + if (entity->next) + entity->next->prev = entity->prev; + + // NOTE: De-attach from parent + TELY_GameEntity *parent = entity->parent; + if (parent->first_child == entity) + parent->first_child = entity->next; + + if (parent->last_child == entity) + parent->last_child = entity->prev; + + if (entity->name.size) + TELY_ChunkPool_Dealloc(&game->chunk_pool, entity->name.data); + + if (new_entity_generation > entity_generation) { + // NOTE: Update the incremented handle disassociating all prior handles + // to this entity which would reference older generation values + *entity = {}; + entity->handle.id = entity_index_from_handle | (new_entity_generation << TELY_GAME_ENTITY_HANDLE_GENERATION_RSHIFT); + + // NOTE: Attach entity to the free list + entity->next = game->entity_free_list; + entity->prev = nullptr; + game->entity_free_list = entity; + } else { + // NOTE: We've cycled through all possible generations for this handle + // We will not increment it and freeze it so it is no longer allocated + // out. This prevents code that is still holding onto *really* old + // handles + } +} + +static void TELY_Game_DeleteEntity(TELY_Game *game, TELY_GameEntityHandle handle) +{ + uint64_t index_from_handle = handle.id & TELY_GAME_ENTITY_HANDLE_INDEX_MASK; + if (!game || !DQN_CHECK(index_from_handle < game->entities.size)) + return; + + TELY_GameEntity *root = game->entities.data + index_from_handle; + if (root->handle != handle) + return; + + // NOTE: The iterator snaps a copy of all the internal n-ary tree pointers + // so as we delete we do not accidentally invalidate any of the pointers. + for (TELY_GameEntityIterator it = {}; TELY_Game_DFSPostOrderWalkEntityTree(game, &it, root); ) { + DQN_ASSERT(it.entity != root); + TELY_GameEntity *entity = it.entity; + TELY_Game_DetachEntityIntoFreeList(game, entity->handle); + } + + TELY_Game_DetachEntityIntoFreeList(game, root->handle); +} + +static Dqn_V2 TELY_Game_CalcEntityWorldPos(TELY_Game const *game, TELY_GameEntityHandle handle) +{ + Dqn_V2 result = {}; + if (!game) + return result; + + for (TELY_GameEntity const *entity = TELY_Game_GetEntity(DQN_CAST(TELY_Game *) game, handle); + entity != game->root_entity; + entity = entity->parent) { + result += entity->local_pos; + } + return result; +} + +static Dqn_Rect TELY_Game_CalcEntityLocalHitBox(TELY_Game const *game, TELY_GameEntityHandle handle) +{ + TELY_GameEntity *entity = TELY_Game_GetEntity(DQN_CAST(TELY_Game *)game, handle); + Dqn_V2 half_hit_box_size = entity->local_hit_box_size * .5f; + Dqn_Rect result = Dqn_Rect_InitV2x2(entity->local_hit_box_offset - half_hit_box_size, entity->local_hit_box_size); + return result; +} + +static Dqn_Rect TELY_Game_CalcEntityWorldHitBox(TELY_Game const *game, TELY_GameEntityHandle handle) +{ + TELY_GameEntity *entity = TELY_Game_GetEntity(DQN_CAST(TELY_Game *) game, handle); + Dqn_V2 world_pos = TELY_Game_CalcEntityWorldPos(game, handle); + Dqn_Rect local_hit_box = TELY_Game_CalcEntityLocalHitBox(game, entity->handle); + Dqn_Rect result = Dqn_Rect_InitV2x2(world_pos + local_hit_box.pos, local_hit_box.size); + return result; +} + +static Dqn_Rect TELY_Game_CalcEntityAttackWorldHitBox(TELY_Game const *game, TELY_GameEntityHandle handle) +{ + TELY_GameEntity *entity = TELY_Game_GetEntity(DQN_CAST(TELY_Game *) game, handle); + Dqn_V2 world_pos = TELY_Game_CalcEntityWorldPos(game, handle); + Dqn_V2 half_hit_box_size = entity->attack_box_size * .5f; + Dqn_Rect result = Dqn_Rect_InitV2x2(world_pos + entity->attack_box_offset - half_hit_box_size, entity->attack_box_size); + return result; +} + +static Dqn_Rect TELY_Game_CalcEntityArrayWorldBoundingBox(TELY_Game const *game, TELY_GameEntityHandle const *handles, Dqn_usize count) +{ + Dqn_Rect result = {}; + if (!game || !handles) + return result; + + DQN_FOR_UINDEX(index, count) { + TELY_GameEntityHandle handle = handles[index]; + TELY_GameEntity const *entity = TELY_Game_GetEntity(DQN_CAST(TELY_Game *) game, handle); + Dqn_Rect bbox = TELY_Game_CalcEntityLocalHitBox(game, entity->handle); + for (TELY_GameShape const &shape_ : entity->shapes) { + TELY_GameShape const *shape = &shape_; + switch (shape->type) { + case TELY_GameShapeType_None: { + } break; + + case TELY_GameShapeType_Circle: { + Dqn_Rect rect = + Dqn_Rect_InitV2x2(shape->p1 - shape->circle_radius, Dqn_V2_InitNx1(shape->circle_radius * 2.f)); + bbox = Dqn_Rect_Union(bbox, rect); + } break; + + case TELY_GameShapeType_Rect: /*FALLTHRU*/ + case TELY_GameShapeType_Line: { + Dqn_V2 min = Dqn_V2_Min(shape->p1, shape->p2); + Dqn_V2 max = Dqn_V2_Max(shape->p1, shape->p2); + Dqn_Rect rect = Dqn_Rect_InitV2x2(min, max - min); + + if (shape->type == TELY_GameShapeType_Rect) + rect.pos -= rect.size * .5f; + + bbox = Dqn_Rect_Union(bbox, rect); + } break; + } + } + bbox.pos += TELY_Game_CalcEntityWorldPos(game, entity->handle); + + if (index) + result = Dqn_Rect_Union(result, bbox); + else + result = bbox; + } + return result; +} + +static Dqn_Rect TELY_Game_CalcEntityWorldBoundingBox(TELY_Game *game, TELY_GameEntityHandle handle) +{ + Dqn_Rect result = TELY_Game_CalcEntityArrayWorldBoundingBox(game, &handle, 1); + return result; +} diff --git a/playground_game.h b/playground_game.h new file mode 100644 index 0000000..db125f7 --- /dev/null +++ b/playground_game.h @@ -0,0 +1,171 @@ +#if defined(__clang__) +#pragma once +#include "playground_unity.h" +#endif + +enum TELY_EntityFlag +{ + TELY_EntityFlag_Clickable = 1 << 0, + TELY_EntityFlag_MoveByKeyboard = 1 << 1, + TELY_EntityFlag_MoveByMouse = 1 << 2, + TELY_EntityFlag_DrawHitBox = 1 << 3, + TELY_EntityFlag_DeriveHitBoxFromChildrenBoundingBox = 1 << 4, +}; + +enum TELY_GameShapeType +{ + TELY_GameShapeType_None, + TELY_GameShapeType_Circle, + TELY_GameShapeType_Rect, + TELY_GameShapeType_Line, +}; + +struct TELY_GameShape +{ + TELY_GameShapeType type; + Dqn_V2 p1; + Dqn_V2 p2; + Dqn_V4 colour; + Dqn_f32 line_thickness; + Dqn_f32 circle_radius; + TELY_RenderShapeMode render_mode; +}; + +const uint64_t TELY_GAME_ENTITY_HANDLE_GENERATION_MASK = 0xFFFF'0000'0000'0000; +const uint64_t TELY_GAME_ENTITY_HANDLE_GENERATION_RSHIFT = 48; +const uint64_t TELY_GAME_ENTITY_HANDLE_GENERATION_MAX = TELY_GAME_ENTITY_HANDLE_GENERATION_MASK >> TELY_GAME_ENTITY_HANDLE_GENERATION_RSHIFT; + +const uint64_t TELY_GAME_ENTITY_HANDLE_INDEX_MASK = 0x0000'FFFF'FFFF'FFFF; +const uint64_t TELY_GAME_ENTITY_HANDLE_INDEX_MAX = TELY_GAME_ENTITY_HANDLE_INDEX_MASK - 1; +struct TELY_GameEntityHandle +{ + uint64_t id; +}; + +enum TELY_GameEntityState +{ + TELY_GameEntityState_Nil, + TELY_GameEntityState_Idle, + TELY_GameEntityState_Attack, + TELY_GameEntityState_Run, +}; + +struct TELY_GameEntityAnimation +{ + uint16_t index; + uint16_t frame; + uint16_t ticks; + uint16_t ticks_per_frame; +}; + +struct TELY_GameWaypoint +{ + Dqn_V2I pos; + TELY_GameWaypoint *next; + TELY_GameWaypoint *prev; +}; + +struct TELY_GameEntity +{ + Dqn_String8 name; + TELY_GameEntityHandle handle; + TELY_AssetSpriteSheet *sprite_sheet; + Dqn_Slice sprite_anims; + Dqn_V2 size_scale; + TELY_GameEntityAnimation anim; + TELY_GameEntityState state; + Dqn_V2 velocity; + + TELY_GameEntityHandle stalk_entity; + Dqn_V2I stalk_entity_last_known_tile; + TELY_GameWaypoint *waypoints; + + // NOTE: The entity hit box is positioned at the center of the entity. + Dqn_V2 local_hit_box_size; + Dqn_V2 local_hit_box_offset; + + Dqn_V2 attack_box_size; + Dqn_V2 attack_box_offset; + + uint64_t flags; + bool facing_left; + Dqn_V2 local_pos; + Dqn_f64 alive_time_s; + Dqn_FArray shapes; + TELY_GameEntity *next; + TELY_GameEntity *prev; + TELY_GameEntity *first_child; + TELY_GameEntity *last_child; + TELY_GameEntity *parent; +}; + +struct TELY_GameEntityIterator +{ + bool init; + Dqn_usize iteration_count; + + TELY_GameEntity *entity; + TELY_GameEntity *last_visited; + + TELY_GameEntity *entity_parent; + TELY_GameEntity *entity_next; + TELY_GameEntity *entity_first_child; +}; + +struct TELY_FreyaGameMath +{ + TELY_GameEntityHandle lec01_group_box; + TELY_GameEntityHandle lec01_axis; + TELY_GameEntityHandle lec01_point_a; + TELY_GameEntityHandle lec01_point_b; + + TELY_GameEntityHandle lec01_task01_group_box; + TELY_GameEntityHandle lec01_task01_radial_trigger; + Dqn_f32 lec01_task01_radial_trigger_radius; + TELY_GameEntityHandle lec01_task01_player; + + TELY_GameEntityHandle lec01_task02_group_box; + TELY_GameEntityHandle lec01_task02_ray_begin; + TELY_GameEntityHandle lec01_task02_ray_end; + TELY_GameEntityHandle lec01_task02_surface; +}; + +struct TELY_GameCamera +{ + Dqn_V2 world_pos; + Dqn_f32 rotate_rads; + Dqn_V2 scale; +}; + +struct TELY_Game +{ + TELY_Platform *platform; + TELY_RFui rfui; + TELY_UI ui; + TELY_ChunkPool chunk_pool; + TELY_AssetFontHandle inter_regular_font; + TELY_AssetFontHandle inter_italic_font; + TELY_AssetFontHandle jetbrains_mono_font; + TELY_AssetAudioHandle test_audio; + + Dqn_Slice hero_sprite_anims; + TELY_AssetSpriteSheet hero_sprite_sheet; + + Dqn_FArray parent_entity_stack; + Dqn_VArray entities; + TELY_GameEntity *root_entity; + TELY_GameEntity *entity_free_list; + + TELY_GameEntityHandle clicked_entity; + TELY_GameEntityHandle hot_entity; + TELY_GameEntityHandle active_entity; + + TELY_GameEntityHandle prev_clicked_entity; + TELY_GameEntityHandle prev_hot_entity; + TELY_GameEntityHandle prev_active_entity; + + TELY_FreyaGameMath freya_game_math; + + TELY_GameCamera camera; + +}; diff --git a/playground_unity.cpp b/playground_unity.cpp new file mode 100644 index 0000000..c577ff8 --- /dev/null +++ b/playground_unity.cpp @@ -0,0 +1 @@ +#include "playground_game.h" diff --git a/playground_unity.h b/playground_unity.h new file mode 100644 index 0000000..03ad373 --- /dev/null +++ b/playground_unity.h @@ -0,0 +1,67 @@ +// ================================================================================================= + +// NOTE(doyle): Work-around for clangd to correctly resolve symbols in unity +// builds by providing symbol definition and prototypes by including this +// mega-header in all files and using the "#pragma once" directive to +// avoid multiple defined symbols when compiling the unity build itself. +// +// See: https://www.frogtoss.com/labs/clangd-with-unity-builds.html + +#pragma once + +// NOTE: DQN ======================================================================================= + +// NOTE: C-strings declared in a ternary cause global-buffer-overflow in +// MSVC2022. +// stb_sprintf assumes c-string literals are 4 byte aligned which is always +// true, however, reading past the end of a string whose size is not a multiple +// of 4 is UB causing ASAN to complain. +#if defined(_MSC_VER) && !defined(__clang__) + #define STBSP__ASAN __declspec(no_sanitize_address) +#endif + +#define DQN_ASAN_POISON 0 +#define DQN_ASAN_VET_POISON 1 +#define DQN_ONLY_RECT +#define DQN_ONLY_V2 +#define DQN_ONLY_V3 +#define DQN_ONLY_V4 +#define DQN_ONLY_WIN +#define DQN_ONLY_FARRAY +#define DQN_ONLY_PROFILER +#define DQN_ONLY_SLICE +#define DQN_ONLY_LIST +#define DQN_ONLY_VARRAY +#define DQN_ONLY_FS +#define _CRT_SECURE_NO_WARNINGS +#define DQN_IMPLEMENTATION +#include "External/tely/External/dqn/dqn.h" + +// NOTE: TELY ====================================================================================== + +DQN_MSVC_WARNING_DISABLE(4505) // warning C4505: unreferenced function with internal linkage has been removed +#include "External/tely/tely_profile.h" +#include "External/tely/tely_platform_input.h" +#include "External/tely/tely_asset.h" +#include "External/tely/tely_colour.h" +#include "External/tely/tely_render.h" +#include "External/tely/tely_tools.h" +#include "External/tely/tely_audio.h" +#include "External/tely/tely_platform.h" +#include "External/tely/tely_ui.h" +#include "External/tely/tely_rfui.h" + +#include "External/tely/tely_tools.cpp" +#include "External/tely/tely_asset.cpp" +#include "External/tely/tely_audio.cpp" +#include "External/tely/tely_render.cpp" +#include "External/tely/tely_platform_input.cpp" +#include "External/tely/tely_ui.cpp" +#include "External/tely/tely_rfui.cpp" + +// NOTE: PExternal/tely/layground ============================================================================ + +#include "playground_game.h" + +#include "playground_game.cpp" +#include "playground.cpp"