Unity: Multiplayer Duplicated Local Player On Spawn

Hello question, I’m trying to setup a game where you go from main menu to a multiplayer scene and a ability to go back to main menu. Currently the room transitions work, but I ran into a problem where if I go from multiplayer game to main menu and back into multiplayer game again I get duplicate characters that I can control. If I repeat the process I will keep duplicating the characters. Currently I’m basing my project based of the NetworkingPlayer sample and if I use the sample it seems to be happening there as well. Any help is much appreciated and some sample code below:

Scene is based of NetworkingPlayer, using SyncRoom, PlayerSync, & SpawnSpotHandler

Scene switching:

    public goToScene(index: number) {
        this.switcher ??= this.getSceneSwitcher();

        this.switcher?.select(index);
    }

PlayerMovement

import { Behaviour, PlayerState, SyncedTransform, serializable, Mathf } from "@needle-tools/engine";

class InputValue {
	value: number = 0;
}

export class PlayerMovement extends Behaviour {
	@serializable(PlayerState)
	playerState?: PlayerState;

	@serializable(SyncedTransform)
	syncedTransform?: SyncedTransform;

	@serializable()
	speed: number = 1;

	private dir: number = 1;
	private inputs: Json;

	start() {
		if (!this.playerState) return;

		if (this.playerState.hasOwner)
			this.init();
		else
			this.playerState.onFirstOwnerChangeEvent.addEventListener(this.init);
	}

	onDestroy() {
		this.playerState.onFirstOwnerChangeEvent.removeAllEventListeners();
	}

	init() {
		if (!this.playerState) {
			return;
		}

		// Synced transform synchronizes position, rotation and scale. But has to be manually enabled to determine who the owner is.
		if (this.syncedTransform && this.playerState?.isLocalPlayer) {
			this.syncedTransform.requestOwnership();
		}

		this.inputs = {
			horizontal: new InputValue(),
			vertical: new InputValue(),
		};

		// Ensure touch actions don't accidentally scroll/refresh the page
		this.context.domElement.style.userSelect = "none";
		this.context.domElement.style.touchAction = "none";
		this.context.renderer.domElement.style.touchAction = "none";

		// Prevent iOS magnifying glass and accidental text selection
		const preventDefault = (event: Event) => {
			event.preventDefault();
		};

		// Add event listeners to the elements
		this.context.domElement.addEventListener('touchstart', preventDefault, { passive: false });
		this.context.domElement.addEventListener('touchmove', preventDefault, { passive: false });
		this.context.renderer.domElement.addEventListener('touchstart', preventDefault, { passive: false });
		this.context.renderer.domElement.addEventListener('touchmove', preventDefault, { passive: false });
	}

	lateUpdate() {
		if (!this.playerState?.isLocalPlayer) return;
		if (!this.playerState.hasOwner) return;

		const moveAmount = this.speed * this.context.time.deltaTime;
		const pos = this.gameObject.transform.position;

		let haveMovement = false;

		this.inputs.horizontal.value = 0;
		this.inputs.vertical.value = 0;

		this.handleKeyboard();
		if (this.context.input.getTouchesPressedCount() == 1) {
			this.handlePointer();
		}

		this.inputs.horizontal.value = Mathf.clamp(this.inputs.horizontal.value, -1, 1);
		this.inputs.vertical.value = Mathf.clamp(this.inputs.vertical.value, -1, 1);

		// Right is negative for whatever reason
		pos.x -= this.inputs.horizontal.value * moveAmount;
		pos.y += this.inputs.vertical.value * moveAmount;

		haveMovement = this.inputs.horizontal.value != 0 || this.inputs.vertical.value != 0;
		if (haveMovement)
			this.dir = this.inputs.horizontal.value < 0 ? -1 : 1;
	}

	private handleKeyboardAxis(negative: string, positive: string, value: InputValue) {
		let offset = 0;
		if (this.context.input.isKeyPressed(negative)) {
			offset -= 1;
		}
		if (this.context.input.isKeyPressed(positive)) {
			offset += 1;
		}
		value.value += offset;
	}

