Skip to content

Commit

Permalink
[Utils/TriggerVolume] Added "trigger" components & system
Browse files Browse the repository at this point in the history
- The TriggerVolume & Triggerer components can be assigned to entities to make them spatially interact with each other

- The TriggerSystem checks for volumes that are triggered and executes their enter, stay or leave actions accordingly
  • Loading branch information
Razakhel committed Dec 15, 2024
1 parent e2a7558 commit 0c17cab
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 2 deletions.
2 changes: 2 additions & 0 deletions include/RaZ/RaZ.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
25 changes: 25 additions & 0 deletions include/RaZ/Utils/TriggerSystem.hpp
Original file line number Diff line number Diff line change
@@ -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
56 changes: 56 additions & 0 deletions include/RaZ/Utils/TriggerVolume.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#pragma once

#ifndef RAZ_TRIGGERVOLUME_HPP
#define RAZ_TRIGGERVOLUME_HPP

#include "RaZ/Component.hpp"
#include "RaZ/Utils/Shape.hpp"

#include <functional>
#include <variant>

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 <typename VolumeT>
explicit TriggerVolume(VolumeT&& volume) : m_volume{ std::forward<VolumeT>(volume) } {
// TODO: the OBB's point containment check isn't implemented yet
static_assert(std::is_same_v<std::decay_t<VolumeT>, AABB> || std::is_same_v<std::decay_t<VolumeT>, Sphere>);
}

void setEnterAction(std::function<void()> enterAction) { m_enterAction = std::move(enterAction); }
void setStayAction(std::function<void()> stayAction) { m_stayAction = std::move(stayAction); }
void setLeaveAction(std::function<void()> 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<AABB, Sphere> m_volume;
std::function<void()> m_enterAction;
std::function<void()> m_stayAction;
std::function<void()> m_leaveAction;

bool m_isCurrentlyTriggered = false;
};

} // namespace Raz

#endif // RAZ_TRIGGERVOLUME_HPP
2 changes: 2 additions & 0 deletions src/RaZ/Script/LuaCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -73,6 +74,7 @@ void LuaWrapper::registerCoreTypes() {
WindowSetting, uint8_t>
#endif
);
world["addTriggerSystem"] = &World::addSystem<TriggerSystem>;
#if defined(RAZ_USE_XR)
world["addXrSystem"] = &World::addSystem<XrSystem, const std::string&>;
#endif
Expand Down
25 changes: 25 additions & 0 deletions src/RaZ/Script/LuaUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -109,6 +111,29 @@ void LuaWrapper::registerUtilsTypes() {
strUtils["trimCopy"] = PickOverload<std::string>(&StrUtils::trimCopy);
strUtils["split"] = PickOverload<std::string, char>(&StrUtils::split);
}

{
state.new_usertype<Triggerer>("Triggerer", sol::constructors<Triggerer()>());
}

{
state.new_usertype<TriggerSystem>("TriggerSystem", sol::constructors<TriggerSystem()>());
}

{
sol::usertype<TriggerVolume> triggerVolume = state.new_usertype<TriggerVolume>("TriggerVolume",
sol::constructors<TriggerVolume(const AABB&),
TriggerVolume(const Sphere&)>());
triggerVolume["setEnterAction"] = &TriggerVolume::setEnterAction;
triggerVolume["setStayAction"] = &TriggerVolume::setStayAction;
triggerVolume["setLeaveAction"] = &TriggerVolume::setLeaveAction;
triggerVolume["enable"] = sol::overload([] (TriggerVolume& v) { v.enable(); },
PickOverload<bool>(&TriggerVolume::enable));
triggerVolume["disable"] = &TriggerVolume::disable;
triggerVolume["resetEnterAction"] = &TriggerVolume::resetEnterAction;
triggerVolume["resetStayAction"] = &TriggerVolume::resetStayAction;
triggerVolume["resetLeaveAction"] = &TriggerVolume::resetLeaveAction;
}
}

} // namespace Raz
54 changes: 54 additions & 0 deletions src/RaZ/Utils/TriggerSystem.cpp
Original file line number Diff line number Diff line change
@@ -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<Triggerer, TriggerVolume>();
}

