Bug: Sandbox example objects can duplicate that should not

We are using the Sandbox sample as basis for a prototype. We realized, that when you create a duplicate of a model from the side (e.g the cactus), that over time (for example by leaving the browser for a while and coming back) you can create a duplicate out of the duplicate.

  • Original = Object 0
  • Drag a copy “out of” object 0 → object 1
  • Place it in the scene
  • leave the browser for a while
  • now you can create a clone = object 2 “out of” object 1

How can we prevent that and why is that so?

Hi FragstreetBoy,

It looks like you’re encountering an issue with object duplication in the Sandbox sample of Needle Engine. When you create a copy of an object and then leave the browser for a while, it seems that references to the original object might not be maintained correctly, allowing for the duplication of duplicates.

Here are some potential reasons for this behavior and how to prevent it:

Reasons:

  1. State Management: When you leave the browser for an extended period, the state of objects might not be saved properly. This can lead to instances where the system doesn’t recognize that there are already existing duplicates.

  2. Event Listeners: If drag-and-drop operations aren’t properly managed with event listeners, a duplicated object may be treated as a new original when manipulations happen after returning from inactivity.

  3. Instance Tracking: The system may not be correctly tracking instances of objects, leading to misidentification of what constitutes a new object versus an existing clone.

Suggestions to Prevent Duplication:

  1. Object Instance Tracking: Implement a unique ID for each created object. When an object is cloned, check if the object has already been duplicated. You can do this by maintaining a list of IDs in a central manager or service.

    const objectManager = {
        createdObjects: {}, // Keep track of created object IDs
    
        createObject() {
            // Generate new unique ID
            const id = generateUniqueId();
            this.createdObjects[id] = new Object(); // Replace with actual object creation logic
            return id;
        },
    
        isDuplicate(id) {
            return id in this.createdObjects;
        },
    };
    
  2. Reinstate State on Return: Use browser session storage or a similar method to save the current state of your objects when the user leaves the site, and restore it when they return.

  3. Enforce Cloning Logic: When a user tries to duplicate an object, check whether the source is already a duplicate. If it is, prevent additional duplicates from being created.

Example

Here’s a pseudocode example of implementing some of the above suggestions in your drag-and-drop logic:

function onDragStart(object) {
    // Track the object's ID or reference
}

function onDrop(newPosition) {
    if (objectManager.isDuplicate(currentObjectId)) {
        alert("Cannot create a duplicate of a duplicate object!");
        return;
    }
    // Proceed with logic to drop the object
}

By tracking object instances and managing state effectively, you can likely prevent this duplication issue. If you need help with specific parts of your code, feel free to share!

1 replies left - Get a PRO license for more AI replies.
thread_ntIc2cSxyXNuLTvsRVQOrXU4
Reply to this message to continue the conversation

Hello Alexander,

that would be a bug. How long do you wait for this issue to appear?

