I'm working on a multiplayer game and I'm encountering a issue when 2 players are loading the same level using PhotonNetwork.LoadLevel().
When I start the game alone, I can control my player and everything is fine. But when we are 2 players, Player A is controlling Player B and vice-versa.
I check a lot of links on the internet these past few days, and I learned the concept of PhotonNetwork.IsMine which I thought would solve all of my problem but it seems to not working with me. Also, I'm using the new input system of Unity but I don't think the issue come from here.
Basically, what I'm doing is:
Instantiate a player (this happened twice since I have 2 players) which have a PlayerManager
Player Manager Get Instance of the local player and synchronize camera with the local player only if isMine = true
CameraManager creates input manager if the script is linked to the local player by using isMine
Link the main camera to this script when the gamemanager request it
Update camera rotation only when isMine is true (second protection)
Here is a piece of my code:
GameManager.cs (Holding by a Scene Object, so it is initially instantiated for everyone with the scene)
void Start()
{
if(PlayerManager.LocalPlayerInstance == null)
{
//Get player's team
string team = (string)PhotonNetwork.LocalPlayer.CustomProperties["Team"];
int indexPlayer = GetSpawnPosition();
//Spawn player depending on its team and its index in the players pool
if (team.Equals("Spy"))
{
PhotonNetwork.Instantiate(this.spyPrefab.name, SpySpawns[indexPlayer].position, SpySpawns[indexPlayer].rotation);
}
else if (team.Equals("Defender"))
{
PhotonNetwork.Instantiate(this.defenderPrefab.name, SpySpawns[indexPlayer].position, SpySpawns[indexPlayer].rotation);
}
}
}
PlayerManager.cs (Holding by the player, so not initially instantiated with the scene)
void Awake()
{
//Keep track of the localPlayer to prevent instanciation when levels are synchronized
if (photonView.IsMine)
{
LocalPlayerInstance = gameObject;
}
//Don't destroy this gameobject so it can survives level synchronization
DontDestroyOnLoad(gameObject);
}
private void Start()
{
//Get Manager of the camera of the player and attach to the local player
CameraLookFPS cameraFPSManager = gameObject.GetComponent<CameraLookFPS>();
if (cameraFPSManager != null)
{
//Ensure that we the local player is controlling its own camera
if(photonView.IsMine == true)
{
cameraFPSManager.SynchronizeWithLocalPlayer();
}
}
else
Debug.Log("This player is missing the CameraLookFPS component");
}
CameraManager.cs (Holding by the player, so not initially instantiated with the scene)
private void Start()
{
//Synchronize camera with local player on start for debug
if(SynchronizeOnStart)
{
SynchronizeWithLocalPlayer();
}
//Bind input for controlling the camera
BindingInput();
}
private void BindingInput()
{
// Prevent control is connected to Photon and represent the localPlayer
if (photonView.IsMine == false && PhotonNetwork.IsConnected == true)
{
return;
}
else
{
//Get Components
Input_Master = new InputMaster();
//Enable
Input_Master.Enable();
//Input binding
Input_Master.Player.Look.performed += context => MoveCamera(context);
Input_Master.Player.Look.Enable();
}
}
public void SynchronizeWithLocalPlayer()
{
if (photonView.IsMine == false && PhotonNetwork.IsConnected == true)
{
return;
}
Player_Camera = Camera.main.transform;
isSynchronized = true;
}
I tried to be clear enough, tell me if something is bad explained. I'll continue my research on my side and I'll keep you in touch if I find something.
Thanks in advance for your help!
Adrien
I finally found my solution. It appears that I already read about it but I have failed when trying to solve it.
So, like a lot of people, it is a Camera issue.
What I did is:
Create a game object holding the camera and put it as a child of the player gameobject
Deactivate the game object holding the camera
When instantiating the player, check if the player is the local player, using PhotonNetwork.IsMine. If yes, activate the gameobject holding the camera through script
If you have questions, send me a message!
Adrien
Related
I am about to try my hand at developing a small fps multiplayer game. I have encountered a problem with networking. Namely, I have a player that can fire a gun.
When I use the player in single player everything works fine and I can shoot and the target registers the hits. But when I shoot at another player it depends if the host or the player is shooting. If the host shoots, everything works and the bullet comes out of the player, so to speak. However, when I shoot from another client, the bullet spawns at 0,0,0. Also, when I shoot at the host, only the life indicator of the client goes down, although that of the host (the other player) should go down.
// This is the OnCollisionEnter for the bullet
[ServerCallback]
void OnCollisionEnter(Collider other)
{
Debug.Log($"...collider name: {other.gameObject.name}");
CustomStats stats = other.gameObject.GetComponent<CustomStats>();
Debug.Log($"stats exist: {stats != null}, collider name: {other.gameObject.name}");
if (stats != null )
{
if (stats is PlayerStats)
{
Debug.Log("Impact on player!");
if (gameObject.GetComponentInParent<InputMananger>()?.gameObject.name != other.gameObject.name)
{
((PlayerStats)stats).TakeDamage(10);
}
} else if (stats is EnemyStats)
{
Debug.Log("Impact on enemy!");
((EnemyStats)stats).TakeDamage(10);
}
}
}
// This is the calss which is called if the player shoots
public void Shoot()
{
if (isLocalPlayer)
{
gun = GetComponentInChildren<Gun>();
Debug.Log("PewPew!");
CmdFire();
}
/*
if (hitInfo.collider == null) return;
if (hitEnemy)
{
hitInfo.collider.GetComponent<EnemyStats>()?.TakeDamage(gun.damage);
hitEnemy = false;
}
if (hitPlayer)
{
CmdFire();
/*
Debug.Log("player");
NetworkIdentity hitPlayerNet = hitInfo.collider.GetComponent<NetworkIdentity>();
if (hitPlayerNet != null)
{
PlayerStats playersStats = hitPlayerNet.gameObject.GetComponent<PlayerStats>();
Debug.Log($"player script: {playersStats == null}");
playersStats?.TakeDamge(gun.damage);
}
hitPlayer = false;
}
*/
hitPlayer = false;
}
// this is called on the server
[Command]
void CmdFire()
{
Debug.Log(" Caller spawn projectile!");
GameObject projectile = Instantiate(projectilePrefab, projectileMount.position, cameraMount.rotation);
projectile.GetComponent<Projectile>()?.SetDamage(10);
projectile.transform.SetParent(projectileMount);
NetworkServer.Spawn(projectile);
RpcOnFire();
}
// this is called on the tank that fired for all observers
[ClientRpc]
void RpcOnFire()
{
Debug.Log(" start animation on caller!");
// shoot animation
}
I have tried many different ways to access the other player. Currently I have a script on the bullet that has an "OnCollisionEnter" function that I want to use to access my collider. In the single player it works as mentioned above but when I shoot from the host to the client it doesn't work.
Sorry if this is a duplicate question.
I found this Reddit post which is exactly my current problem.
https://www.reddit.com/r/Unity2D/comments/lw96s5/multiplayer_cameras/
However, there's only one comment which isn't that descriptive and doesn't have any links to the 'tutorial'. To rephrase, I have a MLAPI game in Unity with a LocalPlayer prefab(has a camera, FPS movement code, and PlayerInput) and a ServerPlayer(does not have a camera or PlayerInput component) prefab. They represent different replicated players in my game.
The current problem is the code to decide whether to spawn a local player or a remote player.
Deciding what prefab to spawn:
private void ApprovalCheck(byte[] connectionData, ulong clientId, MLAPI.NetworkManager.ConnectionApprovedDelegate callback)
{
//Your logic here
bool approve = connectionData == System.Text.Encoding.ASCII.GetBytes("xd");
bool createPlayerObject = true;
if (NetworkManager.Singleton.ConnectedClients[clientId].PlayerObject.IsLocalPlayer)
{
localPlayerPrefab.GetComponent<NetworkObject>()
.SpawnAsPlayerObject(clientId);
}
else
{
serverPlayerPrefab.GetComponent<NetworkObject>()
.SpawnAsPlayerObject(clientId);
}
//If approve is true, the connection gets added. If it's false. The client gets
disconnected
callback(createPlayerObject, null, approve, positionToSpawnAt, Quaternion.identity);
}
Let me know if you need any extra details.
As said I think you should always spawn the same prefab for all players and rather have a component like e.g.
public class LocalPlayerControl : NetworkBehaviour
{
void Start()
{
if (IsLocalPlayer) return;
var cam = GetComponentInChildren<Camera>();
cam.enabled = false;
// or a bit more radical
Destroy(cam);
// or if your cam is on a sub object with additional components (e.g. usually AudioListener etc)
Destroy(cam.gameObject);
// same for all other components that you don't want a remote player to have
}
I've been working on a simple 2D game in unity and it just has three scenes, the start scene, the game scene, and the game over scene. I want to display the score from the game in the game over screen. I created a score manager game object in the game scene that uses the DontDestroyOnLoad() function to carry it over into the game over screen and I gave it access to the score which is managed by the game manager. I've been debugging my code and the score is translated over into the score manager and is maintained when the game over screen loads, but for some reason it won't let me update the score text object. Here is my code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ScoreManager : MonoBehaviour {
public static ScoreManager Instance;
private GameController gameController;
private int scoreInstance;
private Text scoreText;
// When scene is first loaded
void Awake() {
this.InstantiateController();
}
// Use this for initialization
void Start () {
GameObject gameControllerObject = GameObject.FindWithTag("GameController");
if (gameControllerObject != null)
{
gameController = gameControllerObject.GetComponent<GameController>();
}
GameObject scoreTextObject = GameObject.FindWithTag("ScoreText");
if (scoreTextObject != null)
{
scoreText = scoreTextObject.GetComponent<Text>();
}
scoreInstance = 0;
scoreText.text = "";
}
// Update is called once per frame
void Update () {
scoreInstance = gameController.score;
Debug.Log("Score: " + scoreInstance.ToString());
scoreText.text = scoreInstance.ToString();
}
private void InstantiateController ()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(this);
}
else if (this != Instance)
{
Destroy(this.gameObject);
}
}
}
So I tried to programmatically gather the "score text" ui component in the start function because I figured I can't just make it public and drag in the text component because the score manager is actually in a different scene than the score text object. I also tried adding this whole bit of code to gather the text component into the update function so that it can do that when the score manager is actually a part of game over screen. Nothing seems to work and I have no idea why. Can anybody please help me with this? Also I keep getting a "NullReferenceException: Object reference not set to an instance of an object" error. Thanks in advance for any help.
Unity Start function is only called the first time the script is enabled, i.e. not every time the scene changes for a DontDestroyOnLoad object.
So if you need to wire up some changes after a scene change, you need to either detect the scene change, or have an object that starts in that scene trigger the code you want to run.
Having another object on the new scene trigger things is easy and pretty fool-proof, but there's a builtin function you can add to your other objects:
void OnLevelWasLoaded(int currentLevel)
{
}
This will be called on level changes, and give you the level's number (not name sadly). However, the above is deprecated and they want you to use Unity's SceneManager, so the proper way to set this up is now:
Unity 5 OnLevelWasLoaded?
Start()
{
SceneManager.sceneLoaded += this.OnLoadCallback;
}
void OnLoadCallback(Scene scene, LoadSceneMode sceneMode)
{
// you can query the name of the loaded scene here
}
I am working in unity 5 on a game and I am currently getting this error:
"SetDestination" can only be called on an active agent that has been placed on a NavMesh.
UnityEngine.NavMeshAgent:SetDestination(Vector3)
EnemyMovement:Update() (at Assets/Scripts/Enemy/EnemyMovement.cs:25)
My code for the enemyMovement script is:
using UnityEngine;
using System.Collections;
public class EnemyMovement : MonoBehaviour
{
Transform player;
PlayerHealth playerHealth;
EnemyHealth enemyHealth;
NavMeshAgent nav;
void Awake ()
{
player = GameObject.FindGameObjectWithTag ("Player").transform;
playerHealth = player.GetComponent <PlayerHealth> ();
enemyHealth = GetComponent <EnemyHealth> ();
nav = GetComponent <NavMeshAgent> ();
}
void Update ()
{
if(enemyHealth.currentHealth > 0 && playerHealth.currentHealth > 0)
{
nav.SetDestination (player.position);
}
else
{
nav.enabled = false;
}
}
}
Now I know that there is almost an identical question here: Unity - "SetDestination" can only be called on an active agent that has been placed on a NavMesh. UnityEngine.NavMeshAgent:SetDestination(Vector3) However, the question does not seem to have a firm response and I have tried doing some of the responses and nothing appears to work. I will say that my game does work fine. However, I have added an ability that if the player presses 'B' then all of the enemies will die on the screen. When I press 'B' the enemies die but the system 'crashes' and I get that error message.
EnemyManager.cs:
public void Kill(int InstanceID)
{
if (EnemyManager.Instance.EnemyList.ContainsKey(InstanceID))
EnemyManager.Instance.EnemyList.Remove(InstanceID);
}
public void DeathToAll()
{
Dictionary<int, Object> TempEnemyList = new Dictionary<int, Object>(EnemyManager.Instance.EnemyList);
foreach (KeyValuePair<int, Object> kvp in TempEnemyList)
{
// kvp.Key; // = InstanceID
// kvp.Value; // = GameObject
GameObject go = (GameObject)kvp.Value;
go.GetComponent<EnemyHealth>().Death();
}
}
Enemyhealth.cs
public void Death ()
{
// The enemy is dead.
isDead = true;
// Turn the collider into a trigger so shots can pass through it.
capsuleCollider.isTrigger = true;
// Tell the animator that the enemy is dead.
anim.SetTrigger ("Dead");
// Change the audio clip of the audio source to the death clip and play it (this will stop the hurt clip playing).
enemyAudio.clip = deathClip;
enemyAudio.Play ();
// Kill the Enemy (remove from EnemyList)
// Get the game object associated with "this" EneymHealth script
// Then get the InstanceID of that game object.
// That is the game object that needs to be killed.
EnemyManager.Instance.Kill(this.gameObject.GetInstanceID());
}
The problem here is that while you're checking if enemyHealth.currentHealth > 0 before calling nav.SetDestination(), that value doesn't get changed when the enemies are killed by the player's ability. Instead, it seems that the flag isDead is set on the enemy, without affecting the actual health.
To ensure nav.SetDestination() doesn't get called when the enemy is dead (because it seems at that point, the enemy is no longer a valid active agent), just add an additional condition to your if statement:
if(enemyHealth.currentHealth > 0 && !enemyHealth.isDead && playerHealth.currentHealth > 0)
{
nav.SetDestination (player.position);
}
Hope this helps! Let me know if you have any questions. (You may need to make isDead public for this to work.)
An alternative solution is to make a property IsDead which returns whether enemyHealth.currentHealth > 0, and kill enemies by setting their health to 0. Then you don't need to have a health tracker and a separate isDead flag that must be explicitly set. That's more of a design decision though as it won't affect the functionality.
I was having a similar issue where my navmesh agent was not able to reach the setdestination() point ,the navmesh agent just stopped so what i was doing wrong was that my setdestination() point was bit high from navmesh and that point was becoming out of range the agent.
So I created a foot empty gameobject that was near to the navmesh "in height".
and my agent was then able to reach the point
In my game a player can fire a weapon which will launch a projectile that automatically targets the enemy who is closest to a certain object. However, how can I stop the projectile from also targeting my own player? Is there any way to tag all other players with a certain tag, whilst leaving my own player with a different tag? The players are instantiated as prefabs when the game starts.
When launching the projectile, give it a reference to the player who shot it. Then, when calculating the closest player, check if the closest is the same as the one who shot it. If he is, choose the second closest.
public class Projectile : MonoBehaviour
{
public Player player = null;
private Player target = null;
private Player GetClosestPlayer(IList listOfPlayers)
{
Player closestPlayer = ...; // use your algorithm method here
if (player != null && player == closestPlayer)
{
// copy listOfPlayers and remove closestPlayer from it
return GetClosestPlayer(copyOfListOfPlayersWithoutPreviousClosest);
}
return closestPlayer;
}
void Update()
{
if (target != null)
// steer to target
else
target = GetClosestPlayer();
}
}