diff --git a/.gitignore b/.gitignore index 5def43c..7bceffc 100644 --- a/.gitignore +++ b/.gitignore @@ -414,3 +414,4 @@ FodyWeavers.xsd # JetBrains CLion .idea +imgui.ini diff --git a/AGENTS.md b/AGENTS.md index 4b6e9cb..d15fa6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,12 +9,6 @@ cmake --build build # Run the main example ./build/examples/sample-scenes/WeirdSamples - -# muOS cross-compile (aarch64, uses podman) -./scripts/anbernic/build-muos.sh -./scripts/anbernic/deploy-muos.sh # build + deploy via MTP -./scripts/anbernic/deploy-muos.sh --no-build # deploy only -./scripts/anbernic/fetch-logs.sh # pull log.txt + screenshots ``` There are no tests. CI runs `ctest` but no test targets are defined. @@ -27,6 +21,7 @@ There are no tests. CI runs `ctest` but no test targets are defined. | `WEIRD_DISABLE_IMGUI` | `OFF` | Strip ImGui (used for muOS build) | | `WEIRD_TEST_HOOKS` | `OFF` | Enables `WEIRD_AUTO_QUIT_SECONDS` / `WEIRD_SCREENSHOT_FRAME` env vars | | `WEIRD_ENGINE_ENABLE_ASAN` | `OFF` | AddressSanitizer | +| `WEIRD_ENGINE_ENABLE_ASSERTS` | Debug/RelWithDebInfo | Enables `WEIRD_ASSERT` runtime assertions (abort on violation); disabled in Release | | `WEIRD_USE_FBDEV_EGL` | `OFF` | fbdev EGL backend for Mali devices (no GBM/KMS) | | `WEIRD_ENGINE_USE_RUNTIME_ASSETS` | `OFF` | Load shaders/fonts from `./shaders/` `./fonts/` instead of source tree | @@ -41,6 +36,7 @@ There are no tests. CI runs `ctest` but no test targets are defined. - `sample-scenes` → `WeirdSamples` (main demo) - `3d-experiments`, `opengl-experiments` — other demos - `empty-project` — starter template +- **ServiceProvider pattern**: `Scene` state is highly encapsulated. Game systems should use `ServiceProvider` (passed into update/render loops) to interact with rendering, audio, or physics systems instead of accessing `Scene` internals. - Entry point for games: `WeirdEngine::start(sceneManager, ...)` in `include/weird-engine.h`. ## Dependencies @@ -59,3 +55,9 @@ There are no tests. CI runs `ctest` but no test targets are defined. - The `build/` and `build-muos/` directories are separate CMake trees; do not mix them. - `compile_flags.txt` exists for clangd; it does not drive the actual build. - The `.vscode/settings.json` enables `WEIRD_ENGINE_BUILD_EXAMPLES=ON` by default. + +## Commit Guidelines + +- **Naming Convention**: Prefix all commit messages with the affected module or system name, followed by a colon and a space. Keep the prefix lowercase. + - Prefix Examples: `scene:`, `core:`, `physics:`, `renderer:`, `examples:`, `tools:`, `core/assert:` + - Full Example: `physics: add BodyUserData for attaching custom data to rigidbodies` diff --git a/CMakeLists.txt b/CMakeLists.txt index fdd20ec..a59fc5c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,20 @@ if(WEIRD_TEST_HOOKS) add_compile_definitions(WEIRD_TEST_HOOKS) endif() +# Runtime assertions: enabled by default in debug-like configurations +# (Debug, RelWithDebInfo), disabled in Release/MinSizeRel. Games that build +# the engine via add_subdirectory inherit this through the PUBLIC definition. +set(WEIRD_ENGINE_ENABLE_ASSERTS_DEFAULT OFF) +if(CMAKE_BUILD_TYPE MATCHES "^(Debug|RelWithDebInfo)$" + OR CMAKE_CONFIGURATION_TYPES MATCHES "(^|;)Debug(;|$)" + OR CMAKE_CONFIGURATION_TYPES MATCHES "(^|;)RelWithDebInfo(;|$)") + set(WEIRD_ENGINE_ENABLE_ASSERTS_DEFAULT ON) +endif() +option(WEIRD_ENGINE_ENABLE_ASSERTS "Enable runtime assertions (WEIRD_ASSERT)" ${WEIRD_ENGINE_ENABLE_ASSERTS_DEFAULT}) +if(WEIRD_ENGINE_ENABLE_ASSERTS) + target_compile_definitions(${PROJECT_NAME} PUBLIC WEIRD_ENABLE_ASSERTS=1) +endif() + if(NOT WEIRD_DISABLE_IMGUI) # ImGui setup set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third-party/imgui) @@ -222,6 +236,7 @@ if(WEIRD_ENGINE_BUILD_EXAMPLES) add_subdirectory(examples/3d-experiments) add_subdirectory(examples/opengl-experiments) add_subdirectory(examples/sample-scenes) + add_subdirectory(examples/empty-project) endif() # Tools diff --git a/examples/3d-experiments/include/Classic.h b/examples/3d-experiments/include/Classic.h index 27eb98a..2b2466d 100644 --- a/examples/3d-experiments/include/Classic.h +++ b/examples/3d-experiments/include/Classic.h @@ -12,17 +12,17 @@ class ClassicScene : public Scene3D Entity m_ball; // Inherited via Scene - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugFly = true; + services.debug().setDebugFly(true); - auto& redMat = createMaterial(); + auto& redMat = services.materials().createMaterial(); redMat.color = vec4(.8f, 0.2f, 0.2f, 1.0f); - auto& orangeMat = createMaterial(); + auto& orangeMat = services.materials().createMaterial(); orangeMat.color = vec4(.95f, 0.4f, 0.1f, 1.0f); - auto& floorMaterial = createMaterial(); + auto& floorMaterial = services.materials().createMaterial(); floorMaterial.color = vec4(1.0f, 1.0f, 1.0f, 1.0f); floorMaterial.secondaryColor = vec4(0.4f, 0.4f, 0.6f, 1.0f); floorMaterial.metallic = 0.7f; @@ -36,7 +36,7 @@ class ClassicScene : public Scene3D MeshRenderer& mr = ecs.addComponent(entity); - auto id = m_resourceManager.getMeshId(ASSETS_PATH "monkey/demo.gltf", entity, true); + auto id = services.resources().getMeshId("monkey/demo.gltf", entity, true); mr.mesh = id; // mr.materialIndex = floorMaterial.id; @@ -56,28 +56,38 @@ class ClassicScene : public Scene3D { float vars1[8] = {25.0f, 10.0f, 5.0f, 0.5f, 13.0f, 0.0f}; // Custom shape - Entity start = addShape(DefaultShapes::STAR, vars1, orangeMat, CombinationType::Addition, true, 0); + Entity start = + services.shapes().addShape(DefaultShapes::STAR, vars1, orangeMat, CombinationType::Addition, true, 0); } { float vars1[8] = {}; // Custom shape - Entity start = addShape(DefaultShapes3D::PLANE, vars1, floorMaterial, CombinationType::Addition, false); + Entity start = services.shapes().addShape(DefaultShapes3D::PLANE, vars1, floorMaterial, + CombinationType::Addition, false); } - getLigths().push_back(Light{0, glm::vec3(0.0f, 3.0f, 0.0f), 0, glm::vec3(0.35f, 0.45f, 0.5f), - glm::vec4(1.0f, 0.95f, 0.9f, 2.0f)}); + { + Entity entity = ecs.createEntity(); + Transform& t = ecs.addComponent(entity); + t.position = glm::vec3(0.0f, 3.0f, 0.0f); + t.rotation = glm::vec3(0.35f, 0.45f, 0.5f); + + LightComponent& lc = ecs.addComponent(entity); + lc.type = LightType::Directional; + lc.color = glm::vec4(1.0f, 0.95f, 0.9f, 2.0f); + } - ecs.getComponent(m_mainCamera).position = vec3(0, 2, 10); + ecs.getComponent(services.render().getCameraEntity()).position = vec3(0, 2, 10); } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - if (Input::GetKeyDown(Input::Q)) + if (services.input().getKeyDown(Input::Q)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } - Transform& cameraTransform = ecs.getComponent(m_mainCamera); + Transform& cameraTransform = ecs.getComponent(services.render().getCameraEntity()); return; @@ -87,9 +97,9 @@ class ClassicScene : public Scene3D { Transform& t = ecs.getComponent(m_ball); - // t.position.z = 10 * sinf(getTime()); - t.position.x = 2.0f * sinf(-getTime()); - t.position.z = 2.0f * cosf(-getTime()); + // t.position.z = 10 * sinf(services.time().time()); + t.position.x = 2.0f * sinf(-services.time().time()); + t.position.z = 2.0f * cosf(-services.time().time()); } // t.position = cameraTransform.position + vec3(-10, -6, -20); diff --git a/examples/3d-experiments/include/CornellBox.h b/examples/3d-experiments/include/CornellBox.h index 064fde3..9d3f07c 100644 --- a/examples/3d-experiments/include/CornellBox.h +++ b/examples/3d-experiments/include/CornellBox.h @@ -14,25 +14,26 @@ class CornellBox : public Scene3D CornellBox() {}; private: + Entity m_sunLight; // Inherited via Scene - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugFly = true; + services.debug().setDebugFly(true); - auto& ballMat = createMaterial(); + auto& ballMat = services.materials().createMaterial(); ballMat.color = vec4(1.0f); ballMat.metallic = 1.0f; ballMat.roughness = 0.005f; - auto& redMat = createMaterial(); + auto& redMat = services.materials().createMaterial(); redMat.color = vec4(.8f, 0.2f, 0.2f, 1.0f); - auto& greenMat = createMaterial(); + auto& greenMat = services.materials().createMaterial(); greenMat.color = vec4(0.1f, .95f, 0.1f, 1.0f); greenMat.metallic = 0.5f; greenMat.roughness = 0.1f; - auto& whiteMat = createMaterial(); + auto& whiteMat = services.materials().createMaterial(); whiteMat.color = vec4(1.0f, 1.0f, 1.0f, 1.0f); { @@ -55,81 +56,94 @@ class CornellBox : public Scene3D { std::shared_ptr box = std::make_shared(); - auto boxId = registerSDF(box); + auto boxId = services.shapes().registerSDF(box); // Left { float vars1[8] = {-2.0f * 2.6f, 2.6f, 0.0f, 2.6f, 2.6f, 2.6f}; // Custom shape - Entity start = addShape(boxId, vars1, redMat, CombinationType::Addition, false); + Entity start = services.shapes().addShape(boxId, vars1, redMat, CombinationType::Addition, false); } // Right { float vars1[8] = {2.0f * 2.6f, 2.6f, 0.0f, 2.6f, 2.6f, 2.6f}; // Custom shape - Entity start = addShape(boxId, vars1, greenMat, CombinationType::Addition, false); + Entity start = services.shapes().addShape(boxId, vars1, greenMat, CombinationType::Addition, false); } // Back { float vars1[8] = {0.0f, 2.6f, -2.0f * 2.6f, 3.0f * 2.6f, 2.6f, 2.6f}; // Custom shape - Entity start = addShape(boxId, vars1, whiteMat, CombinationType::Addition, false); + Entity start = services.shapes().addShape(boxId, vars1, whiteMat, CombinationType::Addition, false); } // Top { float vars1[8] = {0.0f, 3.0f * 2.6f, -2.6f, 3.0f * 2.6f, 2.6f, 2.0f * 2.6f}; // Custom shape - Entity start = addShape(boxId, vars1, whiteMat, CombinationType::Addition, false); + Entity start = services.shapes().addShape(boxId, vars1, whiteMat, CombinationType::Addition, false); } // Light hole { float vars1[8] = {0.0f, 2.0f * 2.6f, 0.0f, 0.5f, 1.0f, 0.5f}; // Custom shape - Entity start = addShape(boxId, vars1, whiteMat, CombinationType::Subtraction, false); + Entity start = services.shapes().addShape(boxId, vars1, whiteMat, CombinationType::Subtraction, false); } // Floor { float vars1[8] = {0.0f, -1.0f * 2.6f, -2.6f, 3.0f * 2.6f, 2.6f, 2.0f * 2.6f}; // Custom shape - Entity start = addShape(boxId, vars1, whiteMat, CombinationType::Addition, false); + Entity start = services.shapes().addShape(boxId, vars1, whiteMat, CombinationType::Addition, false); } // { // float vars1[8] = {0.0f, 2.6f, 0.0f, 2.7f, 2.7f, 2.7f}; // Custom shape - // Entity start = addShape(boxId, vars1, DisplaySettings::White, CombinationType::Intersection, false); + // Entity start = services.shapes().addShape(boxId, vars1, DisplaySettings::White, + // CombinationType::Intersection, false); // } } // Sun - getLigths().push_back(Light{0, glm::vec3(0.0f, 0.0f, 0.0f), 0, normalize(glm::vec3(0.0f, 0.0f, 0.0f)), - glm::vec4(1.0f, 1.0f, 1.0f, 0.0f)}); + { + m_sunLight = ecs.createEntity(); + Transform& t = ecs.addComponent(m_sunLight); + t.position = glm::vec3(0.0f, 0.0f, 0.0f); + t.rotation = normalize(glm::vec3(0.0f, 0.0f, 0.0f)); + + LightComponent& lc = ecs.addComponent(m_sunLight); + lc.type = LightType::Directional; + lc.color = glm::vec4(1.0f, 1.0f, 1.0f, 0.0f); + } + + { + Entity entity = ecs.createEntity(); + Transform& t = ecs.addComponent(entity); + t.position = glm::vec3(0.0f, (2.0f * 2.6f) + 0.25f, 0.0f); + t.rotation = glm::vec3(0.0f, 0.0f, 0.0f); - getLigths().push_back(Light{1, glm::vec3(0.0f, (2.0f * 2.6f) + 0.25f, 0.0f), 0, glm::vec3(0.0f, 0.0f, 0.0f), - glm::vec4(1.0f, 1.0f, 1.0f, 3.0f)}); + LightComponent& lc = ecs.addComponent(entity); + lc.type = LightType::Point; + lc.color = glm::vec4(1.0f, 1.0f, 1.0f, 3.0f); + } - // getLigths().push_back( + // getLights().push_back( // Light{1, glm::vec3(0.0f, 0.0f, 0.0f), 0, glm::vec3(0.35f, 0.45f, 0.5f), glm::vec4(0.0f, 1.0f, 0.0f, 1.0f)}); - // getLigths().push_back( + // getLights().push_back( // Light{2, glm::vec3(0.0f, 0.0f, 0.0f), 0, glm::vec3(0.0f, 1.0f, 0.0f), glm::vec4(0.0f, 0.0f, 2.0f, 10.0f)}); - ecs.getComponent(m_mainCamera).position = vec3(0, 2.6f, 12.0f); + ecs.getComponent(services.render().getCameraEntity()).position = vec3(0, 2.6f, 12.0f); } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - if (Input::GetKeyDown(Input::Q)) + if (services.input().getKeyDown(Input::Q)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } - auto& cameraTransform = ecs.getComponent(m_mainCamera); - - // getLigths()[0].position.x = cameraTransform.position.x; - // getLigths()[0].position.y = cameraTransform.position.y; - // getLigths()[0].position.z = cameraTransform.position.z; + auto& cameraTransform = ecs.getComponent(services.render().getCameraEntity()); - // getLigths()[0].rotation.x = -cameraTransform.rotation.x; - // getLigths()[0].rotation.y = -cameraTransform.rotation.y; - // getLigths()[0].rotation.z = -cameraTransform.rotation.z; + auto& lightTransform = ecs.getComponent(m_sunLight); + lightTransform.position = cameraTransform.position; + lightTransform.rotation = -cameraTransform.rotation; } }; diff --git a/examples/3d-experiments/include/MaterialShowcase.h b/examples/3d-experiments/include/MaterialShowcase.h index fbc7d88..125f6e9 100644 --- a/examples/3d-experiments/include/MaterialShowcase.h +++ b/examples/3d-experiments/include/MaterialShowcase.h @@ -14,17 +14,18 @@ class MaterialShowcaseScene : public Scene3D MaterialShowcaseScene() {}; private: + Entity m_sunLight; // Inherited via Scene - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugFly = true; + services.debug().setDebugFly(true); { Entity entity = ecs.createEntity(); Transform& t = ecs.addComponent(entity); t.position = vec3(-0.5f, -2.0f, 0); - auto& mat = createMaterial(); + auto& mat = services.materials().createMaterial(); mat.color = vec4(1.0f); mat.metallic = 1.0f; mat.roughness = 0.0f; @@ -46,7 +47,7 @@ class MaterialShowcaseScene : public Scene3D }; { - auto& mat = createMaterial(); + auto& mat = services.materials().createMaterial(); mat.color = vec4(.95f, 0.4f, 0.1f, 1.0f); mat.metallic = 0.5f; mat.roughness = 0.1f; @@ -57,7 +58,7 @@ class MaterialShowcaseScene : public Scene3D } { - auto& mat = createMaterial(); + auto& mat = services.materials().createMaterial(); mat.color = vec4(0.5f, 1.0f, 0.5f, 1.0f); mat.metallic = 0.05f; mat.roughness = 0.99f; @@ -68,7 +69,7 @@ class MaterialShowcaseScene : public Scene3D } { - auto& mat = createMaterial(); + auto& mat = services.materials().createMaterial(); mat.color = vec4(1.0f, 0.3f, .6f, 1.0f); mat.secondaryColor = vec4(1.0f, 0.2f, 0.05f, 1.0f); @@ -80,7 +81,7 @@ class MaterialShowcaseScene : public Scene3D } { - auto& mat = createMaterial(); + auto& mat = services.materials().createMaterial(); mat.color = vec4(0.0f, 10.9f, 10.9f, 1.0f); mat.metallic = 0.05f; mat.roughness = 0.99f; @@ -91,7 +92,7 @@ class MaterialShowcaseScene : public Scene3D } { - auto& mat = createMaterial(); + auto& mat = services.materials().createMaterial(); mat.color = vec4(0.5f, 0.5f, 0.8f, 1.0f); mat.secondaryColor = vec4(1.0f, 1.0f, 1.0f, 1.0f); mat.metallic = 0.3f; @@ -103,7 +104,7 @@ class MaterialShowcaseScene : public Scene3D } { - auto& mat = createMaterial(); + auto& mat = services.materials().createMaterial(); mat.color = vec4(0.85f, 0.7f, 0.1f, 0.5f); mat.metallic = 0.5f; mat.roughness = 0.0f; @@ -122,18 +123,19 @@ class MaterialShowcaseScene : public Scene3D } { - auto& floorMaterial = createMaterial(); + auto& floorMaterial = services.materials().createMaterial(); floorMaterial.color = vec4(1.0f, 1.0f, 1.0f, 1.0f); - floorMaterial.metallic = 0.0f; + floorMaterial.metallic = 0.1f; floorMaterial.roughness = 0.3f; floorMaterial.pattern = MaterialPattern::Checkers; floorMaterial.secondaryColor = floorMaterial.color * 0.8f; float vars[8] = {3}; - Entity floor = addShape(DefaultShapes3D::PLANE, vars, floorMaterial, CombinationType::Addition, false); + Entity floor = services.shapes().addShape(DefaultShapes3D::PLANE, vars, floorMaterial, + CombinationType::Addition, false); } - auto& mirrorMaterial = createMaterial(); + auto& mirrorMaterial = services.materials().createMaterial(); mirrorMaterial.color = vec4(1.0f); mirrorMaterial.metallic = 1.0f; mirrorMaterial.roughness = 0.0f; @@ -141,49 +143,47 @@ class MaterialShowcaseScene : public Scene3D { std::shared_ptr box = std::make_shared(); - auto boxId = registerSDF(box); + auto boxId = services.shapes().registerSDF(box); float vars1[8] = {-5.0f, -2.0f, 0.0f, 0.1f, 1.0f, 3.0f}; // Custom shape - Entity start = addShape(boxId, vars1, mirrorMaterial, CombinationType::Addition, false); + Entity start = services.shapes().addShape(boxId, vars1, mirrorMaterial, CombinationType::Addition, false); } { std::shared_ptr box = std::make_shared(); - auto boxId = registerSDF(box); + auto boxId = services.shapes().registerSDF(box); float vars1[8] = {20.0f, -2.0f, 0.0f, 0.1f, 1.0f, 3.0f}; // Custom shape - Entity start = addShape(boxId, vars1, mirrorMaterial, CombinationType::Addition, false); + Entity start = services.shapes().addShape(boxId, vars1, mirrorMaterial, CombinationType::Addition, false); } - getLigths().push_back(Light{0, glm::vec3(0.0f, 0.0f, 0.0f), 0, normalize(glm::vec3(0.0f, 0.4f, 1.0f)), - glm::vec4(1.0f, 1.0f, 1.0f, 0.5f)}); + { + m_sunLight = ecs.createEntity(); + Transform& t = ecs.addComponent(m_sunLight); + t.position = glm::vec3(0.0f, 0.0f, 0.0f); + t.rotation = normalize(glm::vec3(0.0f, 0.4f, 1.0f)); + + LightComponent& lc = ecs.addComponent(m_sunLight); + lc.type = LightType::Directional; + lc.color = glm::vec4(1.0f, 1.0f, 1.0f, 0.75f); + } - // getLigths().push_back( + // getLights().push_back( // Light{1, glm::vec3(0.0f, 0.0f, 0.0f), 0, glm::vec3(0.35f, 0.45f, 0.5f), glm::vec4(0.0f, 1.0f, 0.0f, 1.0f)}); - // getLigths().push_back( + // getLights().push_back( // Light{2, glm::vec3(0.0f, 0.0f, 0.0f), 0, glm::vec3(0.0f, 1.0f, 0.0f), glm::vec4(0.0f, 0.0f, 2.0f, 10.0f)}); - auto& cameraTransform = ecs.getComponent(m_mainCamera); + auto& cameraTransform = ecs.getComponent(services.render().getCameraEntity()); cameraTransform.position = vec3(12, -1, 12); cameraTransform.rotation.x = -0.95f; } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - if (Input::GetKeyDown(Input::Q)) + if (services.input().getKeyDown(Input::Q)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } - - auto& cameraTransform = ecs.getComponent(m_mainCamera); - - // getLigths()[0].position.x = cameraTransform.position.x; - // getLigths()[0].position.y = cameraTransform.position.y; - // getLigths()[0].position.z = cameraTransform.position.z; - - // getLigths()[0].rotation.x = -cameraTransform.rotation.x; - // getLigths()[0].rotation.y = -cameraTransform.rotation.y; - // getLigths()[0].rotation.z = -cameraTransform.rotation.z; } }; diff --git a/examples/empty-project/src/main.cpp b/examples/empty-project/src/main.cpp index e76cab1..74efd1b 100644 --- a/examples/empty-project/src/main.cpp +++ b/examples/empty-project/src/main.cpp @@ -1,26 +1,23 @@ -#include // Only needed for debug output, can remove if unused #include // Main engine include using namespace WeirdEngine; -// Example scene demonstrating how to create a rope of connected circles using springs. +// Starter template for a new game. Register scenes in main() and the engine +// takes care of the rest. Override the callbacks you need: +// onCreate - once, before the physics thread starts +// onStart(ecs[, tags])- after the ECS is ready and the physics thread runs +// onUpdate(dt, ecs) - game logic, once per frame (pure virtual) +// onRender(target) - extra 3D rendering +// onImGuiRender - debug UI +// onCollision / onShapeCollision - physics thread, no ECS access +// onEntityCollision / onEntityShapeCollision - main thread, ECS safe +// onDestroy - before the scene is replaced during a transition class EmptyScene : public Scene2D { -public: - EmptyScene() - : Scene() - { - } - private: - void onStart() override {} + void onStart(ECSManager& ecs, ServiceProvider& services) override {} - void onUpdate(float delta) override {} - void onCreate() override {} - void onRender(WeirdRenderer::RenderTarget& renderTarget) override {} - void onCollision(WeirdEngine::CollisionEvent& event) override {} - void onShapeCollision(WeirdEngine::ShapeCollisionEvent& event) override {} - void onDestroy() override {} + void onUpdate(ECSManager& ecs, ServiceProvider& services) override {} }; int main(int argc, char* argv[]) diff --git a/examples/opengl-experiments/include/Fire.h b/examples/opengl-experiments/include/Fire.h index 96dc280..1007cd0 100644 --- a/examples/opengl-experiments/include/Fire.h +++ b/examples/opengl-experiments/include/Fire.h @@ -9,6 +9,8 @@ class FireScene : public Scene3D FireScene() {}; private: + Entity m_light0; + Entity m_light1; Shader m_flameShader; Shader m_particlesShader; Shader m_smokeShader; @@ -37,30 +39,48 @@ class FireScene : public Scene3D RenderPlane m_renderPlane; - void onCreate() override + void onCreate(ECSManager& ecs, ServiceProvider& services) override { // Base shaders m_backgroundShader = Shader(SHADERS_PATH "common/screen_plane.vert", SHADERS_PATH "misc/background_spherical_grid.frag"); - m_litShader = Shader(SHADERS_PATH "3d/geometry.vert", ASSETS_PATH "fire/shaders/lit.frag"); + m_litShader = Shader(SHADERS_PATH "3d/geometry.vert", services.resources().assetPath("fire/shaders/lit.frag")); m_bloomShader = Shader(SHADERS_PATH "common/screen_plane.vert", SHADERS_PATH "postprocess/bloom.frag"); m_blurShader = Shader(SHADERS_PATH "common/screen_plane.vert", SHADERS_PATH "postprocess/blur.frag"); m_brightFilterShader = Shader(SHADERS_PATH "common/screen_plane.vert", SHADERS_PATH "postprocess/bright_filter.frag"); // Custom shaders - m_flameShader = Shader(SHADERS_PATH "3d/geometry.vert", ASSETS_PATH "fire/shaders/flame.frag"); - m_particlesShader = - Shader(ASSETS_PATH "fire/shaders/fireParticles.vert", ASSETS_PATH "fire/shaders/fireParticles.frag"); - m_smokeShader = - Shader(ASSETS_PATH "fire/shaders/smokeParticles.vert", ASSETS_PATH "fire/shaders/smokeParticles.frag"); + m_flameShader = + Shader(SHADERS_PATH "3d/geometry.vert", services.resources().assetPath("fire/shaders/flame.frag")); + m_particlesShader = Shader(services.resources().assetPath("fire/shaders/fireParticles.vert"), + services.resources().assetPath("fire/shaders/fireParticles.frag")); + m_smokeShader = Shader(services.resources().assetPath("fire/shaders/smokeParticles.vert"), + services.resources().assetPath("fire/shaders/smokeParticles.frag")); m_heatDistortionShader = - Shader(SHADERS_PATH "3d/geometry.vert", ASSETS_PATH "fire/shaders/heatDistortion.frag"); + Shader(SHADERS_PATH "3d/geometry.vert", services.resources().assetPath("fire/shaders/heatDistortion.frag")); - getLigths().push_back(Light{0, glm::vec3(0.0f), 0, glm::vec3(0.0f), glm::vec4(0.0f)}); - getLigths().push_back( - Light{1, glm::vec3(0.0f, 1.0f, 0.0f), 0, glm::vec3(0.0f), glm::vec4(1.0f, 0.95f, 0.9f, 2.0f)}); + m_light0 = ecs.createEntity(); + { + Transform& t = ecs.addComponent(m_light0); + t.position = glm::vec3(0.0f); + t.rotation = glm::vec3(0.0f); + + LightComponent& lc = ecs.addComponent(m_light0); + lc.type = LightType::Directional; + lc.color = glm::vec4(0.0f); + } + m_light1 = ecs.createEntity(); + { + Transform& t = ecs.addComponent(m_light1); + t.position = glm::vec3(0.0f, 1.0f, 0.0f); + t.rotation = glm::vec3(0.0f); + + LightComponent& lc = ecs.addComponent(m_light1); + lc.type = LightType::Point; + lc.color = glm::vec4(1.0f, 0.95f, 0.9f, 2.0f); + } // Load meshes // Quad geom @@ -144,8 +164,8 @@ class FireScene : public Scene3D } // Fire textures - m_noiseTexture = new Texture(ASSETS_PATH "fire/fire.jpg"); - m_flameShape = new Texture(ASSETS_PATH "fire/flame.png"); + m_noiseTexture = new Texture(services.resources().assetPath("fire/fire.jpg")); + m_flameShape = new Texture(services.resources().assetPath("fire/flame.png")); m_sceneTextureBeforeFire = new Texture(Display::rWidth, Display::rHeight, Texture::TextureType::Data); m_postProcessTextureFront = new Texture(Display::rWidth, Display::rHeight, Texture::TextureType::Data); @@ -166,7 +186,7 @@ class FireScene : public Scene3D m_bloomRenderTarget->bindColorTextureToFrameBuffer(*m_brightPassTexture); } - void onDestroy() override + void onDestroy(ECSManager& ecs, ServiceProvider& services) override { m_flameShader.free(); m_particlesShader.free(); @@ -207,28 +227,29 @@ class FireScene : public Scene3D } // Inherited via Scene - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugFly = false; + services.debug().setDebugFly(false); } float m_time = 3.1416f; - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - if (Input::GetKeyDown(Input::Q)) + float delta = services.time().deltaTime(); + if (services.input().getKeyDown(Input::Q)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } - if (m_debugFly) + if (services.debug().debugFly()) { return; } - if (!Input::GetKey(Input::Space)) + if (!services.input().getKey(Input::Space)) { static float speed = 0.15f; - if (Input::GetKey(Input::R)) + if (services.input().getKey(Input::R)) { m_time -= delta * speed; } @@ -238,7 +259,7 @@ class FireScene : public Scene3D } } - Transform& cameraTransform = ecs.getComponent(m_mainCamera); + Transform& cameraTransform = ecs.getComponent(services.render().getCameraEntity()); static float amplitude = 10.0f; @@ -291,10 +312,11 @@ class FireScene : public Scene3D glDisable(GL_BLEND); } - void onRender(WeirdRenderer::RenderTarget& renderTarget) override + void onRender(ECSManager& ecs, ServiceProvider& services, WeirdRenderer::RenderTarget& renderTarget) override { - WeirdRenderer::Camera& sceneCamera = getCamera(); - float time = getTime(); + WeirdRenderer::Camera& sceneCamera = + ecs.getComponent(services.render().getCameraEntity()).camera; + float time = services.time().time(); glDepthMask(GL_FALSE); glDisable(GL_DEPTH_TEST); @@ -321,12 +343,15 @@ class FireScene : public Scene3D m_litShader.setUniform("u_far", sceneCamera.farPlane); // Pass light rotation - auto& lights = getLigths(); - glm::vec3 position = lights[1].position; + auto& light0_t = ecs.getComponent(m_light0); + auto& light0_lc = ecs.getComponent(m_light0); + auto& light1_t = ecs.getComponent(m_light1); + auto& light1_lc = ecs.getComponent(m_light1); + glm::vec3 position = light1_t.position; m_litShader.setUniform("u_lightPos", position); - glm::vec3 direction = lights[1].rotation; + glm::vec3 direction = light1_t.rotation; m_litShader.setUniform("u_directionalLightDir", direction); - glm::vec4 color = lights[1].color; + glm::vec4 color = light1_lc.color; m_litShader.setUniform("u_lightColor", color); // bind current FBO @@ -399,7 +424,7 @@ class FireScene : public Scene3D // Fire renderFire(sceneCamera, time); - if (Input::GetKey(Input::P)) + if (services.input().getKey(Input::P)) { return; } @@ -451,7 +476,7 @@ class FireScene : public Scene3D RenderTarget* finalTarget = m_postProcessDoubleBuffer[!horizontal]; finalTarget->getColorAttachment()->bind(1); - if (Input::GetKey(Input::B)) + if (services.input().getKey(Input::B)) { finalTarget->getColorAttachment()->bind(0); } diff --git a/examples/opengl-experiments/include/Lines.h b/examples/opengl-experiments/include/Lines.h index b4ce3e7..3be0b32 100644 --- a/examples/opengl-experiments/include/Lines.h +++ b/examples/opengl-experiments/include/Lines.h @@ -24,37 +24,43 @@ class LinesScene : public Scene3D Entity m_monkey; - void onCreate() override + void onCreate(ECSManager& ecs, ServiceProvider& services) override { { - auto& whiteMat = createMaterial(); + auto& whiteMat = services.materials().createMaterial(); m_whiteMatId = whiteMat.id; whiteMat.pattern = MaterialPattern::Checkers; std::shared_ptr plane = std::make_shared(0.0f); - auto planeId = registerSDF(plane); + auto planeId = services.shapes().registerSDF(plane); float vars1[8] = {}; // Custom shape - Entity start = addShape(planeId, vars1, whiteMat); + Entity start = services.shapes().addShape(planeId, vars1, whiteMat); } m_renderPlane = new RenderPlane(); m_colorTextureCopy = new Texture(Display::rWidth, Display::rHeight, Texture::TextureType::Data); - m_lineShader = new Shader(SHADERS_PATH "common/screen_plane.vert", ASSETS_PATH "lines/lines.frag"); + m_lineShader = + new Shader(SHADERS_PATH "common/screen_plane.vert", services.resources().assetPath("lines/lines.frag")); m_lineRender = new RenderTarget(false); m_lineTexture = new Texture(Display::rWidth, Display::rHeight, Texture::TextureType::Data); m_lineRender->bindColorTextureToFrameBuffer(*m_lineTexture); - m_combinationShader = new Shader(SHADERS_PATH "common/screen_plane.vert", ASSETS_PATH "lines/combination.frag"); + m_combinationShader = new Shader(SHADERS_PATH "common/screen_plane.vert", + services.resources().assetPath("lines/combination.frag")); } // Inherited via Scene - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugFly = false; - getLigths().push_back(Light{}); + services.debug().setDebugFly(false); + { + Entity entity = ecs.createEntity(); + ecs.addComponent(entity); + ecs.addComponent(entity); + } { Entity entity = ecs.createEntity(); @@ -63,8 +69,8 @@ class LinesScene : public Scene3D // MeshRenderer &mr = ecs.addComponent(entity); - // auto id = m_resourceManager.getMeshId(ASSETS_PATH "monkey/demo.gltf", entity, true); - // mr.mesh = id; + // auto id = services.resources().getMeshId(services.resources().assetPath("monkey/demo.gltf"), entity, + // true); mr.mesh = id; auto& sdf = ecs.addComponent(entity); sdf.materialId = m_whiteMatId; @@ -73,14 +79,15 @@ class LinesScene : public Scene3D } } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - if (Input::GetKeyDown(Input::Q)) + float delta = services.time().deltaTime(); + if (services.input().getKeyDown(Input::Q)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } - Transform& cameraTransform = ecs.getComponent(m_mainCamera); + Transform& cameraTransform = ecs.getComponent(services.render().getCameraEntity()); cameraTransform.position.y = 5.0f; cameraTransform.position.z -= 10.0f * delta; @@ -89,7 +96,7 @@ class LinesScene : public Scene3D monkeyTransform.position.z -= 5.0f; } - void onRender(WeirdRenderer::RenderTarget& renderTarget) override + void onRender(ECSManager& ecs, ServiceProvider& services, WeirdRenderer::RenderTarget& renderTarget) override { m_lineRender->bind(); glClearColor(0, 0, 0, 0); // Set clear color diff --git a/examples/opengl-experiments/include/Water.h b/examples/opengl-experiments/include/Water.h index 781cfdd..4cbe00c 100644 --- a/examples/opengl-experiments/include/Water.h +++ b/examples/opengl-experiments/include/Water.h @@ -11,6 +11,7 @@ class WaterScene : public Scene3D WaterScene() {}; private: + Entity m_light0; Shader m_waterShader; RenderPlane m_renderPlane; @@ -44,18 +45,27 @@ class WaterScene : public Scene3D // ------------------------------------------------------------------------- - void onCreate() override + void onCreate(ECSManager& ecs, ServiceProvider& services) override { - m_waterShader = Shader(ASSETS_PATH "water/shaders/water.vert", ASSETS_PATH "water/shaders/water.frag"); + m_waterShader = Shader(services.resources().assetPath("water/shaders/water.vert"), + services.resources().assetPath("water/shaders/water.frag")); - getLigths().push_back(Light{0, glm::vec3(0.0f, 0.0f, 0.0f), 0, normalize(glm::vec3(0.0f, 0.4f, 1.0f)), - glm::vec4(1.0f, 1.0f, 1.0f, 0.5f)}); + m_light0 = ecs.createEntity(); + { + Transform& t = ecs.addComponent(m_light0); + t.position = glm::vec3(0.0f, 0.0f, 0.0f); + t.rotation = normalize(glm::vec3(0.0f, 0.4f, 1.0f)); + + LightComponent& lc = ecs.addComponent(m_light0); + lc.type = LightType::Directional; + lc.color = glm::vec4(1.0f, 1.0f, 1.0f, 0.5f); + } m_waterPlane.build(); } - void onDestroy() override + void onDestroy(ECSManager& ecs, ServiceProvider& services) override { m_waterShader.free(); @@ -76,11 +86,11 @@ class WaterScene : public Scene3D float buoyancy = 10.0f; // how strongly this entity is affected by the water surface }; - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugFly = true; + services.debug().setDebugFly(true); - auto& redMat = createMaterial(); + auto& redMat = services.materials().createMaterial(); redMat.color = vec4(.8f, 0.2f, 0.2f, 1.0f); { @@ -98,22 +108,23 @@ class WaterScene : public Scene3D t.position = vec3(3, 0, 0); MeshRenderer& mr = ecs.addComponent(entity); - auto id = m_resourceManager.getMeshId(ASSETS_PATH "monkey/demo.gltf", entity, true); + auto id = services.resources().getMeshId("monkey/demo.gltf", entity, true); mr.mesh = id; ecs.addComponent(entity); } - ecs.getComponent(m_mainCamera).position = vec3(0, 3, 20); + ecs.getComponent(services.render().getCameraEntity()).position = vec3(0, 3, 20); } float m_time = 0.0f; - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - if (Input::GetKeyDown(Input::Q)) + float delta = services.time().deltaTime(); + if (services.input().getKeyDown(Input::Q)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } m_time += delta; @@ -142,12 +153,14 @@ class WaterScene : public Scene3D } } - void onRender(WeirdRenderer::RenderTarget& renderTarget) override + void onRender(ECSManager& ecs, ServiceProvider& services, WeirdRenderer::RenderTarget& renderTarget) override { - WeirdRenderer::Camera& sceneCamera = getCamera(); - float time = getTime(); + WeirdRenderer::Camera& sceneCamera = + ecs.getComponent(services.render().getCameraEntity()).camera; + float time = services.time().time(); - auto& lights = getLigths(); + auto& light0_t = ecs.getComponent(m_light0); + auto& light0_lc = ecs.getComponent(m_light0); // ── Snapshot the current scene colour + depth ──────────────────────── // We need to read from these textures while drawing the water plane, @@ -187,15 +200,15 @@ class WaterScene : public Scene3D m_snapshotDepth.bind(1); m_waterShader.setUniform("u_screenSize", glm::vec2((float)w, (float)h)); - int numLights = (std::min)((int)lights.size(), 8); + int numLights = 1; m_waterShader.setUniform("u_numLights", numLights); for (int i = 0; i < numLights; i++) { std::string prefix = "u_lights[" + std::to_string(i) + "]."; - m_waterShader.setUniform(prefix + "position", lights[i].position); - m_waterShader.setUniform(prefix + "direction", lights[i].rotation); - m_waterShader.setUniform(prefix + "color", lights[i].color); - m_waterShader.setUniform(prefix + "type", (int)lights[i].type); + m_waterShader.setUniform(prefix + "position", light0_t.position); + m_waterShader.setUniform(prefix + "direction", light0_t.rotation); + m_waterShader.setUniform(prefix + "color", light0_lc.color); + m_waterShader.setUniform(prefix + "type", (int)light0_lc.type); } glm::mat4 waterModel = glm::mat4(1.0f); diff --git a/examples/sample-scenes/include/AquariumScene.h b/examples/sample-scenes/include/AquariumScene.h index aaf604e..34ebdf8 100644 --- a/examples/sample-scenes/include/AquariumScene.h +++ b/examples/sample-scenes/include/AquariumScene.h @@ -72,15 +72,16 @@ class AquariumScene : public Scene2D static constexpr float TANK_W = TANK_RIGHT - TANK_LEFT; static constexpr float TANK_H = TANK_TOP - TANK_BOTTOM; - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugInput = true; - m_debugFly = true; + services.debug().setDebugInput(true); + services.debug().setDebugFly(true); - m_background.type = BackgroundType::Sky; - m_background.primaryColor = vec4(98, 129, 240, 255) / 255.0f; - m_background.secondaryColor = vec4(86, 208, 197, 255) / 255.0f; - m_background.scale = 0.15f; + auto& background = services.render().getBackground(); + background.type = BackgroundType::Sky; + background.primaryColor = vec4(98, 129, 240, 255) / 255.0f; + background.secondaryColor = vec4(86, 208, 197, 255) / 255.0f; + background.scale = 0.15f; Entity globalSettingsEnt = ecs.createEntity(); auto& settings = ecs.addComponent(globalSettingsEnt); @@ -88,10 +89,10 @@ class AquariumScene : public Scene2D settings.damping = 0.025f; ecs.setComponentDirty(settings); - createJellyfish(ecs, 0.0f, 25.0f, 4 + 0, 1.3f, 0.0f); - createJellyfish(ecs, 15.0f, 20.0f, 4 + 3, 1.6f, 1.5f); - createJellyfish(ecs, 30.0f, 28.0f, 4 + 6, 1.1f, 3.0f); - createJellyfish(ecs, 40.0f, 15.0f, 4 + 9, 1.4f, 4.5f); + createJellyfish(ecs, services, 0.0f, 25.0f, 4 + 0, 1.3f, 0.0f); + createJellyfish(ecs, services, 15.0f, 20.0f, 4 + 3, 1.6f, 1.5f); + createJellyfish(ecs, services, 30.0f, 28.0f, 4 + 6, 1.1f, 3.0f); + createJellyfish(ecs, services, 40.0f, 15.0f, 4 + 9, 1.4f, 4.5f); createEel(ecs, -10.0f, 50.0f, 20, 1.0f, 4); createEel(ecs, 40.0f, 40.0f, 18, 1.0f, 8); @@ -99,27 +100,27 @@ class AquariumScene : public Scene2D { float seaweedVars[8] = {3.0f, 1.2f, 2.5f}; - Entity seaweed = addShape(DefaultShapes::SINE, seaweedVars, DisplaySettings::Green); + Entity seaweed = services.shapes().addShape(DefaultShapes::SINE, seaweedVars, DisplaySettings::Green); auto& sw = ecs.addComponent(seaweed); sw.animationOffset = 0.0f; } { float seaweedVars[8] = {2.0f, 2.0f, 1.8f}; - Entity seaweed = addShape(DefaultShapes::SINE, seaweedVars, DisplaySettings::LightGreen); + Entity seaweed = services.shapes().addShape(DefaultShapes::SINE, seaweedVars, DisplaySettings::LightGreen); auto& sw = ecs.addComponent(seaweed); sw.animationOffset = 1.5f; } { float boxVars[8] = {TANK_CX, TANK_CY, TANK_W, TANK_H}; - Entity box = - addShape(DefaultShapes::BOX, boxVars, DisplaySettings::LightBlue, CombinationType::Intersection); + Entity box = services.shapes().addShape(DefaultShapes::BOX, boxVars, DisplaySettings::LightBlue, + CombinationType::Intersection); } { float boxVars[8] = {TANK_CX, TANK_CY, TANK_W, TANK_H, 1.0f}; - Entity box = addShape(DefaultShapes::BOX_LINE, boxVars, DisplaySettings::LightBlue); + Entity box = services.shapes().addShape(DefaultShapes::BOX_LINE, boxVars, DisplaySettings::LightBlue); } for (int i = 0; i < 40; i++) @@ -147,10 +148,11 @@ class AquariumScene : public Scene2D fishComp.perceptionRadius = 5.0f; } - ecs.getComponent(m_mainCamera).position = vec3(TANK_CX, TANK_CY, 45.0f); + ecs.getComponent(services.render().getCameraEntity()).position = vec3(TANK_CX, TANK_CY, 45.0f); } - void createJellyfish(ECSManager& ecs, float x, float y, int material, float scale, float phase) + void createJellyfish(ECSManager& ecs, ServiceProvider& services, float x, float y, int material, float scale, + float phase) { Entity bellEntity = ecs.createEntity(); auto& t = ecs.addComponent(bellEntity); @@ -160,7 +162,8 @@ class AquariumScene : public Scene2D auto& rb = ecs.addComponent(bellEntity); float bellVars[8] = {x, y, 2.5f * scale, 0.8f, 6.0f, 2.0f}; - Entity bellShape = addShape(DefaultShapes::STAR, bellVars, material, CombinationType::Addition, false); + Entity bellShape = + services.shapes().addShape(DefaultShapes::STAR, bellVars, material, CombinationType::Addition, false); auto& jf = ecs.addComponent(bellEntity); jf.bellShape = bellShape; @@ -248,22 +251,23 @@ class AquariumScene : public Scene2D } } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - g_cameraPositon = ecs.getComponent(m_mainCamera).position; + float delta = services.time().deltaTime(); + g_cameraPositon = ecs.getComponent(services.render().getCameraEntity()).position; - if (Input::GetKeyDown(Input::Q) || Input::GetGamepadButtonDown(Input::GamepadButton::North)) + if (services.input().getKeyDown(Input::Q) || services.input().getGamepadButtonDown(Input::GamepadButton::North)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } m_time += delta; - auto& cameraTransform = ecs.getComponent(m_mainCamera); - vec2 mouseWorld = - ECS::Camera::screenPositionToWorldPosition2D(cameraTransform, vec2(Input::GetMouseX(), Input::GetMouseY())); + auto& cameraTransform = ecs.getComponent(services.render().getCameraEntity()); + vec2 mouseWorld = ECS::Camera::screenPositionToWorldPosition2D( + cameraTransform, vec2(services.input().getMouseX(), services.input().getMouseY())); - if (Input::GetKeyDown(Input::E)) + if (services.input().getKeyDown(Input::E)) { constexpr int FOOD_COUNT = 30; for (int i = 0; i < FOOD_COUNT; i++) @@ -671,13 +675,12 @@ class AquariumScene : public Scene2D } } - void onEntityShapeCollision(ECSManager& ecs, WeirdEngine::EntityShapeCollisionEvent& event) override + void onEntityShapeCollision(ECSManager& ecs, ServiceProvider& services, + WeirdEngine::EntityShapeCollisionEvent& event) override { - event.raw.friction *= 50.0f; - if (std::rand() % 8 == 0) { - playSound({0.015f, 150.0f + (std::rand() % 150), true, vec3(event.raw.position, 0.0f), 1}); + services.audio().playSound({0.015f, 150.0f + (std::rand() % 150), true, vec3(event.raw.position, 0.0f), 1}); } auto eelArray = ecs.getComponentArray(); @@ -698,7 +701,8 @@ class AquariumScene : public Scene2D } } - void onEntityCollision(ECSManager& ecs, WeirdEngine::EntityCollisionEvent& event) override + void onEntityCollision(ECSManager& ecs, ServiceProvider& services, + WeirdEngine::EntityCollisionEvent& event) override { Entity a = event.entityA; Entity b = event.entityB; @@ -708,7 +712,7 @@ class AquariumScene : public Scene2D if (std::rand() % 10 == 0) { - playSound({0.01f, 300.0f + (std::rand() % 200), true, vec3(0.0f), 1}); + services.audio().playSound({0.01f, 300.0f + (std::rand() % 200), true, vec3(0.0f), 1}); } if (ecs.hasComponent(a) && ecs.hasComponent(b)) diff --git a/examples/sample-scenes/include/CollisionHandling.h b/examples/sample-scenes/include/CollisionHandling.h index 489949e..d7264d4 100644 --- a/examples/sample-scenes/include/CollisionHandling.h +++ b/examples/sample-scenes/include/CollisionHandling.h @@ -12,10 +12,10 @@ class CollisionHandlingScene : public Scene2D private: // Inherited via Scene - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugInput = true; - m_debugFly = true; + services.debug().setDebugInput(true); + services.debug().setDebugFly(true); // Create a random number generator engine @@ -41,35 +41,37 @@ class CollisionHandlingScene : public Scene2D // Floor { float variables[8]{15.0f, 5.0f, 25.0f}; - addShape(DefaultShapes::CIRCLE, variables, 3); + services.shapes().addShape(DefaultShapes::CIRCLE, variables, 3); } { float variables[8]{15.0f, -50.0f, 250.0f, 50.0f}; - auto floor = addShape(DefaultShapes::BOX, variables, 3, CombinationType::SmoothAddition); + auto floor = services.shapes().addShape(DefaultShapes::BOX, variables, 3, CombinationType::SmoothAddition); ecs.getComponent(floor).smoothFactor = 3.0f; } { float variables[8]{15.0f, 5.0f, 20.0f}; - addShape(DefaultShapes::CIRCLE, variables, 3, CombinationType::Subtraction); + services.shapes().addShape(DefaultShapes::CIRCLE, variables, 3, CombinationType::Subtraction); } - ecs.getComponent(m_mainCamera).position = g_cameraPositon; + ecs.getComponent(services.render().getCameraEntity()).position = g_cameraPositon; } - void onUpdate(float delta, ECSManager& ecs) override + float m_currentTime = 0.0f; + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - if (Input::GetKeyDown(Input::Q) || Input::GetGamepadButtonDown(Input::GamepadButton::North)) + m_currentTime = services.time().time(); + if (services.input().getKeyDown(Input::Q) || services.input().getGamepadButtonDown(Input::GamepadButton::North)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } } float m_lastTime = 0.0f; - void onCollision(Simulation2D& simulation, WeirdEngine::CollisionEvent& event) override + void onPhysicsRigidBodyCollision(Simulation2D& simulation, WeirdEngine::PhysicsCollisionEvent& event) override { - float t = getTime(); + float t = m_currentTime; if (t - m_lastTime < 0.1f) return; // Avoid multiple collisions in a short time diff --git a/examples/sample-scenes/include/DestroyScene.h b/examples/sample-scenes/include/DestroyScene.h index 731b274..23d28ef 100644 --- a/examples/sample-scenes/include/DestroyScene.h +++ b/examples/sample-scenes/include/DestroyScene.h @@ -24,21 +24,22 @@ class DestroyScene : public Scene2D float m_timer = 0.0f; // Inherited via Scene - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugInput = true; - m_debugFly = true; + services.debug().setDebugInput(true); + services.debug().setDebugFly(true); - ecs.getComponent(m_mainCamera).position = g_cameraPositon; + ecs.getComponent(services.render().getCameraEntity()).position = g_cameraPositon; } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - g_cameraPositon = ecs.getComponent(m_mainCamera).position; + float delta = services.time().deltaTime(); + g_cameraPositon = ecs.getComponent(services.render().getCameraEntity()).position; - if (Input::GetKeyDown(Input::Q) || Input::GetGamepadButtonDown(Input::GamepadButton::North)) + if (services.input().getKeyDown(Input::Q) || services.input().getGamepadButtonDown(Input::GamepadButton::North)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } m_timer += delta; @@ -80,7 +81,8 @@ class DestroyScene : public Scene2D float h = (float)(std::rand() % 4 + 1); float variables[8]{w, y, x, h, 0.0f, 0.0f, 0.0f, 0.0f}; uint16_t material = std::rand() % 16; - Entity shape = addShape(DefaultShapes::BOX, variables, material, CombinationType::Addition); + Entity shape = services.shapes().addShape(DefaultShapes::BOX, variables, material, + CombinationType::Addition); m_testShapes.push_back(shape); } break; @@ -152,7 +154,8 @@ class DestroyScene : public Scene2D } } - void onEntityCollision(ECSManager& ecs, WeirdEngine::EntityCollisionEvent& event) override + void onEntityCollision(ECSManager& ecs, ServiceProvider& services, + WeirdEngine::EntityCollisionEvent& event) override { if (std::rand() % 5 != 0) return; @@ -166,13 +169,12 @@ class DestroyScene : public Scene2D ecs.getComponent(a).collisionCount++; } - playSound({0.02f, 400.0f + (std::rand() % 200), false, vec3(0.0f), 1}); + services.audio().playSound({0.02f, 400.0f + (std::rand() % 200), false, vec3(0.0f), 1}); } - void onEntityShapeCollision(ECSManager& ecs, WeirdEngine::EntityShapeCollisionEvent& event) override + void onEntityShapeCollision(ECSManager& ecs, ServiceProvider& services, + WeirdEngine::EntityShapeCollisionEvent& event) override { - event.raw.friction *= 100.0f; - if (std::rand() % 20 == 0) { Entity e = event.entity; @@ -197,7 +199,7 @@ class DestroyScene : public Scene2D if (std::rand() % 5 == 0) { - playSound({0.02f, 200.0f + (std::rand() % 100), false, vec3(event.raw.position, 0.0f), 1}); + services.audio().playSound({0.02f, 200.0f + (std::rand() % 100), false, vec3(event.raw.position, 0.0f), 1}); } } }; diff --git a/examples/sample-scenes/include/ImageScene.h b/examples/sample-scenes/include/ImageScene.h index 8fddbb9..e3843df 100644 --- a/examples/sample-scenes/include/ImageScene.h +++ b/examples/sample-scenes/include/ImageScene.h @@ -15,13 +15,14 @@ class ImageScene : public Scene2D private: std::string binaryString; std::string filePath = "cache/image.txt"; - std::string imagePath = ASSETS_PATH "jimmy.jpg"; + std::string imagePath; // Inherited via Scene - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugInput = true; - m_debugFly = true; + services.debug().setDebugInput(true); + services.debug().setDebugFly(true); + imagePath = services.resources().assetPath("jimmy.jpg"); // Check if the folder exists if (!std::filesystem::exists("cache/")) @@ -73,22 +74,22 @@ class ImageScene : public Scene2D // Floor { float variables[8]{15, -5, 25.0f, 5.0f, 0.0f}; - addShape(DefaultShapes::BOX, variables, 3); + services.shapes().addShape(DefaultShapes::BOX, variables, 3); } // Wall right { float variables[8]{30 + 5, 20, 5.0f, 30.0f, 0.0f}; - addShape(DefaultShapes::BOX, variables, 3); + services.shapes().addShape(DefaultShapes::BOX, variables, 3); } // Wall left { float variables[8]{0 - 5, 20, 5.0f, 30.0f, 0.0f}; - addShape(DefaultShapes::BOX, variables, 3); + services.shapes().addShape(DefaultShapes::BOX, variables, 3); } - ecs.getComponent(m_mainCamera).position = g_cameraPositon; + ecs.getComponent(services.render().getCameraEntity()).position = g_cameraPositon; } vec3 getColor(const char* path, float x, float y) @@ -167,15 +168,15 @@ class ImageScene : public Scene2D return closestIndex; } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - if (Input::GetKeyDown(Input::Q) || Input::GetGamepadButtonDown(Input::GamepadButton::North)) + if (services.input().getKeyDown(Input::Q) || services.input().getGamepadButtonDown(Input::GamepadButton::North)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } // Get colors - if (Input::GetKeyDown(Input::P)) + if (services.input().getKeyDown(Input::P)) { auto components = ecs.getComponentArray(); diff --git a/examples/sample-scenes/include/LifeScene.h b/examples/sample-scenes/include/LifeScene.h index 8f7c2b4..23b12cf 100644 --- a/examples/sample-scenes/include/LifeScene.h +++ b/examples/sample-scenes/include/LifeScene.h @@ -27,10 +27,10 @@ class LifeScene : public Scene2D private: // Inherited via Scene - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugInput = true; - m_debugFly = true; + services.debug().setDebugInput(true); + services.debug().setDebugFly(true); Entity globalSettingsEnt = ecs.createEntity(); auto& settings = ecs.addComponent(globalSettingsEnt); @@ -38,7 +38,7 @@ class LifeScene : public Scene2D settings.damping = 0.1f; ecs.setComponentDirty(settings); - const std::filesystem::path organismsDir(ASSETS_PATH "Organisms"); + const std::filesystem::path organismsDir(services.resources().assetPath("Organisms")); { int i = 0; @@ -53,7 +53,7 @@ class LifeScene : public Scene2D { Entity firstCreated = static_cast(ecs.getEntityCount()); - auto tags = loadWeirdFile(entry.path().string()); + auto tags = services.serialization().loadWeirdFile(entry.path().string()); Entity lastCreated = static_cast(ecs.getEntityCount()); @@ -86,24 +86,25 @@ class LifeScene : public Scene2D } } - ecs.getComponent(m_mainCamera).position = g_cameraPositon; + ecs.getComponent(services.render().getCameraEntity()).position = g_cameraPositon; } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - g_cameraPositon = ecs.getComponent(m_mainCamera).position; + float delta = services.time().deltaTime(); + g_cameraPositon = ecs.getComponent(services.render().getCameraEntity()).position; - if (Input::GetKeyDown(Input::Q) || Input::GetGamepadButtonDown(Input::GamepadButton::North)) + if (services.input().getKeyDown(Input::Q) || services.input().getGamepadButtonDown(Input::GamepadButton::North)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } - updateHeads(delta, ecs); + updateHeads(delta, ecs, services); } - void updateHeads(float delta, ECSManager& ecs) + void updateHeads(float delta, ECSManager& ecs, ServiceProvider& services) { - float animationT = std::sin(getTime() * 10.0f) * 0.5f + 0.25f; + float animationT = std::sin(services.time().time() * 10.0f) * 0.5f + 0.25f; auto headArray = ecs.getComponentArray(); diff --git a/examples/sample-scenes/include/MouseCollisionScene.h b/examples/sample-scenes/include/MouseCollisionScene.h index 3d05d5f..ec6edd9 100644 --- a/examples/sample-scenes/include/MouseCollisionScene.h +++ b/examples/sample-scenes/include/MouseCollisionScene.h @@ -20,10 +20,10 @@ class MouseCollisionScene : public Scene2D Entity m_cursorShape; // Inherited via Scene - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugInput = true; - m_debugFly = true; + services.debug().setDebugInput(true); + services.debug().setDebugFly(true); for (size_t i = 0; i < 9900; i++) { @@ -53,46 +53,46 @@ class MouseCollisionScene : public Scene2D // Floor { float variables[8]{0.0f, 1.5f, 1.0f}; - addShape(DefaultShapes::SINE, variables, 3); + services.shapes().addShape(DefaultShapes::SINE, variables, 3); } // Wall right { float variables[8]{30 + 5, 0, 5.0f, 30.0f, 0.0f}; - addShape(DefaultShapes::BOX, variables, 3); + services.shapes().addShape(DefaultShapes::BOX, variables, 3); } // Wall left { float variables[8]{-5, 0, 5.0f, 30.0f, 0.0f}; - addShape(DefaultShapes::BOX, variables, 3); + services.shapes().addShape(DefaultShapes::BOX, variables, 3); } { float variables[8]{-15.0f, 50.0f, 5.0f, 4.5f, 2.0f, 10.0f}; - Entity star = addShape(DefaultShapes::CIRCLE, variables, 7); + Entity star = services.shapes().addShape(DefaultShapes::CIRCLE, variables, 7); m_cursorShape = star; } - ecs.getComponent(m_mainCamera).position = g_cameraPositon; + ecs.getComponent(services.render().getCameraEntity()).position = g_cameraPositon; } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - g_cameraPositon = ecs.getComponent(m_mainCamera).position; + g_cameraPositon = ecs.getComponent(services.render().getCameraEntity()).position; - if (Input::GetKeyDown(Input::Q) || Input::GetGamepadButtonDown(Input::GamepadButton::North)) + if (services.input().getKeyDown(Input::Q) || services.input().getGamepadButtonDown(Input::GamepadButton::North)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } // Move wall to mouse { CustomShape& cs = ecs.getComponent(m_cursorShape); - auto& cameraTransform = ecs.getComponent(m_mainCamera); - float x = Input::GetMouseX(); - float y = Input::GetMouseY(); + auto& cameraTransform = ecs.getComponent(services.render().getCameraEntity()); + float x = services.input().getMouseX(); + float y = services.input().getMouseY(); // Transform mouse coordinates to world space vec2 mousePositionInWorld = ECS::Camera::screenPositionToWorldPosition2D(cameraTransform, vec2(x, y)); @@ -103,7 +103,8 @@ class MouseCollisionScene : public Scene2D } } - void onEntityCollision(ECSManager& ecs, WeirdEngine::EntityCollisionEvent& event) override + void onEntityCollision(ECSManager& ecs, ServiceProvider& services, + WeirdEngine::EntityCollisionEvent& event) override { if (ecs.hasComponent(event.entityA)) { @@ -138,7 +139,8 @@ class MouseCollisionScene : public Scene2D } } - void onEntityShapeCollision(ECSManager& ecs, WeirdEngine::EntityShapeCollisionEvent& event) override + void onEntityShapeCollision(ECSManager& ecs, ServiceProvider& services, + WeirdEngine::EntityShapeCollisionEvent& event) override { if (ecs.hasComponent(event.entity)) { diff --git a/examples/sample-scenes/include/RopeScene.h b/examples/sample-scenes/include/RopeScene.h index 9247159..b111f03 100644 --- a/examples/sample-scenes/include/RopeScene.h +++ b/examples/sample-scenes/include/RopeScene.h @@ -18,10 +18,10 @@ class RopeScene : public Scene2D std::vector m_balls; - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugInput = true; - m_debugFly = true; + services.debug().setDebugInput(true); + services.debug().setDebugFly(true); constexpr int rowWidth = 30; constexpr int numBalls = rowWidth * 2; @@ -112,20 +112,20 @@ class RopeScene : public Scene2D // Add base shapes (walls, ground, custom) float vars0[8] = {1.0f, 0.5f, 1.0f}; // Floor shape - addShape(DefaultShapes::SINE, vars0, 3); + services.shapes().addShape(DefaultShapes::SINE, vars0, 3); float vars1[8] = {25.0f, 10.0f, 5.0f, 0.5f, 13.0f, 5.0f}; // Custom shape - m_star = addShape(DefaultShapes::STAR, vars1, 3); + m_star = services.shapes().addShape(DefaultShapes::STAR, vars1, 3); float vars3[8] = {15.0f, -98.0f, 15.0f, 100.0f}; - addShape(DefaultShapes::BOX, vars3, 3, CombinationType::Addition); + services.shapes().addShape(DefaultShapes::BOX, vars3, 3, CombinationType::Addition); - ecs.getComponent(m_mainCamera).position = g_cameraPositon; + ecs.getComponent(services.render().getCameraEntity()).position = g_cameraPositon; } - void throwBalls(ECSManager& ecs) + void throwBalls(ECSManager& ecs, ServiceProvider& services) { - if (getTime() <= m_lastSpawnTime + 0.1) + if (services.time().time() <= m_lastSpawnTime + 0.1) { return; } @@ -147,23 +147,24 @@ class RopeScene : public Scene2D rb.pendingImpulseForce += vec2(20.0f, 0.0f); } - m_lastSpawnTime = getTime(); + m_lastSpawnTime = services.time().time(); } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - g_cameraPositon = ecs.getComponent(m_mainCamera).position; + float delta = services.time().deltaTime(); + g_cameraPositon = ecs.getComponent(services.render().getCameraEntity()).position; - if (Input::GetKeyDown(Input::Q) || Input::GetGamepadButtonDown(Input::GamepadButton::North)) + if (services.input().getKeyDown(Input::Q) || services.input().getGamepadButtonDown(Input::GamepadButton::North)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } // Animate custom shape over time if (m_star != INVALID_ENTITY) { - // Instead of getSimulation().getSimulationTime(), we can just use getTime() if Scene provides it, or track - // delta. + // Instead of getSimulation().getSimulationTime(), we can just use services.time().time() if Scene provides + // it, or track delta. static float animTime = 0.0f; animTime += delta; auto& cs = ecs.getComponent(m_star); @@ -172,17 +173,17 @@ class RopeScene : public Scene2D ecs.setComponentDirty(cs); } - if (Input::GetKey(Input::E) || Input::GetGamepadButton(Input::GamepadButton::West)) + if (services.input().getKey(Input::E) || services.input().getGamepadButton(Input::GamepadButton::West)) { - throwBalls(ecs); + throwBalls(ecs, services); } static vec2 boxStart; static bool createBoxInUI = true; - if (Input::GetKeyDown(Input::M)) + if (services.input().getKeyDown(Input::M)) { - auto& cam = ecs.getComponent(m_mainCamera); - vec2 screen = {Input::GetMouseX(), Input::GetMouseY()}; + auto& cam = ecs.getComponent(services.render().getCameraEntity()); + vec2 screen = {services.input().getMouseX(), services.input().getMouseY()}; if (createBoxInUI) { @@ -194,10 +195,10 @@ class RopeScene : public Scene2D boxStart = world; } } - else if (Input::GetKeyUp(Input::M)) + else if (services.input().getKeyUp(Input::M)) { - auto& cam = ecs.getComponent(m_mainCamera); - vec2 screen = {Input::GetMouseX(), Input::GetMouseY()}; + auto& cam = ecs.getComponent(services.render().getCameraEntity()); + vec2 screen = {services.input().getMouseX(), services.input().getMouseY()}; vec2 world = ECS::Camera::screenPositionToWorldPosition2D(cam, screen); vec2 boxEnd; @@ -217,23 +218,24 @@ class RopeScene : public Scene2D float vars[8] = {x, y, w, h, 1.2f}; if (createBoxInUI) - addUIShape(DefaultShapes::BOX, vars, 7, CombinationType::SmoothAddition); + services.shapes().addUIShape(DefaultShapes::BOX, vars, 7, CombinationType::SmoothAddition); else - addShape(DefaultShapes::BOX, vars, 4 + ecs.getComponentArray()->getSize() % 12, - CombinationType::SmoothAddition, true, ecs.getComponentArray()->getSize()); + services.shapes().addShape( + DefaultShapes::BOX, vars, 4 + ecs.getComponentArray()->getSize() % 12, + CombinationType::SmoothAddition, true, ecs.getComponentArray()->getSize()); } - if (Input::GetKeyDown(Input::N)) + if (services.input().getKeyDown(Input::N)) { - auto& cam = ecs.getComponent(m_mainCamera); - vec2 screen = {Input::GetMouseX(), Input::GetMouseY()}; + auto& cam = ecs.getComponent(services.render().getCameraEntity()); + vec2 screen = {services.input().getMouseX(), services.input().getMouseY()}; vec2 world = ECS::Camera::screenPositionToWorldPosition2D(cam, screen); float vars[8] = {world.x, world.y, 5.0f, 7.5f, 1.0f}; - addShape(DefaultShapes::STAR, vars, 3); + services.shapes().addShape(DefaultShapes::STAR, vars, 3); } - if (Input::GetKey(Input::R) || Input::GetGamepadButton(Input::GamepadButton::South)) + if (services.input().getKey(Input::R) || services.input().getGamepadButton(Input::GamepadButton::South)) { ecs.forEach( [&](Entity e, RigidBody2D& rb, Transform& t) diff --git a/examples/sample-scenes/include/ServiceShowcaseScene.h b/examples/sample-scenes/include/ServiceShowcaseScene.h new file mode 100644 index 0000000..e4fe7da --- /dev/null +++ b/examples/sample-scenes/include/ServiceShowcaseScene.h @@ -0,0 +1,582 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include "globals.h" +#include "weird-engine/math/Default2DSDFs.h" +#include "weird-renderer/core/Display.h" + +using namespace WeirdEngine; + +// ============================================================================ +// Service showcase scene. +// +// Demonstrates the new system + ServiceProvider API: most Scene callbacks are +// thin wrappers that delegate to a plain free function (a "system") of the +// form: +// +// void system(ECSManager& ecs, ServiceProvider& services, ...); +// +// (onRender and onImGuiRender are inlined in the scene instead, see below.) +// +// Systems never touch Scene internals: everything they need is either on the +// ECSManager& or on the ServiceProvider& passed to the callback. Even the +// scene's own state lives in the ECS (see State below): a single "state" +// entity owns it, and systems reach it through the component array. +// +// Callbacks covered: onCreate, onStart, onUpdate (4 systems), onRender, +// onImGuiRender, onPhysicsStep, onPhysicsRigidBodyCollision, onPhysicsShapeCollision, +// onEntityCollision, onEntityShapeCollision, onDestroy. +// +// Controls: +// Left click spawn a ball at the cursor +// Space pause / resume the physics simulation +// Up / Down gravity up / down (live) +// Left / Right damping down / up (live) +// Ctrl+S save the scene to assets/scenes/service_showcase.weird +// Ctrl+L load the saved scene into the current one +// Q go to the next scene +// ============================================================================ + +namespace ServiceShowcase +{ + // Game-defined per-body user data: derives from BodyUserData and sets a + // type discriminator so the physics callbacks can cast safely (see + // getUserDataAs). + struct CharacterData : BodyUserData + { + static constexpr int TYPE = 1; + float restitution = 1.2f; + }; + + // Scene state as an ECS component: attached to a single "state" entity + // created by onCreateSystem. This is the ECS-native way for systems to + // share state instead of passing a struct around. + struct State + { + Entity timeText = INVALID_ENTITY; + Entity entitiesText = INVALID_ENTITY; + Entity collisionsText = INVALID_ENTITY; + Entity hintsText = INVALID_ENTITY; + + float spawnTimer = 0.0f; + float leaderAngle = 0.0f; + int ballsSpawned = 0; + float gravity = -9.8f; + float damping = 0.001f; + float initialTime = 0.0f; + + // Heap-allocated (see onCreateSystem): the simulation owns per-body + // user data and deletes it when the body is removed or the scene ends. + CharacterData* characterData = nullptr; + + // Each counter is only touched from the main thread (collision + // callbacks); plain ints are fine. Atomicity would require a custom + // component manager since components must be copyable for the ECS + // storage. + int entityCollisions = 0; + int shapeCollisions = 0; + }; + + // State entity lookup: there is exactly one State component in the scene + // (created by onCreateSystem), so it always lives at index 0 of the State + // component array. + inline State& getState(ECSManager& ecs, ServiceProvider& services) + { + return ecs.getComponentArray()->getDataAtIdx(0); + } + + inline Entity spawnBall(ECSManager& ecs, vec2 position) + { + Entity entity = ecs.createEntity(); + auto& t = ecs.addComponent(entity); + t.position = vec3(position, 0.0f); + + auto& dot = ecs.addComponent(entity); + dot.materialId = DisplaySettings::LightGray; + + auto& rb = ecs.addComponent(entity); + rb.velocity = vec2((std::rand() % 200 - 100) / 40.0f, 0.0f); + ecs.setComponentDirty(rb); + + return entity; + } + + // ---------------------------------------------------------------- onCreate + // Runs after the ECS, materials and camera exist, before any scene file is + // loaded and before onStart. Creates the "state" entity that owns the + // scene's State component. + inline void onCreateSystem(ECSManager& ecs, ServiceProvider& services) + { + Entity stateEntity = ecs.createEntity(); + ecs.addComponent(stateEntity); + services.tags().tag(stateEntity, "state"); + services.serialization().blacklistEntity(stateEntity); + + State& state = getState(ecs, services); + state.initialTime = services.time().time(); + state.characterData = new CharacterData(); + std::cout << "[ServiceShowcase] onCreate at simulation time " << state.initialTime << "s" << std::endl; + } + + // ----------------------------------------------------------------- onStart + inline void onStartSystem(ECSManager& ecs, ServiceProvider& services) + { + State& state = getState(ecs, services); + + // Debug flags through the provider + services.debug().setDebugFly(true); + services.debug().setDebugInput(true); + + // Materials through the provider + Material3D& floorMaterial = services.materials().createMaterial(); + floorMaterial.color = vec4(0.8f, 0.8f, 0.8f, 1.0f); + floorMaterial.roughness = 1.0f; + + Material3D& ringMaterial = services.materials().createMaterial(); + ringMaterial.color = vec4(0.9f, 0.3f, 0.2f, 1.0f); + + // Register a custom SDF: a ring (outer circle minus inner circle) + ShapeId ringShape; + { + auto x = std::make_shared(0); + auto y = std::make_shared(1); + auto outerRadius = std::make_shared(2); + auto innerRadius = std::make_shared(3); + + auto outer = std::make_shared(x, y, outerRadius); + auto inner = + std::make_shared(-1.0f, std::make_shared(x, y, innerRadius)); + auto ring = std::make_shared(outer, inner); + + ringShape = services.shapes().registerSDF(ring); + + float vars[8] = {15.0f, 20.0f, 5.0f, 4.0f}; + Entity ringEntity = + services.shapes().addShape(ringShape, vars, ringMaterial, CombinationType::Addition, true, 0); + ecs.getComponent(ringEntity).smoothFactor = 2.0f; + } + + // Floor + { + float vars[8] = {15.0f, -50.0f, 250.0f, 50.0f}; + Entity floor = + services.shapes().addShape(DefaultShapes::BOX, vars, floorMaterial, CombinationType::SmoothAddition); + services.tags().tag(floor, "floor"); + ecs.getComponent(floor).smoothFactor = 3.0f; + } + + // Pit: a subtraction shape; balls that roll into it fall through + { + float vars[8] = {30.0f, 5.0f, 4.0f}; + services.shapes().addShape(DefaultShapes::CIRCLE, vars, 0, CombinationType::Subtraction, true, + CustomShape::GLOBAL_GROUP); + } + + // Camera + ecs.getComponent(services.render().getCameraEntity()).position = g_cameraPositon; + + // Leader ball: orbits a point (moved by FollowSystem through the + // physics simulation) + { + Entity leader = ecs.createEntity(); + auto& t = ecs.addComponent(leader); + t.position = vec3(15.0f, 12.0f, 0.0f); + + auto& dot = ecs.addComponent(leader); + dot.materialId = DisplaySettings::Yellow; + + ecs.addComponent(leader); + services.tags().tag(leader, "leader"); + } + + // Character ball: carries per-body user data so the physics callbacks + // can identify and tune it without touching the ECS (the physics + // thread must not access the ECS). The data pointer is read fresh from + // the RigidBody2D component at spawn time. + { + Entity character = ecs.createEntity(); + auto& t = ecs.addComponent(character); + t.position = vec3(15.0f, 15.0f, 0.0f); + + auto& dot = ecs.addComponent(character); + dot.materialId = DisplaySettings::Orange; + + auto& rb = ecs.addComponent(character); + services.tags().tag(character, "character"); + services.physics().setUserData(rb.simulationId, state.characterData); + } + + // Initial ball pile + for (int i = 0; i < 12; ++i) + { + float x = 8.0f + (i % 4) * 3.0f; + float y = 28.0f + (i / 4) * 4.0f; + spawnBall(ecs, vec2(x, y)); + } + + // UI text (screen space; blacklisted so it is never serialized) + { + auto makeText = [&](const char* initial, vec2 screenPosition, Entity& outEntity, int material) + { + outEntity = ecs.createEntity(); + services.serialization().blacklistEntity(outEntity); + + auto& t = ecs.addComponent(outEntity); + t.position = vec3(screenPosition, 0.0f); + + auto& text = ecs.addComponent(outEntity); + text.text = initial; + text.material = material; + text.horizontalAlignment = TextRenderer::HorizontalAlignment::Left; + text.verticalAlignment = TextRenderer::VerticalAlignment::Bottom; + }; + + makeText("time 0.0s", vec2(10.0f, static_cast(Display::height) - 10.0f), state.timeText, + DisplaySettings::LightGreen); + makeText("entities 0", vec2(10.0f, static_cast(Display::height) - 22.0f), state.entitiesText, + DisplaySettings::Cyan); + makeText("collisions 0 / 0", vec2(10.0f, static_cast(Display::height) - 34.0f), state.collisionsText, + DisplaySettings::Magenta); + makeText("click: spawn | space: pause | arrows: gravity/damping | ctrl+s: save | ctrl+l: load | q: next", + vec2(10.0f, 10.0f), state.hintsText, DisplaySettings::Orange); + } + } + + // ----------------------------------------------------- update: spawn system + // Periodically drops a new ball from the top of the world. + inline void spawnSystem(ECSManager& ecs, ServiceProvider& services) + { + State& state = getState(ecs, services); + + state.spawnTimer += services.time().deltaTime(); + if (state.spawnTimer > 0.35f && ecs.getEntityCount() < 160) + { + state.spawnTimer = 0.0f; + float x = 3.0f + static_cast(std::rand() % 240) / 10.0f; + spawnBall(ecs, vec2(x, 35.0f)); + state.ballsSpawned++; + } + } + + // ----------------------------------------------------- update: input system + inline void inputSystem(ECSManager& ecs, ServiceProvider& services) + { + State& state = getState(ecs, services); + + // Scene transition through the provider + if (services.input().getKeyDown(Input::Q) || services.input().getGamepadButtonDown(Input::GamepadButton::North)) + { + services.sceneControl().goToNextScene(); + } + + // Pause / resume through the provider + if (services.input().getKeyDown(Input::Space)) + { + if (services.physics().isPaused()) + services.physics().resume(); + else + services.physics().pause(); + } + + // Real-time physics settings through the provider + if (services.input().getKeyDown(Input::Up)) + { + state.gravity = std::clamp(state.gravity + 1.0f, -30.0f, 0.0f); + services.physics().setGravity(state.gravity); + } + if (services.input().getKeyDown(Input::Down)) + { + state.gravity = std::clamp(state.gravity - 1.0f, -30.0f, 0.0f); + services.physics().setGravity(state.gravity); + } + if (services.input().getKeyDown(Input::Left)) + { + state.damping = std::max(0.0f, state.damping - 0.05f); + services.physics().setDamping(state.damping); + } + if (services.input().getKeyDown(Input::Right)) + { + state.damping += 0.05f; + services.physics().setDamping(state.damping); + } + + // Spawn a ball where the mouse points + if (services.input().getMouseButtonDown(Input::LeftClick) && !services.input().isUIClick()) + { + auto& cameraTransform = ecs.getComponent(services.render().getCameraEntity()); + vec2 mouseWorld = ECS::Camera::screenPositionToWorldPosition2D( + cameraTransform, vec2(services.input().getMouseX(), services.input().getMouseY())); + spawnBall(ecs, mouseWorld); + state.ballsSpawned++; + } + + // Serialization through the provider + if (services.input().getKeyDown(Input::S) && services.input().getKey(Input::LeftCtrl)) + { + services.serialization().saveScene(services.resources().assetPath("scenes/service_showcase.weird")); + std::cout << "[ServiceShowcase] scene saved" << std::endl; + } + if (services.input().getKeyDown(Input::L) && services.input().getKey(Input::LeftCtrl)) + { + // blacklistEntities = true: entities loaded from disk are excluded + // from future saves, so saving again does not duplicate them. + TagMap loaded = services.serialization().loadWeirdFile( + services.resources().assetPath("scenes/service_showcase.weird"), true); + for (const auto& [name, entity] : loaded) + std::cout << "[ServiceShowcase] loaded tag '" << name << "' -> entity " << entity << std::endl; + } + } + + // ----------------------------------------------------- update: follow system + // Orbits the "leader" ball around a point by writing its velocity straight + // into the physics simulation through the provider. + inline void followSystem(ECSManager& ecs, ServiceProvider& services) + { + State& state = getState(ecs, services); + + Entity leader = services.tags().getEntityByTag("leader"); + if (leader == INVALID_ENTITY) + return; + + state.leaderAngle += services.time().deltaTime() * 1.5f; + + glm::vec2 center(15.0f, 18.0f); + glm::vec2 target = center + 6.0f * glm::vec2(std::cos(state.leaderAngle), std::sin(state.leaderAngle)); + + auto& rb = ecs.getComponent(leader); + glm::vec2 current = glm::vec2(ecs.getComponent(leader).position); + rb.velocity = (target - current) * 2.0f; + ecs.setComponentDirty(rb); + } + + // -------------------------------------------------------- update: ui system + inline void uiSystem(ECSManager& ecs, ServiceProvider& services) + { + State& state = getState(ecs, services); + + char buffer[64]; + + auto& timeText = ecs.getComponent(state.timeText); + std::snprintf(buffer, sizeof(buffer), "time %.1fs", services.time().time()); + timeText.text = buffer; + ecs.setComponentDirty(timeText); + + auto& entitiesText = ecs.getComponent(state.entitiesText); + std::snprintf(buffer, sizeof(buffer), "entities %d (balls spawned: %d)", services.ecs().getEntityCount(), + state.ballsSpawned); + entitiesText.text = buffer; + ecs.setComponentDirty(entitiesText); + + auto& collisionsText = ecs.getComponent(state.collisionsText); + std::snprintf(buffer, sizeof(buffer), "collisions %d body / %d shape", state.entityCollisions, + state.shapeCollisions); + collisionsText.text = buffer; + ecs.setComponentDirty(collisionsText); + } + + // ------------------------------------------------------------ onPhysicsStep + // Physics thread. Simulation-coupled logic only: no ECS access here (the + // engine does not allow touching the ECS from the physics thread), so the + // step counter is a local static rather than a component. + // Applies a gentle "wind" to every body every 60 steps. Physics-thread + // systems only receive the Simulation2D& (no ECS, no ServiceProvider). + inline void onPhysicsStepSystem(Simulation2D& simulation) + { + static int stepCounter = 0; + if (++stepCounter % 60 != 0) + return; + + for (SimulationID id = 0; id < simulation.getSize(); ++id) + simulation.addImpulseForce(id, vec2(0.5f, 0.0f)); + + // Per-body user data: iterate only the bodies that opted in, and + // branch on the type discriminator. No ECS access needed here. + simulation.forEachUserData( + [&](SimulationID id, BodyUserData& data) + { + if (data.type == CharacterData::TYPE) + { + // Give the character a little extra lift each gust + simulation.addImpulseForce(id, vec2(0.0f, 4.0f)); + } + }); + } + + // -------------------------------------------------------------- onPhysicsRigidBodyCollision + // Physics thread. Body-body collisions: push the pair apart based on their + // relative velocity. + inline void onPhysicsRigidBodyCollisionSystem(Simulation2D& simulation, PhysicsCollisionEvent& event) + { + vec2 va = simulation.getPhysicsVelocity(event.bodyA); + vec2 vb = simulation.getPhysicsVelocity(event.bodyB); + + vec2 separation = (va - vb) * 0.05f; + simulation.addImpulseForce(event.bodyA, -separation); + simulation.addImpulseForce(event.bodyB, separation); + } + + // --------------------------------------------------------- onPhysicsShapeCollision + // Physics thread. Shape collisions: bounce the body off the shape normal + // when the penetration is deep enough. The character (identified via its + // per-body user data) gets a bouncier, more slippery response by tuning + // the event before the solver reads it. + inline void onPhysicsShapeCollisionSystem(Simulation2D& simulation, PhysicsShapeCollisionEvent& event) + { + if (event.state == CollisionState::START && event.penetration > 0.1f) + { + simulation.addImpulseForce(event.body, event.normal * (2.0f + event.penetration * 10.0f)); + } + + // Type-checked cast: nullptr unless this body carries CharacterData + if (auto* data = simulation.getUserDataAs(event.body)) + { + event.absortion *= 0.5f; // less normal damping = bouncier + event.friction *= 0.5f; // slippery character + } + } + + // ------------------------------------------------------- onEntityCollision + // Main thread. Body-body collisions mapped to entities: count them, flash + // the colliding ball and play a sound through the provider. + inline void onEntityCollisionSystem(ECSManager& ecs, ServiceProvider& services, EntityCollisionEvent& event) + { + State& state = getState(ecs, services); + state.entityCollisions++; + + if (event.entityA != INVALID_ENTITY && ecs.hasComponent(event.entityA)) + { + auto& dot = ecs.getComponent(event.entityA); + dot.materialId = DisplaySettings::Orange; + ecs.setComponentDirty(dot); + } + + services.audio().playSound({0.05f, 300.0f, false, vec3(0.0f), 1}); + } + + // --------------------------------------------------- onEntityShapeCollision + // Main thread. Shape collisions mapped to entities: count them and play a + // spatial sound at the contact point. + inline void onEntityShapeCollisionSystem(ECSManager& ecs, ServiceProvider& services, + EntityShapeCollisionEvent& event) + { + State& state = getState(ecs, services); + state.shapeCollisions++; + + if (event.entity != INVALID_ENTITY) + { + float frequency = 200.0f + static_cast(state.shapeCollisions % 40) * 5.0f; + services.audio().playSound({0.04f, frequency, true, vec3(event.raw.position, 0.0f), 1}); + } + } + + // ---------------------------------------------------------------- onDestroy + inline void onDestroySystem(ECSManager& ecs, ServiceProvider& services) + { + std::cout << "[ServiceShowcase] scene destroyed at " << services.time().time() << "s" << std::endl; + + State& state = getState(ecs, services); + state.ballsSpawned = 0; + state.entityCollisions = 0; + state.shapeCollisions = 0; + } +} // namespace ServiceShowcase + +class ServiceShowcaseScene : public Scene2D +{ +public: + ServiceShowcaseScene() = default; + +private: + // Most callbacks are thin wrappers around a system. The ServiceProvider + // only reaches callbacks through their `services` parameter (getServices() + // is private); the scene owns no state at all (it lives in the State + // component on the "state" entity). onRender and onImGuiRender are + // inlined below instead of using systems. + + void onCreate(ECSManager& ecs, ServiceProvider& services) override + { + ServiceShowcase::onCreateSystem(ecs, services); + } + + void onStart(ECSManager& ecs, ServiceProvider& services) override + { + ServiceShowcase::onStartSystem(ecs, services); + } + + void onUpdate(ECSManager& ecs, ServiceProvider& services) override + { + ServiceShowcase::spawnSystem(ecs, services); + ServiceShowcase::inputSystem(ecs, services); + ServiceShowcase::followSystem(ecs, services); + ServiceShowcase::uiSystem(ecs, services); + } + + void onEntityCollision(ECSManager& ecs, ServiceProvider& services, EntityCollisionEvent& event) override + { + ServiceShowcase::onEntityCollisionSystem(ecs, services, event); + } + + void onEntityShapeCollision(ECSManager& ecs, ServiceProvider& services, EntityShapeCollisionEvent& event) override + { + ServiceShowcase::onEntityShapeCollisionSystem(ecs, services, event); + } + + void onDestroy(ECSManager& ecs, ServiceProvider& services) override + { + ServiceShowcase::onDestroySystem(ecs, services); + } + + // Don't use systems for these callbacks (they are inlined below). + // Note the firing rules: onRender only fires for 3D / both render modes, + // while onImGuiRender fires for every scene, 2D and 3D alike (it is just + // the debug UI). + void onRender(ECSManager& ecs, ServiceProvider& services, WeirdRenderer::RenderTarget& renderTarget) override + { + static bool logged = false; + if (!logged) + { + logged = true; + std::cout << "[ServiceShowcase] onRender (3D render path)" << std::endl; + } + } + + void onImGuiRender(ECSManager& ecs, ServiceProvider& services) override + { + auto& state = ServiceShowcase::getState(ecs, services); + + ImGui::Text("Time: %.2fs", services.time().time()); + ImGui::Text("Entities: %d", services.ecs().getEntityCount()); + ImGui::Text("Gravity: %.1f | Damping: %.2f | Physics %s", state.gravity, state.damping, + services.physics().isPaused() ? "paused" : "running"); + ImGui::Text("Collisions: %d body / %d shape", state.entityCollisions, state.shapeCollisions); + ImGui::Text("Balls spawned: %d", state.ballsSpawned); + + ImGui::Separator(); + ImGui::Text("Left click: spawn ball | Space: pause/resume"); + ImGui::Text("Up/Down: gravity | Left/Right: damping"); + ImGui::Text("Ctrl+S: save scene | Ctrl+L: load scene | Q: next scene"); + } + + // Physics thread callbacks. Fire on the physics thread mid-step; no ECS + // access here. They delegate to Simulation2D-only systems. + void onPhysicsStep(Simulation2D& simulation) override + { + ServiceShowcase::onPhysicsStepSystem(simulation); + } + + void onPhysicsRigidBodyCollision(Simulation2D& simulation, PhysicsCollisionEvent& event) override + { + ServiceShowcase::onPhysicsRigidBodyCollisionSystem(simulation, event); + } + + void onPhysicsShapeCollision(Simulation2D& simulation, PhysicsShapeCollisionEvent& event) override + { + ServiceShowcase::onPhysicsShapeCollisionSystem(simulation, event); + } +}; diff --git a/examples/sample-scenes/include/ShapesCombinations.h b/examples/sample-scenes/include/ShapesCombinations.h index 958aefe..6884e60 100644 --- a/examples/sample-scenes/include/ShapesCombinations.h +++ b/examples/sample-scenes/include/ShapesCombinations.h @@ -20,15 +20,15 @@ class ShapeCombinatiosScene : public Scene2D std::vector m_uiPoints; - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugInput = true; - m_debugFly = true; + services.debug().setDebugInput(true); + services.debug().setDebugFly(true); // Floor shape { float vars0[8] = {0.5f, 2.5f, 1.0f}; - addShape(DefaultShapes::SINE, vars0, 2, CombinationType::Addition, true, 0); + services.shapes().addShape(DefaultShapes::SINE, vars0, 2, CombinationType::Addition, true, 0); } std::random_device rd; @@ -46,32 +46,33 @@ class ShapeCombinatiosScene : public Scene2D float y = -2.0f + distribY(gen); float vars2[8] = {x, y, 3.0f, 5.0f, 1.0f, 0.0f}; // Custom shape - addShape(DefaultShapes::BOX, vars2, 4 + i, CombinationType::Addition, true, 1); + services.shapes().addShape(DefaultShapes::BOX, vars2, 4 + i, CombinationType::Addition, true, 1); } } // Circle { float vars[8] = {15.0f, 7.5f, 5.0f}; - addShape(DefaultShapes::CIRCLE, vars, 7, CombinationType::Addition, true, 2); + services.shapes().addShape(DefaultShapes::CIRCLE, vars, 7, CombinationType::Addition, true, 2); } // Subtract star { float vars[8] = {-2.5f + 15.0f, 12.5f, 5.0f, 0.5f, 13.0f, 5.0f}; - addShape(DefaultShapes::STAR, vars, 0, CombinationType::SmoothSubtraction, true, 2); + services.shapes().addShape(DefaultShapes::STAR, vars, 0, CombinationType::SmoothSubtraction, true, 2); } // Cursor circle { float vars2[8] = {250.0f, 10.0f, 0.0f, 0.0f, 0.0f, 0.0f}; // Custom shape - m_circle = addShape(DefaultShapes::CIRCLE, vars2, 0, CombinationType::Subtraction, true, - CustomShape::GLOBAL_GROUP); + m_circle = services.shapes().addShape(DefaultShapes::CIRCLE, vars2, 0, CombinationType::Subtraction, true, + CustomShape::GLOBAL_GROUP); } { float vars2[8] = {15.0f, 0.0f, 30.0f, 0.0f, 0.0f, 0.0f}; // Custom shape - addShape(DefaultShapes::CIRCLE, vars2, 0, CombinationType::Intersection, true, CustomShape::GLOBAL_GROUP); + services.shapes().addShape(DefaultShapes::CIRCLE, vars2, 0, CombinationType::Intersection, true, + CustomShape::GLOBAL_GROUP); } for (int i = 0; i < 10; ++i) @@ -86,30 +87,31 @@ class ShapeCombinatiosScene : public Scene2D m_uiPoints.push_back(ee); } - ecs.getComponent(m_mainCamera).position = g_cameraPositon; + ecs.getComponent(services.render().getCameraEntity()).position = g_cameraPositon; } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - g_cameraPositon = ecs.getComponent(m_mainCamera).position; + float delta = services.time().deltaTime(); + g_cameraPositon = ecs.getComponent(services.render().getCameraEntity()).position; - if (Input::GetKeyDown(Input::Q) || Input::GetGamepadButtonDown(Input::GamepadButton::North)) + if (services.input().getKeyDown(Input::Q) || services.input().getGamepadButtonDown(Input::GamepadButton::North)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } - auto& cameraTransform = ecs.getComponent(m_mainCamera); - float x = Input::GetMouseX(); - float y = Input::GetMouseY(); + auto& cameraTransform = ecs.getComponent(services.render().getCameraEntity()); + float x = services.input().getMouseX(); + float y = services.input().getMouseY(); // Transform mouse coordinates to world space vec2 mousePositionInWorld = ECS::Camera::screenPositionToWorldPosition2D(cameraTransform, vec2(x, y)); - if (Input::GetMouseButtonDown(Input::RightClick)) + if (services.input().getMouseButtonDown(Input::RightClick)) { m_initialMousePositionInWorld = mousePositionInWorld; } - else if (Input::GetGamepadButtonDown(Input::GamepadButton::LeftShoulder)) + else if (services.input().getGamepadButtonDown(Input::GamepadButton::LeftShoulder)) { float halfWidth = Display::width / 2.0f; float halfHeight = Display::height / 2.0f; @@ -118,12 +120,12 @@ class ShapeCombinatiosScene : public Scene2D ECS::Camera::screenPositionToWorldPosition2D(cameraTransform, vec2(halfWidth, halfHeight)); } - if (Input::GetMouseButton(Input::RightClick)) + if (services.input().getMouseButton(Input::RightClick)) { vec2 v = mousePositionInWorld - m_initialMousePositionInWorld; m_circleRadious = (std::min)(10.0f, length(v)); } - else if (Input::GetGamepadButton(Input::GamepadButton::LeftShoulder)) + else if (services.input().getGamepadButton(Input::GamepadButton::LeftShoulder)) { m_circleRadious = (std::min)(10.0f, m_circleRadious + (10.0f * delta)); } @@ -152,7 +154,7 @@ class ShapeCombinatiosScene : public Scene2D for (int i = 0; i < m_uiPoints.size(); i++) { // Calculate angle: Time moves them, 'i' spreads them out - float angle = (getTime() * speed) + (i * spacing); + float angle = (services.time().time() * speed) + (i * spacing); float x = center.x + std::cos(angle) * radius; float y = center.y + std::sin(angle) * radius; diff --git a/examples/sample-scenes/include/TextScene.h b/examples/sample-scenes/include/TextScene.h index 7774a40..f7560bf 100644 --- a/examples/sample-scenes/include/TextScene.h +++ b/examples/sample-scenes/include/TextScene.h @@ -24,17 +24,18 @@ class TextScene : public Scene2D int m_counter = 0; int m_lastResolutionHash = 0; - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugInput = true; - m_debugFly = true; + services.debug().setDebugInput(true); + services.debug().setDebugFly(true); { float vars[8] = {15.0f, -50.0f, 250.0f, 50.0f}; - addShape(DefaultShapes::BOX, vars, DisplaySettings::LightGray, CombinationType::SmoothAddition); + services.shapes().addShape(DefaultShapes::BOX, vars, DisplaySettings::LightGray, + CombinationType::SmoothAddition); } - ecs.getComponent(m_mainCamera).position = g_cameraPositon; + ecs.getComponent(services.render().getCameraEntity()).position = g_cameraPositon; m_lastResolutionHash = Display::width + Display::height; { @@ -123,11 +124,11 @@ class TextScene : public Scene2D } } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - if (Input::GetKeyDown(Input::Q) || Input::GetGamepadButtonDown(Input::GamepadButton::North)) + if (services.input().getKeyDown(Input::Q) || services.input().getGamepadButtonDown(Input::GamepadButton::North)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } m_counter++; @@ -137,13 +138,13 @@ class TextScene : public Scene2D ecs.setComponentDirty(text); auto& t = ecs.getComponent(m_counterText); - t.position.x = Input::GetMouseX() + 20.0f; - t.position.y = Input::GetMouseY() + 10.0f; + t.position.x = services.input().getMouseX() + 20.0f; + t.position.y = services.input().getMouseY() + 10.0f; } { - auto& cameraTransform = ecs.getComponent(m_mainCamera); - vec2 mouseScreen = vec2(Input::GetMouseX() + 20.0f, Input::GetMouseY() - 10.0f); + auto& cameraTransform = ecs.getComponent(services.render().getCameraEntity()); + vec2 mouseScreen = vec2(services.input().getMouseX() + 20.0f, services.input().getMouseY() - 10.0f); vec2 mouseWorld = ECS::Camera::screenPositionToWorldPosition2D(cameraTransform, mouseScreen); auto& t = ecs.getComponent(m_worldMouseText); diff --git a/examples/sample-scenes/include/WalkScene.h b/examples/sample-scenes/include/WalkScene.h index d0c76fa..1f97bb2 100644 --- a/examples/sample-scenes/include/WalkScene.h +++ b/examples/sample-scenes/include/WalkScene.h @@ -30,17 +30,18 @@ class WalkScene : public Scene2D Entity m_head; // Inherited via Scene - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { - m_debugInput = true; - m_debugFly = true; + services.debug().setDebugInput(true); + services.debug().setDebugFly(true); - m_background.type = BackgroundType::Sky; - m_background.primaryColor = vec4(0.2f, 0.55f, 0.9f, 1.0f); - m_background.secondaryColor = vec4(0.4f, 0.75f, 0.85f, 1.0f); - m_background.scale = 0.2f; + auto& background = services.render().getBackground(); + background.type = BackgroundType::Sky; + background.primaryColor = vec4(0.2f, 0.55f, 0.9f, 1.0f); + background.secondaryColor = vec4(0.4f, 0.75f, 0.85f, 1.0f); + background.scale = 0.2f; - auto tags = loadWeirdFile(ASSETS_PATH "man.weird"); + auto tags = services.serialization().loadWeirdFile(services.resources().assetPath("man.weird")); Entity firstCreated = static_cast(ecs.getEntityCount()); @@ -61,10 +62,10 @@ class WalkScene : public Scene2D m_head = tags["head"]; float boundsVars2[8]{0.0f, -24.0f, 200.0f, 20.0f}; - Entity inside = - addShape(DefaultShapes::BOX, boundsVars2, DisplaySettings::LightGreen, CombinationType::Addition); + Entity inside = services.shapes().addShape(DefaultShapes::BOX, boundsVars2, DisplaySettings::LightGreen, + CombinationType::Addition); - ecs.getComponent(m_mainCamera).position = g_cameraPositon; + ecs.getComponent(services.render().getCameraEntity()).position = g_cameraPositon; Entity globalSettingsEnt = ecs.createEntity(); auto& settings = ecs.addComponent(globalSettingsEnt); @@ -72,13 +73,15 @@ class WalkScene : public Scene2D ecs.setComponentDirty(settings); } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { - g_cameraPositon = ecs.getComponent(m_mainCamera).position; + float delta = services.time().deltaTime(); - if (Input::GetKeyDown(Input::Q) || Input::GetGamepadButtonDown(Input::GamepadButton::North)) + g_cameraPositon = ecs.getComponent(services.render().getCameraEntity()).position; + + if (services.input().getKeyDown(Input::Q) || services.input().getGamepadButtonDown(Input::GamepadButton::North)) { - setSceneComplete(); + services.sceneControl().goToNextScene(); } updatePhysics(delta, ecs); @@ -167,7 +170,8 @@ class WalkScene : public Scene2D m_feetTouching = false; } - void onEntityCollision(ECSManager& ecs, WeirdEngine::EntityCollisionEvent& event) override + void onEntityCollision(ECSManager& ecs, ServiceProvider& services, + WeirdEngine::EntityCollisionEvent& event) override { Entity entityA = event.entityA; Entity entityB = event.entityB; @@ -178,7 +182,8 @@ class WalkScene : public Scene2D } } - void onEntityShapeCollision(ECSManager& ecs, WeirdEngine::EntityShapeCollisionEvent& event) override + void onEntityShapeCollision(ECSManager& ecs, ServiceProvider& services, + WeirdEngine::EntityShapeCollisionEvent& event) override { Entity entity = event.entity; if (ecs.hasComponent(entity)) diff --git a/examples/sample-scenes/src/main.cpp b/examples/sample-scenes/src/main.cpp index 19f07eb..9e31fa0 100644 --- a/examples/sample-scenes/src/main.cpp +++ b/examples/sample-scenes/src/main.cpp @@ -8,6 +8,7 @@ #include "LifeScene.h" #include "MouseCollisionScene.h" #include "RopeScene.h" +#include "ServiceShowcaseScene.h" #include "ShapesCombinations.h" #include "TextScene.h" #include "WalkScene.h" @@ -21,6 +22,7 @@ int main(int argc, char* argv[]) { SceneManager& sceneManager = SceneManager::getInstance(); + sceneManager.registerScene("service-showcase"); sceneManager.registerScene("shapes"); sceneManager.registerScene("rope"); sceneManager.registerScene("text"); diff --git a/include/weird-engine.h b/include/weird-engine.h index 0bcc52c..0c34245 100644 --- a/include/weird-engine.h +++ b/include/weird-engine.h @@ -165,6 +165,7 @@ namespace WeirdEngine else { Input::handleEvent(event); + ctx.renderer.handleEvent(event); } } } @@ -248,9 +249,11 @@ namespace WeirdEngine #endif } // namespace Detail - void start(SceneManager& sceneManager, DisplaySettings displaySettings = {}, PhysicsSettings physicsSettings = {}, - AudioSettings audioSettings = {}, int argc = 0, char** argv = nullptr) + inline void start(SceneManager& sceneManager, DisplaySettings displaySettings = {}, + PhysicsSettings physicsSettings = {}, AudioSettings audioSettings = {}, int argc = 0, + char** argv = nullptr, const std::string& assetsPath = ASSETS_PATH) { + sceneManager.setAssetsPath(assetsPath); WeirdEngine::Logger::log("Starting Weird Engine..."); std::string startupScene; diff --git a/include/weird-engine/Assert.h b/include/weird-engine/Assert.h new file mode 100644 index 0000000..d60ed1b --- /dev/null +++ b/include/weird-engine/Assert.h @@ -0,0 +1,31 @@ +#pragma once + +// Weird Engine runtime assertions. +// +// Enabled when the WEIRD_ENABLE_ASSERTS compile definition is set (done by the +// engine's CMake for Debug and RelWithDebInfo builds by default). These remain +// active in RelWithDebInfo even though CMake defines NDEBUG there, which is +// why this is not a wrapper around the standard assert(). +// +// When disabled, the macro compiles away entirely and imposes zero overhead. +#if defined(WEIRD_ENABLE_ASSERTS) + +#include +#include + +#define WEIRD_ASSERT(condition, message) \ + do \ + { \ + if (!(condition)) \ + { \ + std::fprintf(stderr, "WEIRD ASSERT FAILED: %s\n %s\n at %s:%d\n", #condition, message, __FILE__, \ + __LINE__); \ + std::abort(); \ + } \ + } while (false) + +#else + +#define WEIRD_ASSERT(condition, message) ((void)0) + +#endif diff --git a/include/weird-engine/Scene.h b/include/weird-engine/Scene.h index 49d28af..6ca8e19 100644 --- a/include/weird-engine/Scene.h +++ b/include/weird-engine/Scene.h @@ -12,6 +12,7 @@ #include "weird-engine/Background.h" #include "weird-engine/Material3D.h" +#include "weird-engine/services/ServiceProvider.h" #include "weird-physics/PhysicsSettings.h" #include "weird-physics/Simulation2D.h" @@ -25,56 +26,142 @@ namespace WeirdEngine struct EntityCollisionEvent { - CollisionEvent& raw; + // Raw event data from the physics thread. Read-only: the physics + // response has already been applied by the time this is dispatched. + const PhysicsCollisionEvent& raw; Entity entityA; Entity entityB; }; struct EntityShapeCollisionEvent { - ShapeCollisionEvent& raw; + // Raw event data from the physics thread. Read-only: the physics + // response has already been applied by the time this is dispatched. + const PhysicsShapeCollisionEvent& raw; Entity entity; }; - constexpr int SOUND_QUEUE_SIZE = 16; - // Forward declaration – full definition in SceneSerializer.h class SceneSerializer; + class SceneManager; + + namespace WeirdRenderer + { + class AudioEngine; + class Renderer; + class MeshRenderPipeline; + } // namespace WeirdRenderer + + namespace Detail + { + struct RuntimeContext; + void runFrame(RuntimeContext& ctx); + } // namespace Detail class Scene { + // Serialization and the service provider reach into the scene's + // private state (storage lives here; the provider is a facade). + friend class SceneManager; friend class SceneSerializer; + friend class ServiceProvider; + friend struct SerializationService; + + friend class WeirdRenderer::AudioEngine; + friend class WeirdRenderer::Renderer; + friend void Detail::runFrame(Detail::RuntimeContext& ctx); public: + // ---- Types /// Map from tag name (std::string) to the entity that owns it. - using TagMap = std::unordered_map; + using TagMap = ::WeirdEngine::TagMap; + + using RenderMode = ::WeirdEngine::RenderMode; + + using RaymarchResult = ::WeirdEngine::RaymarchResult; - Scene(); virtual ~Scene(); + + // ---- Global SDF registry (engine-level, shared across scenes) + static ShapeId registerDefaultSDF(std::shared_ptr sdf); + static const std::vector>& getGlobalSDFs(); + + protected: + // Internal constructors: sets the render mode for Scene2D/Scene3D/SceneBoth. + Scene(); + Scene(RenderMode mode); + + // ---- Lifecycle callbacks + virtual void onCreate(ECSManager& ecs, ServiceProvider& services) {}; + virtual void onStart(ECSManager& ecs, ServiceProvider& services) {} + virtual void onUpdate(ECSManager& ecs, ServiceProvider& services) {}; + virtual void onDestroy(ECSManager& ecs, ServiceProvider& services) {}; + virtual void onImGuiRender(ECSManager& ecs, ServiceProvider& services) {}; + virtual void onRender(ECSManager& ecs, ServiceProvider& services, WeirdRenderer::RenderTarget& renderTarget) {}; + + // ---- Main thread collision callbacks (onEntity* family). Fire after + // the physics response has been applied; the events are read-only. + // m_ecs is safe to use here (the physics thread only ever touches + // Simulation2D internals). See the onPhysics* callbacks below for the + // pre-response, mutable equivalents. + virtual void onEntityCollision(ECSManager& ecs, ServiceProvider& services, + WeirdEngine::EntityCollisionEvent& event) {}; + virtual void onEntityShapeCollision(ECSManager& ecs, ServiceProvider& services, + WeirdEngine::EntityShapeCollisionEvent& event) {}; + + // ---- Physics thread callbacks (onPhysics* family). Fire on the + // physics thread mid-step; no ECS access here. The collision events + // are pre-response and mutable: tune the event fields (friction, + // absorption), queue impulses, or call fix() to alter the solver's + // behavior. Everything else belongs in the onEntity* main-thread + // callbacks above (post-response, read-only). + virtual void onPhysicsStep(Simulation2D& simulation) {}; + virtual void onPhysicsRigidBodyCollision(Simulation2D& simulation, WeirdEngine::PhysicsCollisionEvent& event) { + }; + virtual void onPhysicsShapeCollision(Simulation2D& simulation, WeirdEngine::PhysicsShapeCollisionEvent& event) { + }; + + private: + // ---- Lifecycle (engine-driven) void start(); + void update(double delta, double time); + void destroy() + { + onDestroy(m_ecs, m_services); + } + // ---- Rendering pipeline (engine-driven) void renderExtra(WeirdRenderer::RenderTarget& renderTarget); - void update2DWorldShader(WeirdRenderer::Shader& shader); void update3DWorldShader(WeirdRenderer::Shader& shader); void updateUIShader(WeirdRenderer::Shader& shader); void forceShaderRefresh(); - void update(double delta, double time); - void get2DShapesData(vec4*& data, uint32_t& size, uint32_t& customShapeCount); void get3DShapesData(vec4*& data, uint32_t& size, uint32_t& customShapeCount); void getUIData(vec4*& uiData, uint32_t& size, uint32_t& customShapeCount); + void renderImGui(); + void renderPhysicsStatsUI(); + // ---- Scene state access (engine-driven) WeirdRenderer::Camera& getCamera(); - std::vector& getLigths(); + std::vector& getLights(); + const std::vector& getDrawQueue() const; + AudioRingBuffer& getAudioQueue(); + float getFrictionSound(); - Simulation2D& getSimulation2D() + BackgroundParams& getBackground() { - return m_simulation2D; + return m_background; } - Material3D& createMaterial(); + const BackgroundParams& getBackground() const + { + return m_background; + } + RenderMode getRenderMode() const; + float getTime(); + Material3D& createMaterial(); Material3D& getMaterial(int index) { return m_materials[index]; @@ -84,30 +171,7 @@ namespace WeirdEngine return m_materials; } - BackgroundParams& getBackground() - { - return m_background; - } - const BackgroundParams& getBackground() const - { - return m_background; - } - - float getTime(); - - enum class RenderMode - { - RayMarching3D, - RayMarching2D, - RayMarchingBoth - }; - - RenderMode getRenderMode() const; - - float getFrictionSound(); - const std::vector& getDrawQueue() const; - AudioRingBuffer& getAudioQueue(); - + // ---- Scene control bool isSceneComplete() const { return m_isSceneComplete; @@ -117,171 +181,77 @@ namespace WeirdEngine return m_nextScene; }; - static ShapeId registerDefaultSDF(std::shared_ptr sdf); - static const std::vector>& getGlobalSDFs(); - - ShapeId registerSDF(std::shared_ptr sdf); - // Set the path to a .weird file to load when the scene starts void setSceneFilePath(const std::string& path) { m_sceneFilePath = path; } - struct RaymarchResult - { - float distance; - Entity entity; - }; - - // Physics queries - RaymarchResult raymarch(glm::vec2 origin, glm::vec2 direction, float epsilon = 0.001f, - float maxDistance = 150.0f); - - void renderImGui(); - void renderPhysicsStatsUI(); - - protected: - virtual void onCreate() {}; - virtual void onStart(ECSManager& ecs, const TagMap& tags) - { - onStart(ecs); - } - virtual void onStart(ECSManager& ecs) {} - virtual void onUpdate(float delta, ECSManager& ecs) = 0; - virtual void onRender(WeirdRenderer::RenderTarget& renderTarget) {}; - virtual void onImGuiRender() {}; - - // Physics thread callbacks (No m_ecs access recommended!) - virtual void onPhysicsStep(Simulation2D& simulation) {}; - virtual void onCollision(Simulation2D& simulation, WeirdEngine::CollisionEvent& event) {}; - virtual void onShapeCollision(Simulation2D& simulation, WeirdEngine::ShapeCollisionEvent& event) {}; - - // Main thread callbacks (m_ecs is safe to use here) // ARE YOU SURE??? - virtual void onEntityCollision(ECSManager& ecs, WeirdEngine::EntityCollisionEvent& event) {}; - virtual void onEntityShapeCollision(ECSManager& ecs, WeirdEngine::EntityShapeCollisionEvent& event) {}; - virtual void onDestroy() {}; + // ---- Internal helpers + static void handlePhysicsStep(void* userData); + static void handleCollision(PhysicsCollisionEvent& event, void* userData); + static void handleShapeCollision(PhysicsShapeCollisionEvent& event, void* userData); + // Load scene state from a .weird JSON file + void loadFromWeirdFile(const std::string& path); + void playSound(const WeirdRenderer::SimpleAudioRequest& audio); + // Resolve a physics SimulationID to the owning entity. + Entity getEntityForSimulationId(SimulationID simulationId, + std::shared_ptr> rigidBodies); - void setSceneComplete(std::string nextScene = "") - { - m_isSceneComplete = true; - m_nextScene = nextScene; - }; + ServiceProvider m_services; + // ---- Shared state (managed via ServiceProvider) Entity m_mainCamera; ResourceManager m_resourceManager; - - Material3D m_materials[16]; - uint16_t m_materialCount = 0; - std::vector> m_sdfs; - - Entity addShape(ShapeId shapeId, float* variables, uint16_t material, - CombinationType combination = CombinationType::Addition, bool hasCollision = true, - int group = 0); - Entity addShape(ShapeId shapeId, float* variables, const Material3D& material, - CombinationType combination = CombinationType::Addition, bool hasCollision = true, - int group = 0) - { - return addShape(shapeId, variables, material.id, combination, hasCollision, group); - } - - Entity addUIShape(ShapeId shapeId, float* variables, uint16_t material, - CombinationType combination = CombinationType::Addition, int group = 0); - Entity addUIShape(ShapeId shapeId, float* variables, const Material3D& material, - CombinationType combination = CombinationType::Addition, int group = 0) - { - return addUIShape(shapeId, variables, material.id, combination, group); - } - UIShape& addUIShape(ShapeId shapeId, float* variables, Entity& entity, int group = 0); - - void lookAt(Entity entity); - - // Entities in this set will be skipped during scene serialization - std::unordered_set m_serializationBlacklist; - void blacklistEntity(Entity e) - { - m_serializationBlacklist.insert(e); - } - - // Tag management - // Assign a unique tag to an entity. If the tag is already owned by - // another entity, it is moved to this one. An empty name is treated - // as a removal request (equivalent to calling removeTag). - void tag(Entity entity, const std::string& name); - // Remove any tag currently assigned to an entity. - void removeTag(Entity entity); - // Return the tag of an entity, or "" if none. - std::string getEntityTag(Entity entity) const; - // Return the entity that owns a tag, or MAX_ENTITIES if none. - Entity getEntityByTag(const std::string& name) const; - - SDFRenderSystemContext m_2DWorldRenderContext; - SDFRenderSystemContext m_3DWorldRenderContext; - SDFRenderSystemContext m_UIRenderContext; - // Resolve a physics SimulationID to the owning entity. - Entity getEntityForSimulationId(SimulationID simulationId, - std::shared_ptr> rigidBodies); - bool m_debugFly = false; bool m_debugInput = false; - RenderMode m_renderMode = RenderMode::RayMarching2D; - - void playSound(const WeirdRenderer::SimpleAudioRequest& audio); - - // Save the current scene state to a .weird JSON file - void saveScene(const std::string& filename); - - // Dynamically load a .weird file and add its contents to the scene. - // If blacklistEntities is true, all entities created by the load will be - // excluded from future scene serialization. - // Returns a map of tag names to their corresponding entities. - TagMap loadWeirdFile(const std::string& path, bool blacklistEntities = false); - - // Path to a .weird file to load when the scene starts (set via setSceneFilePath or registerScene) - std::string m_sceneFilePath; - - BackgroundParams m_background; - - private: - // Load scene state from a .weird JSON file - void loadFromWeirdFile(const std::string& path); - + // ---- Simulation + ECSManager m_ecs; + Simulation2D m_simulation2D; bool m_runSimulationInThread; + bool m_simulationIsPaused = false; + + // ---- Collision queues (physics thread pushes, main thread drains) + std::mutex m_collisionQueueMutex; + std::vector m_queuedCollisions; + std::vector m_queuedShapeCollisions; + // ---- Audio, draw queue, lights AudioRingBuffer m_audioQueue; float m_frictionSoundLevel{0.0f}; std::atomic m_frictionSoundLevelRead{0.0f}; - std::vector m_drawQueue; std::vector m_lights; - static void handlePhysicsStep(void* userData); - static void handleCollision(CollisionEvent& event, void* userData); - static void handleShapeCollision(ShapeCollisionEvent& event, void* userData); + // ---- Serialization & visuals + std::unordered_set m_serializationBlacklist; + std::string m_sceneFilePath; + BackgroundParams m_background; + Material3D m_materials[16]; + uint16_t m_materialCount = 0; + SDFRenderSystemContext m_2DWorldRenderContext; + SDFRenderSystemContext m_3DWorldRenderContext; + SDFRenderSystemContext m_UIRenderContext; + RenderMode m_renderMode = RenderMode::RayMarching2D; + // ---- Scene control state std::string m_nextScene; bool m_isSceneComplete = false; - ECSManager m_ecs; - Simulation2D m_simulation2D; - bool m_simulationIsPaused = false; - - std::mutex m_collisionQueueMutex; - std::vector m_queuedCollisions; - std::vector m_queuedShapeCollisions; - - // Entity tag storage (bidirectional maps kept in sync) + // ---- Entity tag storage (bidirectional maps kept in sync) TagMap m_tagToEntity; std::unordered_map m_entityToTag; + + float m_lastDelta = 0.0f; }; class Scene2D : public Scene { public: Scene2D() + : Scene(RenderMode::RayMarching2D) { - m_renderMode = RenderMode::RayMarching2D; } }; @@ -289,8 +259,8 @@ namespace WeirdEngine { public: Scene3D() + : Scene(RenderMode::RayMarching3D) { - m_renderMode = RenderMode::RayMarching3D; } }; @@ -298,8 +268,8 @@ namespace WeirdEngine { public: SceneBoth() + : Scene(RenderMode::RayMarchingBoth) { - m_renderMode = RenderMode::RayMarchingBoth; } }; } // namespace WeirdEngine diff --git a/include/weird-engine/SceneManager.h b/include/weird-engine/SceneManager.h index 6c555ec..08860a4 100644 --- a/include/weird-engine/SceneManager.h +++ b/include/weird-engine/SceneManager.h @@ -16,8 +16,6 @@ namespace WeirdEngine public: ~SceneManager(); - void loadProject(std::string projectDir); - Scene* getCurrentScene(); void setPhysicsSettings(const PhysicsSettings& settings) @@ -29,6 +27,11 @@ namespace WeirdEngine return m_physicsSettings; } + void setAssetsPath(const std::string& path) + { + m_assetsPath = path; + } + static SceneManager& getInstance() { static SceneManager _instance; @@ -50,6 +53,7 @@ namespace WeirdEngine int currentSceneIdx = 0; int targetSceneIdx = 0; PhysicsSettings m_physicsSettings; + std::string m_assetsPath; }; // ChatGPT: Template method declarations and definitions are usually placed in header files. diff --git a/include/weird-engine/ecs/ECS.h b/include/weird-engine/ecs/ECS.h index 72964bd..39ae14f 100644 --- a/include/weird-engine/ecs/ECS.h +++ b/include/weird-engine/ecs/ECS.h @@ -311,6 +311,7 @@ namespace WeirdEngine #include "weird-renderer/components/Camera.h" #include "weird-renderer/components/CustomShape.h" #include "weird-renderer/components/InstancedMeshRenderer.h" +#include "weird-renderer/components/LightComponent.h" #include "weird-renderer/components/MeshRenderer.h" #include "weird-renderer/components/SDFRenderer.h" #include "weird-renderer/components/TextRenderer.h" diff --git a/include/weird-engine/math/MathExpressions.h b/include/weird-engine/math/MathExpressions.h index eb7befb..6277ba9 100644 --- a/include/weird-engine/math/MathExpressions.h +++ b/include/weird-engine/math/MathExpressions.h @@ -215,8 +215,8 @@ namespace WeirdEngine } }; - // Substract - struct Substraction : TwoFloatOperation + // Subtraction + struct Subtraction : TwoFloatOperation { using TwoFloatOperation::TwoFloatOperation; @@ -233,6 +233,9 @@ namespace WeirdEngine } }; + // Deprecated alias of Subtraction + using Substraction = Subtraction; + // Multiplication struct Multiplication : TwoFloatOperation { diff --git a/include/weird-engine/services/ServiceProvider.h b/include/weird-engine/services/ServiceProvider.h new file mode 100644 index 0000000..59b498a --- /dev/null +++ b/include/weird-engine/services/ServiceProvider.h @@ -0,0 +1,733 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "weird-engine/Background.h" +#include "weird-engine/ecs/ECS.h" +#include "weird-engine/Input.h" +#include "weird-engine/Material3D.h" +#include "weird-engine/ResourceManager.h" +#include "weird-engine/systems/SDFRenderSystem.h" +#include "weird-engine/Utils.h" +#include "weird-engine/vec.h" +#include "weird-physics/components/RigidBody.h" +#include "weird-physics/Simulation2D.h" +#include "weird-renderer/audio/AudioRingBuffer.h" +#include "weird-renderer/audio/SimpleAudioRequest.h" +#include "weird-renderer/components/Camera.h" +#include "weird-renderer/components/CustomShape.h" +#include "weird-renderer/scene/Light.h" + +namespace WeirdEngine +{ + class Scene; + + constexpr int SOUND_QUEUE_SIZE = 16; + + /// Map from tag name (std::string) to the entity that owns it. + using TagMap = std::unordered_map; + + struct RaymarchResult + { + float distance; + Entity entity; + }; + + enum class RenderMode + { + RayMarching3D, + RayMarching2D, + RayMarchingBoth + }; + + // Shared raymarch implementation used by both Scene::raymarch and + // PhysicsService::raymarch. Defined in Scene.cpp. + RaymarchResult raymarchScene(ECSManager& ecs, std::vector>& sdfs, + Simulation2D& simulation, float time, glm::vec2 origin, glm::vec2 direction, + float epsilon, float maxDistance); + + struct TimeService + { + Simulation2D& simulation; + const float& delta; + + float time() const + { + return static_cast(simulation.getSimulationTime()); + } + + float deltaTime() const + { + return delta; + } + + double fixedDeltaTime() const + { + return simulation.getDeltaTime(); + } + }; + + struct PhysicsService + { + ECSManager& ecs; + Simulation2D& simulation; + std::vector>& sdfs; + + // Simulation2D& sim() + // { + // return simulation; + // } + + void setGravity(float gravity) + { + simulation.setGravity(gravity); + } + + void setDamping(float damping) + { + simulation.setDamping(damping); + } + + void pause() + { + simulation.pause(); + } + + void resume() + { + simulation.resume(); + } + + bool isPaused() const + { + return simulation.isPaused(); + } + + Entity entityForSimulationId(SimulationID simulationId) const + { + auto rigidBodies = ecs.getComponentArray(); + if (simulationId >= static_cast(rigidBodies->getSize())) + return INVALID_ENTITY; + + return rigidBodies->getEntityAtIdx(static_cast(simulationId)); + } + + // Per-body user data. Set the data right after adding the RigidBody2D + // component (read rb.simulationId from it). The pointer must be + // heap-allocated: the simulation owns it and deletes it when the body + // is removed or when the simulation is destroyed. + void setUserData(SimulationID id, BodyUserData* data) + { + simulation.setUserData(id, data); + } + + BodyUserData* getUserData(SimulationID id) + { + return simulation.getUserData(id); + } + + template T* getUserDataAs(SimulationID id) + { + return simulation.getUserDataAs(id); + } + + template void forEachUserData(Fn&& fn) + { + simulation.forEachUserData(std::forward(fn)); + } + + RaymarchResult raymarch(glm::vec2 origin, glm::vec2 direction, float epsilon = 0.001f, + float maxDistance = 150.0f) + { + return raymarchScene(ecs, sdfs, simulation, static_cast(simulation.getSimulationTime()), origin, + direction, epsilon, maxDistance); + } + }; + + struct ShapeService + { + ECSManager& ecs; + Simulation2D& simulation; + std::vector>& sdfs; + + static ShapeId registerDefaultSDF(std::shared_ptr sdf); + + ShapeId registerSDF(std::shared_ptr sdf) + { + sdfs.push_back(std::move(sdf)); + simulation.setSDFs(sdfs); + + return static_cast(sdfs.size() - 1); + } + + const std::vector>& getSDFs() const + { + return sdfs; + } + + Entity addShape(ShapeId shapeId, float* variables, uint16_t material, + CombinationType combination = CombinationType::Addition, bool hasCollision = true, + int group = 0) + { + Entity entity = ecs.createEntity(); + CustomShape& shape = ecs.addComponent(entity); + shape.distanceFieldId = shapeId; + shape.combination = combination; + shape.hasCollisions = hasCollision; + shape.groupIdx = group; + shape.material = material; + std::copy(variables, variables + 8, shape.parameters); + + return entity; + } + + Entity addShape(ShapeId shapeId, float* variables, const Material3D& material, + CombinationType combination = CombinationType::Addition, bool hasCollision = true, + int group = 0) + { + return addShape(shapeId, variables, material.id, combination, hasCollision, group); + } + + Entity addUIShape(ShapeId shapeId, float* variables, uint16_t material, + CombinationType combination = CombinationType::Addition, int group = 0) + { + Entity entity = ecs.createEntity(); + UIShape& shape = ecs.addComponent(entity); + shape.distanceFieldId = shapeId; + shape.combination = combination; + shape.groupIdx = group; + shape.material = material; + std::copy(variables, variables + 8, shape.parameters); + + return entity; + } + + Entity addUIShape(ShapeId shapeId, float* variables, const Material3D& material, + CombinationType combination = CombinationType::Addition, int group = 0) + { + return addUIShape(shapeId, variables, material.id, combination, group); + } + + UIShape& addUIShape(ShapeId shapeId, float* variables, Entity& entity, int group = 0) + { + entity = ecs.createEntity(); + UIShape& component = ecs.addComponent(entity); + component.distanceFieldId = shapeId; + component.groupIdx = group; + component.smoothFactor = 100.0f; + std::copy(variables, variables + 8, component.parameters); + + return component; + } + }; + + struct RenderService + { + ECSManager& ecs; + Entity& cameraEntity; + SDFRenderSystemContext& context2D; + SDFRenderSystemContext& context3D; + SDFRenderSystemContext& contextUI; + std::vector& lights; + BackgroundParams& background; + RenderMode& renderMode; + + WeirdRenderer::Camera& camera() + { + return ecs.getComponent(cameraEntity).camera; + } + + Entity getCameraEntity() const + { + return cameraEntity; + } + + std::vector& getLights() + { + return lights; + } + + BackgroundParams& getBackground() + { + return background; + } + + const BackgroundParams& getBackground() const + { + return background; + } + + RenderMode getRenderMode() const + { + return renderMode; + } + + SDFRenderSystemContext& getContext2D() + { + return context2D; + } + + SDFRenderSystemContext& getContext3D() + { + return context3D; + } + + SDFRenderSystemContext& getContextUI() + { + return contextUI; + } + + // Force the shader to regenerate the next frame. Materials are baked + // into the shader, so changing a shape's material (or any parameter + // that affects shader generation) requires a refresh. Use the + // granular variants to refresh only the affected render target. + void forceShaderRefresh2D() + { + context2D.shapesNeedUpdate = true; + } + + void forceShaderRefresh3D() + { + context3D.shapesNeedUpdate = true; + } + + void forceShaderRefreshUI() + { + contextUI.shapesNeedUpdate = true; + } + + void forceShaderRefresh() + { + forceShaderRefresh2D(); + forceShaderRefresh3D(); + forceShaderRefreshUI(); + } + }; + + struct MaterialService + { + Material3D (&materials)[16]; + uint16_t& count; + + Material3D& createMaterial() + { + if (count >= 16) + { + // Max materials reached, return the last one + return materials[15]; + } + + Material3D& mat = materials[count]; + mat.id = count; + count++; + + return mat; + } + + Material3D& getMaterial(int index) + { + return materials[index]; + } + + const Material3D* getMaterials() const + { + return materials; + } + + uint16_t getMaterialCount() const + { + return count; + } + }; + + struct AudioService + { + AudioRingBuffer& queue; + const std::atomic& frictionSoundLevel; + + void playSound(const WeirdRenderer::SimpleAudioRequest& audio) + { + queue.push(audio); + } + + float getFrictionSound() const + { + return frictionSoundLevel.load(std::memory_order_acquire); + } + + AudioRingBuffer& audioQueue() + { + return queue; + } + }; + + struct TagService + { + TagMap& tagToEntity; + std::unordered_map& entityToTag; + + // Assign a unique tag to an entity. If the tag is already owned by + // another entity, it is moved to this one. An empty name is treated + // as a removal request (equivalent to calling removeTag). + void tag(Entity entity, const std::string& name) + { + if (name.empty()) + { + removeTag(entity); + return; + } + + // If the tag is already owned by another entity, remove it from that entity + auto existingOwner = tagToEntity.find(name); + if (existingOwner != tagToEntity.end() && existingOwner->second != entity) + { + entityToTag.erase(existingOwner->second); + } + + // Remove any previous tag this entity had + auto existingTag = entityToTag.find(entity); + if (existingTag != entityToTag.end() && existingTag->second != name) + { + tagToEntity.erase(existingTag->second); + } + + tagToEntity[name] = entity; + entityToTag[entity] = name; + } + + void removeTag(Entity entity) + { + auto it = entityToTag.find(entity); + if (it == entityToTag.end()) + return; + tagToEntity.erase(it->second); + entityToTag.erase(it); + } + + std::string getEntityTag(Entity entity) const + { + auto it = entityToTag.find(entity); + if (it == entityToTag.end()) + return ""; + return it->second; + } + + Entity getEntityByTag(const std::string& name) const + { + auto it = tagToEntity.find(name); + if (it == tagToEntity.end()) + return MAX_ENTITIES; + return it->second; + } + }; + + struct SerializationService + { + Scene& scene; + std::unordered_set& blacklist; + std::string& sceneFilePath; + + // Save the current scene state to a .weird JSON file + void saveScene(const std::string& filename); + + // Dynamically load a .weird file and add its contents to the scene. + // If blacklistEntities is true, all entities created by the load will be + // excluded from future scene serialization. + // Returns a map of tag names to their corresponding entities. + TagMap loadWeirdFile(const std::string& path, bool blacklistEntities = false); + + void blacklistEntity(Entity entity) + { + blacklist.insert(entity); + } + + // Set the path to a .weird file to load when the scene starts + void setSceneFilePath(const std::string& path) + { + sceneFilePath = path; + } + }; + + struct SceneControlService + { + bool& isComplete; + std::string& nextScene; + + void goToNextScene(std::string next = "") + { + isComplete = true; + nextScene = std::move(next); + } + }; + + struct InputService + { + bool getKey(Input::KeyCode key) const + { + return Input::GetKey(key); + } + bool getKeyDown(Input::KeyCode key) const + { + return Input::GetKeyDown(key); + } + bool getKeyUp(Input::KeyCode key) const + { + return Input::GetKeyUp(key); + } + + float getMouseX() const + { + return Input::GetMouseX(); + } + float getMouseY() const + { + return Input::GetMouseY(); + } + float getMouseDeltaX() const + { + return Input::GetMouseDeltaX(); + } + float getMouseDeltaY() const + { + return Input::GetMouseDeltaY(); + } + float getMouseDeltaXRaw() const + { + return Input::GetMouseDeltaXRaw(); + } + float getMouseDeltaYRaw() const + { + return Input::GetMouseDeltaYRaw(); + } + bool getMouseButton(Input::MouseButton button) const + { + return Input::GetMouseButton(button); + } + bool getMouseButtonDown(Input::MouseButton button) const + { + return Input::GetMouseButtonDown(button); + } + bool getMouseButtonUp(Input::MouseButton button) const + { + return Input::GetMouseButtonUp(button); + } + void setMousePosition(float x, float y) + { + Input::SetMousePosition(x, y); + } + void showMouse() + { + Input::ShowMouse(); + } + void hideMouse() + { + Input::HideMouse(); + } + bool isUIClick() const + { + return Input::isUIClick(); + } + void flagUIClick() + { + Input::flagUIClick(); + } + + bool getGamepadButton(Input::GamepadButton button) const + { + return Input::GetGamepadButton(button); + } + bool getGamepadButtonDown(Input::GamepadButton button) const + { + return Input::GetGamepadButtonDown(button); + } + bool getGamepadButtonUp(Input::GamepadButton button) const + { + return Input::GetGamepadButtonUp(button); + } + float getGamepadAxis(Input::GamepadAxis axis) const + { + return Input::GetGamepadAxis(axis); + } + + void suppressMouseInput() + { + Input::suppressMouseInput(); + } + void suppressKeyboardInput() + { + Input::suppressKeyboardInput(); + } + }; + + struct ResourceService + { + ResourceManager& resourceManager; + std::string assetsBasePath; + + ResourceManager& resources() + { + return resourceManager; + } + + void setAssetsBasePath(const std::string& path) + { + assetsBasePath = path; + } + + std::string assetPath(const std::string& relative) const + { + return assetsBasePath + relative; + } + + MeshID getMeshId(const std::string& path, Entity entity, bool instancing = false) + { + return resourceManager.getMeshId(assetPath(path).c_str(), entity, instancing); + } + + std::string readTextFile(const std::string& path) const + { + return get_file_contents(path.c_str()); + } + + void writeTextFile(const std::string& path, const std::string& content) const + { + saveToFile(path.c_str(), content); + } + + bool fileExists(const std::string& path) const + { + return checkIfFileExists(path.c_str()); + } + + void ensureDirectory(const std::string& path) const + { + if (!std::filesystem::exists(path)) + { + std::filesystem::create_directory(path); + } + } + }; + + struct DebugService + { + bool& fly; + bool& input; + + bool debugFly() const + { + return fly; + } + + void setDebugFly(bool value) + { + fly = value; + } + + bool debugInput() const + { + return input; + } + + void setDebugInput(bool value) + { + input = value; + } + }; + + // Central access point for all non-ECS scene functionality. Systems take + // a ServiceProvider& (plus the ECSManager&) and use it instead of reaching + // into Scene internals. Owned by Scene, which binds it to its storage. + class ServiceProvider + { + public: + explicit ServiceProvider(Scene& scene); + + ECSManager& ecs() + { + return m_ecs; + } + + TimeService& time() + { + return m_time; + } + + PhysicsService& physics() + { + return m_physics; + } + + ShapeService& shapes() + { + return m_shapes; + } + + RenderService& render() + { + return m_render; + } + + MaterialService& materials() + { + return m_materials; + } + + AudioService& audio() + { + return m_audio; + } + + TagService& tags() + { + return m_tags; + } + + SerializationService& serialization() + { + return m_serialization; + } + + SceneControlService& sceneControl() + { + return m_sceneControl; + } + + ResourceService& resources() + { + return m_resources; + } + + DebugService& debug() + { + return m_debug; + } + + InputService& input() + { + return m_input; + } + + private: + ECSManager& m_ecs; + TimeService m_time; + PhysicsService m_physics; + ShapeService m_shapes; + RenderService m_render; + MaterialService m_materials; + AudioService m_audio; + TagService m_tags; + SerializationService m_serialization; + SceneControlService m_sceneControl; + ResourceService m_resources; + DebugService m_debug; + InputService m_input; + }; +} // namespace WeirdEngine diff --git a/include/weird-engine/systems/PhysicsSystem2D.h b/include/weird-engine/systems/PhysicsSystem2D.h index 7f373bd..a589fcc 100644 --- a/include/weird-engine/systems/PhysicsSystem2D.h +++ b/include/weird-engine/systems/PhysicsSystem2D.h @@ -23,6 +23,8 @@ namespace WeirdEngine inline void update(ECSManager& ecs, Simulation2D& simulation) { + // Pass 1: ECS -> physics. Writes are queued as commands and the + // physics thread applies them on its next step. ecs.forEach( [&](Entity entity, RigidBody2D& rb, Transform& transform) { @@ -56,8 +58,6 @@ namespace WeirdEngine simulation.setContinuousForce(rb.simulationId, rb.pendingContinuousForce); rb.pendingContinuousForce = glm::vec2(0.0f); } - - simulation.updateTransform(transform, rb.simulationId); }); ecs.forEach( @@ -122,6 +122,24 @@ namespace WeirdEngine // initialization commands (position, velocity, mass, etc.) // have been queued ahead of this in m_pendingCommands. simulation.activatePendingBodies(); + + // Pass 2: physics -> ECS readback. The published buffers are + // copied into a reusable snapshot under a short lock; the ECS + // iteration then runs without holding the simulation mutex, so + // the physics thread can keep stepping meanwhile. + static Simulation2D::ReadBufferSnapshot readSnapshot; + + simulation.copyReadBuffers(readSnapshot); + + ecs.forEach( + [&](Entity entity, RigidBody2D& rb, Transform& transform) + { + vec2 position = readSnapshot.positions[rb.simulationId]; + transform.position.x = position.x; + transform.position.y = position.y; + + rb.velocity = readSnapshot.velocities[rb.simulationId]; + }); } } // namespace PhysicsSystem2D } // namespace ECS diff --git a/include/weird-engine/systems/RenderSystem.h b/include/weird-engine/systems/RenderSystem.h index c5da1b3..b6ed627 100644 --- a/include/weird-engine/systems/RenderSystem.h +++ b/include/weird-engine/systems/RenderSystem.h @@ -3,6 +3,7 @@ #include "weird-engine/ResourceManager.h" #include "weird-renderer/resources/DrawCommand.h" +#include "weird-renderer/scene/Light.h" #include namespace WeirdEngine @@ -12,9 +13,11 @@ namespace WeirdEngine namespace RenderSystem { inline void update(ECSManager& ecs, ResourceManager& resourceManager, - std::vector& drawQueue) + std::vector& drawQueue, + std::vector& lights) { drawQueue.clear(); + lights.clear(); ecs.forEach( [&](Entity mOwner, MeshRenderer& mr, Transform& t) @@ -28,6 +31,18 @@ namespace WeirdEngine drawQueue.push_back(cmd); }); + + ecs.forEach( + [&](Entity mOwner, LightComponent& lc, Transform& t) + { + WeirdRenderer::Light light; + light.type = static_cast(lc.type); + light.color = lc.color; + light.position = t.position; + light.rotation = t.rotation; + + lights.push_back(light); + }); } } // namespace RenderSystem -} // namespace WeirdEngine \ No newline at end of file +} // namespace WeirdEngine diff --git a/include/weird-physics/BodyUserData.h b/include/weird-physics/BodyUserData.h new file mode 100644 index 0000000..2ca1cbe --- /dev/null +++ b/include/weird-physics/BodyUserData.h @@ -0,0 +1,18 @@ +#pragma once + +namespace WeirdEngine +{ + // Base class for per-body user data attached to rigid bodies via + // Simulation2D::setUserData(). The simulation only stores the pointer and + // never interprets it: derive from this, set `type` to a game-specific + // discriminator, and use Simulation2D::getUserDataAs() (which checks + // `T::TYPE` against `type` before casting) or check `type` manually. + // + // The pointer must be heap-allocated with `new`. The simulation takes + // ownership: it deletes the data when the body is removed and deletes all + // remaining data when the simulation is destroyed (scene teardown). + struct BodyUserData + { + int type = 0; + }; +} // namespace WeirdEngine diff --git a/include/weird-physics/PhysicsSettings.h b/include/weird-physics/PhysicsSettings.h index 7a0ff2f..4f1a47d 100644 --- a/include/weird-physics/PhysicsSettings.h +++ b/include/weird-physics/PhysicsSettings.h @@ -8,5 +8,6 @@ namespace WeirdEngine float damping = 0.001f; float simulationFrequency = 100.0f; int relaxationSteps = 10; + bool runSimulationInThread = true; }; } // namespace WeirdEngine diff --git a/include/weird-physics/Simulation2D.h b/include/weird-physics/Simulation2D.h index 22ba11c..dee2b1c 100644 --- a/include/weird-physics/Simulation2D.h +++ b/include/weird-physics/Simulation2D.h @@ -20,6 +20,7 @@ #include "weird-engine/vec.h" #include "PhysicsSettings.h" +#include "weird-physics/BodyUserData.h" namespace WeirdEngine { @@ -54,14 +55,14 @@ namespace WeirdEngine END }; - struct CollisionEvent + struct PhysicsCollisionEvent { // CollisionState state; SimulationID bodyA; SimulationID bodyB; }; - struct ShapeCollisionEvent + struct PhysicsShapeCollisionEvent { CollisionState state; SimulationID body; @@ -78,8 +79,8 @@ namespace WeirdEngine using StepCallbackFn = void (*)(void*); // Define the function pointer type and include a user data pointer - using CollisionCallbackFn = void (*)(CollisionEvent&, void*); - using ShapeCollisionCallbackFn = void (*)(ShapeCollisionEvent&, void*); + using CollisionCallbackFn = void (*)(PhysicsCollisionEvent&, void*); + using ShapeCollisionCallbackFn = void (*)(PhysicsShapeCollisionEvent&, void*); struct SpatialGridSnapshot { @@ -151,14 +152,41 @@ namespace WeirdEngine return m_stats; } - // Retrieve results + // Retrieve published results. Safe from any thread, including physics + // callbacks (they just pay a per-call lock). For reading many bodies + // at once on the main thread, copy them into a ReadBufferSnapshot via + // copyReadBuffers() instead. vec2 getPosition(SimulationID id); void setPosition(SimulationID id, vec2 pos); vec2 getVelocity(SimulationID id); void setVelocity(SimulationID id, vec2 vel); - void updateTransform(Transform& transform, SimulationID id); void setMass(SimulationID id, float mass); + // Published physics state copied out under a brief lock. Fill it once + // per frame with copyReadBuffers(), then iterate the ECS without + // holding the simulation mutex. + struct ReadBufferSnapshot + { + std::vector positions; + std::vector velocities; + }; + + // Copies the published positions/velocities into the snapshot under a + // short lock. The snapshot's buffers grow as needed but keep their + // capacity across calls. Main thread only; must NOT be called from + // physics execution (WEIRD_ASSERT enforces this in debug builds). + void copyReadBuffers(ReadBufferSnapshot& snapshot); + + // Current working physics state. PHYSICS EXECUTION ONLY: call these + // from onPhysicsStep/onCollision/onShapeCollision callbacks, never + // from the main thread (WEIRD_ASSERT enforces this in debug builds). + vec2 getPhysicsPosition(SimulationID id) const; + vec2 getPhysicsVelocity(SimulationID id) const; + + // True while inside a physics step (physics thread in threaded mode, + // main thread in single-threaded mode). + static bool isPhysicsExecutionContext(); + void setSDFs(std::vector>& sdfs); std::shared_ptr getSpatialGridSnapshot() @@ -183,6 +211,49 @@ namespace WeirdEngine m_damping = damping; } + // Per-body user data, keyed by SimulationID (entity-free: the ECS maps + // simulation IDs back to entities via the RigidBody2D component array). + // Must be heap-allocated: the simulation owns the pointer and deletes + // it when the body is removed (removeObject) and when the simulation + // is destroyed. setUserData() is main-thread only; getUserData()/ + // getUserDataAs()/forEachUserData() are safe from the physics + // callbacks without locks. + void setUserData(SimulationID id, BodyUserData* data); + BodyUserData* getUserData(SimulationID id); + + // Type-checked cast: returns nullptr unless the attached data exists + // and its `type` matches T::TYPE. + template T* getUserDataAs(SimulationID id) + { + BodyUserData* data = getUserData(id); + if (!data || data->type != T::TYPE) + return nullptr; + return static_cast(data); + } + + // Calls fn(SimulationID, BodyUserData&) for every active body that has + // user data attached. Lock-free from physics callbacks (the step + // already holds the structural mutex); serialized on the main thread. + template void forEachUserData(Fn&& fn) + { + if (isPhysicsExecutionContext()) + { + for (SimulationID id = 0; id < m_size; ++id) + { + if (m_userData[id]) + fn(id, *m_userData[id]); + } + return; + } + + std::lock_guard lock(m_structuralMutex); + for (SimulationID id = 0; id < m_size; ++id) + { + if (m_userData[id]) + fn(id, *m_userData[id]); + } + } + // Constraint structs (public for serialization) struct DistanceConstraint { @@ -335,9 +406,9 @@ namespace WeirdEngine float m_fixedDeltaTimeF; int m_relaxationSteps; - bool m_isPaused; - bool m_simulating; - double m_simulationDelay; + std::atomic m_isPaused{false}; + std::atomic m_simulating{false}; + std::atomic m_simulationDelay{0.0}; std::atomic m_simulationTime{0.0}; bool m_useSimdOperations; @@ -366,6 +437,10 @@ namespace WeirdEngine float* m_mass; float* m_invMass; + // Per-body user data, parallel to the body arrays. Swapped in + // removeObject() so the data follows the body through renumbering. + BodyUserData** m_userData; + const float m_diameter; const float m_diameterSquared; const float m_radious; @@ -381,7 +456,7 @@ namespace WeirdEngine std::vector m_objects; std::vector m_collisionMap; - std::vector m_collisionQueue; + std::vector m_collisionQueue; float map(vec2 p); float map(vec2 p, int& closestShape); diff --git a/include/weird-renderer/components/LightComponent.h b/include/weird-renderer/components/LightComponent.h new file mode 100644 index 0000000..8034413 --- /dev/null +++ b/include/weird-renderer/components/LightComponent.h @@ -0,0 +1,18 @@ +#pragma once +#include + +namespace WeirdEngine +{ + enum class LightType : uint32_t + { + Directional = 0, + Point = 1, + Spot = 2 + }; + + struct LightComponent + { + LightType type = LightType::Directional; + glm::vec4 color = glm::vec4(1.0f); + }; +} // namespace WeirdEngine diff --git a/include/weird-renderer/core/MeshRenderPipeline.h b/include/weird-renderer/core/MeshRenderPipeline.h index 064e5b2..7ac7bf9 100644 --- a/include/weird-renderer/core/MeshRenderPipeline.h +++ b/include/weird-renderer/core/MeshRenderPipeline.h @@ -1,18 +1,13 @@ #pragma once #include "weird-renderer/core/RenderTarget.h" +#include "weird-renderer/resources/DrawCommand.h" #include "weird-renderer/resources/Shader.h" #include "weird-renderer/resources/Texture.h" #include "weird-renderer/scene/Camera.h" #include "weird-renderer/scene/Light.h" #include -// Forward declaration to avoid pulling in the full Scene header -namespace WeirdEngine -{ - class Scene; -} - namespace WeirdEngine { namespace WeirdRenderer @@ -42,8 +37,8 @@ namespace WeirdEngine // Renders 3D models into the internal GBuffer. // outputTarget is forwarded to Scene::onRender for custom per-scene rendering. - void render(Scene& scene, RenderTarget& outputTarget, const Camera& camera, - const std::vector& lights); + void render(RenderTarget& outputTarget, const std::vector& drawQueue, + const Camera& camera, const std::vector& lights); // Recreates internal textures for a new resolution. void resize(unsigned int newWidth, unsigned int newHeight); diff --git a/include/weird-renderer/core/Renderer.h b/include/weird-renderer/core/Renderer.h index 430ee39..b18a398 100644 --- a/include/weird-renderer/core/Renderer.h +++ b/include/weird-renderer/core/Renderer.h @@ -31,6 +31,7 @@ namespace WeirdEngine void render(Scene& scene, const double time, const double delta); void setWindowTitle(const char* name); void setWindowSize(unsigned int width, unsigned int height); + void handleEvent(const SDL_Event& event); SDL_Window* getWindow(); @@ -78,6 +79,7 @@ namespace WeirdEngine std::string m_lastScreenshotPath; // Stats UI (F4) + bool m_showDebugUI = false; bool m_showStatsUI = false; static constexpr int STATS_HISTORY_SIZE = 128; float m_frametimeHistory[STATS_HISTORY_SIZE] = {}; diff --git a/include/weird-renderer/core/SDF3DRenderPipeline.h b/include/weird-renderer/core/SDF3DRenderPipeline.h index 9c02834..6499a62 100644 --- a/include/weird-renderer/core/SDF3DRenderPipeline.h +++ b/include/weird-renderer/core/SDF3DRenderPipeline.h @@ -38,13 +38,21 @@ namespace WeirdEngine Shader& getShader(); + struct GBuffer + { + Texture& albedo; + Texture& worldPos; + Texture& normal; + Texture& material; + Texture& depth; + Texture& backDepth; + }; + // Renders the SDF 3D scene using ray marching with path-traced accumulation. // GBuffer textures come from MeshRenderPipeline and allow the shader to composite // mesh surfaces with SDF lighting (SDFs cast light on meshes; meshes don't affect SDFs). void render(vec4* shapeData, uint32_t dataSize, uint32_t shapeCount, const std::vector& lights, - const Camera& camera, double time, Texture& gbufferAlbedo, Texture& gbufferWorldPos, - Texture& gbufferNormal, Texture& gbufferMaterial, Texture& gbufferDepth, - Texture& gbufferBackDepth, const Material3D* materials); + const Camera& camera, double time, const GBuffer& gbuffer, const Material3D* materials); RenderTarget& getRenderTarget(); Texture& getOutputTexture(); diff --git a/include/weird-renderer/resources/Shader.h b/include/weird-renderer/resources/Shader.h index a6f21f1..5cba4c8 100644 --- a/include/weird-renderer/resources/Shader.h +++ b/include/weird-renderer/resources/Shader.h @@ -22,6 +22,10 @@ namespace WeirdEngine GLuint ID = -1; // Constructor that build the Shader Program from 2 different shaders Shader(const char* vertexFile, const char* fragmentFile); + Shader(const std::string& vertexFile, const std::string& fragmentFile) + : Shader(vertexFile.c_str(), fragmentFile.c_str()) + { + } Shader() {}; // Activates the Shader Program diff --git a/include/weird-renderer/resources/Texture.h b/include/weird-renderer/resources/Texture.h index 841e0e1..e828e35 100644 --- a/include/weird-renderer/resources/Texture.h +++ b/include/weird-renderer/resources/Texture.h @@ -35,6 +35,10 @@ namespace WeirdEngine , height(0) {}; Texture(const char* image); + Texture(const std::string& image) + : Texture(image.c_str()) + { + } Texture(glm::vec4 color); diff --git a/src/weird-engine/Scene.cpp b/src/weird-engine/Scene.cpp index 05331fe..fa7c5c7 100644 --- a/src/weird-engine/Scene.cpp +++ b/src/weird-engine/Scene.cpp @@ -45,10 +45,17 @@ namespace WeirdEngine Scene::Scene() : m_simulation2D(MAX_ENTITIES, SceneManager::getInstance().getPhysicsSettings()) - , m_runSimulationInThread(true) + , m_runSimulationInThread(SceneManager::getInstance().getPhysicsSettings().runSimulationInThread) + , m_services(*this) { } + Scene::Scene(RenderMode mode) + : Scene() + { + m_renderMode = mode; + } + Scene::~Scene() { m_simulation2D.stopSimulationThread(); @@ -59,6 +66,8 @@ namespace WeirdEngine void Scene::start() { + onCreate(m_ecs, m_services); + // Custom component managers std::shared_ptr rbManager = std::make_shared(m_simulation2D); m_ecs.registerComponent(rbManager); @@ -115,23 +124,19 @@ namespace WeirdEngine // Create camera m_mainCamera = m_ecs.createEntity(); - tag(m_mainCamera, "mainCamera"); + m_services.tags().tag(m_mainCamera, "mainCamera"); Transform& t = m_ecs.addComponent(m_mainCamera); t.rotation = vec3(0, 0, -1.0f); ECS::Camera& c = m_ecs.addComponent(m_mainCamera); - onCreate(); - // If a .weird file path was provided (via setSceneFilePath / registerScene), // restore saved scene state before the derived class's onStart() runs. - TagMap loadedTags; if (!m_sceneFilePath.empty()) { SceneSerializer::load(*this, m_sceneFilePath); - loadedTags = m_tagToEntity; } - onStart(m_ecs, loadedTags); + onStart(m_ecs, m_services); switch (m_renderMode) { @@ -158,6 +163,8 @@ namespace WeirdEngine { PROFILE_SCOPE("Scene Update"); + m_lastDelta = static_cast(delta); + if (Input::GetKey(Input::LeftCtrl) && Input::GetKeyDown((Input::R))) { forceShaderRefresh(); @@ -193,8 +200,8 @@ namespace WeirdEngine // Process queued collisions // Static vectors retain heap capacity across frames, avoiding // repeated allocations when thousands of collisions are generated. - static std::vector collisions; - static std::vector shapeCollisions; + static std::vector collisions; + static std::vector shapeCollisions; collisions.clear(); shapeCollisions.clear(); { @@ -209,13 +216,13 @@ namespace WeirdEngine { EntityCollisionEvent entityEvent{ev, getEntityForSimulationId(ev.bodyA, rigidBodies), getEntityForSimulationId(ev.bodyB, rigidBodies)}; - onEntityCollision(m_ecs, entityEvent); + onEntityCollision(m_ecs, m_services, entityEvent); } for (auto& ev : shapeCollisions) { EntityShapeCollisionEvent entityEvent{ev, getEntityForSimulationId(ev.body, rigidBodies)}; - onEntityShapeCollision(m_ecs, entityEvent); + onEntityShapeCollision(m_ecs, m_services, entityEvent); const float m_soundFalloff = 0.1f; bool spatialAudio = false; @@ -246,12 +253,12 @@ namespace WeirdEngine { PROFILE_SCOPE("OnUpdate"); - onUpdate(static_cast(delta), m_ecs); + onUpdate(m_ecs, m_services); } { PROFILE_SCOPE("Render Queue update"); - RenderSystem::update(m_ecs, m_resourceManager, m_drawQueue); + RenderSystem::update(m_ecs, m_resourceManager, m_drawQueue, m_lights); } m_ecs.freeRemovedComponents(); @@ -268,19 +275,19 @@ namespace WeirdEngine self->onPhysicsStep(self->m_simulation2D); } - void Scene::handleCollision(CollisionEvent& event, void* userData) + void Scene::handleCollision(PhysicsCollisionEvent& event, void* userData) { Scene* self = static_cast(userData); - self->onCollision(self->m_simulation2D, event); + self->onPhysicsRigidBodyCollision(self->m_simulation2D, event); std::lock_guard lock(self->m_collisionQueueMutex); self->m_queuedCollisions.push_back(event); } - void Scene::handleShapeCollision(ShapeCollisionEvent& event, void* userData) + void Scene::handleShapeCollision(PhysicsShapeCollisionEvent& event, void* userData) { Scene* self = static_cast(userData); - self->onShapeCollision(self->m_simulation2D, event); + self->onPhysicsShapeCollision(self->m_simulation2D, event); { std::lock_guard lock(self->m_collisionQueueMutex); @@ -348,7 +355,7 @@ namespace WeirdEngine return m_drawQueue; } - std::vector& Scene::getLigths() + std::vector& Scene::getLights() { return m_lights; } @@ -357,20 +364,12 @@ namespace WeirdEngine { if (m_renderMode == RenderMode::RayMarching3D || m_renderMode == RenderMode::RayMarchingBoth) { - onRender(renderTarget); + onRender(m_ecs, m_services, renderTarget); } } // SDFs - ShapeId Scene::registerSDF(std::shared_ptr sdf) - { - m_sdfs.push_back(sdf); - m_simulation2D.setSDFs(m_sdfs); - - return static_cast(m_sdfs.size() - 1); - } - // AUDIO AudioRingBuffer& Scene::getAudioQueue() @@ -390,95 +389,70 @@ namespace WeirdEngine // Serialization - void Scene::tag(Entity entity, const std::string& name) + Entity Scene::getEntityForSimulationId(SimulationID simulationId, + std::shared_ptr> rigidBodies) { - if (name.empty()) - { - removeTag(entity); - return; - } - - // If the tag is already owned by another entity, remove it from that entity - auto existingOwner = m_tagToEntity.find(name); - if (existingOwner != m_tagToEntity.end() && existingOwner->second != entity) - { - m_entityToTag.erase(existingOwner->second); - } - - // Remove any previous tag this entity had - auto existingTag = m_entityToTag.find(entity); - if (existingTag != m_entityToTag.end() && existingTag->second != name) - { - m_tagToEntity.erase(existingTag->second); - } + if (simulationId >= static_cast(rigidBodies->getSize())) + return INVALID_ENTITY; - m_tagToEntity[name] = entity; - m_entityToTag[entity] = name; + return rigidBodies->getEntityAtIdx(static_cast(simulationId)); } - void Scene::removeTag(Entity entity) + void Scene::loadFromWeirdFile(const std::string& path) { - auto it = m_entityToTag.find(entity); - if (it == m_entityToTag.end()) - return; - m_tagToEntity.erase(it->second); - m_entityToTag.erase(it); + SceneSerializer::load(*this, path); } - std::string Scene::getEntityTag(Entity entity) const - { - auto it = m_entityToTag.find(entity); - if (it == m_entityToTag.end()) - return ""; - return it->second; - } + // ServiceProvider - Entity Scene::getEntityByTag(const std::string& name) const + ServiceProvider::ServiceProvider(Scene& scene) + : m_ecs(scene.m_ecs) + , m_time(scene.m_simulation2D, scene.m_lastDelta) + , m_physics(scene.m_ecs, scene.m_simulation2D, scene.m_sdfs) + , m_shapes(scene.m_ecs, scene.m_simulation2D, scene.m_sdfs) + , m_render(scene.m_ecs, scene.m_mainCamera, scene.m_2DWorldRenderContext, scene.m_3DWorldRenderContext, + scene.m_UIRenderContext, scene.m_lights, scene.m_background, scene.m_renderMode) + , m_materials(scene.m_materials, scene.m_materialCount) + , m_audio(scene.m_audioQueue, scene.m_frictionSoundLevelRead) + , m_tags(scene.m_tagToEntity, scene.m_entityToTag) + , m_serialization(scene, scene.m_serializationBlacklist, scene.m_sceneFilePath) + , m_sceneControl(scene.m_isSceneComplete, scene.m_nextScene) + , m_resources{scene.m_resourceManager, ""} + , m_debug(scene.m_debugFly, scene.m_debugInput) + , m_input() { - auto it = m_tagToEntity.find(name); - if (it == m_tagToEntity.end()) - return MAX_ENTITIES; - return it->second; } - Entity Scene::getEntityForSimulationId(SimulationID simulationId, - std::shared_ptr> rigidBodies) + ShapeId ShapeService::registerDefaultSDF(std::shared_ptr sdf) { - if (simulationId >= static_cast(rigidBodies->getSize())) - return INVALID_ENTITY; - - return rigidBodies->getEntityAtIdx(static_cast(simulationId)); + return Scene::registerDefaultSDF(std::move(sdf)); } - void Scene::saveScene(const std::string& filename) + void SerializationService::saveScene(const std::string& filename) { - SceneSerializer::save(*this, filename); + SceneSerializer::save(scene, filename); } - Scene::TagMap Scene::loadWeirdFile(const std::string& path, bool blacklistEntities) + TagMap SerializationService::loadWeirdFile(const std::string& path, bool blacklistEntities) { TagMap loadedTags; - Entity firstNewEntity = m_ecs.getEntityCount(); - SceneSerializer::load(*this, path, &loadedTags); + Entity firstNewEntity = scene.m_ecs.getEntityCount(); + SceneSerializer::load(scene, path, &loadedTags); if (blacklistEntities) { - Entity lastNewEntity = m_ecs.getEntityCount(); + Entity lastNewEntity = scene.m_ecs.getEntityCount(); for (Entity entity = firstNewEntity; entity < lastNewEntity; ++entity) - m_serializationBlacklist.insert(entity); + scene.m_serializationBlacklist.insert(entity); } return loadedTags; } - void Scene::loadFromWeirdFile(const std::string& path) - { - SceneSerializer::load(*this, path); - } - - Scene::RaymarchResult Scene::raymarch(glm::vec2 origin, glm::vec2 direction, float epsilon, float maxDistance) + RaymarchResult raymarchScene(ECSManager& ecs, std::vector>& sdfs, + Simulation2D& simulation, float time, glm::vec2 origin, glm::vec2 direction, + float epsilon, float maxDistance) { float traveled = 0.0f; - float time = getTime(); - auto gridSnapshot = m_simulation2D.getSpatialGridSnapshot(); + auto gridSnapshot = simulation.getSpatialGridSnapshot(); if (epsilon <= 0.0f) { @@ -503,9 +477,9 @@ namespace WeirdEngine groups.reserve(16); // Cache the rigid bodies component array to avoid repeated lookups in the ECS during the raymarching loop - auto rigidBodies = m_ecs.getComponentArray(); + auto rigidBodies = ecs.getComponentArray(); - auto shapeArray = m_ecs.getComponentArray(); + auto shapeArray = ecs.getComponentArray(); for (size_t j = 0; j < shapeArray->getSize(); j++) { auto& shape = shapeArray->getDataAtIdx(j); @@ -513,7 +487,7 @@ namespace WeirdEngine if (!shape.hasCollisions) continue; - if (shape.distanceFieldId >= m_sdfs.size()) + if (shape.distanceFieldId >= sdfs.size()) continue; float parameters[11]; @@ -522,7 +496,7 @@ namespace WeirdEngine parameters[9] = p.x; parameters[10] = p.y; - float dist = m_sdfs[shape.distanceFieldId]->getValue(parameters); + float dist = sdfs[shape.distanceFieldId]->getValue(parameters); float currentMinDistance = d; Entity currentEntity = INVALID_ENTITY; @@ -613,6 +587,14 @@ namespace WeirdEngine float minRigidbodyDist = 1000.0f; Entity closestRbEntity = INVALID_ENTITY; + auto entityForSimulationId = [&](SimulationID simulationId) -> Entity + { + if (simulationId >= static_cast(rigidBodies->getSize())) + return INVALID_ENTITY; + + return rigidBodies->getEntityAtIdx(static_cast(simulationId)); + }; + int gx = static_cast(std::floor(p.x * gridSnapshot->invCellSize)); int gy = static_cast(std::floor(p.y * gridSnapshot->invCellSize)); const int TABLE_SIZE = 8191; // Must match Simulation2D.cpp @@ -643,7 +625,7 @@ namespace WeirdEngine if (dist < minRigidbodyDist) { minRigidbodyDist = dist; - closestRbEntity = getEntityForSimulationId(rbIndex, rigidBodies); + closestRbEntity = entityForSimulationId(rbIndex); } rbIndex = gridSnapshot->next[rbIndex]; @@ -712,7 +694,7 @@ namespace WeirdEngine ImGui::Separator(); - onImGuiRender(); + onImGuiRender(m_ecs, m_services); ImGui::PopID(); } @@ -730,7 +712,7 @@ namespace WeirdEngine if (componentIDs.empty()) continue; - std::string tag = getEntityTag(e); + std::string tag = m_services.tags().getEntityTag(e); std::string label = tag.empty() ? ("Entity " + std::to_string(e)) : (tag + " (ID: " + std::to_string(e) + ")"); @@ -767,47 +749,6 @@ namespace WeirdEngine #endif } - Entity Scene::addShape(ShapeId shapeId, float* variables, uint16_t material, CombinationType combination, - bool hasCollision, int group) - { - Entity entity = m_ecs.createEntity(); - CustomShape& shape = m_ecs.addComponent(entity); - shape.distanceFieldId = shapeId; - shape.combination = combination; - shape.hasCollisions = hasCollision; - shape.groupIdx = group; - shape.material = material; - std::copy(variables, variables + 8, shape.parameters); - - return entity; - } - - Entity Scene::addUIShape(ShapeId shapeId, float* variables, uint16_t material, CombinationType combination, - int group) - { - Entity entity = m_ecs.createEntity(); - UIShape& shape = m_ecs.addComponent(entity); - shape.distanceFieldId = shapeId; - shape.combination = combination; - shape.groupIdx = group; - shape.material = material; - std::copy(variables, variables + 8, shape.parameters); - - return entity; - } - - UIShape& Scene::addUIShape(ShapeId shapeId, float* variables, Entity& entity, int group) - { - entity = m_ecs.createEntity(); - UIShape& component = m_ecs.addComponent(entity); - component.distanceFieldId = shapeId; - component.groupIdx = group; - component.smoothFactor = 100.0f; - std::copy(variables, variables + 8, component.parameters); - - return component; - } - Material3D& Scene::createMaterial() { if (m_materialCount >= 16) diff --git a/src/weird-engine/SceneManager.cpp b/src/weird-engine/SceneManager.cpp index ec4e05c..14c0779 100644 --- a/src/weird-engine/SceneManager.cpp +++ b/src/weird-engine/SceneManager.cpp @@ -1,5 +1,7 @@ #include "weird-engine/SceneManager.h" +#include + namespace WeirdEngine { SceneManager::SceneManager() {} @@ -23,15 +25,32 @@ namespace WeirdEngine void SceneManager::loadScene(const std::string& sceneName) { - if (sceneFactories.find(sceneName) != sceneFactories.end()) + if (sceneFactories.find(sceneName) == sceneFactories.end()) + return; + + // Keep the current/target indices in sync so a later loadNextScene() + // cycles from the scene that is actually loaded. + auto it = std::find(names.begin(), names.end(), sceneName); + if (it != names.end()) + { + currentSceneIdx = static_cast(it - names.begin()); + targetSceneIdx = currentSceneIdx; + } + + if (currentScene) { - currentScene = nullptr; - currentScene = sceneFactories[sceneName](); // Instantiate the scene - currentScene->start(); + // Main-thread cleanup hook. The physics thread may still be + // stepping, so only touch ECS/sim state from the main thread here. + currentScene->destroy(); + } + + currentScene = nullptr; + currentScene = sceneFactories[sceneName](); // Instantiate the scene + currentScene->m_services.resources().setAssetsBasePath(m_assetsPath); + currentScene->start(); #ifndef NDEBUG - WeirdEngine::Logger::log("Changed to " + sceneName + " scene"); + WeirdEngine::Logger::log("Changed to " + sceneName + " scene"); #endif - } } void SceneManager::loadScene(int idx) diff --git a/src/weird-engine/SceneSerializer.cpp b/src/weird-engine/SceneSerializer.cpp index c2773e3..8e100bc 100644 --- a/src/weird-engine/SceneSerializer.cpp +++ b/src/weird-engine/SceneSerializer.cpp @@ -418,8 +418,8 @@ namespace WeirdEngine } else { - // Use scene's tag() method to keep both maps in sync - scene.tag(newEntity, name); + // Use service provider to keep both maps in sync + scene.m_services.tags().tag(newEntity, name); } } } diff --git a/src/weird-physics/Simulation2D.cpp b/src/weird-physics/Simulation2D.cpp index 52619a2..59fbdcb 100644 --- a/src/weird-physics/Simulation2D.cpp +++ b/src/weird-physics/Simulation2D.cpp @@ -3,11 +3,38 @@ #include #include "glm/gtx/norm.hpp" +#include "weird-engine/Assert.h" #include "weird-engine/Logger.h" namespace WeirdEngine { + namespace + { + // True on whatever thread currently runs physics steps. Lets + // Simulation2D::isPhysicsExecutionContext() and the getPhysics*() + // assertions work in both threaded and single-threaded simulation. + thread_local bool g_inPhysicsExecution = false; + + class PhysicsExecutionScope + { + public: + PhysicsExecutionScope() + : m_previous(g_inPhysicsExecution) + { + g_inPhysicsExecution = true; + } + + ~PhysicsExecutionScope() + { + g_inPhysicsExecution = m_previous; + } + + private: + bool m_previous; + }; + } // namespace + #define MEASURE_PERFORMANCE false #define INTEGRATION_METHOD 1 @@ -17,8 +44,7 @@ namespace WeirdEngine const float EPSILON = 0.0001f; Simulation2D::Simulation2D(size_t size, const PhysicsSettings& settings) - : m_isPaused(false) - , m_positions(new vec2[size]) + : m_positions(new vec2[size]) , m_positionsRead(new vec2[size]) , m_positionsAux(new vec2[size]) , m_previousPositions(new vec2[size]) @@ -32,11 +58,10 @@ namespace WeirdEngine , m_continuousForcesWrite(new vec2[size]) , m_mass(new float[size]) , m_invMass(new float[size]) + , m_userData(new BodyUserData*[size]) , m_maxSize(size) , m_size(0) , m_allocated(0) - , m_simulationDelay(0) - , m_simulationTime(0) , m_substeps(1) , m_simulationFrequency(settings.simulationFrequency) , m_fixedDeltaTime(1.0 / static_cast(settings.simulationFrequency)) @@ -45,7 +70,6 @@ namespace WeirdEngine , m_gravity(settings.gravity) , m_push(10.0f * settings.simulationFrequency) , m_damping(settings.damping) - , m_simulating(false) , m_collisionDetectionMethod(MethodNaive) , m_useSimdOperations(false) , m_diameter(1.0f) @@ -69,6 +93,7 @@ namespace WeirdEngine m_mass[i] = 1000.0f; m_invMass[i] = 0.001f; + m_userData[i] = nullptr; } m_sdfs = std::make_shared>>(); @@ -76,6 +101,14 @@ namespace WeirdEngine Simulation2D::~Simulation2D() { + // Free any user data still attached to live bodies (the simulation + // owns these pointers; removed bodies free theirs in removeObject). + for (size_t i = 0; i < m_allocated; ++i) + { + delete m_userData[i]; + m_userData[i] = nullptr; + } + delete[] m_positions; delete[] m_positionsRead; delete[] m_positionsAux; @@ -89,6 +122,7 @@ namespace WeirdEngine delete[] m_continuousForcesWrite; delete[] m_mass; delete[] m_invMass; + delete[] m_userData; } void Simulation2D::pause() @@ -122,6 +156,8 @@ namespace WeirdEngine void Simulation2D::process() { + PhysicsExecutionScope physicsExecution; + int steps = 0; while (m_simulationDelay >= m_fixedDeltaTime && steps < MAX_STEPS) @@ -292,6 +328,38 @@ namespace WeirdEngine // std::lock_guard lock(g_simulationTimeMutex); return m_simulationTime; } + void Simulation2D::setUserData(SimulationID id, BodyUserData* data) + { + WEIRD_ASSERT(!isPhysicsExecutionContext(), "setUserData() may not be called from physics execution context"); + + std::lock_guard lock(m_structuralMutex); + + // Bounds-check against m_allocated, not m_size: bodies can carry user + // data before they are activated (ActivatePending) later in the frame. + if (id >= m_allocated) + return; + + m_userData[id] = data; + } + + BodyUserData* Simulation2D::getUserData(SimulationID id) + { + // Inside a physics step the structural mutex is already held, so the + // read is lock-free; on the main thread it is serialized against + // structural changes (removeObject renumbering). + if (isPhysicsExecutionContext()) + { + if (id >= m_allocated) + return nullptr; + return m_userData[id]; + } + + std::lock_guard lock(m_structuralMutex); + + if (id >= m_allocated) + return nullptr; + return m_userData[id]; + } void Simulation2D::startSimulationThread() { @@ -433,7 +501,7 @@ namespace WeirdEngine // Check bool currentCollision = false; - ShapeCollisionEvent collisionEvent; + PhysicsShapeCollisionEvent collisionEvent; collisionEvent.body = static_cast(i); // Static shapes @@ -769,8 +837,8 @@ namespace WeirdEngine // Notify collision callback if (m_collisionCallback) { - CollisionEvent event{col.A, col.B}; - m_collisionCallback(event, m_callbackUserData); + PhysicsCollisionEvent event{col.A, col.B}; + m_collisionCallback(event, m_callbackUserData); // Why am I creating a new event and not saving it?????? } } @@ -954,6 +1022,7 @@ namespace WeirdEngine SimulationID Simulation2D::generateSimulationID() { std::lock_guard lock(m_structuralMutex); + std::lock_guard readLock(m_readMutex); SimulationID id = static_cast(m_allocated); @@ -973,6 +1042,7 @@ namespace WeirdEngine m_mass[id] = 1.0f; m_invMass[id] = 1.0f; m_collisionMap[id] = false; + m_userData[id] = nullptr; m_allocated++; return id; @@ -989,7 +1059,7 @@ namespace WeirdEngine void Simulation2D::removeObject(SimulationID id) { - std::scoped_lock lock(m_structuralMutex, m_externalForcesMutex, m_fixMutex); + std::scoped_lock lock(m_structuralMutex, m_externalForcesMutex, m_fixMutex, m_readMutex); if (m_size == 0 || id >= m_size) { @@ -1001,6 +1071,12 @@ namespace WeirdEngine if (toId != fromId) { + // The simulation owns user data: free the removed body's data and + // move the swapped body's data along with it. + delete m_userData[toId]; + m_userData[toId] = m_userData[fromId]; + m_userData[fromId] = nullptr; + m_positions[toId] = m_positions[fromId]; m_positionsRead[toId] = m_positionsRead[fromId]; m_positionsAux[toId] = m_positionsAux[fromId]; @@ -1020,6 +1096,12 @@ namespace WeirdEngine m_collisionMap[toId] = m_collisionMap[fromId]; } } + else + { + // Removing the last body: just free its user data. + delete m_userData[toId]; + m_userData[toId] = nullptr; + } // Fix constraints (potentially slow...) @@ -1221,6 +1303,9 @@ namespace WeirdEngine return std::find(m_fixedObjects.begin(), m_fixedObjects.end(), id) != m_fixedObjects.end(); } + // Safe from any thread (main thread, or physics callbacks while the + // physics thread runs). Prefer copyReadBuffers() when reading many + // bodies at once on the main thread. vec2 Simulation2D::getPosition(SimulationID entity) { std::lock_guard lock(m_readMutex); @@ -1243,8 +1328,12 @@ namespace WeirdEngine } } + // Safe from any thread (main thread, or physics callbacks while the + // physics thread runs). Prefer copyReadBuffers() when reading many + // bodies at once on the main thread. vec2 Simulation2D::getVelocity(SimulationID id) { + std::lock_guard lock(m_readMutex); return m_velocitiesRead[id]; } @@ -1264,11 +1353,51 @@ namespace WeirdEngine } } - void Simulation2D::updateTransform(Transform& transform, SimulationID id) + void Simulation2D::copyReadBuffers(ReadBufferSnapshot& snapshot) { + // Reading the published buffers while physics is executing would race + // against the read-buffer swap that happens at the end of every step. + WEIRD_ASSERT(!isPhysicsExecutionContext(), "copyReadBuffers() may not be called from physics execution " + "(onPhysicsStep/onCollision/onShapeCollision). " + "Use simulation.getPhysicsPosition()/getPhysicsVelocity() instead."); + std::lock_guard lock(m_readMutex); - transform.position.x = m_positionsRead[id].x; - transform.position.y = m_positionsRead[id].y; + + // Copy every allocated slot, not just the active bodies: bodies + // created this frame have ids in [m_size, m_allocated) until their + // ActivatePending command is processed, and the readback must be able + // to look up those ids. + size_t count = m_allocated; + if (snapshot.positions.size() < count) + snapshot.positions.resize(count); + if (snapshot.velocities.size() < count) + snapshot.velocities.resize(count); + + std::copy(m_positionsRead, m_positionsRead + count, snapshot.positions.begin()); + std::copy(m_velocitiesRead, m_velocitiesRead + count, snapshot.velocities.begin()); + } + + bool Simulation2D::isPhysicsExecutionContext() + { + return g_inPhysicsExecution; + } + + vec2 Simulation2D::getPhysicsPosition(SimulationID id) const + { + WEIRD_ASSERT(isPhysicsExecutionContext(), "getPhysicsPosition() may only be called from physics execution " + "(onPhysicsStep/onCollision/onShapeCollision callbacks). " + "Use simulation.getPosition() from the main thread instead."); + + return m_positions[id]; + } + + vec2 Simulation2D::getPhysicsVelocity(SimulationID id) const + { + WEIRD_ASSERT(isPhysicsExecutionContext(), "getPhysicsVelocity() may only be called from physics execution " + "(onPhysicsStep/onCollision/onShapeCollision callbacks). " + "Use simulation.getVelocity() from the main thread instead."); + + return m_velocities[id]; } void Simulation2D::setMass(SimulationID id, float mass) diff --git a/src/weird-renderer/audio/AudioEngine.cpp b/src/weird-renderer/audio/AudioEngine.cpp index de7439c..587deae 100644 --- a/src/weird-renderer/audio/AudioEngine.cpp +++ b/src/weird-renderer/audio/AudioEngine.cpp @@ -175,9 +175,6 @@ namespace WeirdEngine if (m_mute) return; - if (Input::GetKeyDown(Input::C)) - playSineSound(getPleasantFrequency(200.0f), 1.0f, 0.1f); - float frictionValue = scene.getFrictionSound(); setFrictionLevel(frictionValue); diff --git a/src/weird-renderer/core/MeshRenderPipeline.cpp b/src/weird-renderer/core/MeshRenderPipeline.cpp index 7e93487..064ca95 100644 --- a/src/weird-renderer/core/MeshRenderPipeline.cpp +++ b/src/weird-renderer/core/MeshRenderPipeline.cpp @@ -5,8 +5,6 @@ #include #endif -#include "weird-engine/Scene.h" - namespace WeirdEngine { namespace WeirdRenderer @@ -67,7 +65,8 @@ namespace WeirdEngine return m_backDepthTexture; } - void MeshRenderPipeline::render(Scene& scene, RenderTarget& outputTarget, const Camera& camera, + void MeshRenderPipeline::render(RenderTarget& outputTarget, + const std::vector& drawQueue, const Camera& camera, const std::vector& lights) { // Set GBuffer uniforms for both shaders @@ -87,7 +86,6 @@ namespace WeirdEngine glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Render scene meshes into the GBuffer. - const auto& drawQueue = scene.getDrawQueue(); for (const auto& cmd : drawQueue) { cmd.mesh->draw(m_gbufferShader, camera, cmd.translation, cmd.rotation, cmd.scale, cmd.materialIndex); diff --git a/src/weird-renderer/core/Renderer.cpp b/src/weird-renderer/core/Renderer.cpp index ec5669a..0d207f1 100644 --- a/src/weird-renderer/core/Renderer.cpp +++ b/src/weird-renderer/core/Renderer.cpp @@ -171,34 +171,12 @@ namespace WeirdEngine #ifndef WEIRD_DISABLE_IMGUI { PROFILE_SCOPE("ImGui"); - static bool showDebugUI = false; - static bool showStatsUI = false; - - if (Input::GetKeyDown(Input::F3)) - { - showDebugUI = !showDebugUI; - } - - if (Input::GetKeyDown(Input::F4)) - { - showStatsUI = !showStatsUI; - if (showStatsUI) - Profiler::get().enableRealtime(); - else - Profiler::get().disableRealtime(); - } - - if (Input::GetKeyDown(Input::F11)) - { - bool isFullscreen = (SDL_GetWindowFlags(m_window) & SDL_WINDOW_FULLSCREEN) != 0; - SDL_SetWindowFullscreen(m_window, !isFullscreen); - } ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplSDL3_NewFrame(); ImGui::NewFrame(); - if (showDebugUI) + if (m_showDebugUI) { ImGui::Begin("Engine Settings"); @@ -309,7 +287,7 @@ namespace WeirdEngine ImGui::End(); } - if (showStatsUI) + if (m_showStatsUI) { drawStatsUI(scene, delta); } @@ -768,7 +746,7 @@ namespace WeirdEngine { PROFILE_SCOPE("3D Render", enable2D); - auto& lights = scene.getLigths(); + auto& lights = scene.getLights(); // --- 1. GBuffer pass: render mesh geometry first --- // Depth testing and culling must be enabled for correct GBuffer writes. @@ -782,7 +760,8 @@ namespace WeirdEngine glFrontFace(GL_CCW); // outputTarget (SDF render target) is forwarded to Scene::onRender callbacks - m_meshPipeline->render(scene, m_3DWorldPipeline->getRenderTarget(), sceneCamera, lights); + m_meshPipeline->render(m_3DWorldPipeline->getRenderTarget(), scene.getDrawQueue(), sceneCamera, + lights); Profiler::get().gpuSync(); } @@ -803,12 +782,13 @@ namespace WeirdEngine static vec4* data3D = nullptr; scene.get3DShapesData(data3D, dataSize3D, shapeCount3D); - m_3DWorldPipeline->render(data3D, dataSize3D, shapeCount3D, lights, sceneCamera, scene.getTime(), - m_meshPipeline->getGBufferAlbedo(), m_meshPipeline->getGBufferWorldPos(), - m_meshPipeline->getGBufferNormal(), m_meshPipeline->getGBufferMaterial(), - m_meshPipeline->getDepthTexture(), m_meshPipeline->getBackDepthTexture(), - scene.getMaterials()); + SDF3DRenderPipeline::GBuffer gbuffer = { + m_meshPipeline->getGBufferAlbedo(), m_meshPipeline->getGBufferWorldPos(), + m_meshPipeline->getGBufferNormal(), m_meshPipeline->getGBufferMaterial(), + m_meshPipeline->getDepthTexture(), m_meshPipeline->getBackDepthTexture()}; + m_3DWorldPipeline->render(data3D, dataSize3D, shapeCount3D, lights, sceneCamera, scene.getTime(), + gbuffer, scene.getMaterials()); glEnable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); @@ -852,4 +832,35 @@ namespace WeirdEngine } } // namespace WeirdRenderer +} // namespace WeirdEngine +namespace WeirdEngine +{ + namespace WeirdRenderer + { + void Renderer::handleEvent(const SDL_Event& event) + { + if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) + { + switch (event.key.key) + { + case SDLK_F3: + m_showDebugUI = !m_showDebugUI; + break; + case SDLK_F4: + m_showStatsUI = !m_showStatsUI; + if (m_showStatsUI) + Profiler::get().enableRealtime(); + else + Profiler::get().disableRealtime(); + break; + case SDLK_F11: + { + bool isFullscreen = (SDL_GetWindowFlags(m_window) & SDL_WINDOW_FULLSCREEN) != 0; + SDL_SetWindowFullscreen(m_window, !isFullscreen); + break; + } + } + } + } + } // namespace WeirdRenderer } // namespace WeirdEngine \ No newline at end of file diff --git a/src/weird-renderer/core/SDF3DRenderPipeline.cpp b/src/weird-renderer/core/SDF3DRenderPipeline.cpp index 779bf73..86325ca 100644 --- a/src/weird-renderer/core/SDF3DRenderPipeline.cpp +++ b/src/weird-renderer/core/SDF3DRenderPipeline.cpp @@ -56,9 +56,7 @@ namespace WeirdEngine void SDF3DRenderPipeline::render(vec4* shapeData, uint32_t dataSize, uint32_t shapeCount, const std::vector& lights, const Camera& camera, double time, - Texture& gbufferAlbedo, Texture& gbufferWorldPos, Texture& gbufferNormal, - Texture& gbufferMaterial, Texture& gbufferDepth, Texture& gbufferBackDepth, - const Material3D* materials) + const GBuffer& gbuffer, const Material3D* materials) { // Reset frame counter when path tracer is disabled (no accumulation) if (!m_config.enablePathTracer) @@ -117,7 +115,7 @@ namespace WeirdEngine m_accumTexture[previousAccumIdx].bind(0); m_sdfShader.setUniform("t_depthTexture", 1); - gbufferDepth.bind(1); + gbuffer.depth.bind(1); // Shape data buffer m_sdfShader.setUniform("t_shapeBuffer", 2); @@ -126,16 +124,16 @@ namespace WeirdEngine // GBuffer colour attachments m_sdfShader.setUniform("t_gbufferAlbedo", 3); - gbufferAlbedo.bind(3); + gbuffer.albedo.bind(3); m_sdfShader.setUniform("t_gbufferWorldPos", 4); - gbufferWorldPos.bind(4); + gbuffer.worldPos.bind(4); m_sdfShader.setUniform("t_gbufferNormal", 5); - gbufferNormal.bind(5); + gbuffer.normal.bind(5); m_sdfShader.setUniform("t_gbufferMaterial", 6); - gbufferMaterial.bind(6); + gbuffer.material.bind(6); m_sdfShader.setUniform("t_gbufferBackDepth", 7); - gbufferBackDepth.bind(7); + gbuffer.backDepth.bind(7); m_sdfShader.setUniform("u_loadedObjects", (int)dataSize); m_sdfShader.setUniform("u_customShapeCount", (int)shapeCount); diff --git a/tools/molecule-editor/include/MoleculeEditor.h b/tools/molecule-editor/include/MoleculeEditor.h index c170cc9..4b91b73 100644 --- a/tools/molecule-editor/include/MoleculeEditor.h +++ b/tools/molecule-editor/include/MoleculeEditor.h @@ -29,6 +29,7 @@ class MoleculeEditor : public Scene2D MoleculeEditor() {} ECSManager* m_tempEcs = nullptr; + ServiceProvider* m_tempSvc = nullptr; private: enum class RightMouseMode @@ -125,14 +126,15 @@ class MoleculeEditor : public Scene2D static constexpr float TAG_INNER_RADIUS = 25.0f; static constexpr int TAG_RING_GROUP = 8; - void onStart(ECSManager& ecs) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { m_tempEcs = &ecs; - m_debugFly = true; + m_tempSvc = &services; + m_tempSvc->debug().setDebugFly(true); g_cameraPositon.x = 0.0f; g_cameraPositon.y = 0.0f; - m_tempEcs->getComponent(m_mainCamera).position = g_cameraPositon; + m_tempEcs->getComponent(m_tempSvc->render().getCameraEntity()).position = g_cameraPositon; // Request neutral simulation behavior for this editor scene. Entity globalSettingsEnt = m_tempEcs->createEntity(); @@ -147,29 +149,32 @@ class MoleculeEditor : public Scene2D { float boundsVars[8]{0.0f, 0.0f, 3000.0f}; - Entity outside = addShape(DefaultShapes::CIRCLE, boundsVars, 17, CombinationType::Addition); + Entity outside = + m_tempSvc->shapes().addShape(DefaultShapes::CIRCLE, boundsVars, 17, CombinationType::Addition); float boundsVars2[8]{0.0f, 0.0f, 20.0f, 20.0f}; - Entity inside = - addShape(DefaultShapes::BOX, boundsVars2, DisplaySettings::Black, CombinationType::Subtraction); + Entity inside = m_tempSvc->shapes().addShape(DefaultShapes::BOX, boundsVars2, DisplaySettings::Black, + CombinationType::Subtraction); - blacklistEntity(outside); - blacklistEntity(inside); + m_tempSvc->serialization().blacklistEntity(outside); + m_tempSvc->serialization().blacklistEntity(inside); } } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { m_tempEcs = &ecs; - g_cameraPositon = m_tempEcs->getComponent(m_mainCamera).position; + m_tempSvc = &services; + g_cameraPositon = m_tempEcs->getComponent(m_tempSvc->render().getCameraEntity()).position; - if (Input::GetKeyDown(Input::Q) || Input::GetGamepadButtonDown(Input::GamepadButton::North)) + if (m_tempSvc->input().getKeyDown(Input::Q) || + m_tempSvc->input().getGamepadButtonDown(Input::GamepadButton::North)) { - setSceneComplete(); + m_tempSvc->sceneControl().goToNextScene(); return; } - if (Input::GetKey(Input::LeftCtrl) && Input::GetKeyDown(Input::S)) + if (m_tempSvc->input().getKey(Input::LeftCtrl) && m_tempSvc->input().getKeyDown(Input::S)) { WeirdEngine::Logger::log("Save scene name: "); @@ -185,11 +190,11 @@ class MoleculeEditor : public Scene2D { fileName += ".weird"; } - saveScene(ASSETS_PATH "Organisms/" + fileName); + m_tempSvc->serialization().saveScene(m_tempSvc->resources().assetPath("Organisms/") + fileName); } } - if (Input::GetKey(Input::LeftCtrl) && Input::GetKeyDown(Input::L)) + if (m_tempSvc->input().getKey(Input::LeftCtrl) && m_tempSvc->input().getKeyDown(Input::L)) { WeirdEngine::Logger::log("Load scene name: "); @@ -205,11 +210,11 @@ class MoleculeEditor : public Scene2D { fileName += ".weird"; } - loadMolecule(ASSETS_PATH "Organisms/" + fileName); + loadMolecule(m_tempSvc->resources().assetPath("Organisms/") + fileName); } } - if (Input::GetMouseButtonDown(Input::LeftClick) && !Input::isUIClick()) + if (m_tempSvc->input().getMouseButtonDown(Input::LeftClick) && !m_tempSvc->input().isUIClick()) { spawnBallAtMouse(); } @@ -310,7 +315,7 @@ class MoleculeEditor : public Scene2D float px = START_X + i * MAT_SPACING; float p[8]{px, MAT_Y, BTN_SIZE - 4.0f}; Entity e; - UIShape& sh = addUIShape(DefaultShapes::CIRCLE, p, e); + UIShape& sh = m_tempSvc->shapes().addUIShape(DefaultShapes::CIRCLE, p, e); sh.material = static_cast(i); auto& tog = m_tempEcs->addComponent(e); @@ -319,7 +324,7 @@ class MoleculeEditor : public Scene2D tog.modifierAmount = 5.0f; m_materialToggles[i] = e; - blacklistEntity(e); + m_tempSvc->serialization().blacklistEntity(e); } m_tempEcs->getComponent(m_materialToggles[m_selectedMaterial]).active = true; @@ -369,14 +374,14 @@ class MoleculeEditor : public Scene2D { float y = (Display::height - TOOL_Y_START) - (i * TOOL_SPACING); float p[8]{TOOL_X, y, TOOL_BTN_HALF, TOOL_BTN_HALF}; - Entity e = addUIShape(DefaultShapes::BOX, p, static_cast(2)); + Entity e = m_tempSvc->shapes().addUIShape(DefaultShapes::BOX, p, static_cast(2)); auto& tog = m_tempEcs->addComponent(e); tog.clickPadding = TOOL_BTN_HALF + 8.0f; tog.parameterModifierMask.set(2); tog.parameterModifierMask.set(3); tog.modifierAmount = 3.0f; m_toolToggles[i] = e; - blacklistEntity(e); + m_tempSvc->serialization().blacklistEntity(e); Entity lbl = m_tempEcs->createEntity(); auto& lt = m_tempEcs->addComponent(lbl); @@ -386,27 +391,27 @@ class MoleculeEditor : public Scene2D tx.material = 1; tx.horizontalAlignment = TextRenderer::HorizontalAlignment::Left; tx.verticalAlignment = TextRenderer::VerticalAlignment::Center; - blacklistEntity(lbl); + m_tempSvc->serialization().blacklistEntity(lbl); } m_tempEcs->getComponent(m_toolToggles[0]).active = true; float starP[8]{Display::width - GRAV_Y, Display::height - GRAV_Y, GRAV_Y * 0.5f, 5.0f, 10.0f, 0.0f}; - m_gravityToggleEntity = addUIShape(DefaultShapes::STAR, starP, static_cast(2)); + m_gravityToggleEntity = m_tempSvc->shapes().addUIShape(DefaultShapes::STAR, starP, static_cast(2)); auto& gravTog = m_tempEcs->addComponent(m_gravityToggleEntity); gravTog.clickPadding = 18.0f; // gravTog.parameterModifierMask.set(2); gravTog.parameterModifierMask.set(5); gravTog.modifierAmount = 10.0f; - blacklistEntity(m_gravityToggleEntity); + m_tempSvc->serialization().blacklistEntity(m_gravityToggleEntity); float gridP[8]{Display::width - GRAV_Y, Display::height - GRID_Y, 12.0f, 12.0f}; - m_gridToggleEntity = addUIShape(DefaultShapes::BOX, gridP, static_cast(2)); + m_gridToggleEntity = m_tempSvc->shapes().addUIShape(DefaultShapes::BOX, gridP, static_cast(2)); auto& gridTog = m_tempEcs->addComponent(m_gridToggleEntity); gridTog.clickPadding = 18.0f; gridTog.parameterModifierMask.set(2); gridTog.parameterModifierMask.set(3); gridTog.modifierAmount = 3.0f; - blacklistEntity(m_gridToggleEntity); + m_tempSvc->serialization().blacklistEntity(m_gridToggleEntity); } void syncToolbar() @@ -457,8 +462,9 @@ class MoleculeEditor : public Scene2D void spawnBallAtMouse() { - auto& cam = m_tempEcs->getComponent(m_mainCamera); - vec2 world = ECS::Camera::screenPositionToWorldPosition2D(cam, vec2(Input::GetMouseX(), Input::GetMouseY())); + auto& cam = m_tempEcs->getComponent(m_tempSvc->render().getCameraEntity()); + vec2 world = ECS::Camera::screenPositionToWorldPosition2D( + cam, vec2(m_tempSvc->input().getMouseX(), m_tempSvc->input().getMouseY())); if (m_gridMode) world = snapToGrid(world); @@ -479,8 +485,9 @@ class MoleculeEditor : public Scene2D vec2 getMouseWorldPosition() { - auto& cam = m_tempEcs->getComponent(m_mainCamera); - return ECS::Camera::screenPositionToWorldPosition2D(cam, vec2(Input::GetMouseX(), Input::GetMouseY())); + auto& cam = m_tempEcs->getComponent(m_tempSvc->render().getCameraEntity()); + return ECS::Camera::screenPositionToWorldPosition2D( + cam, vec2(m_tempSvc->input().getMouseX(), m_tempSvc->input().getMouseY())); } vec2 snapToGrid(vec2 pos, Entity exclude = static_cast(-1)) @@ -545,7 +552,7 @@ class MoleculeEditor : public Scene2D void handleRightMouseDragInput() { - bool rightDown = Input::GetMouseButton(Input::RightClick); + bool rightDown = m_tempSvc->input().getMouseButton(Input::RightClick); if (rightDown && !m_rightWasDown) { @@ -627,7 +634,7 @@ class MoleculeEditor : public Scene2D if (m_draggedBall == static_cast(-1) || m_draggedSimulationId < 0) return; - if (Input::GetKeyDown(Input::F)) + if (m_tempSvc->input().getKeyDown(Input::F)) { m_keepFixedAfterDrag = true; } @@ -691,8 +698,9 @@ class MoleculeEditor : public Scene2D Entity pickBallAtMouse() { - auto& cam = m_tempEcs->getComponent(m_mainCamera); - vec2 world = ECS::Camera::screenPositionToWorldPosition2D(cam, vec2(Input::GetMouseX(), Input::GetMouseY())); + auto& cam = m_tempEcs->getComponent(m_tempSvc->render().getCameraEntity()); + vec2 world = ECS::Camera::screenPositionToWorldPosition2D( + cam, vec2(m_tempSvc->input().getMouseX(), m_tempSvc->input().getMouseY())); float best = BALL_HIT_RADIUS; Entity bestEntity = static_cast(-1); @@ -768,13 +776,13 @@ class MoleculeEditor : public Scene2D float lineVars[8]{}; computeScreenLineParams(pa, pb, lineVars); - Entity line = addUIShape(DefaultShapes::LINE, lineVars, lineColor); + Entity line = m_tempSvc->shapes().addUIShape(DefaultShapes::LINE, lineVars, lineColor); auto& btn = m_tempEcs->addComponent(line); btn.clickPadding = 8.0f; btn.modifierAmount = 0.0f; - blacklistEntity(line); + m_tempSvc->serialization().blacklistEntity(line); m_links.push_back({a, b, idA, idB, restDistance, line, type, constraintEnt}); } @@ -805,7 +813,7 @@ class MoleculeEditor : public Scene2D void handleConstraintLineClicks() { // Detect click-down via ShapeButton state. - // ButtonSystem already calls Input::flagUIClick() when a line is clicked, + // ButtonSystem already calls m_tempSvc->input().flagUIClick() when a line is clicked, // so ball spawning is suppressed automatically. for (auto& link : m_links) { @@ -815,7 +823,7 @@ class MoleculeEditor : public Scene2D if (btn.state == ButtonState::Down) { m_draggedLink = &link; - m_linkDragStartX = Input::GetMouseX(); + m_linkDragStartX = m_tempSvc->input().getMouseX(); m_linkDragStartDist = link.restDistance; m_dragLinkSimIdA.store(link.simulationIdA, std::memory_order_relaxed); m_dragLinkSimIdB.store(link.simulationIdB, std::memory_order_relaxed); @@ -823,7 +831,7 @@ class MoleculeEditor : public Scene2D } } - if (!Input::GetMouseButton(Input::LeftClick)) + if (!m_tempSvc->input().getMouseButton(Input::LeftClick)) { m_draggedLink = nullptr; return; @@ -832,7 +840,7 @@ class MoleculeEditor : public Scene2D if (m_draggedLink == nullptr) return; - float dx = (Input::GetMouseX() - m_linkDragStartX) * 0.3f; + float dx = (m_tempSvc->input().getMouseX() - m_linkDragStartX) * 0.3f; float newDist = std::round((m_linkDragStartDist + dx) * 10.0f) / 10.0f; newDist = (std::clamp)(newDist, 1.0f, 10.0f); m_draggedLink->restDistance = newDist; @@ -843,7 +851,7 @@ class MoleculeEditor : public Scene2D void updateConstraintLines() { - auto& cam = m_tempEcs->getComponent(m_mainCamera); + auto& cam = m_tempEcs->getComponent(m_tempSvc->render().getCameraEntity()); for (auto& link : m_links) { @@ -869,7 +877,7 @@ class MoleculeEditor : public Scene2D void computeScreenLineParams(const vec2& aWorld, const vec2& bWorld, float outParams[8]) { - auto& cam = m_tempEcs->getComponent(m_mainCamera); + auto& cam = m_tempEcs->getComponent(m_tempSvc->render().getCameraEntity()); vec2 aScreen = ECS::Camera::worldPosition2DToScreenPosition(cam, aWorld); vec2 bScreen = ECS::Camera::worldPosition2DToScreenPosition(cam, bWorld); @@ -907,7 +915,7 @@ class MoleculeEditor : public Scene2D tx.horizontalAlignment = TextRenderer::HorizontalAlignment::Center; tx.verticalAlignment = TextRenderer::VerticalAlignment::Center; m_tagLabelEntity = lbl; - blacklistEntity(lbl); + m_tempSvc->serialization().blacklistEntity(lbl); } // "edit tag" button (a small box) @@ -915,11 +923,11 @@ class MoleculeEditor : public Scene2D static constexpr float BW = 40.0f; static constexpr float BH = 14.0f; float p[8]{Display::width * 0.5f, 90.0f, BW, BH}; - m_tagEditButton = addUIShape(DefaultShapes::BOX, p, static_cast(2)); + m_tagEditButton = m_tempSvc->shapes().addUIShape(DefaultShapes::BOX, p, static_cast(2)); auto& btn = m_tempEcs->addComponent(m_tagEditButton); btn.clickPadding = 6.0f; btn.modifierAmount = 0.0f; - blacklistEntity(m_tagEditButton); + m_tempSvc->serialization().blacklistEntity(m_tagEditButton); } } @@ -950,16 +958,18 @@ class MoleculeEditor : public Scene2D if (m_tagCircleOuter == static_cast(-1)) { float p[8]{}; - m_tagCircleOuter = addUIShape(DefaultShapes::CIRCLE, p, static_cast(DisplaySettings::Yellow), - CombinationType::Addition, TAG_RING_GROUP); - blacklistEntity(m_tagCircleOuter); - - m_tagCircleInner = addUIShape(DefaultShapes::CIRCLE, p, static_cast(DisplaySettings::Yellow), - CombinationType::Subtraction, TAG_RING_GROUP); - blacklistEntity(m_tagCircleInner); + m_tagCircleOuter = + m_tempSvc->shapes().addUIShape(DefaultShapes::CIRCLE, p, static_cast(DisplaySettings::Yellow), + CombinationType::Addition, TAG_RING_GROUP); + m_tempSvc->serialization().blacklistEntity(m_tagCircleOuter); + + m_tagCircleInner = + m_tempSvc->shapes().addUIShape(DefaultShapes::CIRCLE, p, static_cast(DisplaySettings::Yellow), + CombinationType::Subtraction, TAG_RING_GROUP); + m_tempSvc->serialization().blacklistEntity(m_tagCircleInner); } - auto& cam = m_tempEcs->getComponent(m_mainCamera); + auto& cam = m_tempEcs->getComponent(m_tempSvc->render().getCameraEntity()); const auto& ht = m_tempEcs->getComponent(m_tagSelectedEntity); vec2 world(ht.position.x, ht.position.y); vec2 screen = ECS::Camera::worldPosition2DToScreenPosition(cam, world); @@ -977,7 +987,7 @@ class MoleculeEditor : public Scene2D // Update the tag label text if (m_tagLabelEntity != static_cast(-1)) { - std::string currentTag = getEntityTag(m_tagSelectedEntity); + std::string currentTag = m_tempSvc->tags().getEntityTag(m_tagSelectedEntity); auto& tx = m_tempEcs->getComponent(m_tagLabelEntity); std::string newText = currentTag.empty() ? "tag: (none)" : ("tag: " + currentTag); if (tx.text != newText) @@ -1000,11 +1010,11 @@ class MoleculeEditor : public Scene2D std::getline(std::cin, newTag); if (newTag.empty()) { - removeTag(m_tagSelectedEntity); + m_tempSvc->tags().removeTag(m_tagSelectedEntity); } else { - tag(m_tagSelectedEntity, newTag); + m_tempSvc->tags().tag(m_tagSelectedEntity, newTag); } } } @@ -1050,12 +1060,12 @@ class MoleculeEditor : public Scene2D size_t prevDistCount = m_tempEcs->getComponentArray()->getSize(); // Load the file — creates new entities / rigid bodies / constraints - TagMap loadedTags = loadWeirdFile(path); + TagMap loadedTags = m_tempSvc->serialization().loadWeirdFile(path); // Apply loaded tags to the scene for (const auto& [name, entity] : loadedTags) { - tag(entity, name); + m_tempSvc->tags().tag(entity, name); } // Collect new balls: find entities with both Dot and RigidBody2D @@ -1107,12 +1117,12 @@ class MoleculeEditor : public Scene2D float lineVars[8]{}; computeScreenLineParams(pa, pb, lineVars); - Entity line = addUIShape(DefaultShapes::LINE, lineVars, lineColor); + Entity line = m_tempSvc->shapes().addUIShape(DefaultShapes::LINE, lineVars, lineColor); auto& btn = m_tempEcs->addComponent(line); btn.clickPadding = 8.0f; btn.modifierAmount = 0.0f; - blacklistEntity(line); + m_tempSvc->serialization().blacklistEntity(line); int idA = m_tempEcs->getComponent(a).simulationId; int idB = m_tempEcs->getComponent(b).simulationId; @@ -1141,12 +1151,12 @@ class MoleculeEditor : public Scene2D float lineVars[8]{}; computeScreenLineParams(pa, pb, lineVars); - Entity line = addUIShape(DefaultShapes::LINE, lineVars, lineColor); + Entity line = m_tempSvc->shapes().addUIShape(DefaultShapes::LINE, lineVars, lineColor); auto& btn = m_tempEcs->addComponent(line); btn.clickPadding = 8.0f; btn.modifierAmount = 0.0f; - blacklistEntity(line); + m_tempSvc->serialization().blacklistEntity(line); int idA = m_tempEcs->getComponent(a).simulationId; int idB = m_tempEcs->getComponent(b).simulationId; @@ -1159,9 +1169,10 @@ class MoleculeEditor : public Scene2D WeirdEngine::Logger::log(loadMsg); } - void onEntityShapeCollision(ECSManager& ecs, WeirdEngine::EntityShapeCollisionEvent& event) override + void onEntityShapeCollision(ECSManager& ecs, ServiceProvider& services, + WeirdEngine::EntityShapeCollisionEvent& event) override { m_tempEcs = &ecs; - event.raw.friction *= 100.0f; + m_tempSvc = &services; } }; diff --git a/tools/scene-editor/include/SceneEditor.h b/tools/scene-editor/include/SceneEditor.h index 5bf99ac..63ea7d1 100644 --- a/tools/scene-editor/include/SceneEditor.h +++ b/tools/scene-editor/include/SceneEditor.h @@ -23,6 +23,7 @@ class SceneEditor : public Scene2D } ECSManager* m_tempEcs = nullptr; + ServiceProvider* m_tempSvc = nullptr; private: // ===================================================================== @@ -86,12 +87,13 @@ class SceneEditor : public Scene2D // ===================================================================== // Lifecycle // ===================================================================== - void onStart(ECSManager& ecs, const TagMap& tags) override + void onStart(ECSManager& ecs, ServiceProvider& services) override { m_tempEcs = &ecs; - m_debugInput = true; - m_debugFly = true; - m_tempEcs->getComponent(m_mainCamera).position = g_cameraPositon; + m_tempSvc = &services; + m_tempSvc->debug().setDebugInput(true); + m_tempSvc->debug().setDebugFly(true); + m_tempEcs->getComponent(m_tempSvc->render().getCameraEntity()).position = g_cameraPositon; buildShapeButtons(); buildCombToggles(); @@ -99,30 +101,33 @@ class SceneEditor : public Scene2D buildParamPanel(); } - void onUpdate(float delta, ECSManager& ecs) override + void onUpdate(ECSManager& ecs, ServiceProvider& services) override { m_tempEcs = &ecs; - g_cameraPositon = m_tempEcs->getComponent(m_mainCamera).position; + m_tempSvc = &services; + g_cameraPositon = m_tempEcs->getComponent(m_tempSvc->render().getCameraEntity()).position; - if (Input::GetKeyDown(Input::Q) || Input::GetGamepadButtonDown(Input::GamepadButton::North)) - setSceneComplete(); - if (Input::GetKey(Input::LeftCtrl) && Input::GetKeyDown(Input::S)) - saveScene(ASSETS_PATH "example.weird"); + if (m_tempSvc->input().getKeyDown(Input::Q) || + m_tempSvc->input().getGamepadButtonDown(Input::GamepadButton::North)) + m_tempSvc->sceneControl().goToNextScene(); + if (m_tempSvc->input().getKey(Input::LeftCtrl) && m_tempSvc->input().getKeyDown(Input::S)) + m_tempSvc->serialization().saveScene(m_tempSvc->resources().assetPath("example.weird")); syncMaterialToggles(); syncCombToggles(); - if (Input::GetMouseButtonDown(Input::LeftClick)) + if (m_tempSvc->input().getMouseButtonDown(Input::LeftClick)) onLeftClick(); - if (Input::GetMouseButton(Input::LeftClick)) + if (m_tempSvc->input().getMouseButton(Input::LeftClick)) { - auto& cam = m_tempEcs->getComponent(m_mainCamera); - vec2 wp = ECS::Camera::screenPositionToWorldPosition2D(cam, vec2(Input::GetMouseX(), Input::GetMouseY())); + auto& cam = m_tempEcs->getComponent(m_tempSvc->render().getCameraEntity()); + vec2 wp = ECS::Camera::screenPositionToWorldPosition2D( + cam, vec2(m_tempSvc->input().getMouseX(), m_tempSvc->input().getMouseY())); spawnPhysicsEntity(wp); } - if (Input::GetMouseButtonDown(Input::RightClick)) + if (m_tempSvc->input().getMouseButtonDown(Input::RightClick)) onRightClick(); - if (Input::GetKeyDown(Input::X) && m_hasSelection) + if (m_tempSvc->input().getKeyDown(Input::X) && m_hasSelection) deleteSelected(); refreshPanel(); @@ -139,7 +144,7 @@ class SceneEditor : public Scene2D { auto& transform = transformArray->getDataAtIdx(i); Entity entity = transformArray->getEntityAtIdx(i); - if (entity == m_mainCamera) + if (entity == m_tempSvc->render().getCameraEntity()) continue; if (transform.position.y < -10.0f) @@ -164,13 +169,13 @@ class SceneEditor : public Scene2D float p[8]{}; previewParams(types[i], cx, cy, p); - Entity e = addUIShape(types[i], p, 2); + Entity e = m_tempSvc->shapes().addUIShape(types[i], p, 2); auto& b = m_tempEcs->addComponent(e); b.modifierAmount = 1.0f; b.clickPadding = 8.0f; m_shapeButtons.push_back({e, types[i]}); - blacklistEntity(e); + m_tempSvc->serialization().blacklistEntity(e); } } @@ -244,10 +249,11 @@ class SceneEditor : public Scene2D int g = COMB_GRP_BASE + i; float p1[8]{cx - off * 0.5f, cy, r}; - Entity e1 = addUIShape(DefaultShapes::CIRCLE, p1, static_cast(1), CombinationType::Addition, g); + Entity e1 = m_tempSvc->shapes().addUIShape(DefaultShapes::CIRCLE, p1, static_cast(1), + CombinationType::Addition, g); float p2[8]{cx + off * 0.5f, cy, r}; - Entity e2 = addUIShape(DefaultShapes::CIRCLE, p2, static_cast(1), ct[i], g); + Entity e2 = m_tempSvc->shapes().addUIShape(DefaultShapes::CIRCLE, p2, static_cast(1), ct[i], g); if (ct[i] == CombinationType::SmoothAddition || ct[i] == CombinationType::SmoothSubtraction) m_tempEcs->getComponent(e2).smoothFactor = 5.0f; @@ -265,9 +271,9 @@ class SceneEditor : public Scene2D tx.horizontalAlignment = TextRenderer::HorizontalAlignment::Center; m_combButtons.push_back({e1, ct[i]}); - blacklistEntity(e1); - blacklistEntity(e2); - blacklistEntity(lbl); + m_tempSvc->serialization().blacklistEntity(e1); + m_tempSvc->serialization().blacklistEntity(e2); + m_tempSvc->serialization().blacklistEntity(lbl); } m_tempEcs->getComponent(m_combButtons[0].toggleEntity).active = true; } @@ -282,7 +288,7 @@ class SceneEditor : public Scene2D float px = START_X + i * MAT_SPACING; float p[8]{px, MAT_Y, BTN_SIZE - 4.0f}; Entity e; - UIShape& sh = addUIShape(DefaultShapes::CIRCLE, p, e); + UIShape& sh = m_tempSvc->shapes().addUIShape(DefaultShapes::CIRCLE, p, e); sh.material = static_cast(i); auto& tog = m_tempEcs->addComponent(e); @@ -291,7 +297,7 @@ class SceneEditor : public Scene2D tog.modifierAmount = 5.0f; m_materialToggles[i] = e; - blacklistEntity(e); + m_tempSvc->serialization().blacklistEntity(e); } m_tempEcs->getComponent(m_materialToggles[m_selectedMaterial]).active = true; } @@ -309,14 +315,14 @@ class SceneEditor : public Scene2D auto& hdr = m_tempEcs->addComponent(m_selInfoText); hdr.material = 1; hdr.horizontalAlignment = TextRenderer::HorizontalAlignment::Right; - blacklistEntity(m_selInfoText); + m_tempSvc->serialization().blacklistEntity(m_selInfoText); for (int i = 0; i < 8; i++) { float py = PANEL_TOP_Y - i * PARAM_GAP; float bp[8]{HIDDEN, py, P_BTN_W, P_BTN_H}; - Entity be = addUIShape(DefaultShapes::BOX, bp, static_cast(3)); + Entity be = m_tempSvc->shapes().addUIShape(DefaultShapes::BOX, bp, static_cast(3)); auto& btn = m_tempEcs->addComponent(be); btn.modifierAmount = 1.0f; btn.clickPadding = 3.0f; @@ -331,8 +337,8 @@ class SceneEditor : public Scene2D tx.verticalAlignment = TextRenderer::VerticalAlignment::Center; m_paramBtns[i] = {be, te}; - blacklistEntity(be); - blacklistEntity(te); + m_tempSvc->serialization().blacklistEntity(be); + m_tempSvc->serialization().blacklistEntity(te); } } @@ -365,8 +371,9 @@ class SceneEditor : public Scene2D void onRightClick() { - auto& cam = m_tempEcs->getComponent(m_mainCamera); - vec2 wp = ECS::Camera::screenPositionToWorldPosition2D(cam, vec2(Input::GetMouseX(), Input::GetMouseY())); + auto& cam = m_tempEcs->getComponent(m_tempSvc->render().getCameraEntity()); + vec2 wp = ECS::Camera::screenPositionToWorldPosition2D( + cam, vec2(m_tempSvc->input().getMouseX(), m_tempSvc->input().getMouseY())); selectNearest(wp); } @@ -392,7 +399,7 @@ class SceneEditor : public Scene2D std::copy(std::begin(s.parameters), std::end(s.parameters), p); p[9] = pos.x; p[10] = pos.y; - float d = m_sdfs[s.distanceFieldId]->getValue(p); + float d = m_tempSvc->shapes().getSDFs()[s.distanceFieldId]->getValue(p); if (d < best) { best = d; @@ -644,7 +651,8 @@ class SceneEditor : public Scene2D { float p[8]{}; fillRandomParams(type, p); - Entity e = addShape(type, p, static_cast(m_selectedMaterial), m_selectedCombination); + Entity e = + m_tempSvc->shapes().addShape(type, p, static_cast(m_selectedMaterial), m_selectedCombination); if (m_selectedCombination == CombinationType::SmoothAddition || m_selectedCombination == CombinationType::SmoothSubtraction) m_tempEcs->getComponent(e).smoothFactor = 1.5f; @@ -660,7 +668,7 @@ class SceneEditor : public Scene2D auto& sdf = m_tempEcs->addComponent(e); sdf.materialId = static_cast(m_selectedMaterial); m_tempEcs->addComponent(e); - blacklistEntity(e); + m_tempSvc->serialization().blacklistEntity(e); } // ===================================================================== @@ -755,7 +763,7 @@ class SceneEditor : public Scene2D vec2 camCentre() { - auto& t = m_tempEcs->getComponent(m_mainCamera); + auto& t = m_tempEcs->getComponent(m_tempSvc->render().getCameraEntity()); return vec2(t.position.x, t.position.y); }