diff --git a/include/RaZ/RaZ.hpp b/include/RaZ/RaZ.hpp index a8e549cf..1507198c 100644 --- a/include/RaZ/RaZ.hpp +++ b/include/RaZ/RaZ.hpp @@ -92,6 +92,8 @@ #include "Utils/StrUtils.hpp" #include "Utils/Threading.hpp" #include "Utils/ThreadPool.hpp" +#include "Utils/TriggerSystem.hpp" +#include "Utils/TriggerVolume.hpp" #include "Utils/TypeUtils.hpp" #include "XR/XrContext.hpp" #include "XR/XrSession.hpp" diff --git a/include/RaZ/Utils/TriggerSystem.hpp b/include/RaZ/Utils/TriggerSystem.hpp new file mode 100644 index 00000000..4c412da6 --- /dev/null +++ b/include/RaZ/Utils/TriggerSystem.hpp @@ -0,0 +1,25 @@ +#pragma once + +#ifndef RAZ_TRIGGERSYSTEM_HPP +#define RAZ_TRIGGERSYSTEM_HPP + +#include "RaZ/System.hpp" + +namespace Raz { + +class Transform; +class TriggerVolume; + +class TriggerSystem final : public System { +public: + TriggerSystem(); + + bool update(const FrameTimeInfo& timeInfo) override; + +private: + static void processTrigger(TriggerVolume& triggerVolume, const Transform& triggererTransform); +}; + +} // namespace Raz + +#endif // RAZ_TRIGGERSYSTEM_HPP diff --git a/include/RaZ/Utils/TriggerVolume.hpp b/include/RaZ/Utils/TriggerVolume.hpp new file mode 100644 index 00000000..5c5a5327 --- /dev/null +++ b/include/RaZ/Utils/TriggerVolume.hpp @@ -0,0 +1,56 @@ +#pragma once + +#ifndef RAZ_TRIGGERVOLUME_HPP +#define RAZ_TRIGGERVOLUME_HPP + +#include "RaZ/Component.hpp" +#include "RaZ/Utils/Shape.hpp" + +#include +#include + +namespace Raz { + +/// Triggerer component, representing an entity that can interact with triggerable entities. +/// \see TriggerVolume +class Triggerer final : public Component {}; + +/// TriggerVolume component, holding a volume that can be triggered and actions that can be executed accordingly. +/// \see Triggerer, TriggerSystem +class TriggerVolume final : public Component { + friend class TriggerSystem; + +public: + template + explicit TriggerVolume(VolumeT&& volume) : m_volume{ std::forward(volume) } { + // TODO: the OBB's point containment check isn't implemented yet + static_assert(std::is_same_v, AABB> || std::is_same_v, Sphere>); + } + + void setEnterAction(std::function enterAction) { m_enterAction = std::move(enterAction); } + void setStayAction(std::function stayAction) { m_stayAction = std::move(stayAction); } + void setLeaveAction(std::function leaveAction) { m_leaveAction = std::move(leaveAction); } + + /// Changes the trigger volume's state. + /// \param enabled True if the trigger volume should be enabled (triggerable), false otherwise. + void enable(bool enabled = true) noexcept { m_enabled = enabled; } + /// Disables the trigger volume, making it non-triggerable. + void disable() noexcept { enable(false); } + void resetEnterAction() { setEnterAction(nullptr); } + void resetStayAction() { setStayAction(nullptr); } + void resetLeaveAction() { setLeaveAction(nullptr); } + +private: + bool m_enabled = true; + + std::variant m_volume; + std::function m_enterAction; + std::function m_stayAction; + std::function m_leaveAction; + + bool m_isCurrentlyTriggered = false; +}; + +} // namespace Raz + +#endif // RAZ_TRIGGERVOLUME_HPP diff --git a/src/RaZ/Script/LuaCore.cpp b/src/RaZ/Script/LuaCore.cpp index 3f40d872..22ef8ecc 100644 --- a/src/RaZ/Script/LuaCore.cpp +++ b/src/RaZ/Script/LuaCore.cpp @@ -7,6 +7,7 @@ #include "RaZ/Physics/PhysicsSystem.hpp" #include "RaZ/Render/RenderSystem.hpp" #include "RaZ/Script/LuaWrapper.hpp" +#include "RaZ/Utils/TriggerSystem.hpp" #include "RaZ/Utils/TypeUtils.hpp" #include "RaZ/XR/XrSystem.hpp" @@ -73,6 +74,7 @@ void LuaWrapper::registerCoreTypes() { WindowSetting, uint8_t> #endif ); + world["addTriggerSystem"] = &World::addSystem; #if defined(RAZ_USE_XR) world["addXrSystem"] = &World::addSystem; #endif diff --git a/src/RaZ/Script/LuaUtils.cpp b/src/RaZ/Script/LuaUtils.cpp index b02e6e16..30c2a5b0 100644 --- a/src/RaZ/Script/LuaUtils.cpp +++ b/src/RaZ/Script/LuaUtils.cpp @@ -5,6 +5,8 @@ #include "RaZ/Utils/Ray.hpp" #include "RaZ/Utils/Shape.hpp" #include "RaZ/Utils/StrUtils.hpp" +#include "RaZ/Utils/TriggerSystem.hpp" +#include "RaZ/Utils/TriggerVolume.hpp" #include "RaZ/Utils/TypeUtils.hpp" #define SOL_ALL_SAFETIES_ON 1 @@ -109,6 +111,29 @@ void LuaWrapper::registerUtilsTypes() { strUtils["trimCopy"] = PickOverload(&StrUtils::trimCopy); strUtils["split"] = PickOverload(&StrUtils::split); } + + { + state.new_usertype("Triggerer", sol::constructors()); + } + + { + state.new_usertype("TriggerSystem", sol::constructors()); + } + + { + sol::usertype triggerVolume = state.new_usertype("TriggerVolume", + sol::constructors()); + triggerVolume["setEnterAction"] = &TriggerVolume::setEnterAction; + triggerVolume["setStayAction"] = &TriggerVolume::setStayAction; + triggerVolume["setLeaveAction"] = &TriggerVolume::setLeaveAction; + triggerVolume["enable"] = sol::overload([] (TriggerVolume& v) { v.enable(); }, + PickOverload(&TriggerVolume::enable)); + triggerVolume["disable"] = &TriggerVolume::disable; + triggerVolume["resetEnterAction"] = &TriggerVolume::resetEnterAction; + triggerVolume["resetStayAction"] = &TriggerVolume::resetStayAction; + triggerVolume["resetLeaveAction"] = &TriggerVolume::resetLeaveAction; + } } } // namespace Raz diff --git a/src/RaZ/Utils/TriggerSystem.cpp b/src/RaZ/Utils/TriggerSystem.cpp new file mode 100644 index 00000000..8e69c8df --- /dev/null +++ b/src/RaZ/Utils/TriggerSystem.cpp @@ -0,0 +1,54 @@ +#include "RaZ/Entity.hpp" +#include "RaZ/Math/Transform.hpp" +#include "RaZ/Utils/TriggerSystem.hpp" +#include "RaZ/Utils/TriggerVolume.hpp" + +namespace Raz { + +TriggerSystem::TriggerSystem() { + registerComponents(); +} + +bool TriggerSystem::update(const FrameTimeInfo&) { + for (const Entity* triggererEntity : m_entities) { + if (!triggererEntity->hasComponent() || !triggererEntity->hasComponent()) + continue; + + const auto& triggererTransform = triggererEntity->getComponent(); + + for (Entity* triggerVolumeEntity : m_entities) { + if (!triggerVolumeEntity->hasComponent()) + continue; + + auto& triggerVolume = triggerVolumeEntity->getComponent(); + + if (!triggerVolume.m_enabled) + continue; + + processTrigger(triggerVolume, triggererTransform); + } + } + + return true; +} + +void TriggerSystem::processTrigger(TriggerVolume& triggerVolume, const Transform& triggererTransform) { + const bool wasBeingTriggered = triggerVolume.m_isCurrentlyTriggered; + + triggerVolume.m_isCurrentlyTriggered = std::visit([&triggererTransform] (const auto& volume) { + // TODO: handle all transform info for both the triggerer & the volume + return volume.contains(triggererTransform.getPosition()); + }, triggerVolume.m_volume); + + if (!wasBeingTriggered && !triggerVolume.m_isCurrentlyTriggered) + return; + + const std::function& action = (!wasBeingTriggered && triggerVolume.m_isCurrentlyTriggered ? triggerVolume.m_enterAction + : (wasBeingTriggered && triggerVolume.m_isCurrentlyTriggered ? triggerVolume.m_stayAction + : triggerVolume.m_leaveAction)); + + if (action) + action(); +} + +} // namespace Raz diff --git a/tests/src/RaZ/Physics/PhysicsSystem.cpp b/tests/src/RaZ/Physics/PhysicsSystem.cpp index 3f88a5a4..b986a67f 100644 --- a/tests/src/RaZ/Physics/PhysicsSystem.cpp +++ b/tests/src/RaZ/Physics/PhysicsSystem.cpp @@ -22,9 +22,9 @@ TEST_CASE("PhysicsSystem basic", "[physics]") { TEST_CASE("PhysicsSystem accepted components", "[physics]") { Raz::World world(2); - auto& physics = world.addSystem(); + const auto& physics = world.addSystem(); - const Raz::Entity& rigidBody = world.addEntityWithComponent(1.f, 1.f); // RenderSystem::update() needs a Camera with a Transform component + const Raz::Entity& rigidBody = world.addEntityWithComponent(1.f, 1.f); const Raz::Entity& collider = world.addEntityWithComponent(Raz::Plane(0.f)); world.update({}); diff --git a/tests/src/RaZ/Script/LuaCore.cpp b/tests/src/RaZ/Script/LuaCore.cpp index dd2a255a..99eb936b 100644 --- a/tests/src/RaZ/Script/LuaCore.cpp +++ b/tests/src/RaZ/Script/LuaCore.cpp @@ -97,6 +97,7 @@ TEST_CASE("LuaCore World", "[script][lua][core]") { world:addPhysicsSystem() world:addRenderSystem() world:addRenderSystem(1, 1) + world:addTriggerSystem() )")); #if defined(RAZ_USE_AUDIO) diff --git a/tests/src/RaZ/Script/LuaUtils.cpp b/tests/src/RaZ/Script/LuaUtils.cpp index 2195ccba..ff2f518d 100644 --- a/tests/src/RaZ/Script/LuaUtils.cpp +++ b/tests/src/RaZ/Script/LuaUtils.cpp @@ -185,3 +185,23 @@ TEST_CASE("LuaUtils StrUtils", "[script][lua][utils]") { assert(splittedStr[4] == "test") )")); } + +TEST_CASE("LuaUtils TriggerVolume", "[script][lua][utils]") { + CHECK(TestUtils::executeLuaScript(R"( + local triggerer = Triggerer.new() + + local triggerSystem = TriggerSystem.new() + + local triggerVolume = TriggerVolume.new(AABB.new(Vec3f.new(-1), Vec3f.new(1))) + triggerVolume = TriggerVolume.new(Sphere.new(Vec3f.new(0), 1)) + triggerVolume:setEnterAction(function () end) + triggerVolume:setStayAction(function () end) + triggerVolume:setLeaveAction(function () end) + triggerVolume:enable() + triggerVolume:enable(true) + triggerVolume:disable() + triggerVolume:resetEnterAction() + triggerVolume:resetStayAction() + triggerVolume:resetLeaveAction() + )")); +} diff --git a/tests/src/RaZ/Utils/TriggerSystem.cpp b/tests/src/RaZ/Utils/TriggerSystem.cpp new file mode 100644 index 00000000..feb080f1 --- /dev/null +++ b/tests/src/RaZ/Utils/TriggerSystem.cpp @@ -0,0 +1,116 @@ +#include "RaZ/Application.hpp" +#include "RaZ/World.hpp" +#include "RaZ/Math/Transform.hpp" +#include "RaZ/Utils/TriggerSystem.hpp" +#include "RaZ/Utils/TriggerVolume.hpp" + +#include + +TEST_CASE("TriggerSystem accepted components", "[utils]") { + Raz::World world; + + const auto& triggerSystem = world.addSystem(); + + const Raz::Entity& triggerer = world.addEntityWithComponent(); + const Raz::Entity& triggerVolume = world.addEntityWithComponent(Raz::Sphere(Raz::Vec3f(0.f), 1.f)); + + world.update({}); + + CHECK(triggerSystem.containsEntity(triggerer)); + CHECK(triggerSystem.containsEntity(triggerVolume)); +} + +TEST_CASE("TriggerSystem trigger actions", "[utils]") { + Raz::World world; + + world.addSystem(); + + Raz::Entity& triggererEntity = world.addEntityWithComponent(); + triggererEntity.addComponent(); + + auto& triggerBox = world.addEntityWithComponent().addComponent(Raz::AABB(Raz::Vec3f(-1.f), Raz::Vec3f(1.f))); + auto& triggerSphere = world.addEntityWithComponent().addComponent(Raz::Sphere(Raz::Vec3f(0.f), 1.f)); + + int boxEnterCount = 0; + int boxStayCount = 0; + int boxLeaveCount = 0; + + triggerBox.setEnterAction([&boxEnterCount] () noexcept { ++boxEnterCount; }); + triggerBox.setStayAction([&boxStayCount] () noexcept { ++boxStayCount; }); + triggerBox.setLeaveAction([&boxLeaveCount] () noexcept { ++boxLeaveCount; }); + + int sphereEnterCount = 0; + int sphereStayCount = 0; + int sphereLeaveCount = 0; + + triggerSphere.setEnterAction([&sphereEnterCount] () noexcept { ++sphereEnterCount; }); + triggerSphere.setStayAction([&sphereStayCount] () noexcept { ++sphereStayCount; }); + triggerSphere.setLeaveAction([&sphereLeaveCount] () noexcept { ++sphereLeaveCount; }); + + world.update({}); + + CHECK(boxEnterCount == 1); + CHECK(boxStayCount == 0); + CHECK(boxLeaveCount == 0); + + CHECK(sphereEnterCount == 1); + CHECK(sphereStayCount == 0); + CHECK(sphereLeaveCount == 0); + + world.update({}); + world.update({}); + + CHECK(boxEnterCount == 1); + CHECK(boxStayCount == 2); + CHECK(boxLeaveCount == 0); + + CHECK(sphereEnterCount == 1); + CHECK(sphereStayCount == 2); + CHECK(sphereLeaveCount == 0); + + // Moving the triggerer out of the sphere, but still inside the box + triggererEntity.getComponent().setPosition(Raz::Vec3f(0.75f)); + + world.update({}); + + CHECK(boxEnterCount == 1); + CHECK(boxStayCount == 3); + CHECK(boxLeaveCount == 0); + + CHECK(sphereEnterCount == 1); + CHECK(sphereStayCount == 2); + CHECK(sphereLeaveCount == 1); + + // Moving the triggerer out of both volumes + triggererEntity.getComponent().setPosition(Raz::Vec3f(1.5f)); + + world.update({}); + + CHECK(boxEnterCount == 1); + CHECK(boxStayCount == 3); + CHECK(boxLeaveCount == 1); + + CHECK(sphereEnterCount == 1); + CHECK(sphereStayCount == 2); + CHECK(sphereLeaveCount == 1); + + // Moving the triggerer inside both volumes and resetting all actions + triggererEntity.getComponent().setPosition(Raz::Vec3f(0.f)); + triggerBox.resetEnterAction(); + triggerBox.resetStayAction(); + triggerBox.resetLeaveAction(); + triggerSphere.resetEnterAction(); + triggerSphere.resetStayAction(); + triggerSphere.resetLeaveAction(); + + // Even though both volumes are triggered, nothing is done as no action is set + world.update({}); + + CHECK(boxEnterCount == 1); + CHECK(boxStayCount == 3); + CHECK(boxLeaveCount == 1); + + CHECK(sphereEnterCount == 1); + CHECK(sphereStayCount == 2); + CHECK(sphereLeaveCount == 1); +}