Structure cohesion

This module allows to collaboratively build a network object made a several parts, forming a common structure.
Goals
The design goals of this system where the following:
- a structure can be made of several structure parts, that can be independently moving (grabbed, in most cases)
- when a structure part is moving, all the structure parts of the same structure should follow its move seamlessly
- every part of the structure can be as much as possible added/removed freely by several people at the same time. To limit possible conflict/transition times, authority changes are as limited as possible
- when a structure part is released near an existing structure (or single structure parts), it should snap to it along predefined attachment point
- when a structure is grabbed on 2 distinct structure part, it should split in 2. The breaking point should occur on be the most recently sticky together parts
- to allow breaking a structure with one hand only, some parts can have a lower "weight". When grabbed a structure by a part of lower weight than the max weight of the structure, the structure should break near this part.
- optionally, we could configure the structure to limit bandwidth usage (to network only one change position when the structure moves, instead of transmitting a new position for all its parts)
Logic overview
To allow collaborative manipulation of a structure made of several structure parts, the logic is the following:
- structure parts are networked concepts, as well as the link between them. The structure is a local reconstruction, rebuild on each client based on the detection of created/deleted attachments between structure parts, including the one on which the player does not have authority
- a structure part can contain several attachment points, specific points that can be attached to another structure part's attachment point (unless the part is already in the structure)
- a link between two attachment point is stored on one of them only, so the state authority is only needed on this one to modify the networked attachment info
- the state authority of the structure parts in a structure can be shared among several players. The state authority is needed when you manipulate it (grab,...) or when an attachment is created to another attachment point
- structure part store a networked
IsMoving
property. This allow all players to know when the structure should break due to moving parts - all attachment have an order associated to them, that increase for each new attachment (application wise). This allows to know what are the most recent attachments, which is used in determining where to break a structure when needed
- structure can break for 2 reasons:
- when 2 of their parts move at the same time
- when a structure part is moved, if its
StructuralCohesionMode
is set toWeightBasedCohesion
, and if it has a lower weight than another part included in their common structure
- a
StructureManager
keeps track of all structures and deal with new structures creation - an optional
StructurePartsManager
can help to keep track of all parts and their attachment points' tags (for visual effects, lookup, ...)

Attachment creation on ungrabbing
When a structure part is ungrabbed, AttachClosestPartInProximity()
is called. In it, we look in its current structure for the attachment point that is the closest to another structure part's attachment point that is not already in the structure. The link is stored on the structure part that was in the ungrabbed structure, as there is higher chances that this structure part is already under the ungrabbing user authority. If it is not the case, we take the authority before storing the attachment

The ungrabbed part, that store the attachment, is snapped to match the part it has attached: it is more natural that the part we were not moving stay immobile, while the ungrabbed part position itself to match the existing part/structure position.
AttachmentPoint
is an abstract class, and concrete implementation have to determine how to find the closest attachment point (TryFindClosestAttachmentPoint(out AttachmentPoint closestPoint, out float minDistance, bool excludeSameGroupId = true)
override), and how to determine the snap position when a matching attachment point is chosen (Snap(AttachmentPoint other)
override).
Attachment deletion when 2 parts are moving at the same time in a given structure
When 2 parts are moving in a structure, the structure look for the most recent attachment in the structure that is between the 2 moving parts. The structure will break on this attachment, to give a "rewind" impression by removing the most recent link between the 2 grabbed parts.

