From 27c562e2427fa1671f9dfa1d9a90ce59545c65d1 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 00:01:34 -0500 Subject: [PATCH] feat: switch trigger physics volumes on and off at runtime The ActivatePhysics trigger command was a TODO. The client activates or deactivates the object's physics component (LWOPhysicsSystemComponent::msgActivatePhysics, 1.10.64 0x00ccf970); the server now does the same to phantom physics: switching it off takes the volume out of the physics world (dpWorld::DetachEntity, without deleting it) and makes whatever was inside leave, switching it on adds it back and whatever is inside enters on the next step. This lets trigger driven volumes like the monument lasers turn on and off. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/PhantomPhysicsComponent.cpp | 28 +++++++++++++++-- dGame/dComponents/PhantomPhysicsComponent.h | 11 +++++++ dGame/dComponents/TriggerComponent.cpp | 15 ++++++--- dPhysics/dpEntity.h | 3 ++ dPhysics/dpGrid.cpp | 13 ++++++-- dPhysics/dpGrid.h | 5 +++ dPhysics/dpWorld.cpp | 14 +++++++++ dPhysics/dpWorld.h | 5 +++ tests/dGameTests/CollisionFilterTests.cpp | 31 +++++++++++++++++++ 9 files changed, 114 insertions(+), 11 deletions(-) diff --git a/dGame/dComponents/PhantomPhysicsComponent.cpp b/dGame/dComponents/PhantomPhysicsComponent.cpp index 9a90dbe37..0ac8129d4 100644 --- a/dGame/dComponents/PhantomPhysicsComponent.cpp +++ b/dGame/dComponents/PhantomPhysicsComponent.cpp @@ -87,9 +87,31 @@ PhantomPhysicsComponent::PhantomPhysicsComponent(Entity* parent, const int32_t c } } +void ApplyCollisionEffect(const LWOOBJID& target, const ePhysicsEffectType effectType, const float effectScale); + PhantomPhysicsComponent::~PhantomPhysicsComponent() { - if (m_dpEntity) { - dpWorld::RemoveEntity(m_dpEntity); + if (!m_dpEntity) return; + + if (m_PhysicsActive) dpWorld::RemoveEntity(m_dpEntity); + else delete m_dpEntity; +} + +void PhantomPhysicsComponent::SetPhysicsActive(const bool active) { + if (!m_dpEntity || m_PhysicsActive == active) return; + m_PhysicsActive = active; + + if (active) { + // Anything already inside enters on the next physics step + dpWorld::AddEntity(m_dpEntity); + return; + } + + dpWorld::DetachEntity(m_dpEntity); + const std::vector inside(m_dpEntity->GetCurrentlyCollidingObjects().begin(), m_dpEntity->GetCurrentlyCollidingObjects().end()); + m_dpEntity->ClearCollisions(); + for (const auto id : inside) { + ApplyCollisionEffect(id, m_EffectType, 1.0f); + m_Parent->OnCollisionLeavePhantom(id); } } @@ -151,7 +173,7 @@ void ApplyCollisionEffect(const LWOOBJID& target, const ePhysicsEffectType effec } void PhantomPhysicsComponent::Update(float deltaTime) { - if (!m_dpEntity) return; + if (!m_dpEntity || !m_PhysicsActive) return; //Process enter events for (const auto id : m_dpEntity->GetNewObjects()) { diff --git a/dGame/dComponents/PhantomPhysicsComponent.h b/dGame/dComponents/PhantomPhysicsComponent.h index 9a9e18801..d12e238d5 100644 --- a/dGame/dComponents/PhantomPhysicsComponent.h +++ b/dGame/dComponents/PhantomPhysicsComponent.h @@ -99,6 +99,14 @@ public: dpEntity* GetdpEntity() const { return m_dpEntity; } + /** + * Switches the volume on or off (the ActivatePhysics trigger command; the client activates or deactivates + * the physics component the same way). Whatever was inside leaves when it is switched off. + */ + void SetPhysicsActive(bool active); + + bool GetPhysicsActive() const { return m_PhysicsActive; } + /** * Spawns an object at each of the vertices for debugging purposes */ @@ -161,6 +169,9 @@ private: */ dpEntity* m_dpEntity; + // Whether m_dpEntity is in the physics world + bool m_PhysicsActive = true; + /** * Whether or not this physics object represents an object that updates the respawn pos of an entity that crosses it */ diff --git a/dGame/dComponents/TriggerComponent.cpp b/dGame/dComponents/TriggerComponent.cpp index 46037f6ac..a159ff654 100644 --- a/dGame/dComponents/TriggerComponent.cpp +++ b/dGame/dComponents/TriggerComponent.cpp @@ -467,13 +467,18 @@ void TriggerComponent::HandleDestroySpawnerNetworkObjects(std::string args) { } void TriggerComponent::HandleActivatePhysics(Entity* targetEntity, std::string args) { - if (args == "true") { - // TODO add physics entity if there isn't one - } else if (args == "false") { - // TODO remove Phsyics entity if there is one - } else { + if (args != "true" && args != "false") { LOG_DEBUG("Invalid argument for ActivatePhysics Trigger: %s", args.c_str()); + return; } + + auto* const phantomPhysicsComponent = targetEntity->GetComponent(); + if (!phantomPhysicsComponent) { + LOG_DEBUG("ActivatePhysics on %llu:%i which has no phantom physics", targetEntity->GetObjectID(), targetEntity->GetLOT()); + return; + } + + phantomPhysicsComponent->SetPhysicsActive(args == "true"); } void TriggerComponent::HandleSetPath(Entity* targetEntity, std::vector argArray) { diff --git a/dPhysics/dpEntity.h b/dPhysics/dpEntity.h index 1c302bf11..81db85d46 100644 --- a/dPhysics/dpEntity.h +++ b/dPhysics/dpEntity.h @@ -57,6 +57,9 @@ public: void PreUpdate() { m_NewObjects.clear(); m_RemovedObjects.clear(); } + // Forgets everything it touches (it was taken out of the world) + void ClearCollisions() { m_NewObjects.clear(); m_RemovedObjects.clear(); m_CurrentlyCollidingObjects.clear(); } + const LWOOBJID& GetObjectID() const { return m_ObjectID; } void SetGrid(dpGrid* grid); diff --git a/dPhysics/dpGrid.cpp b/dPhysics/dpGrid.cpp index c338efe32..79e434468 100644 --- a/dPhysics/dpGrid.cpp +++ b/dPhysics/dpGrid.cpp @@ -73,6 +73,12 @@ void dpGrid::Move(dpEntity* entity, float x, float z) { } void dpGrid::Delete(dpEntity* entity) { + if (!entity) return; + Remove(entity); + delete entity; +} + +void dpGrid::Remove(dpEntity* entity) { if (!entity) return; int oldCellX = static_cast(std::round(entity->m_Position.x)) / dpGrid::CELL_SIZE + NUM_CELLS / 2; int oldCellZ = static_cast(std::round(entity->m_Position.z)) / dpGrid::CELL_SIZE + NUM_CELLS / 2; @@ -88,10 +94,11 @@ void dpGrid::Delete(dpEntity* entity) { cell.pop_back(); } - m_GargantuanObjects.erase(entity->m_ObjectID); + const auto gargantuan = m_GargantuanObjects.find(entity->m_ObjectID); + if (gargantuan != m_GargantuanObjects.end() && gargantuan->second == entity) m_GargantuanObjects.erase(gargantuan); - if (entity) delete entity; - entity = nullptr; + // Out of the grid: moving it mustn't put it back into a cell + entity->m_Grid = nullptr; } void dpGrid::Update(float deltaTime) { diff --git a/dPhysics/dpGrid.h b/dPhysics/dpGrid.h index 39a9cfe73..329776d3b 100644 --- a/dPhysics/dpGrid.h +++ b/dPhysics/dpGrid.h @@ -20,6 +20,11 @@ public: void Move(dpEntity* entity, float x, float z); void Delete(dpEntity* entity); + /** + * Takes an entity out of the grid without deleting it + */ + void Remove(dpEntity* entity); + void Update(float deltaTime); /** diff --git a/dPhysics/dpWorld.cpp b/dPhysics/dpWorld.cpp index a3df390c8..df2ea072b 100644 --- a/dPhysics/dpWorld.cpp +++ b/dPhysics/dpWorld.cpp @@ -128,6 +128,20 @@ void dpWorld::AddEntity(dpEntity* entity) { } } +void dpWorld::DetachEntity(dpEntity* entity) { + if (!entity) return; + + if (m_Grid) { + m_Grid->Remove(entity); + return; + } + + auto& entities = entity->GetIsStatic() ? m_StaticEntities : m_DynamicEntites; + for (auto& other : entities) { + if (other == entity) other = nullptr; + } +} + void dpWorld::RemoveEntity(dpEntity* entity) { if (!entity) return; diff --git a/dPhysics/dpWorld.h b/dPhysics/dpWorld.h index fc23a15ca..6514ed4b5 100644 --- a/dPhysics/dpWorld.h +++ b/dPhysics/dpWorld.h @@ -18,5 +18,10 @@ namespace dpWorld { void AddEntity(dpEntity* entity); void RemoveEntity(dpEntity* entity); + /** + * Takes an entity out of the world without deleting it, so it can be added back later (a volume switched off) + */ + void DetachEntity(dpEntity* entity); + dNavMesh* GetNavMesh(); }; diff --git a/tests/dGameTests/CollisionFilterTests.cpp b/tests/dGameTests/CollisionFilterTests.cpp index b53bb288b..d05a63e68 100644 --- a/tests/dGameTests/CollisionFilterTests.cpp +++ b/tests/dGameTests/CollisionFilterTests.cpp @@ -77,3 +77,34 @@ TEST(CollisionFilterTests, RotatedBoxesKeepTheirShape) { EXPECT_NEAR(box->SquaredDistanceTo(NiPoint3(100.0f, 12.0f, 100.0f)), 4.0f, 1e-3f); EXPECT_NEAR(box->SquaredDistanceTo(NiPoint3(100.0f, -3.0f, 100.0f)), 9.0f, 1e-3f); } + +#include "dpWorld.h" +#include "dpGrid.h" + +TEST(CollisionFilterTests, DetachedVolumesStopColliding) { + dpGrid grid(8, 100); + auto* volume = new dpEntity(1, 5.0f); + auto* player = new dpEntity(2, 1.0f, false); + volume->SetPosition(NiPoint3Constant::ZERO); + player->SetPosition(NiPoint3(1.0f, 0.0f, 0.0f)); + volume->SetGrid(&grid); + player->SetGrid(&grid); + + grid.Update(0.1f); + ASSERT_EQ(volume->GetNewObjects().size(), 1u); + + // Switched off: out of the grid, moving it doesn't sneak it back in, and it stops seeing anything + grid.Remove(volume); + volume->ClearCollisions(); + volume->SetPosition(NiPoint3(0.5f, 0.0f, 0.0f)); + grid.Update(0.1f); + EXPECT_TRUE(volume->GetCurrentlyCollidingObjects().empty()); + + // Switched back on: sees the player again + volume->SetGrid(&grid); + grid.Update(0.1f); + EXPECT_EQ(volume->GetNewObjects().size(), 1u); + + grid.Delete(volume); + grid.Delete(player); +}