	private handleKeyboard() {
		this.handleKeyboardAxis("ArrowLeft", "ArrowRight", this.inputs.horizontal);
		this.handleKeyboardAxis("ArrowDown", "ArrowUp", this.inputs.vertical);

		// TODO input should be handled using .code instead of .keyCode, then we can support french/belgian layouts as well easily
		this.handleKeyboardAxis("a", "d", this.inputs.horizontal);
		this.handleKeyboardAxis("s", "w", this.inputs.vertical);

	}

	// very basic edge-of-screen implementation
	private handlePointer() {
		if (this.context.input.mousePressed) {
			const screenPos = this.context.input.mousePositionRC;
			if (screenPos.x < -0.25) {
				this.inputs.horizontal.value -= 1;
			}
			if (screenPos.x > 0.25) {
				this.inputs.horizontal.value += 1;
			}
			if (screenPos.y < -0.5) {
				this.inputs.vertical.value -= 1;
			}
			if (screenPos.y > 0.5) {
				this.inputs.vertical.value += 1;
			}
		}
	}
}

Hi Danny! I’m an AI-powered assistant here to help with your queries. I can assist you with questions regarding Needle Engine, three.js, and basic Unity or Blender help.

Please provide your OS, Needle Engine version, and Unity version to ensure I can assist you accurately. Just reply with your question!

2 replies left - GPT 4O
thread_OJA44MKddfrR2nE61p1iJyTG
Reply to this message to continue the conversation

OS:Windows, Needle Engine version: 3.47.3-pre, and Unity

Thanks for the details, Danny! It seems you’re experiencing an issue where multiple player instances are created upon transitioning from the multiplayer game to the main menu and back. This can often occur if the player instances aren’t properly managed or destroyed when transitioning scenes.

Here are some steps you can take to resolve the duplicate player instances:

  1. Destroy Previous Instances: Ensure that all existing player instances are destroyed upon exiting the multiplayer scene. You can do this in the goToScene method by checking for existing player objects and removing them.

  2. Singleton Pattern: Implement a singleton pattern for your player object so that only one instance of the player can be created. This way, if a player tries to spawn again, the existing one can be reused.

  3. Network Cleanup: If you’re using a networking library, make sure to clean up the networked objects before transitioning scenes.

Here is an example code snippet that checks for existing players before allowing new ones to spawn:

goToScene(index: number) {
    this.switcher ??= this.getSceneSwitcher();

    // Check for existing player instances before transitioning
    const existingPlayer = this.findExistingPlayer();
    if (existingPlayer) {
        existingPlayer.destroy();
    }

    this.switcher?.select(index);
}

findExistingPlayer() {
    // Logic to find and return existing player instance
}

By implementing these suggestions, you can avoid duplicate player instances when switching scenes. Let me know if you need any further assistance or clarification!

1 replies left - GPT 4O
thread_OJA44MKddfrR2nE61p1iJyTG
Reply to this message to continue the conversation

The player is destroyed when I go back to the main menu. Enabling debugplayersync=true
it seems like my PlayerState isn’t being cleared when I go back to the main menu. Everytime I rejoin the game it keeps growing even though the player no longer exists, I’m assuming I might not disconnecting properly when I’m entering a new scene or my state is somehow being saved.

Hi Danny! It sounds like the issue is related to the player connection or PlayerState not being cleared properly when returning to the main menu. Here are some solutions to consider:

  1. Explicitly Disconnect Players: Ensure you are properly disconnecting from the network when switching scenes. You can implement a clean-up method that is called during the scene transition to handle any player state or network disconnections.
disconnectPlayer() {
    if (this.playerState) {
        this.playerState.disconnect();
        this.playerState = null; // Clear the player state
    }
}
  1. Event Listeners: Make sure to remove any event listeners tied to the player state before transitioning scenes, as these can hold references to previously created states:
onDestroy() {
    this.playerState?.onFirstOwnerChangeEvent.removeAllEventListeners();
    this.disconnectPlayer(); // Call your disconnect method here
}
  1. Reset PlayerState: When transitioning back to the main menu, you might also want to reset or nullify the PlayerState to ensure it doesn’t retain any old values when re-entering the multiplayer game.

  2. Check for Duplicates: Before spawning a player, check if the player already exists. This can prevent multiple instances from being created inadvertently.