Attachment deletion when 1 "lightweight" part is moving in a structure
When just one part is moving in a structure, and this structure has a structuralCohesionMode
set to StructuralCohesionMode.WeightBasedCohesion
, its partWeight
is compared to the greatest weight of other parts in the structure.
If its weight is lower, then the structure will break to separate this light-weight part, alongside its "half" of the structure.
If the lightweight part is a "leaf", attached with just one point to the structure, we simply break this attachment.
Otherwise, to determine on which attachment point to break its structure, we look for the attachment points linked to sibling parts. For each of them, we compute the whole structure section "starting" from them, to determine the highest weight of this section. Then, we split on priority on the attachment point leading to the section of the structure with the highest weight, or, in case of equality, on the one with highest attachment order (aka, the most recent attachment).
Repositioning
When a part is moved, other parts have to move too to make sure the structure cohesion persists.
To do so, one part is designed as a reference part, and from there the structure attachments are browsed to reposition each parts to match the already repositioned ones.
This reference parts is usually:
- the moving part if there is one (when want the structure to follow the grabbed part if there is any)
- if no part is moving, the one with the lowest id (it is a deterministic criteria, so that all player rebuild the positions in the same way, as the state authority, and hence the authority on positioning is shared among several player)
- as much as possible, the last known reference part is memorized and kept, to avoid "jumps" in repositioning choices (so we try to stay on the last known moving part instead on using immediately the lowest id as soon as an object is ungrabbed). In rare cases (simultaneous ungrab of 2 objects by 2 players, where the "last moving" part is not the same on all clients), this could lead to distinct repositioning on clients, so this temporary preference is always replaced by the previous deterministic ones after a few seconds. This can lead to one jump of the whole structure in these rare cases, but the structure remains coherent in the meantime.
The repositioning is done during the FixedUpdateNetwork
(in fact, more precisely in AfterTick
) so that the actual position are stored and synched over the network, but it is also done in the Render
phase (in fact, after it in LateUpdate
) so that we display a coherent structure locally even if we don't have the authority on it to actually update the positions in real time. The position of each object will be logically coherent in the end, and the render extrapolation of proxy structure part make it visually coherent in between.
Tag compatibility
In AttachmentPoint
:
AttachmentPointTags
are tags describing the attachmentCompatibleAttachmentPointTags
are a list of compatible tags, one of them being expected to be present in attachedAttachmentPoint
'sAttachmentPointTags
list. Concrete implementation ofTryFindClosestAttachmentPoint
should enforce this (not enforced inAttachmentPoint
itself)
Underlying snap detection
The StructurePart
class is independent of any grabbing logic. To set its IsMoving
based on its grabbing status, or to call AttachClosestPartInProximity()
, the GrabbableStructurePart
subclass handles the connection with the XRShared add-on grabbing logic.
Regarding the proximity detection, and snap position logic, the add-on provide an implementation based on the Magnet add-on. In this implementation, the AttachmentPointTags
/CompatibleAttachmentPointTags
relies on the magnet tags (the magnet tags are automatically set up based on those attachment point fields at start)

Optional containment
To optimize bandwidth, it is possible to use NetworkTransform
's parenting: when a parent moves, no data is set on the network for its child repositioning (only the relative position is synched, and it does not change here).
The optional ContainmentHandler
class automatically creates a parent container object for its sibling structure parts, if it is in a structure and it has no container available for it. It ensure that all the structure pairs end up in the same container.
The ProxyGrabbing
class ensures that moving a GrabbableStructurePart
's NetworkGrabbable
moves in fact its parent (here, the container).
Finally, the Structure
class can decide to skip repositioning its structure parts, if the structure is considered to be stable, aka:
- no change to the structure occurred recently
- and all structure parts have a container
Note that the current implementation would imply recurrent object creations / despawns, and should be improved with containers pooling.
Setup
To keep track of available structures and create new ones, a StructureManager
needs to be present in the scene.
Optional containment Setup
To be able to use the optional containment logic, a ContainmentManager
needs to be present in the scene.
Also, every GrabbableStructurePart
needs to have sibling ContainmentHandler
and ProxyGrabbing
components.
To quickly compare results with or without containment, the ContainmentManager
's disableContainerParenting
option is similar to removing it from the scene, even while ContainmentHandler
components are set up next to StructurePart
components.
Stand alone usage of AttachmentPoint
The AttachmentPoint
base abstract class can be used out of a structure context: its goal is simply to keep track, synchronize and and notify all clients of a "link" between 2 Attachment
. To be used in a structure, an AttachmentPoint
subclass must also implement IStructurePartPoint (which is the case of MagnetStructureAttachmentPoint
).
MagnetAttachmentPoint
For instance, it can be used to track that a Magnet
component just snapped on another (without them being part of a structure).
This is demonstrated in the MagnetAttachmentPoint
class. This class will store an attachment on a magnet snap, and will remove it when the 2 magnets are moved apart.
Dependencies
- XRShared addon
- Magnets
Download
This addon latest version is included into the Industries addon project
Supported topologies
- shared mode
Changelog
- Version 2.0.0: First release
- Goals
- Logic overview
- Attachment creation on ungrabbing
- Attachment deletion when 2 parts are moving at the same time in a given structure
- Attachment deletion when 1 "lightweight" part is moving in a structure
- Repositioning
- Tag compatibility
- Underlying snap detection
- Optional containment
- Setup
- Stand alone usage of AttachmentPoint
- Dependencies
- Download
- Supported topologies
- Changelog