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.
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);
}
}
}
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 ;)