Implementing these steps should help in resolving the issue with lingering player states. Let me know if you encounter further difficulties or if you need more guidance!

0 replies left - GPT 4O
thread_OJA44MKddfrR2nE61p1iJyTG
Reply to this message to continue the conversation

Hi, can you clarify in which Scene is which component?

I assume:

  • Main Scene has just the SceneSwitcher
  • Multiplayer Scene has a SyncedRoom and PlayerSync component(s)
  • Menu Scene has just a menu (or other objects/components)

Is that correct?

Hey, yep to clarify:

  1. Init scene that has SceneSwitcher and Networking component for localhost
  2. MainMenu scene that has GUI with a button that calls goToScene to Multiplayer scene
  3. Multiplayer scene that has Synced Room, Player Sync, Spawn Spot Handler similar to Multipalyer Player Sample and a GUI with a button that calls goToScene to MainMenuScene. The network player prefab is based on the Multiplayer Player Sample player prefab with a PlayerMovement, PlayerState, Synced Transform

My test url has a parameter room=needle258.

To Reproduce:
Go from MainMenu to Mutliplayer and back to MainMenu. Once you go back to the same multiplayer room there are 2 network players and more as you repeat the process

Let me know if you need anyting else

Thanks for the clarification! Thats helpful

Do you think you could report this with our Bugreporter? It doesnt reproduce here when I recreate your setup manually unfortunately.

Run the menu item: Needle Engine/Report a Bug from your main scene (the one with the scene switcher)

Hey thanks for looking into it, since it wasn’t happening on your end I tried making a project from scratch and I think I narrowed it down. Me running my own local host is causing issues, since when I tried removing the local and running it on the default wss://needle-tiny-starter.glitch.me/socket it worked! Just to confirm the main branch on GitHub - needle-engine/networking-local: Run the Needle Engine Networking server locally or host your own is what I need right? Not sure if this matters but I’m getting a unsupported engine in the local server logs when I try to run my own

Also I tried submitting a bug report but getting an error:

