Skip to content

Commit e617820

Browse files
committed
refactor: split responsibility in the body package
- Create `IGrabbable` interface instead of abstract `VxBody` class - Rework `body.player` code structure with using responsibility splitting concept
1 parent cf1e8e0 commit e617820

23 files changed

+418
-330
lines changed

common/src/main/java/net/timtaran/interactivemc/body/player/PlayerBodyManager.java

Lines changed: 35 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,24 @@
1010
import com.github.stephengold.joltjni.enumerate.EConstraintSpace;
1111
import com.github.stephengold.joltjni.enumerate.EMotionType;
1212
import com.github.stephengold.joltjni.operator.Op;
13-
import com.github.stephengold.joltjni.readonly.RVec3Arg;
1413
import net.minecraft.world.InteractionHand;
1514
import net.minecraft.world.entity.player.Player;
1615
import net.minecraft.world.level.Level;
16+
import net.timtaran.interactivemc.body.player.interaction.GrabInteraction;
17+
import net.timtaran.interactivemc.body.player.physics.PlayerBodyPartGhostRigidBody;
18+
import net.timtaran.interactivemc.body.player.physics.PlayerBodyPartRigidBody;
19+
import net.timtaran.interactivemc.body.player.store.PlayerBodyDataStore;
1720
import net.timtaran.interactivemc.init.registry.BodyRegistry;
18-
import net.timtaran.interactivemc.init.InteractiveMC;
21+
import net.xmx.velthoric.core.body.VxBody;
1922
import net.xmx.velthoric.core.body.VxRemovalReason;
2023
import net.xmx.velthoric.core.body.server.VxServerBodyManager;
21-
import net.xmx.velthoric.core.body.VxBody;
22-
import net.xmx.velthoric.core.constraint.VxConstraint;
23-
import net.xmx.velthoric.core.constraint.manager.VxConstraintManager;
24-
import net.xmx.velthoric.core.intersection.VxPhysicsIntersector;
2524
import net.xmx.velthoric.core.physics.VxJoltBridge;
26-
import net.xmx.velthoric.core.physics.VxPhysicsLayers;
2725
import net.xmx.velthoric.core.physics.world.VxPhysicsWorld;
2826
import net.xmx.velthoric.math.VxConversions;
2927
import net.xmx.velthoric.math.VxTransform;
3028
import org.jetbrains.annotations.Nullable;
3129

3230
import java.util.*;
33-
import java.util.concurrent.ConcurrentHashMap;
3431

