Shapes, generated bodies, and constraints
Use shape handles for geometry, a GeneratedBodyService for Bethesda/Havok
body graphs, and a ConstraintService for live world constraints. The service
objects are part of the ownership contract, not temporary factories.
Build primitive and convex shapes
#include <RPS/Runtime/Shape.h>
using namespace RPS::Runtime;
using namespace RPS::Runtime::Physics;
ShapeFactory shapes{module};
auto sphere = shapes.buildSphereGame(4.0f);
if (!sphere) {
return;
}
std::array<Vector4, 4> points{
Vector4{-0.1f, 0.0f, 0.0f, 0.0f},
Vector4{ 0.1f, 0.0f, 0.0f, 0.0f},
Vector4{ 0.0f, 0.1f, 0.0f, 0.0f},
Vector4{ 0.0f, 0.0f, 0.1f, 0.0f},
};
auto convex = shapes.buildConvex(points, 0.01f);
buildSphereGame() accepts game units. Convex points and radius are Havok
space. Inputs must be finite and stay within the engine's supported vertex
budget.
ShapeHandle owns one caller-side Havok reference and is move-only. Keep it
alive through the native create call. A successfully created compound or
generated body acquires its own native shape reference, so the caller handle
may be reset afterward when the caller has no other use for it.
Static and dynamic compounds
Each child contains a borrowed shape and a complete transform/scale:
CompoundChild child{};
child.shape = sphere.get();
child.transform.transform.translation = {0.0f, 0.0f, 0.1f, 1.0f};
std::array<CompoundChild, 1> children{child};
auto compound = shapes.buildStaticCompound(children);
Use DynamicCompoundShape when child transforms must change after creation:
DynamicCompoundShape dynamicCompound{module};
if (!dynamicCompound.create(children)) {
return;
}
std::array<ChildTransform, 1> transforms{child.transform};
WorldWriteGuard write{module, hknpWorld};
const auto updated = dynamicCompound.updateTransforms(
write,
hknpWorld,
transforms);
Creation preallocates stable instance IDs and update scratch. updateTransforms
uses the matching world write epoch and updates only changed children without
hot-path allocation.
Create a generated collider
Keep one GeneratedBodyService in the subsystem that owns the scene/world
generation:
#include <RPS/Runtime/GeneratedBody.h>
GeneratedBodyService bodies{module};
if (!bodies) {
return;
}
GeneratedBodyCreateInfo info{};
info.hknpWorld = hknpWorld;
info.bhkWorld = bhkWorld;
info.shape = sphere.get();
info.collisionFilterInfo = filterInfo;
info.materialId = materialId;
info.motionType = GeneratedMotionType::Keyframed;
info.name = "MyPlugin_Collider";
GeneratedBodyCreateError error{};
GeneratedBody body = bodies.create(info, error);
if (!body) {
logOnce(toString(error));
return;
}
const BodyId id = body.bodyId();
void* collisionObject = body.collisionObject();
void* ownerNode = body.ownerNode();
The factory builds the native physics-system data, Bethesda physics system,
collision object, and owner NiNode, inserts the body, then verifies material,
motion, filter, flags, world identity, body ID, and collision back-pointer.
The successful native body graph owns its own reference to the source shape;
the caller may release its ShapeHandle afterward. Do not treat the returned
collision object or node as a separately owned reference.
Retire generated bodies
Prefer explicit retirement on the service's constructing thread:
if (!bodies.retire(body)) {
// Keep the service alive and diagnose; do not manually free native pieces.
}
Destruction from another thread queues removal. Call
serviceOwnerThreadRetirements() on the owner thread. After removal, call
serviceCompletedPhysicsSteps() once per real completed post-solve physics
step. References are released after eight completed steps.
Use shutdownAfterWorldLoss() only after the world is gone and no physics
reader can run. Destroying the service early does not make outstanding body
ownership safe.
Create an owned stock constraint
Construct one service for the exact hknp world:
#include <RPS/Runtime/Constraint.h>
ConstraintService constraints{module, hknpWorld};
if (!constraints) {
return;
}
BallAndSocketConstraintInfo info{};
info.bodyA = bodyA;
info.bodyB = bodyB;
info.localPivotA = pivotA;
info.localPivotB = pivotB;
WorldWriteGuard write{module, hknpWorld};
if (!write.active()) {
return;
}
auto created = constraints.createBallAndSocket(write, info);
if (!created) {
return;
}
OwnedConstraint joint = std::move(created.constraint);
Available stock constructors are:
- ball and socket with local body pivots;
- limited hinge with body-world transforms, world pivot/axis, and ordered relative-angle limits;
- prismatic with body-world transforms, world pivot/axis, and ordered Havok-unit distance limits.
Axes must be finite and non-degenerate. Both bodies must be complete in the same guarded world.
Retire constraints
Retire explicitly under the matching write guard:
WorldWriteGuard write{module, hknpWorld};
if (write.active()) {
(void)constraints.retire(write, joint);
(void)constraints.servicePendingRetirements(write);
}
Cross-thread destruction enters a fixed 512-entry pending queue. Service it on
the owner thread. On a confirmed world loss, shutdownAfterWorldLoss() clears
the service's retained bookkeeping without calling a dead world.
Position motors
PositionMotor::create() owns one native Havok motor reference and validates
force, tau, damping, and recovery-velocity tuning:
PositionMotorTuning tuning{};
tuning.minimumForce = -500.0f;
tuning.maximumForce = 500.0f;
auto motor = PositionMotor::create(module, tuning);
if (!motor) {
return;
}
The current stable constraint factories do not attach the motor for you. If a
consumer-owned native constraint references it, detach/destroy that constraint
before calling resetDetached() or allowing the motor owner to die.
:::danger Never manually dismantle an owned graph
Do not release a generated body's node, collision object, physics system, body, or shape piecemeal. Do not destroy a live constraint ID without its service and write epoch. The wrappers exist because the required ordering and deferred lifetimes cross several native ownership domains.
:::