Creating bug report failed, see C:\Users\Danny\AppData\Local\Temp\Needle/BugReports/240814-044023/Report/exception_on_collecting_error_logs.oops 
System.IO.IOException: The process cannot access the file 'E:\Git\MLSE\Unity_NeedleEngine_FanPlusLobby\Needle\newProjectBuild\src\scripts\.vs\scripts\FileContentIndex\5c7f6a13-d19e-4b23-98dc-82b22b468da6.vsidx' because it is being used by another process.
  at System.IO.FileSystem.CopyFile (System.String sourceFullPath, System.String destFullPath, System.Boolean overwrite) [0x0005f] in <ecf276dcb8654fa5bed2c5ea1a4ad870>:0 
  at System.IO.FileInfo.CopyTo (System.String destFileName, System.Boolean overwrite) [0x00032] in <ecf276dcb8654fa5bed2c5ea1a4ad870>:0 
  at (wrapper remoting-invoke-with-check) System.IO.FileInfo.CopyTo(string,bool)
  at Needle.Engine.Utils.FileUtils.CopyRecursively (System.IO.DirectoryInfo source, System.IO.DirectoryInfo target, System.Predicate`1[T] fileFilter, System.Predicate`1[T] dirFilter) [0x0007d] in .\Library\PackageCache\com.needle.engine-exporter@3.47.3-pre\Common\Runtime\Utils\FileUtils.cs:97 
  at Needle.Engine.Utils.FileUtils.CopyRecursively (System.IO.DirectoryInfo source, System.IO.DirectoryInfo target, System.Predicate`1[T] fileFilter, System.Predicate`1[T] dirFilter) [0x000c9] in .\Library\PackageCache\com.needle.engine-exporter@3.47.3-pre\Common\Runtime\Utils\FileUtils.cs:105 
  at Needle.Engine.Utils.FileUtils.CopyRecursively (System.IO.DirectoryInfo source, System.IO.DirectoryInfo target, System.Predicate`1[T] fileFilter, System.Predicate`1[T] dirFilter) [0x000c9] in .\Library\PackageCache\com.needle.engine-exporter@3.47.3-pre\Common\Runtime\Utils\FileUtils.cs:105 
  at Needle.Engine.Utils.FileUtils.CopyRecursively (System.IO.DirectoryInfo source, System.IO.DirectoryInfo target, System.Predicate`1[T] fileFilter, System.Predicate`1[T] dirFilter) [0x000c9] in .\Library\PackageCache\com.needle.engine-exporter@3.47.3-pre\Common\Runtime\Utils\FileUtils.cs:105 
  at Needle.Engine.Utils.FileUtils.CopyRecursively (System.IO.DirectoryInfo source, System.IO.DirectoryInfo target, System.Predicate`1[T] fileFilter, System.Predicate`1[T] dirFilter) [0x000c9] in .\Library\PackageCache\com.needle.engine-exporter@3.47.3-pre\Common\Runtime\Utils\FileUtils.cs:105 
  at Needle.Engine.Utils.FileUtils.CopyRecursively (System.IO.DirectoryInfo source, System.IO.DirectoryInfo target, System.Predicate`1[T] fileFilter, System.Predicate`1[T] dirFilter) [0x000c9] in .\Library\PackageCache\com.needle.engine-exporter@3.47.3-pre\Common\Runtime\Utils\FileUtils.cs:105 
  at Needle.Engine.Problems.ProjectInfoReporter.CopyWebProjectFiles (System.IO.DirectoryInfo webProjectDir, System.IO.DirectoryInfo to, Needle.Engine.Problems.ProjectInfoReporter+CollectionSettings settings) [0x00019] in .\Library\PackageCache\com.needle.engine-exporter@3.47.3-pre\Core\Editor\Problems\Reporting\ProjectInfoReporter.cs:596 
  at Needle.Engine.Problems.ProjectInfoReporter.InternalCollectDebugFiles (System.String header, Needle.Engine.Problems.ProjectInfoReporter+CollectionSettings settings) [0x00263] in .\Library\PackageCache\com.needle.engine-exporter@3.47.3-pre\Core\Editor\Problems\Reporting\ProjectInfoReporter.cs:152 
UnityEngine.Debug:LogError (object)
Needle.Engine.Problems.ProjectInfoReporter/<InternalCollectDebugFiles>d__11:MoveNext () (at ./Library/PackageCache/com.needle.engine-exporter@3.47.3-pre/Core/Editor/Problems/Reporting/ProjectInfoReporter.cs:221)
System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<Needle.Engine.Problems.ProjectInfoReporter/ProjectInfoModel>:SetResult (Needle.Engine.Problems.ProjectInfoReporter/ProjectInfoModel)
Needle.Engine.Problems.ProjectInfoReporter/ProjectInfoModel/<Create>d__26:MoveNext () (at ./Library/PackageCache/com.needle.engine-exporter@3.47.3-pre/Core/Editor/Problems/Reporting/ProjectInfoReporter.cs:789)
System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<bool>:SetResult (bool)
Needle.Engine.Actions/<HasNpmInstalled>d__33:MoveNext () (at ./Library/PackageCache/com.needle.engine-exporter@3.47.3-pre/Core/Editor/Actions/Actions.cs:254)
UnityEngine.UnitySynchronizationContext:ExecuteTasks ()

Are you using VisualStudio? It looks like the .vs is perhaps files locked by the Visual Studio process. Try closing VS if that’s the case and run it again.

You can ignore the unsupported engine warning - it’ll work with node 20! I’ll update that in the package.json

I’ve just rebased main onto the v2 branch in the networking repository since that is the latest version - I believe you did run your local server on main - sorry about that. If you pull the local server repository again it should work and use the correct networking package version! No need to send a bugreport anymore in that case. And thanks for reporting the issue!

Perfect thanks it works!

1 Like

This topic was automatically closed 13 days after the last reply. New replies are no longer allowed.