Unet Spawnable prefab not following Player Prefab

2019-08-27 19:52发布

I have got a problem with Gun not sticking to a Player. It only happens for a client. As you can see on the screen, the gun position is fine for the host(Player on the right). Gun Prefab has Network Identity with Local Player Authority checked, and Network Transform, same for Player. This is my code for Player:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;

public class Player : NetworkBehaviour 
{

    [SerializeField] float speed = 10f;
    [HideInInspector] public GameObject playerGun;
    public GameObject gunPrefab;

    void Update() 
    {
        if (!isLocalPlayer)
        {
            return;
        }

        Movement();
        if (Input.GetKeyDown(KeyCode.I))
        {
            CmdGetGun();
        }

        if (playerGun)
            CarryGun();
    }

    private void Movement()
    {
        Vector3 position = new Vector3(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical")).normalized * Time.deltaTime * speed;
        transform.position += position;
        MouseMovement();
    }

    private void MouseMovement()
    {
        Vector3 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition) - transform.position;
        mousePosition.Normalize();
        float rotation_z = Mathf.Atan2(mousePosition.y, mousePosition.x) * Mathf.Rad2Deg;
        transform.rotation = Quaternion.Euler(0f, 0f, rotation_z);
    }

    [Command]
    public void CmdGetGun()
    {
        Debug.Log("SPAWNING A GUN");
        playerGun = (GameObject)Instantiate(gunPrefab, transform.position, transform.rotation);
        NetworkServer.SpawnWithClientAuthority(playerGun, connectionToClient);
    }

    public void CarryGun()
    {
        Debug.Log("carring A GUN");
        playerGun.transform.position = new Vector3(transform.position.x, transform.position.y, transform.position.z - 1);
        playerGun.transform.rotation = transform.rotation;
    }
}

I spent days trying to figure it out. I am new to Unity, especially Unet and maybe i do not understand something.

I know the position of a gun is wrong but i will change it after i deal with this problem. For now i just want it to stick to a Player both on Client and Host side.

enter image description here

1条回答
地球回转人心会变
2楼-- · 2019-08-27 20:19

Problem

This Command method is always executed on the server .. so also with the properties on the server.

Meaning: You never set playerGun on the client side only on the server in

playerGun = Instantiate ...

So, since your client never gets its playerGun value set CarryGun is never executed on the client.


Solution 1

To avoid that you should use a ClientRpc method to set the value also on all clients.

[Command]
public void CmdGetGun()
{
    Debug.Log("SPAWNING A GUN");
    playerGun = (GameObject)Instantiate(gunPrefab, transform.position, transform.rotation);
    NetworkServer.SpawnWithClientAuthority(playerGun, connectionToClient);

    // Tell the clients to set the playerGun reference. 
    // You can pass it as GameObject since it has a NetworkIdentity
    RpcSetGunOnClients(playerGun);
}

// Executed on ALL clients
// (which does not mean on all components but just the one of 
// this synched GameObject, just to be clear)
[ClientRpc]
private void RpcSetGunOnClients (GameObject gun)
{
    playerGun = gun;
}

Solution 2

This is an alternative solution which does basically the same steps from above but it would not longer need the CarryGun method and a NetworkTransform, making your code more efficient by saving both, method calls and network bandwidth:

Instead of spawning the gun on top level in hierarchy to a certain global position and rotation

 playerGun = (GameObject)Instantiate(gunPrefab, transform.position, transform.rotation);

and than updating it's position and rotation all the time to the player's tranfroms and transfere them separately via the NetworkTransforms you could simply make it a child of the Player object itself using e.g. Instantiate(Object original, Vector3 position, Quaternion rotation, Transform parent); on the server:

 playerGun = (GameObject)Instantiate(gunPrefab, gunLocalPositionOffset, gunLocalRotationOffset, transform);

This should allways keep it in the correct position without having to Update and synchronize it's position and rotation all the time and without any further transfere methods and values.

All you have to do is again have a ClientRpc in order to tell the clients as well to make this gun a a child of your Player using e.g. SetParent(Trasnform parent, bool worldPositionStays):

playerGun.transform.SetParent(transform, false);

and if necessary apply the local position and rotation offsets. Again you could use a value, everyone has acces to or pass it to the ClientRpc from the server - your choice ;)

