Reputation: 349
Ok this is getting frustrating for me because I've never done this kind of thing before and I need a solution.
So my game is 2D and involves you going against another player to collect as many resources in the map as possible in the time limit. The world is generated by instantiating a sprite at every tile in the map with a random block and the player digs their way through and gets points depending on which resources they mine. The blocks are also saved to a public 2D array.
I have a script called LevelGenerator which runs when the player starts a game. In the script, the blocks are chosen at random, then spawned through instantiate and then by NetworkServer.Spawn. The whole generation is called as a [Command]
The gameobject that holds the levelgenerator script has a network identity with neither server only or local player authority.
Within the player script, I have some code that casts a raycast from the player to the mouse position, and it works fine, however the destroying of the gameobject is broken. When the player presses left mouse and the raycast is hitting a block, I destroy the hit object, add the same amount of points as the block's points value in the 2D array of blocks, and then set that block in the array to null. When the host breaks a block, the client can see that happening but when the client breaks a block, the client can obviously see it breaking but the host cannot see changes from that client. This may not be related, but an error is thrown saying that the array of blocks is null, even though I am directly finding the levelgenerator through findobjectoftype. This does not happen on the host however. The maps are the same so networkserver.spawn is working, but I am having an issue with syncing the blocks and whether or not they are broken or not.
The blocks are prefabs, and all have a network identity with neither box checked, and a network transform with the network send rate of 0 as they are not moving from when they are spawned.
So yeah I can't figure this out. Any help is appreciated. Thanks.
LevelGenerator:
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Networking;
public class LevelGenerator : NetworkBehaviour
{
[SerializeField] private List<Block> blocks; // Blocks are pre-defined in the Unity Editor.
[SerializeField] private int mapWidth; // How wide the map is.
[SerializeField] private int mapHeight; // How deep the map is.
public const int blockSize = 1;
public Block[,] blockMatrix; // 2D array of all blocks.
private List<Block> resourceBlocks = new List<Block>();
private List<GameObject> blockObjects = new List<GameObject>();
void Start()
{
CmdSpawnMap();
}
[Command]
public void CmdSpawnMap()
{
blockMatrix = new Block[mapWidth, mapHeight];
resourceBlocks = blocks.Where(z => z.blockType == "Resource").ToList(); // List of all the blocks that are tagged as resources.
resourceBlocks = resourceBlocks.OrderBy(z => z.rarity).ToList();
for (int r = 0; r < resourceBlocks.Count; r++)
{
resourceBlocks[r].spawnPercentagesAtLevels = new float[mapHeight];
for (int s = 1; s < resourceBlocks[r].spawnPercentagesAtLevels.Length; s++)
{
resourceBlocks[r].spawnPercentagesAtLevels[s] = resourceBlocks[r].basePercentage * Mathf.Pow(resourceBlocks[r].percentageMultiplier, resourceBlocks[r].spawnPercentagesAtLevels.Length - s);
}
}
// For every block in the map.
System.Random random = new System.Random();
for (int y = 0; y < mapHeight; y++)
{
for (int x = 0; x < mapWidth; x++)
{
if (y == mapHeight - 1)
{
Block grass = blocks.Where(z => z.blockName == "Grass").FirstOrDefault();
blockMatrix[x, y] = grass;
GameObject blockInstance = Instantiate(grass.blockPrefabs[random.Next(0, grass.blockPrefabs.Count - 1)]);
blockInstance.transform.position = new Vector3(x * blockSize, y * blockSize, 0);
blockInstance.transform.SetParent(transform, false);
blockInstance.name = blockMatrix[x, y].blockName + " => " + x + ", " + y;
blockObjects.Add(blockInstance);
NetworkServer.Spawn(blockInstance);
continue;
}
bool resourceSpawned = false;
for (int i = resourceBlocks.Count - 1; i >= 0; i--)
{
float roll = Random.Range(0.0f, 100.0f);
if (roll <= resourceBlocks[i].spawnPercentagesAtLevels[y])
{
blockMatrix[x, y] = resourceBlocks[i];
GameObject blockInstance = Instantiate(resourceBlocks[i].blockPrefabs[random.Next(0, resourceBlocks[i].blockPrefabs.Count - 1)]);
blockInstance.transform.position = new Vector3(x * blockSize, y * blockSize, 0);
blockInstance.transform.SetParent(transform, false);
blockInstance.name = blockMatrix[x, y].blockName + " => " + x + ", " + y;
blockObjects.Add(blockInstance);
resourceSpawned = true;
NetworkServer.Spawn(blockInstance);
break;
}
}
if (!resourceSpawned)
{
Block ground = blocks.Where(z => z.blockName == "Ground").FirstOrDefault();
blockMatrix[x, y] = ground;
GameObject blockInstance = Instantiate(ground.blockPrefabs[random.Next(0, ground.blockPrefabs.Count - 1)]);
blockInstance.transform.position = new Vector3(x * blockSize, y * blockSize, 0);
blockInstance.transform.SetParent(transform, false);
blockInstance.name = blockMatrix[x, y].blockName + " => " + x + ", " + y;
blockObjects.Add(blockInstance);
NetworkServer.Spawn(blockInstance);
}
resourceSpawned = false;
}
}
}
}
Player:
using UnityEngine;
using UnityEngine.Networking;
[RequireComponent(typeof(Rigidbody2D))]
public class Player : NetworkBehaviour
{
private Rigidbody2D rb;
[Header("Player Movement Settings")]
[SerializeField] private float moveSpeed;
private bool facingRight;
[HideInInspector] public int points;
void Start()
{
rb = GetComponent<Rigidbody2D>();
facingRight = true; // Start facing right
}
void FixedUpdate()
{
float horizontalSpeed = Input.GetAxis("Horizontal") * moveSpeed;
float jumpSpeed = 0;
if (horizontalSpeed > 0 && !facingRight || horizontalSpeed < 0 && facingRight) // If you were moving left and now have a right velocity
{ // or were moving right and now have a left velocity,
facingRight = !facingRight; // change your direction
Vector3 s = transform.localScale;
transform.localScale = new Vector3(s.x * -1, s.y, s.z); // Flip the player when the direction has been switched
}
rb.velocity = new Vector2(horizontalSpeed, jumpSpeed);
}
void Update()
{
GameManager gm = FindObjectOfType<GameManager>();
LevelGenerator levelGen = FindObjectOfType<LevelGenerator>();
if (gm.playing)
{
Camera playerCam = GetComponentInChildren<Camera>();
Vector3 direction = playerCam.ScreenToWorldPoint(Input.mousePosition) - transform.position;
direction.z = 0;
RaycastHit2D hit = Physics2D.Raycast(transform.position, direction, 1);
Debug.DrawRay(transform.position, direction, Color.red);
if (hit)
{
if (Input.GetButtonDown("BreakBlock"))
{
Destroy(hit.transform.gameObject);
points += levelGen.blockMatrix[Mathf.FloorToInt(hit.transform.position.x), Mathf.FloorToInt(hit.transform.position.y)].blockPointsValue;
levelGen.blockMatrix[Mathf.FloorToInt(hit.transform.position.x), Mathf.FloorToInt(hit.transform.position.y)] = null;
}
}
}
}
}
Upvotes: 2
Views: 3293
Reputation: 198
Quite simply, you are never sending a network message from the client to tell the server to destroy the block. When you destroy the block on the server, it will automatically tell all of the clients to get rid of that block too (though you should be using NetworkServer.Destroy()
, Destroy()
is just nice enough to do this too). On the other hand, the client has no authority or way to tell other clients that a block has been broken.
What you will need to do is send a message from the client to the server to destroy the block, and then the server can tell all of the clients that the block should be destroyed.
This can be accomplished through a Command call from the client to the server. Instead of destroying the block, send a message from the client to the server with the block you want to destroy (as the block GameObject has a network identity you can pass GameObjects as arguments).
if (hit)
{
if (Input.GetButtonDown("BreakBlock"))
{
CmdBreakBlock(hit.transform.gameObject);
points += levelGen.blockMatrix[Mathf.FloorToInt(hit.transform.position.x), Mathf.FloorToInt(hit.transform.position.y)].blockPointsValue;
levelGen.blockMatrix[Mathf.FloorToInt(hit.transform.position.x), Mathf.FloorToInt(hit.transform.position.y)] = null;
}
}
And then add your Cmd function (which will be run on the server).
[Command]
void CmdBreakBlock(GameObject block)
{
NetworkServer.Destroy(block);
}
Right away though you should notice an issue. There is now a bit of latency between when a client wants to break a block and when it actually happens. If the client hits the block again before the server destroys it and sends a message back to the client we're going to have a lot of issues (the client will send another message to destroy the now null block).
So why not just destroy the block on the client and tell the server to destroy their version of it too? Well when the server destroys it and sends a message to that client, the client won't know what should be destroyed as they have already destroyed it.
Unfortunately using the high level API there isn't much of a clean solution to this that I know of. Your best bet would be to deactivate the object on the client that is trying to destroy it while it is actually destroyed on the server, but that's a little bit of a janky solution.
Unfortunately even then that's not enough to fix this problem. Like you mentioned, there's the problem of the blocks matrix being null for clients. That makes sense because the client's version of LevelGenerator
is never run, so let me explain why that is.
When you have an object with network identity but no authority, what will happen is that the Cmd call won't work for any of the clients. Without authority commands can only be sent from the server, so the only LevelGenerator
script that is running on the server.
This probably a good thing though as if all of the LevelGenerators for each client and server ran, all they would do is tell the server to run the map generation script on the server a bunch of times; still nothing would happen for your clients (except there would be a lot more block GameObjects spawned on top of eachother).
At this point I would say you have three options:
OnSerialize
and OnDeserialize
function which is not fun so I'm not sure I recommend it.The last one I do recommend, and you could maybe try it like this:
if (Input.GetButtonDown("BreakBlock")) // Ran on client
{
CmdBreakBlock(hit.transform.gameObject);
hit.transform.gameObject.SetActive(false); // Mimic that it was destroyed on the client so they don't try to mine it again.
}
//...
[Command]
void CmdBreakBlock(GameObject hit) // Ran on server, arguments from client
{
NetworkServer.Destroy(hit);
RpcAddPoints(levelGen.blockMatrix[Mathf.FloorToInt(hit.transform.position.x), Mathf.FloorToInt(hit.transform.position.y)].blockPointsValue);
levelGen.blockMatrix[Mathf.FloorToInt(hit.transform.position.x), Mathf.FloorToInt(hit.transform.position.y)] = null;
}
[ClientRpc]
void RpcAddPoints(int p) // Ran on client, arguments from server
{
points += p;
}
Sorry if I couldn't be much help, I'm more used to the Low Level API but if you have any questions I would be happy to try to answer them!
Upvotes: 2