diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 5143e587..57ad2364 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -22,6 +22,35 @@ jobs: os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: + - name: Cache CMake dependency source code + uses: actions/cache@v2 + env: + cache-name: cache-cmake-dependency-sources + with: + # CMake cache is at ${{github.workspace}}/build/_deps but we only will cache folders ending in '-src' to cache source code + path: ${{github.workspace}}/build/_deps/*-src + # Cache hash is dependent on CMakeLists files anywhere as these can change what's in the cache, as well as cmake modules files + key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} + # it's acceptable to reuse caches for different CMakeLists content if exact match is not available and unlike build caches, we + # don't need to restrict these by OS or compiler as it's only source code that's being cached + restore-keys: | + ${{ env.cache-name }}- + + - name: Cache CMake dependency build objects + uses: actions/cache@v2 + env: + cache-name: cache-cmake-dependency-builds + with: + # CMake cache is at ${{github.workspace}}/build/_deps but we only care about the folders ending in -build or -subbuild + path: | + ${{github.workspace}}/build/_deps/*-build + ${{github.workspace}}/build/_deps/*-subbuild + # Cache hash is dependent on CMakeLists files anywhere as these can change what's in the cache, as well as cmake modules files + key: ${{ env.cache-name }}-${{ matrix.os }}-${{ matrix.cxx }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} + # it's acceptable to reuse caches for different CMakeLists content if exact match is not available + # as long as the OS and Compiler match exactly + restore-keys: | + ${{ env.cache-name }}-${{ matrix.os }}-${{ matrix.cxx }}- - name: Using the builtin GitHub Cache Action for .conan id: github-cache-conan uses: actions/cache@v2 diff --git a/CMakeLists.txt b/CMakeLists.txt index ebda3800..e3aa2362 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,12 +10,11 @@ include(cmake/stacktrace.cmake) include(cmake/setup_cpm.cmake) conan_cmake_configure(REQUIRES - catch2/3.4.0 sdl/2.28.3 sdl_image/2.6.3 sdl_ttf/2.20.2 - fmt/10.1.1 - entt/3.12.2 + #fmt/10.1.1 + #entt/3.12.2 libpng/1.6.42 #resolve conflicts GENERATORS CMakeDeps CMakeToolchain ) @@ -25,15 +24,47 @@ conan_cmake_install(PATH_OR_REFERENCE . BUILD missing REMOTE conancenter SETTINGS ${settings}) +CPMAddPackage( + NAME fmt + GITHUB_REPOSITORY "fmtlib/fmt" + GIT_TAG 10.2.1 +) +CPMAddPackage( + NAME EnTT + GITHUB_REPOSITORY "skypjack/entt" + GIT_TAG v3.13.1 +) CPMAddPackage( - NAME sdlpp + NAME sdlpp GITHUB_REPOSITORY "mika314/sdlpp" GIT_TAG HEAD #OPTIONS USE_SDLGFX ) + +CPMAddPackage( + NAME cereal + + GITHUB_REPOSITORY "USCiLab/cereal" + GIT_TAG HEAD + OPTIONS "SKIP_PERFORMANCE_COMPARISON ON" "BUILD_SANDBOX OFF" +) + +CPMAddPackage( + NAME Catch2 + + GITHUB_REPOSITORY "catchorg/Catch2" + GIT_TAG v3.5.2 + OPTIONS + "CATCH_BUILD_TESTING OFF" + "CATCH_BUILD_EXAMPLES OFF" + "CATCH_BUILD_EXTRA_TESTS OFF" + "CATCH_BUILD_FUZZERS OFF" +) + + enable_testing() add_subdirectory(libs) add_subdirectory(apps) diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index 0505bf93..54913934 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -5,3 +5,4 @@ add_subdirectory(asteroids) add_subdirectory(textdemo) add_subdirectory(stellarfield) add_subdirectory(galaxy) +add_subdirectory(serialization) diff --git a/apps/serialization/CMakeLists.txt b/apps/serialization/CMakeLists.txt new file mode 100644 index 00000000..27098116 --- /dev/null +++ b/apps/serialization/CMakeLists.txt @@ -0,0 +1,29 @@ +project(serialization) + +set(CMAKE_CXX_STANDARD 23) +file(GLOB_RECURSE HEADER_FILES CONFIGURE_DEPENDS "*.h*") +file(GLOB_RECURSE CPP_FILES CONFIGURE_DEPENDS "*.cpp") + + +add_executable(${PROJECT_NAME} ${HEADER_FILES} ${CPP_FILES} ) + +find_package(fmt REQUIRED) +find_package(cereal REQUIRED) +find_package(EnTT REQUIRED) + +target_link_libraries(${PROJECT_NAME} + PUBLIC + pgEngine::pgEngine + fmt::fmt + cereal::cereal + EnTT::EnTT +) +target_include_directories(${PROJECT_NAME} + PRIVATE + $ + $ +) + +enable_coverage(${PROJECT_NAME}) + +install(TARGETS ${PROJECT_NAME}) \ No newline at end of file diff --git a/apps/serialization/customLoading.hpp b/apps/serialization/customLoading.hpp new file mode 100644 index 00000000..74d08b6d --- /dev/null +++ b/apps/serialization/customLoading.hpp @@ -0,0 +1,178 @@ +#include +#include + +#include +#include +#include +#include +#include + +struct IdMetaAny +{ + entt::id_type id; + entt::meta_any any; + + template + void save(Ar& ar) const + { + ar(id); + any.invoke(entt::hashed_string("save"), entt::forward_as_meta(ar)); + } + + template + void load(Ar& ar) + { + ar(id); + any = entt::resolve(id).construct(); + any.invoke(entt::hashed_string("load"), entt::forward_as_meta(ar)); + } +}; + +template +void move_emplace(Component& elem, entt::sparse_set& storage, entt::entity entity) +{ + static_cast&>(storage).emplace(entity, std::move(elem)); +} + +template +void RegisterComponentForSerialize() +{ + using namespace entt::literals; + auto&& f = entt::meta().template func<&move_emplace>("emplace"_hs); + f = f.type(entt::type_id().hash()); + f = f.template func<&Component::template save>(entt::hashed_string("save")); + f = f.template func<&Component::template load>(entt::hashed_string("load")); +} + +struct EntityWrapper +{ + entt::registry& registry; + entt::entity handle; +}; + +template +void save(Ar& ar, const EntityWrapper& wrapper) +{ + std::map components; + + for (auto&& [id, storage] : wrapper.registry.storage()) + { + if (!storage.contains(wrapper.handle)) { continue; } + + if (auto type = entt::resolve(id); type) + { + components.emplace(id, IdMetaAny{id, type.from_void(storage.value(wrapper.handle))}); + } + } + ar(components); +} + +template +void load(Ar& ar, EntityWrapper& wrapper) +{ + using namespace entt::literals; + + std::map components; + ar(components); + + for (auto&& [id, storage] : wrapper.registry.storage()) + { + if (auto itr = components.find(id); itr == components.end()) + { + // undo for add component. + storage.remove(wrapper.handle); + } + else if (storage.contains(wrapper.handle)) + { + // undo for update component + auto& any = itr->second.any; + storage.remove(wrapper.handle); + any.type().invoke("emplace"_hs, any, entt::forward_as_meta(storage), wrapper.handle); + } + } + + for (const auto& [id, id_any] : components) + { + if (auto storage = wrapper.registry.storage(id); !storage->contains(wrapper.handle)) + { + // undo for remove component + id_any.any.type().invoke("emplace"_hs, id_any.any, entt::forward_as_meta(storage), wrapper.handle); + } + } +} + +struct Position +{ + double x, y, z; + + template + void save(Ar& ar) const + { + ar(x, y, z); + } + + template + void load(Ar& ar) + { + ar(x, y, z); + } +}; + +void no_entity_wrapper_case() +{ + entt::registry registry; + auto entity = registry.create(); + // initialize + registry.emplace(entity, 1.0, 2.0, 3.0); + + // back up + std::stringstream ss; + { + cereal::BinaryOutputArchive ar(ss); + ar(EntityWrapper{registry, entity}); + } + // update + { + auto& comp = registry.get(entity); + comp.x = 10; + comp.y = 20; + comp.z = 30; + } + // undo + { + cereal::BinaryInputArchive ar(ss); + EntityWrapper wrapper{registry, entity}; + ar(wrapper); + } + // check + { + auto& comp = registry.get(entity); + assert((comp.x == 1.0) && (comp.y == 2.0) && (comp.z == 3.0)); + } +} + +void testCustomLoading() +{ + RegisterComponentForSerialize(); + + no_entity_wrapper_case(); + + entt::registry registry; + for (auto i = 0; i < 10; ++i) + { + auto entity = registry.create(); + registry.emplace(entity, i * 0.1f, i * 0.01f, i * 1.0f); + } + // save all + // todo: for initial loading we need to store the list of entities as well + std::stringstream ss; + { + cereal::BinaryOutputArchive ar(ss); + auto view = registry.view(); + + for (const auto entity : view) + { + ar(EntityWrapper{registry, entity}); + } + } +} \ No newline at end of file diff --git a/apps/serialization/main2.cpp b/apps/serialization/main2.cpp new file mode 100644 index 00000000..d9981b37 --- /dev/null +++ b/apps/serialization/main2.cpp @@ -0,0 +1,9 @@ +#include +#include +#include +#include "customLoading.hpp" + +int main() +{ + testCustomLoading(); +} \ No newline at end of file diff --git a/apps/serialization/simpleLoadSave.hpp b/apps/serialization/simpleLoadSave.hpp new file mode 100644 index 00000000..8c62c5b0 --- /dev/null +++ b/apps/serialization/simpleLoadSave.hpp @@ -0,0 +1,48 @@ +#include +#include + +struct Position +{ + int x; + int y; +}; + +template +void serialize(Archive& archive, Position& position) +{ + archive(position.x, position.y); +} + +void saveRegistry(entt::registry& reg, std::string_view file_name) +{ + std::ofstream storage("../data/test.out"); + + cereal::JSONOutputArchive output{storage}; + + entt::snapshot{reg}.get(output).get(output); +} + +void loadRegistry(entt::registry& reg, std::string_view file_name) +{ + std::ifstream storage("../data/test.out"); + + cereal::JSONInputArchive output{storage}; + + entt::basic_snapshot_loader{reg}.get(output).get(output); +} + +void testSimpleLoading() +{ // + entt::registry registry; + auto entity = registry.create(); + registry.emplace(entity, Position{42, 42}); + // registry.add(registry.create(), {3, 14}); + saveRegistry(registry, "data/test.out"); + { + auto& pos = registry.get(entity); + pos.x = 0; + } + registry.clear(); + loadRegistry(registry, "data/test.out"); + auto& pos2 = registry.get(entity); +} \ No newline at end of file diff --git a/doc/setup_env.sh b/doc/setup_env.sh index b700c214..5139ba2b 100644 --- a/doc/setup_env.sh +++ b/doc/setup_env.sh @@ -14,3 +14,5 @@ sudo -Hiu $USER env conan profile new default --detect sudo -Hiu $USER env conan profile update settings.compiler.libcxx=libstdc++11 default sudo -Hiu $USER env conan profile update conf.tools.system.package_manager:mode=install default sudo -Hiu $USER env conan profile update conf.tools.system.package_manager:sudo=True default + +export CPM_SOURCE_CACHE=$HOME/.cache/CPM \ No newline at end of file diff --git a/doc/todo.md b/doc/todo.md new file mode 100644 index 00000000..34f38acc --- /dev/null +++ b/doc/todo.md @@ -0,0 +1,9 @@ +* use a virtual 'filesystem' https://github.com/icculus/physfs +* experiment using serialization to save an load inital scenes and game state +** check https://github.com/skypjack/entt/issues/10 00 for iterating over all components +* use a scripting language to define game logic and behavior +* use a scripting language to define game assets +* use json/yaml config files +* use homogenous matrices for all transformations +* https://github.com/matepek/catch2-with-gmock?tab=readme-ov-file mocking +* replace cache with entt types (some inspiration here: https://github.com/trollworks/sdk-core) diff --git a/libs/pgEngine/math/Matrix.hpp b/libs/pgEngine/math/Matrix.hpp new file mode 100644 index 00000000..f78f4dc7 --- /dev/null +++ b/libs/pgEngine/math/Matrix.hpp @@ -0,0 +1,151 @@ +#pragma once +#include + +namespace pg { +template +struct Matrix +{ + static constexpr uint8_t rows = 3; + static constexpr uint8_t cols = 3; + std::array data = {0, 0, 0, 0, 1, 0, 0, 0, 1}; + + T& operator()(uint8_t row, uint8_t col) { return data[row * cols + col]; } + + const T& operator()(uint8_t row, uint8_t col) const { return data[row * cols + col]; } + + // filled with zeros + static Matrix makeZero() + { + Matrix result; + result.data.fill(0); + return result; + } + + // unity matrix + static Matrix makeIdentity() + { + Matrix result; + return result; + } + + // from translation vector + static Matrix makeTrans(const Vec2& v) + { + Matrix result; + result(0, 2) = v[0]; + result(1, 2) = v[1]; + return result; + } + + // from scale vector + static Matrix makeScale(const Vec2& v) + { + Matrix result; + result(0, 0) = v[0]; + result(1, 1) = v[1]; + return result; + } + + // from rotation angle + static Matrix makeRot(T angle) + { + Matrix result; + result(0, 0) = std::cos(angle); + result(0, 1) = -std::sin(angle); + result(1, 0) = std::sin(angle); + result(1, 1) = std::cos(angle); + return result; + } + + // projection matrix + static Matrix makeProjection(T left, T right, T bottom, T top, T near, T far) + { + Matrix result; + result(0, 0) = 2 / (right - left); + result(1, 1) = 2 / (top - bottom); + result(2, 2) = -2 / (far - near); + result(0, 3) = -(right + left) / (right - left); + result(1, 3) = -(top + bottom) / (top - bottom); + result(2, 3) = -(far + near) / (far - near); + result(3, 3) = 1; + return result; + } + + // matrix multiplication + Matrix operator*(const Matrix& other) const + { + Matrix result; + for (auto i = 0u; i < rows; ++i) + { + for (auto j = 0u; j < cols; ++j) + { + for (uint8_t k = 0; k < cols; ++k) + { + result(i, j) += (*this)(i, k) * other(k, j); + } + } + } + return result; + } + + // matrix vector multiplication + Vec2 operator*(const Vec2& v) const + { + Vec2 result; + for (auto i = 0u; i < rows; ++i) + { + for (auto j = 0u; j < cols; ++j) + { + result[i] += (*this)(i, j) * v[j]; + } + } + return result; + } + + // transpose matrix + + Matrix transpose() const + { + Matrix result; + for (auto i = 0u; i < rows; ++i) + { + for (auto j = 0u; j < cols; ++j) + { + result(i, j) = (*this)(j, i); + } + } + return result; + } + + // matrix determinant + T determinant() const + { + return (*this)(0, 0) * (*this)(1, 1) * (*this)(2, 2) + (*this)(0, 1) * (*this)(1, 2) * (*this)(2, 0) + + (*this)(0, 2) * (*this)(1, 0) * (*this)(2, 1) - (*this)(0, 2) * (*this)(1, 1) * (*this)(2, 0) - + (*this)(0, 1) * (*this)(1, 0) * (*this)(2, 2) - (*this)(0, 0) * (*this)(1, 2) * (*this)(2, 1); + } + + // matrix inverse + Matrix inverse() const + { + Matrix result; + T det = determinant(); + if (det == 0) { return result; } + result(0, 0) = ((*this)(1, 1) * (*this)(2, 2) - (*this)(1, 2) * (*this)(2, 1)) / det; + result(0, 1) = ((*this)(0, 2) * (*this)(2, 1) - (*this)(0, 1) * (*this)(2, 2)) / det; + result(0, 2) = ((*this)(0, 1) * (*this)(1, 2) - (*this)(0, 2) * (*this)(1, 1)) / det; + result(1, 0) = ((*this)(1, 2) * (*this)(2, 0) - (*this)(1, 0) * (*this)(2, 2)) / det; + result(1, 1) = ((*this)(0, 0) * (*this)(2, 2) - (*this)(0, 2) * (*this)(2, 0)) / det; + result(1, 2) = ((*this)(0, 2) * (*this)(1, 0) - (*this)(0, 0) * (*this)(1, 2)) / det; + result(2, 0) = ((*this)(1, 0) * (*this)(2, 1) - (*this)(1, 1) * (*this)(2, 0)) / det; + result(2, 1) = ((*this)(0, 1) * (*this)(2, 0) - (*this)(0, 0) * (*this)(2, 1)) / det; + result(2, 2) = ((*this)(0, 0) * (*this)(1, 1) - (*this)(0, 1) * (*this)(1, 0)) / det; + return result; + } + + void rotate(T angle) { *this = rotation(angle) * *this; } + + void scale(const Vec2& v) { *this = scale(v) * *this; } +}; + +} // namespace pg \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index aba666e9..28715756 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,11 +1,12 @@ project(unittests LANGUAGES CXX) -find_package(Catch2 CONFIG REQUIRED) -include(CTest) -include(Catch) +find_package(Catch2 REQUIRED) + # Tests add_library(testMain OBJECT testMain.cpp) target_link_libraries(testMain PUBLIC Catch2::Catch2) - +list(APPEND CMAKE_MODULE_PATH ${Catch2_SOURCE_DIR}/extras) +include(CTest) +include(Catch) if(BUILD_TESTING) diff --git a/tests/Matrix/MatrixTests.cpp b/tests/Matrix/MatrixTests.cpp new file mode 100644 index 00000000..fb18e112 --- /dev/null +++ b/tests/Matrix/MatrixTests.cpp @@ -0,0 +1,4 @@ +#include +#include +#include +#include