3532
/**
3633
* Manages the creation, tracking, and interaction of player bodies in the physics world.
@@ -48,29 +45,14 @@
4845
* @author timtaran
4946
*/
5047
public class PlayerBodyManager {
51-
public static final float GRAB_RADIUS = 0.3f;
52-
private static final Vec3 SHAPE_SCALE = new Vec3(1f, 1f, 1f);
53-
5448
private static final HashMap<VxPhysicsWorld, PlayerBodyManager> managers = new HashMap<>();
5549

56-
private record PlayerBodyPartData(UUID bodyPartId, UUID ghostBodyPartId, @Nullable UUID grabbedBodyId,
57-
@Nullable UUID grabConstraintId) {
58-
}
59-
60-
/**
61-
* Contains all bodies associated with each player, indexed by their UUID.
62-
*/
63-
private static final HashMap<UUID, EnumMap<PlayerBodyPart, PlayerBodyPartData>> playersBodies = new HashMap<>();
64-
65-
/**
66-
* Contains the Jolt body IDs of all player bodies for quick lookup during interactions.
67-
*/
68-
private static final ConcurrentHashMap<UUID, List<Integer>> playersJoltBodies = new ConcurrentHashMap<>();
69-
7050
private final VxPhysicsWorld world;
51+
private final GrabInteraction grabInteraction;
7152

7253
private PlayerBodyManager(VxPhysicsWorld world) {
7354
this.world = world;
55+
this.grabInteraction = new GrabInteraction(world);
7456
}
7557

7658
/**
@@ -97,7 +79,7 @@ public static PlayerBodyManager get(VxPhysicsWorld world) {
9779
* Creates a body part for the given player and body part type.
9880
*
9981
* @param partType the type of body part (head, hands, etc.)
100-
* @param player the player who owns this body part
82+
* @param player the player who owns this body part
10183
* @return data about the created body part, including the IDs of both the main and ghost bodies
10284
*/
10385
private PlayerBodyPartData createBodyPart(PlayerBodyPart partType, Player player) {
@@ -133,7 +115,6 @@ private PlayerBodyPartData createBodyPart(PlayerBodyPart partType, Player player
133115
VxJoltBridge.INSTANCE.getJoltBody(world, bodyPartGhost).setMotionType(EMotionType.Kinematic);
134116
// Workaround until https://github.com/xI-Mx-Ix/Velthoric/issues/31 will be resolved
135117

136-
137118
try (SixDofConstraintSettings settings = new SixDofConstraintSettings()) {
138119
settings.setSpace(EConstraintSpace.LocalToBodyCom);
139120

@@ -178,7 +159,7 @@ private PlayerBodyPartData createBodyPart(PlayerBodyPart partType, Player player
178159
* @param player the player to spawn bodies for
179160
*/
180161
public void spawnPlayer(Player player) {
181-
if (playersBodies.containsKey(player.getUUID())) {
162+
if (PlayerBodyDataStore.playersBodies.containsKey(player.getUUID())) {
182163
removePlayer(player);
183164
}
184165

@@ -192,16 +173,16 @@ public void spawnPlayer(Player player) {
192173
playerBodies.put(partType, bodyPartData);
193174

194175
joltBodyIds.add(
195-
bodyManager.getVxBody(bodyPartData.bodyPartId).getBodyId()
176+
bodyManager.getVxBody(bodyPartData.bodyPartId()).getBodyId()
196177
);
197178

198179
joltBodyIds.add(
199-
bodyManager.getVxBody(bodyPartData.ghostBodyPartId).getBodyId()
180+
bodyManager.getVxBody(bodyPartData.ghostBodyPartId()).getBodyId()
200181
);
201182
}
202183

203-
playersBodies.put(player.getUUID(), playerBodies);
204-
playersJoltBodies.put(player.getUUID(), joltBodyIds);
184+
PlayerBodyDataStore.playersBodies.put(player.getUUID(), playerBodies);
185+
PlayerBodyDataStore.playersJoltBodies.put(player.getUUID(), joltBodyIds);
205186
}
206187

207188
/**
@@ -210,36 +191,33 @@ public void spawnPlayer(Player player) {
210191
* @param player the player to remove bodies for
211192
*/
212193
public void removePlayer(Player player) {
213-
EnumMap<PlayerBodyPart, PlayerBodyPartData> playerBodies = playersBodies.remove(player.getUUID());
194+
EnumMap<PlayerBodyPart, PlayerBodyPartData> playerBodies = PlayerBodyDataStore.playersBodies.remove(player.getUUID());
214195
if (playerBodies == null) return;
215196

216-
playersJoltBodies.remove(player.getUUID());
197+
PlayerBodyDataStore.playersJoltBodies.remove(player.getUUID());
217198

218199
for (PlayerBodyPartData bodyData : playerBodies.values()) {
219-
world.getBodyManager().removeBody(bodyData.bodyPartId, VxRemovalReason.DISCARD);
220-
world.getBodyManager().removeBody(bodyData.ghostBodyPartId, VxRemovalReason.DISCARD);
200+
world.getBodyManager().removeBody(bodyData.bodyPartId(), VxRemovalReason.DISCARD);
201+
world.getBodyManager().removeBody(bodyData.ghostBodyPartId(), VxRemovalReason.DISCARD);
221202
// Constraints are being removed internally in removeBody, so we don't need to worry about them here.
222203
}
223204
}
224205

225206
/**
226207
* Attempts to grab an object using the specified player's hand.
227-
* <p>
228-
* This method performs a sphere cast from the grab point and tries to grab the closest
229-
* non-player body within the grab radius.
230-
* </p>
231208
*
232-
* @param player the player attempting to grab
209+
* @param player the player attempting to grab
233210
* @param interactionHand the hand to use for grabbing (main or off-hand)
234211
* @return the body that was grabbed, or null if no body was grabbed
212+
* @see GrabInteraction#grab(Player, VxBody, PlayerBodyPart)
235213
*/
236214
@Nullable
237215
public VxBody grab(Player player, InteractionHand interactionHand) {
238216
PlayerBodyPart playerBodyPart = PlayerBodyPart.fromInteractionHand(interactionHand);
239217
if (playerBodyPart == null)
240218
return null;
241219

242-
EnumMap<PlayerBodyPart, PlayerBodyPartData> playerBodies = playersBodies.get(player.getUUID());
220+
EnumMap<PlayerBodyPart, PlayerBodyPartData> playerBodies = PlayerBodyDataStore.playersBodies.get(player.getUUID());
243221
if (playerBodies == null)
244222
return null;
245223

@@ -254,122 +232,40 @@ public VxBody grab(Player player, InteractionHand interactionHand) {
254232
if (playerBodyPartData.grabbedBodyId() != null)
255233
return null; // already grabbing something
256234

257-
VxBody body = world.getBodyManager().getVxBody(playerBodyPartData.bodyPartId);
235+
VxBody body = world.getBodyManager().getVxBody(playerBodyPartData.bodyPartId());
258236
if (body == null) {
259237
throw new IllegalStateException(
260238
"Body not found for body part " + playerBodyPart + " of player " + player.getUUID()
261239
);
262240
}
263241

264-
try (ObjectLayerFilter olFilter = new ObjectLayerFilter() {
265-
@Override
266-
public boolean shouldCollide(int objectLayer) {
267-
return objectLayer != VxPhysicsLayers.NON_MOVING;
268-
}
269-
};
270-
BroadPhaseLayerFilter bplFilter = new BroadPhaseLayerFilter();
271-
BodyFilter bodyFilter = new BodyFilter(); // runtime checks works really strange so we will check body ids below
272-
SphereShape shape = new SphereShape(GRAB_RADIUS)) {
273-
274-
RVec3Arg base = new RVec3(0.0f, 0.0f, 0.0f);
275-
276-
VxTransform vxTransform = body.getTransform();
277-
278-
RVec3 worldGrabPoint = vxTransform.getTranslation();
279-
RVec3 localGrabPoint = playerBodyPart.getLocalGrabPoint();
280-
281-
// Rotate the local grab point by the body's rotation to get the correct world offset.
282-
RVec3 localGrabPointRotated = new RVec3(localGrabPoint);
283-
localGrabPointRotated.rotateInPlace(vxTransform.getRotation());
284-
285-
// Add the rotated local grab point to the body's position to get the final grab point in world space.
286-
worldGrabPoint.addInPlace(localGrabPointRotated.xx(), localGrabPointRotated.yy(), localGrabPointRotated.zz());
287-
RMat44 comTransform = new VxTransform(worldGrabPoint, vxTransform.getRotation()).toRMat44();
242+
GrabInteraction.GrabResult grabResult = grabInteraction.grab(player, body, playerBodyPart);
288243

289-
List<VxPhysicsIntersector.IntersectShapeResult> intersections = VxPhysicsIntersector.narrowIntersectShape(world, shape, SHAPE_SCALE, comTransform, base, bplFilter, olFilter, bodyFilter);
290-
291-
intersections.sort(Comparator.comparingDouble(result -> { // sort by closest intersection point to base.
292-
Vec3 p = result.bodyContactPoint();
293-
294-
double dx = p.getX() - vxTransform.getTranslation().x();
295-
double dy = p.getY() - vxTransform.getTranslation().y();
296-
double dz = p.getZ() - vxTransform.getTranslation().z();
297-
298-
return dx * dx + dy * dy + dz * dz;
299-
}));
300-
301-
VxConstraintManager constraintManager = world.getConstraintManager();
302-
303-
for (VxPhysicsIntersector.IntersectShapeResult intersection : intersections) {
304-
if (
305-
!playersJoltBodies.get(player.getUUID()).contains(intersection.bodyId())
306-
307-
) {
308-
Body grabbedJoltBody = VxJoltBridge.INSTANCE.getJoltBody(world, intersection.bodyId());
309-
310-
if (grabbedJoltBody.getObjectLayer() != VxPhysicsLayers.TERRAIN) {
311-
VxBody grabbedBody = world.getBodyManager().getByJoltBodyId(intersection.bodyId());
312-
313-
if (grabbedBody == null) {
314-
InteractiveMC.LOGGER.warn("vxBody1 is null for body ID: {}", intersection.bodyId());
315-
continue;
316-
}
317-
318-
if (grabbedJoltBody.getMotionType() == EMotionType.Dynamic) {
319-
VxTransform grabbedBodyTransform = grabbedBody.getTransform();
320-
// Calculate a new world-space position for the body so that the local contact point
321-
// aligns exactly with the desired grab point in world space.
322-
RVec3 worldGrabPointOnBody = new RVec3(
323-
worldGrabPoint.xx() - (intersection.bodyContactPoint().getX() - grabbedBodyTransform.getTranslation().xx()),
324-
worldGrabPoint.yy() - (intersection.bodyContactPoint().getY() - grabbedBodyTransform.getTranslation().yy()),
325-
worldGrabPoint.zz() - (intersection.bodyContactPoint().getZ() - grabbedBodyTransform.getTranslation().zz())
326-
);
327-
328-
grabbedJoltBody.setPositionAndRotationInternal(worldGrabPointOnBody, grabbedBodyTransform.getRotation());
329-
}
330-
else {
331-
// todo move grabber body if grabbed body not meant to be moved by physics
332-
}
333-
334-
try (FixedConstraintSettings settings = new FixedConstraintSettings()) {
335-
settings.setSpace(EConstraintSpace.WorldSpace);
336-
settings.setPoint1(worldGrabPoint);
337-
settings.setPoint2(worldGrabPoint);
338-
339-
// todo rework
340-
//if (body instanceof Grabber grabber)
341-
// grabbedJoltBody.setCollisionGroup(new CollisionGroup(GroupFilters.PLAYER_BODY_FILTER, GroupFilters.PLAYER_BODY_GROUP_ID, grabber.getSubGroupId()));
342-
343-
VxConstraint constraint = constraintManager.createConstraint(settings, body.getPhysicsId(), grabbedBody.getPhysicsId());
344-
constraint.setPersistent(false);
244+
if (grabResult.grabbedBody() == null)
245+
return null;
345246

346-
playerBodies.put(playerBodyPart, new PlayerBodyPartData(playerBodyPartData.bodyPartId, playerBodyPartData.ghostBodyPartId, grabbedBody.getPhysicsId(), constraint.getConstraintId()));
247+
playerBodies.put(playerBodyPart, new PlayerBodyPartData(
248+
playerBodyPartData.bodyPartId(), playerBodyPartData.ghostBodyPartId(),
249+
grabResult.grabbedBody().getPhysicsId(),
250+
grabResult.grabConstraint() != null ? grabResult.grabConstraint().getConstraintId() : null
251+
));
347252

348-
return grabbedBody;
349-
}
350-
} // todo add terrain grab after implementing client-side prediction
351-
}
352-
}
353-
}
354-
355-
return null;
253+
return grabResult.grabbedBody();
356254
}
357255

358256
/**
359257
* Releases any object being grabbed by the specified player's hand.
360-
* <p>
361-
* This removes the grab constraint, allowing the grabbed body to move freely again.
362-
* </p>
363258
*
364-
* @param player the player releasing the grab
259+
* @param player the player releasing the grab
365260
* @param interactionHand the hand to release (main or off-hand)
261+
* @see GrabInteraction#release(PlayerBodyPartData)
366262
*/
367263
public void release(Player player, InteractionHand interactionHand) {
368264
PlayerBodyPart playerBodyPart = PlayerBodyPart.fromInteractionHand(interactionHand);
369265
if (playerBodyPart == null)
370266
return;
371267

372-
EnumMap<PlayerBodyPart, PlayerBodyPartData> playerBodies = playersBodies.get(player.getUUID());
268+
EnumMap<PlayerBodyPart, PlayerBodyPartData> playerBodies = PlayerBodyDataStore.playersBodies.get(player.getUUID());
373269
if (playerBodies == null)
374270
return;
375271

@@ -384,8 +280,8 @@ public void release(Player player, InteractionHand interactionHand) {
384280
return;
385281
}
386282

387-
world.getConstraintManager().removeConstraint(playerBodyPartData.grabConstraintId);
283+
grabInteraction.release(playerBodyPartData);
388284

389-
playerBodies.put(playerBodyPart, new PlayerBodyPartData(playerBodyPartData.bodyPartId, playerBodyPartData.ghostBodyPartId, null, null));
285+
playerBodies.put(playerBodyPart, new PlayerBodyPartData(playerBodyPartData.bodyPartId(), playerBodyPartData.ghostBodyPartId(), null, null));
390286
}
391287
}

common/src/main/java/net/timtaran/interactivemc/body/player/PlayerBodyPart.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import com.github.stephengold.joltjni.RVec3;
88
import com.github.stephengold.joltjni.Vec3;
99
import net.minecraft.world.InteractionHand;
10+
import net.timtaran.interactivemc.body.player.physics.PlayerBodyPartGhostRigidBody;
11+
import net.timtaran.interactivemc.body.player.physics.PlayerBodyPartRigidBody;
1012
import org.joml.Vector3f;
1113
import org.vivecraft.api.data.VRBodyPart;
1214

@@ -19,8 +21,8 @@
1921
*
2022
* @author timtaran
2123
* @see net.timtaran.interactivemc.body.player.PlayerBodyManager
22-
* @see net.timtaran.interactivemc.body.player.PlayerBodyPartRigidBody
23-
* @see net.timtaran.interactivemc.body.player.PlayerBodyPartGhostRigidBody
24+
* @see PlayerBodyPartRigidBody
25+
* @see PlayerBodyPartGhostRigidBody
2426
*/
2527
public enum PlayerBodyPart {
2628
HEAD(new Vec3(0.5f, 0.5f, 0.5f)),
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* This file is part of InteractiveMC.
3+
* Licensed under LGPL 3.0.
4+
*/
5+
package net.timtaran.interactivemc.body.player;
6+
7+
import org.jetbrains.annotations.Nullable;
8+
9+
import java.util.UUID;
10+
11+
/**
12+
* Record storing player body part data used by player physics systems.
13+
* <p>Grabbing states:
14+
* <ul>
15+
* <li>{@code grabbedBodyId == null} — nothing is grabbed</li>
16+
* <li>{@code grabbedBodyId != null && grabConstraintId == null} — body is being pulled</li>
17+
* <li>{@code grabbedBodyId != null && grabConstraintId != null} — body is mounted using a constraint</li>
18+
* </ul>
19+
*
20+
* @param bodyPartId ID of dynamic body part
21+
* @param ghostBodyPartId ID of the ghost body part
22+
* @param grabbedBodyId ID of the grabbed/pulled body ({@code null} if body not grabbing anything)
23+
* @param grabConstraintId ID of the grab constraint ({@code null} if not mounted)
24+
* @author timtaran
25+
*/
26+
public record PlayerBodyPartData(UUID bodyPartId, UUID ghostBodyPartId, @Nullable UUID grabbedBodyId,
27+
@Nullable UUID grabConstraintId) {
28+
}

0 commit comments

Comments
 (0)