It`s just a few minutes. I tested it with a new room and within two minutes it happend

Hello, it does reproduce and believe to know why it happens:

I think the second duplication happens because the networking room disconnects due to inactivity (e.g. when your browser pages is not visible anymore) and when you focus it again it reconnects and the object gets spawned again at the last known position (which happens to be the exact position of the duplicated object)
The problem is here that the object that is spawned with syncedInstantiate is not automatically deleted by the engine when the room disconnects.
You can make this issue even more apparent by using the Leave Room and Join Room menu button in the Needle Menu. Each time you re-join you will see one more instance of each object of the duplicated object.

I will create an issue to track and think about how to solve this properly.

Thanks for reporting!

As a workaround you could add a custom component to your objects that are duplicated with the Duplicatable component and listen to the LeftRoom networking event to destroy the object again

Edit: it should be fixed in the next Needle Engine 4.0 alpha. → Preview of the fixed version

I added the following code to fix this problem for me now.
But I can’t receive the event “RoomEvents.UserLeftRoom” and my method “userLeftRoom” is never called.

I’m very confused about your networking manual too.
In the chapter “Networking Lifecycle Events” you wrote I should add the listener like:
this.context.beginListen(RoomEvents.UserLeftRoom, ({userId}) => { 
 });

But with “this.context.beginListen” i got errors I have to use “this.context.connection.beginListen”
And your manual did’t explain where I get the “userId” from too. So I had to guess. But I think for this fix I will not need the userid.

This is my code. What did I make wrong?
awake() {
// Listen to the event when another user has left your networked room
this.context.connection.beginListen(RoomEvents.UserLeftRoom, this.userLeftRoom.bind(this));
}

onDestroy() {
this.context.connection.stopListen(RoomEvents.UserLeftRoom, this.userLeftRoom.bind(this));
}

private userLeftRoom(_: UserJoinedOrLeftRoomModel) {
console.log("User left: " + _.userId);
syncDestroy(this.gameObject, this.context.connection);
}

Hi, you’re using the event for when another user is disconnecting from a networked room - the user id in the event callback is the ID of the other user.

Please use the RoomEvents.LeftRoom event.

// Listen to the event when *you* have left a networked room
this.context.beginListen(RoomEvents.LeftRoom, ({room}) => { ... });

For completeness: you can get your own user id from context.connection.connectionId - but you dont need that for this case

I have no idea anymore. I don’t know how I should write a workaround for this bug.

When I get the “LeftRoom” callback and like you wrote I destroy the object with “GameObject.destroy” or with “syncDestroy”. It doesn’t matters. I will get lot of these error messages:
“TransformControls: The attached 3D object must be a part of the scene graph.
could not find object that was instantiated: 
”

I don’t know how to fix it without side effects?

That sounds like an issue with TransformControls and might not be related - could you share more info on the objects and your scene setup that you’re working with here? Because the Sandbox scene doesn’t contain objects with TransformControls.

It would also be helpful to see the full stacktrace of the error.

Edit: I just tried to reproduce the error locally but could not, so more info about your scene is needed

My project is based on the Sandbox example. I made a script for it.
I call it “DragRotate”. Its extends DragControls.
With allows me to move a object like with “DragControls”.
Then I combined it with the code from “TransformGizmo” with is using TransformControls.

With this combination I have a script for object I can drag and when its selected it has a rotation gizmo active which allows me to rotate the object on the y axis.

For the Stacktrace i don’t know how to add files here. So in short the TransformControls has some problems with the “AnimationFrame”:

chunk-TNM5YZYQ.js?v=6aaa151c:139 TransformControls: The attached 3D object must be a part of the scene graph.
updateMatrixWorld @ chunk-TNM5YZYQ.js?v=6aaa151c:139
updateMatrixWorld @ chunk-VU7D7ALY.js?v=6aaa151c:5149
WebGLRenderer.render @ chunk-VU7D7ALY.js?v=6aaa151c:18349
wrappedFunction @ chunk-G2Z4LREE.js?v=6aaa151c:20831
renderer.render @ chunk-G2Z4LREE.js?v=6aaa151c:25759
wrappedFunction @ chunk-G2Z4LREE.js?v=6aaa151c:20831
renderNow @ chunk-G2Z4LREE.js?v=6aaa151c:41791
internalOnRender @ chunk-G2Z4LREE.js?v=6aaa151c:41739
internalStep @ chunk-G2Z4LREE.js?v=6aaa151c:41594
update @ chunk-G2Z4LREE.js?v=6aaa151c:41564
(anonymous) @ chunk-G2Z4LREE.js?v=6aaa151c:41553
onAnimationFrame @ chunk-VU7D7ALY.js?v=6aaa151c:18322
onAnimationFrame @ chunk-VU7D7ALY.js?v=6aaa151c:8669
requestAnimationFrame
onAnimationFrame @ chunk-VU7D7ALY.js?v=6aaa151c:8670
requestAnimationFrame
onAnimationFrame @ chunk-VU7D7ALY.js?v=6aaa151c:8670
requestAnimationFrame
onAnimationFrame @ chunk-VU7D7ALY.js?v=6aaa151c:8670

Could you share some of the code on how you’re setting up the TransformControls script?

Here is the initialisation part for it. I have to say I’m more a Unity C# programmer then typescript.

onEnable() {
super.onEnable();

// this is nearly the same like in TransformGizmo
if (!this.context.mainCamera) return;

if (!this.control) {
	this.control = new TransformControls(this.context.mainCamera, this.context.renderer.domElement);
	this.control.visible = true;
	this.control.enabled = true;
	this.control.getRaycaster().layers.set(2);
	this.control.size = 1;
	this.control.traverse(x => {
		const mesh = x as Mesh;
		mesh.layers.set(2);
		if (mesh) {
			const gizmoMat = mesh.material as MeshBasicMaterial;
			if (gizmoMat) {
				gizmoMat.opacity = 0.8;                        
			}
		}
	});
	this.orbit = GameObject.getComponentInParent(this.context.mainCamera, OrbitControls) ?? undefined;
}

if (this.control) {
	this.context.scene.add(this.control);

	this.control.setMode('rotate');
	this.control.showX = false;
	this.control.showY = false;
	this.control.showZ = false;
}

}

The rest happens whe the object is clicked:

onPointerDown(args: PointerEventData) {
if (args.used) return;

if (args.button === 0) {
	if (this.control) {
		this.registerGizmo();
		this.control.showY = true;
	}
	//args.use(); // Not used it will be called in super.onPointerDown(args);
}

super.onPointerDown(args);

}

private registerGizmo(): void {
if (this.control) {
this.control.attach(this.gameObject);
this.control?.addEventListener(‘dragging-changed’, this.onControlDraggingChangedEvent);
this.control?.addEventListener(“mouseDown”, this.onControlGizmoMouseDownChangedEvent);
this.control?.addEventListener(“mouseUp”, this.onControlGizmoMouseUpChangedEvent);

	this.control.setMode('rotate');
}

}

Do you at any point remove the TransformGizmo again from the scene?

here’s a snipped from the built-in script

    /** @internal */
    onDisable() {
        this.control?.removeFromParent();
        this.control?.removeEventListener('dragging-changed', this.onControlChangedEvent);
        window.removeEventListener('keydown', this.windowKeyDownListener);
        window.removeEventListener('keyup', this.windowKeyUpListener);
    }

Yes.
I remove all listeners onDisable() and when i have a mouseup from the Gizmo the gizmo should disapear after time then it will deregisterd too ( will added agian on object click)

But i remove the object only with
this.control?.remove(this.gameObject)

Not
this.control?.removeFromParent();
or
this.control.detach();

Maybe that’s the problem.

With
this.control.detach();

in OnDestroy

the Error:
“TransformControls: The attached 3D object must be a part of the scene graph.”

is gone but the object is totally gone too just have this warining then:

chunk-G2Z4LREE.js?v=316a88b3:35083 could not find object that was instantiated: 564cf67c-3906-5328-a631-43c301cbee7a
(anonymous) @ chunk-G2Z4LREE.js?v=316a88b3:35083
await in (anonymous)
handleIncomingStringMessage @ chunk-G2Z4LREE.js?v=316a88b3:30559
onMessage @ chunk-G2Z4LREE.js?v=316a88b3:30435
(anonymous) @ chunk-G2Z4LREE.js?v=316a88b3:30416
(anonymous) @ src-XB5GKDX2.js?v=316a88b3:433
dispatchEvent @ src-XB5GKDX2.js?v=316a88b3:432
handleEvent @ src-XB5GKDX2.js?v=316a88b3:474
Websocket.handleMessageEvent @ src-XB5GKDX2.js?v=316a88b3:209

Hello,

I could not reproduce this error with the Transform Gizmo in the bugreport you sent.

Please note that we just published alpha 4.0.1 which contains the synced instantiate fix as well as the screenshot fix for instanced objects.


While looking at your code I noticef that you’re not correctly subscribing to events in some places in your code (e.g. GameObjectExtension) which leads to a lot of functions being created and added to the list but never removed.

E.g. the code below in GameObjectExtension doesnt work because you create a new function here so and later try to unsubscribe from it with another new function


awake () {
    this.context.connection.beginListen(RoomEvents.LeftRoom, () => { this.leftRoom(); });
}

onDestroy() {
        this.context.connection.stopListen(RoomEvents.LeftRoom, () => { this.leftRoom(); });
}

one solution would be this:

awake() {
   this.context.connection.beginListen(RoomEvents.LeftRoom, this.leftRoom);
}
onDestroy() {
   this.context.connection.stopListen(RoomEvents.LeftRoom, this.leftRoom);
}
// make sure to change the leftRoom function to use the arrow function syntax:   
private leftRoom = () =>  { ... }

See this docs page for details (note that this is not Needle specific but how javascript events/functions fundamentally work)