so your methods could now look like

// In order to spawn the gun with an offset later
// It is up to you where those values should come from / be passed arround
// If you crate your Prefab "correctly" you don't need them at all
// 
// correctly means: the most top GameObject of the prefab has the 
// default values position(0,0,0) and rotation (0,0,0) and the gun fits perfect
// when the prefab is a child of the Player => You don't need any offsets at all
Vector3 gunLocalPositionOffset= Vector3.zero;
Quaternion gunLocalRotationOffset= Quaternion.identity;

[Command]
public void CmdGetGun()
{
    Debug.Log("SPAWNING A GUN");

    // instantiates the prefab as child of this gameObject
    // you still can spawn it with a local offset position
    // This will make its position be already synched in the Players own
    // NetworkTransform -> no need for a second one
    playerGun = (GameObject)Instantiate(gunPrefab, gunLocalPositionOffset, gunLocalRotationOffset, gameobject);

    // you wouldn't need this anymore since you won't change the spoition manually
    // NetworkServer.SpawnWithClientAuthority(playerGun, connectionToClient);
    NetworkServer.Spawn(playerGun);

    // Tell the clients to set the playerGun reference. 
    // You can pass it as GameObject since it has a NetworkIdentity
    RpcSetGunOnClients(playerGun);
}

// Executed on all clients
[ClientRpc]
private void RpcSetGunOnClients (GameObject gun)
{
    // set the reference
    playerGun = gun;

    // NetworkServer.Spawn or NetworkServer.SpawnWithClientAuthority doesn't apply 
    // the hierachy so on the Client we also have to make the gun a child of the player manually

    // use the flag worldPositionStays to avoid a localPosition offset
    playerGun.transform.SetParent(transform, false);

    // just to be very sure you also could (re)set the local position and rotation offset later
    playerGun.transform.localPosition = gunLocalPositionOffset;
    playerGun.transform.localrotation = gunLocalRotationOffset;
}

Update1

For overcoming the problem with later connected clients refer to this answer.

It suggests using a [SyncVar] and override OnStartClient. An adopted version could look like

 // Will be synched to the clients
 // In our case we know the parent but need the reference to the GunObject
 [SyncVar] public NetworkInstanceId playerGunNetId;

[Command]
public void CmdGetGun()
{
    Debug.Log("SPAWNING A GUN");
    playerGun = (GameObject)Instantiate(gunPrefab, gunLocalPositionOffset, gunLocalRotationOffset, transform);

    // Set the playerGunNetId on the Server
    // SyncVar will set on all clients including
    // newly connected clients
    playerGunNetId = playerGun.GetComponent<NetworkIdentity>().netId;

    NetworkServer.Spawn(playerGun);

    RpcSetGunOnClients(playerGun);
}

public override void OnStartClient()
{
    // When we are spawned on the client,
    // find the gun object using its ID,
    // and set it to be our child
    GameObject gunObject = ClientScene.FindLocalObject(playerGunNetId);
    gunObject.transform.SetParent(transform, false);
    gunObject.transform.localPosition = gunLocalPositionOffset;
    gunObject.transform.localrotation = gunLocalRotationOffset;
}

(Note you still need the ClientRpc for the already connected clients.)


Update2

The method from Update1 might not work because playetGunNetId might not be set yet when OnStartPlayer is executed.

To overcome this you can use a hook method for our SyncVar. Something like

// The null reference might somehow still come from
// ClientScene.FindLocalObject
[SyncVar(hook = "OnGunIdChanged"]
private NetworkInstanceId playerGunNetId;

// This method is executed on all clients when the playerGunNetId value changes. 
// Works when clients are already connected and also for new connections
private void OnGunIdChanged (NetworkInstanceId newValue)
{
    // Honestly I'm never sure if this is needed or not...
    // but in worst case it's just redundant
    playerGunNetId = newValue;

    GameObject gunObject = ClientScene.FindLocalObject(playerGunNetId);   
    gunObject.transform.SetParent(transform, false);
    gunObject.transform.localPosition = gunLocalPositionOffset;
    gunObject.transform.localrotation = gunLocalRotationOffset;
}

Now you shouldn't even need the ClientRpc anymore since everything is handled by the hook for both, already connected and newly connected clients.

查看更多
登录 后发表回答