bool TriggerSystem::update(const FrameTimeInfo&) {
for (const Entity* triggererEntity : m_entities) {
if (!triggererEntity->hasComponent<Triggerer>() || !triggererEntity->hasComponent<Transform>())
continue;

const auto& triggererTransform = triggererEntity->getComponent<Transform>();

for (Entity* triggerVolumeEntity : m_entities) {
if (!triggerVolumeEntity->hasComponent<TriggerVolume>())
continue;

auto& triggerVolume = triggerVolumeEntity->getComponent<TriggerVolume>();

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<void()>& action = (!wasBeingTriggered && triggerVolume.m_isCurrentlyTriggered ? triggerVolume.m_enterAction
: (wasBeingTriggered && triggerVolume.m_isCurrentlyTriggered ? triggerVolume.m_stayAction
: triggerVolume.m_leaveAction));

if (action)
action();
}

} // namespace Raz
4 changes: 2 additions & 2 deletions tests/src/RaZ/Physics/PhysicsSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ TEST_CASE("PhysicsSystem basic", "[physics]") {
TEST_CASE("PhysicsSystem accepted components", "[physics]") {
Raz::World world(2);

auto& physics = world.addSystem<Raz::PhysicsSystem>();
const auto& physics = world.addSystem<Raz::PhysicsSystem>();

const Raz::Entity& rigidBody = world.addEntityWithComponent<Raz::RigidBody>(1.f, 1.f); // RenderSystem::update() needs a Camera with a Transform component
const Raz::Entity& rigidBody = world.addEntityWithComponent<Raz::RigidBody>(1.f, 1.f);
const Raz::Entity& collider = world.addEntityWithComponent<Raz::Collider>(Raz::Plane(0.f));

world.update({});
Expand Down
1 change: 1 addition & 0 deletions tests/src/RaZ/Script/LuaCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions tests/src/RaZ/Script/LuaUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)"));
}
116 changes: 116 additions & 0 deletions tests/src/RaZ/Utils/TriggerSystem.cpp
Original file line number Diff line number Diff line change
@@ -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 <catch2/catch_test_macros.hpp>

TEST_CASE("TriggerSystem accepted components", "[utils]") {
Raz::World world;

const auto& triggerSystem = world.addSystem<Raz::TriggerSystem>();

const Raz::Entity& triggerer = world.addEntityWithComponent<Raz::Triggerer>();
const Raz::Entity& triggerVolume = world.addEntityWithComponent<Raz::TriggerVolume>(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::TriggerSystem>();

Raz::Entity& triggererEntity = world.addEntityWithComponent<Raz::Transform>();
triggererEntity.addComponent<Raz::Triggerer>();

auto& triggerBox = world.addEntityWithComponent<Raz::Transform>().addComponent<Raz::TriggerVolume>(Raz::AABB(Raz::Vec3f(-1.f), Raz::Vec3f(1.f)));
auto& triggerSphere = world.addEntityWithComponent<Raz::Transform>().addComponent<Raz::TriggerVolume>(Raz::Sphere(Raz::Vec3f(0.f), 1.f));

int boxEnterCount = 0;
int boxStayCount = 0;
int boxLeaveCount = 0;

triggerBox.setEnterAction([&boxEnterCount] () { ++boxEnterCount; });
triggerBox.setStayAction([&boxStayCount] () { ++boxStayCount; });
triggerBox.setLeaveAction([&boxLeaveCount] () { ++boxLeaveCount; });

int sphereEnterCount = 0;
int sphereStayCount = 0;
int sphereLeaveCount = 0;

triggerSphere.setEnterAction([&sphereEnterCount] () { ++sphereEnterCount; });
triggerSphere.setStayAction([&sphereStayCount] () { ++sphereStayCount; });
triggerSphere.setLeaveAction([&sphereLeaveCount] () { ++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<Raz::Transform>().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<Raz::Transform>().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<Raz::Transform>().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);
}

0 comments on commit 0c17cab

Please sign in to comment.