Template Project Entity Overlapping and Sticking Issues

Hello,

There are three issues I am running into with a default template project + additional minimal code changes:

  1. the Sphere_Left entity always overlaps the Sphere_Right entity.
  2. when I release the Sphere_Left entity, it does not remain sticking to the Sphere_Right entity
  3. when I release the Sphere_Left entity, it distances itself from the Sphere_Right entity

When I manipulate the Sphere_Right entity, these above 3 issues do not occur: I get a correct and expected behavior.

These issues are simple to replicate:

  1. Create a new project in XCode
  2. Choose visionOS -> App, then click Next
  3. Name your project, and leave all other options as defaults: Initial Scene: Window, Immersive Space Renderer: RealityKit, Immersive Space: Mixed, then click Next
  4. Save you project anywhere...
  5. Replace the entire ImmersiveView.swift file with the below code.
  6. Run.
  7. Try to manipulate the left sphere, you should get the same issues I mentioned above
  8. If you restart the project, and manipulate only the right sphere, you should get the correct expected behaviors, and no issues.

I am running this in macOS 26, XCode 26, on visionOS 26, all released lately.

ImmersiveView Code:

//
//  ImmersiveView.swift
//

import OSLog
import SwiftUI
import RealityKit
import RealityKitContent

struct ImmersiveView: View {
    private let logger = Logger(subsystem: "com.testentitiessticktogether", category: "ImmersiveView")
    @State var collisionBeganUnfiltered: EventSubscription?
    var body: some View {
        RealityView { content in
            // Add the initial RealityKit content
            if let immersiveContentEntity = try? await Entity(named: "Immersive", in: realityKitContentBundle) {
                content.add(immersiveContentEntity)

                // Add manipulation components
                setupManipulationComponents(in: immersiveContentEntity)

                collisionBeganUnfiltered = content.subscribe(to: CollisionEvents.Began.self) { collisionEvent in
                    Task { @MainActor in
                        handleCollision(entityA: collisionEvent.entityA, entityB: collisionEvent.entityB)
                    }
                }
            }
        }
    }
    
    private func setupManipulationComponents(in rootEntity: Entity) {
        logger.info("\(#function) \(#line) ")

        let sphereNames = ["Sphere_Left", "Sphere_Right"]

        for name in sphereNames {
            guard let sphere = rootEntity.findEntity(named: name) else {
                logger.error("\(#function) \(#line) Failed to find \(name) entity")
                assertionFailure("Failed to find \(name) entity")
                continue
            }

            ManipulationComponent.configureEntity(sphere)
            var manipulationComponent = ManipulationComponent()
            manipulationComponent.releaseBehavior = .stay
            sphere.components.set(manipulationComponent)
        }

        logger.info("\(#function) \(#line) Successfully set up manipulation components")
    }
    
    private func handleCollision(entityA: Entity, entityB: Entity) {
        logger.info("\(#function) \(#line) Collision between \(entityA.name) and \(entityB.name)")

        guard entityA !== entityB else { return }

        if entityB.isAncestor(of: entityA) {
            logger.debug("\(#function) \(#line) \(entityA.name) already under \(entityB.name); skipping reparent")
            return
        }

        if entityA.isAncestor(of: entityB) {
            logger.info("\(#function) \(#line) Skip reparent: \(entityA.name) is an ancestor of \(entityB.name)")
            return
        }

        reparentEntities(child: entityA, parent: entityB)
        entityA.components[ParticleEmitterComponent.self]?.burst()
    }
    
    private func reparentEntities(child: Entity, parent: Entity) {
        let childBounds = child.visualBounds(relativeTo: nil)
        let parentBounds = parent.visualBounds(relativeTo: nil)
        let maxEntityWidth = max(childBounds.extents.x, parentBounds.extents.x)

        let childPosition = child.position(relativeTo: nil)
        let parentPosition = parent.position(relativeTo: nil)
        let currentDistance = distance(childPosition, parentPosition)

        child.setParent(parent, preservingWorldTransform: true)
        logger.info("\(#function) \(#line) Set \(child.name) parent to \(parent.name)")

        child.components.remove(ManipulationComponent.self)
        logger.info("\(#function) \(#line) Removed ManipulationComponent from child \(child.name)")

        if currentDistance > maxEntityWidth {
            let direction = normalize(childPosition - parentPosition)
            let newPosition = parentPosition + direction * maxEntityWidth
            child.setPosition(newPosition - parentPosition, relativeTo: parent)
            logger.info("\(#function) \(#line) Adjusted position: distance was \(currentDistance), now \(maxEntityWidth)")
        }
    }
}

fileprivate extension Entity {
    func isAncestor(of other: Entity) -> Bool {
        var current: Entity? = other.parent
        while let node = current {
            if node === self { return true }
            current = node.parent
        }
        return false
    }

}

#Preview(immersionStyle: .mixed) {
    ImmersiveView()
        .environment(AppModel())
}

There is actually a 4th issue with this example project:

  • When we run this on the real Apple Vision Pro hardware, we get very sluggish and slow dragging behavior, either on first run or subsequent runs.

And a 5th issue on the real device, visible when you run in debug mode with the Debug Vizualization turned ON, with the options Axes, Bounds, and Collision Shapes and Axes selected: the rotation axis of the spheres are off point (ie: the rotation point appears to not be the center of the entity, but rather on 1 of it's corners).

FB20298276 with repo and videos for simulator and hardware.

Template Project Entity Overlapping and Sticking Issues
 
 
Q