feat(physics): simple and phantom physics objects register as movement blockers

A fixed simple physics object, or a phantom volume, that its data marks as a
wall for enemies (a navmesh carver, or solid with a group only enemies touch)
adds its shape to the world's movement blockers, and takes it out again when it
goes away. Only shapes the server knows are used; an unknown asset's stand in
cube would be a guess, so it is logged and left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 17:37:45 -05:00
parent 425b913848
commit 62f5f13846
4 changed files with 56 additions and 3 deletions

View File

@@ -50,6 +50,9 @@ PhantomPhysicsComponent::PhantomPhysicsComponent(Entity* parent, const int32_t c
m_IsDirectional = false;
m_Direction = NiPoint3(); // * m_DirectionalMultiplier
// A phantom volume enemies can't walk into when it carves the navmesh (e.g. the Sentinel camp walls)
RegisterMovementBlocker(ComponentType, false, m_Scale);
if (m_Parent->GetVar<bool>(u"create_physics")) {
m_dpEntity = CreatePhysicsLnv(m_Scale, ComponentType);
}

View File

@@ -12,11 +12,14 @@
#include "dpEntity.h"
#include "dpWorld.h"
#include "dpMovementBlockers.h"
#include "dpShapeBox.h"
#include "dpShapeSphere.h"
#include "EntityInfo.h"
#include "Amf3.h"
#include "Entity.h"
#include "Logger.h"
PhysicsComponent::PhysicsComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
m_Position = NiPoint3Constant::ZERO;
@@ -37,6 +40,37 @@ PhysicsComponent::PhysicsComponent(Entity* parent, const int32_t componentID) :
RegisterMsg(&PhysicsComponent::OnGetPosition);
}
PhysicsComponent::~PhysicsComponent() {
if (!m_MovementBlocker) return;
dpWorld::RemoveMovementBlocker(m_MovementBlocker);
delete m_MovementBlocker;
}
void PhysicsComponent::RegisterMovementBlocker(const eReplicaComponentType type, const bool solid, const float scale) {
if (m_MovementBlocker) return;
const auto filter = dpMovementBlockers::BlockingFilter(m_Parent->GetVar<bool>(u"navmesh_carver"), solid, static_cast<uint32_t>(m_CollisionGroup));
if (!filter) return;
// Some shapes move the object's position to line up with the client's; that is for the shape only
const auto placed = m_Position;
bool isFallback = false;
auto* const blocker = CreatePhysicsEntity(type, &isFallback);
const auto shapePosition = m_Position;
m_Position = placed;
if (!blocker) return;
if (isFallback) {
LOG_DEBUG("%llu (LOT %i) should block movement, but the server doesn't know its shape", m_Parent->GetObjectID(), m_Parent->GetLOT());
delete blocker;
return;
}
blocker->SetScale(scale);
blocker->SetRotation(m_Rotation);
blocker->SetPosition(shapePosition);
m_MovementBlocker = blocker;
dpWorld::AddMovementBlocker(m_MovementBlocker, *filter);
}
bool PhysicsComponent::OnGetPosition(GameMessages::GetPosition& msg) {
msg.pos = GetPosition();
return true;
@@ -56,7 +90,8 @@ void PhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitia
}
}
dpEntity* PhysicsComponent::CreatePhysicsEntity(eReplicaComponentType type) {
dpEntity* PhysicsComponent::CreatePhysicsEntity(eReplicaComponentType type, bool* isFallback) {
if (isFallback) *isFallback = false;
CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
auto componentID = compRegistryTable->GetByIDAndType(m_Parent->GetLOT(), type);
@@ -115,6 +150,7 @@ dpEntity* PhysicsComponent::CreatePhysicsEntity(eReplicaComponentType type) {
//add fallback cube:
toReturn = new dpEntity(m_Parent->GetObjectID(), 2.0f, 2.0f, 2.0f);
if (isFallback) *isFallback = true;
}
// Only touch what the client lets this group touch (e.g. POI walls ignore enemies, threat clearing walls ignore players)
toReturn->SetCollisionGroup(static_cast<uint32_t>(m_CollisionGroup));

View File

@@ -21,7 +21,7 @@ class dpEntity;
class PhysicsComponent : public Component {
public:
PhysicsComponent(Entity* parent, const int32_t componentID);
virtual ~PhysicsComponent() = default;
virtual ~PhysicsComponent();
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;
@@ -36,7 +36,15 @@ public:
protected:
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& msg);
dpEntity* CreatePhysicsEntity(eReplicaComponentType type);
// isFallback, when given, says whether the asset had no known shape and got a stand in cube
dpEntity* CreatePhysicsEntity(eReplicaComponentType type, bool* isFallback = nullptr);
/**
* Makes this object a wall the server's movers can't walk through when its data says it is one
* (dpMovementBlockers::BlockingFilter: a navmesh carver, or a solid object only enemies collide with). Only
* shapes the server knows are used; a stand in cube would be a guess.
*/
void RegisterMovementBlocker(eReplicaComponentType type, bool solid, float scale);
dpEntity* CreatePhysicsLnv(const float scale, const eReplicaComponentType type) const;
@@ -51,6 +59,9 @@ protected:
bool m_DirtyPosition;
int32_t m_CollisionGroup{};
// This object's shape as a movement blocker, owned here and not stepped with the physics world
dpEntity* m_MovementBlocker{};
};
#endif //!__PHYSICSCOMPONENT__H__

View File

@@ -33,6 +33,9 @@ SimplePhysicsComponent::SimplePhysicsComponent(Entity* parent, const int32_t com
}
// Without a motionType the object is fixed (live: Fixed on 38,970 of 40,786 constructions, never the invalid 0)
if (m_Parent->HasVar(u"motionType")) m_PhysicsMotionState = m_Parent->GetVarAs<uint32_t>(u"motionType");
// A fixed solid object can be a wall enemies walk into (an enemy only blocker, or a navmesh carver)
if (m_PhysicsMotionState == MOTION_TYPE_FIXED) RegisterMovementBlocker(eReplicaComponentType::SIMPLE_PHYSICS, true, m_Parent->GetDefaultScale());
}
SimplePhysicsComponent::~SimplePhysicsComponent() {