unity-game-enginetransformmultiplayerraycastingunity3d-unet

Unity network transform problem - Client spawning objects at wrong location


I'm working on a Fortnite-esque game for Unity.

Player spawns in, has ability to spawn cubes to make a "base".

Everything works perfectly well in mono but I'm having a strange issue with networking. On the server, my player can spawn the cubes perfectly according to the Raycast hitpoint while on the client, even though the player is a clone of the Player prefab, the spawned objects always either end up at the world origin, rather than Raycast hitpoint -or- if I remove if (!isPlayLocal) {return;} from the player's script containing Raycast info, the cube spawns inaccurately and without its corresponding material.

I'll try and pinpoint the code so I can place it here but I imagine it could be a number of things.

Local Player Auth is checked off on spawn prefabs and all prefabs have been registered in Network Manager.

enter image description here

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

public class BuildingSystemNew : NetworkBehaviour
{

    [SerializeField] private Camera playerCamera; 
    [SerializeField] private GameObject blockTemplatePrefab; 
    [SerializeField] private GameObject blockPrefab; 
    [SerializeField] private Material templateMaterial;  
    [SerializeField] private LayerMask buildableSurfacesLayer;

    private bool buildModeOn = false;
    private bool canBuild = false;
    private bool crossHairOn = false;
    private BlockSystem bSys;
    public Texture2D crosshairImage;
    private int blockSelectCounter = 0;
    private GameObject weapon;
    private Vector3 buildPos;
    private GameObject currentTemplateBlock;  

    private void Start()
    {
        bSys = GetComponent<BlockSystem>();
    }

    private void Update()
    {
        if (isLocalPlayer == false)
            return;  

        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;

        if (Input.GetKeyDown("e"))
        {
            buildModeOn = !buildModeOn;

            if (buildModeOn)
            {
                // weapon.SetActive(false);
                crossHairOn = true;
            }
            else
            {
                // weapon.SetActive(true);
                crossHairOn = false;
            }
        }

        if (Input.GetKeyDown("r"))
        {
            blockSelectCounter++;
            if (blockSelectCounter >= bSys.allBlocks.Count) blockSelectCounter = 0;
        }

        if (buildModeOn)
        {
            RaycastHit buildPosHit;

            if (Physics.Raycast(playerCamera.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2, 0)), out buildPosHit, 10, buildableSurfacesLayer))
            {
                Vector3 point = buildPosHit.point;
                buildPos = new Vector3(Mathf.Round(point.x), Mathf.Round(point.y), Mathf.Round(point.z));
                canBuild = true;
            }
            else
            {
                Destroy(currentTemplateBlock.gameObject);
                canBuild = false;
            }
        }

        if (!buildModeOn && currentTemplateBlock != null)
        {
            Destroy(currentTemplateBlock.gameObject);
            canBuild = false;
        }

        if (canBuild && currentTemplateBlock == null)
        {
            currentTemplateBlock = Instantiate(blockTemplatePrefab, buildPos, Quaternion.identity);
            currentTemplateBlock.GetComponent<MeshRenderer>().material = templateMaterial;
        }

        if (canBuild && currentTemplateBlock != null)
        {
            currentTemplateBlock.transform.position = buildPos;

            if (Input.GetMouseButtonDown(0))
            {
                CmdPlaceBlock();
            }
            else if (Input.GetMouseButtonDown(1))
            {
                CmdDestroyBlock();
            }
        }
    }

    [Command]
    public void CmdPlaceBlock()
    {
        GameObject newBlock = Instantiate(blockPrefab, buildPos, Quaternion.identity);
        Block tempBlock = bSys.allBlocks[blockSelectCounter];
        newBlock.name = tempBlock.blockName + "-Block";
        newBlock.GetComponent<MeshRenderer>().material = tempBlock.blockMaterial;
        NetworkServer.SpawnWithClientAuthority(newBlock, connectionToClient);  
    }

    [Command]
    private void CmdDestroyBlock()
    {
        RaycastHit hit;
        Ray ray = playerCamera.ScreenPointToRay(Input.mousePosition);

        if (Physics.Raycast(ray, out hit)) 
        {
            var objectHit = hit.collider.gameObject;

            if (hit.collider.gameObject.tag == "Block")
            {
                Destroy(objectHit);
            }
        }
    }

    void OnGUI()
    {
        if (crossHairOn == true) 
        {
            float xMin = (Screen.width / 2) - (crosshairImage.width / 2);
            float yMin = (Screen.height / 2) - (crosshairImage.height / 2);
            GUI.DrawTexture(new Rect(xMin, yMin, crosshairImage.width, crosshairImage.height), crosshairImage);
        }
    }
}

Solution

  • The Problem

    You are calling

    [Command]
    public void CmdPlaceBlock()
    {
        GameObject newBlock = Instantiate(blockPrefab, buildPos, Quaternion.identity);
        Block tempBlock = bSys.allBlocks[blockSelectCounter];
        newBlock.name = tempBlock.blockName + "-Block";
        newBlock.GetComponent<MeshRenderer>().material = tempBlock.blockMaterial;
        NetworkServer.SpawnWithClientAuthority(newBlock, connectionToClient);
    }
    

    without a parameter.

    A Command is called on the client but executed on the Server

    => with the local variables of the Server!

    So e.g. buildPos will always have the default value 0,0,0 on the server since due to

    if(!isLocalPlayer) return;
    

    later the line

    buildPos = new Vector3(Mathf.Round(point.x), Mathf.Round(point.y), Mathf.Round(point.z));
    

    is never executed on the server. The same also applies e.g. to blockSelectCounter and probably other values your CmdPlaceBlock depends on.


    Solution

    You should pass your client's buildPos value (and also all other values that are different on client and server) to the server command so the server knows at which correct position the new object should be placed:

    //...
        if (Input.GetMouseButtonDown(0))
        {
            CmdPlaceBlock(buildPos);
        }
    //...
    
    
    
    [Command]
    public void CmdPlaceBlock(Vector3 spawnPosition)
    {
    
        GameObject newBlock = Instantiate(blockPrefab, spawnPosition, Quaternion.identity);
        Block tempBlock = bSys.allBlocks[blockSelectCounter];
        newBlock.name = tempBlock.blockName + "-Block";
        newBlock.GetComponent<MeshRenderer>().material = tempBlock.blockMaterial;
        NetworkServer.SpawnWithClientAuthority(newBlock, connectionToClient);
    
    }
    

    I added only an example for the position knowing it will always be different on client and server. But it also might apply to your other values like e.g. blockSelectCounter.

    Do this change for all values that have to be the ones of the client and not the ones of the server.

    Note that the types that can be passed between networking methods are limited! You can't pass e.g. any component references.

    The allowed argument types are;

    • Basic type (byte, int, float, string, UInt64, etc)
    • Built-in Unity math type (Vector3, Quaternion, etc),
    • Arrays of basic types
    • Structs containing allowable types
    • NetworkIdentity
    • NetworkInstanceId
    • NetworkHash128
    • GameObject with a NetworkIdentity component attached.

    Additional Hints

    For readability and amount of lines you should change things like e.g.

    if (buildModeOn)
    {
        // weapon.SetActive(false);
        crossHairOn = true;
    }
    else
    {
        // weapon.SetActive(true);
        crossHairOn = false;
    }
    

    to simply

    // weapon.SetActive(!buildModeOn);
    crossHairOn = buildModeOn;
    

    and check bools not like

    if (isLocalPlayer == false)
    

    but rather

    if(!isLocalPlayer)
    

    It simply reads/writes easier ;)