好的,这对我来说很令人沮丧,因为我之前从未做过这种事情,我需要一个解决方案。
因此,我的游戏是2D游戏,让您与其他玩家对抗,以便在时间限制内尽可能多地收集地图中的资源。通过使用随机块在地图中的每个图块实例化一个精灵来生成世界,并且玩家挖掘他们的路径并根据他们挖掘的资源获得积分。这些块也会保存到公共2D阵列中。
我有一个名为LevelGenerator的脚本,它在玩家开始游戏时运行。在脚本中,随机选择块,然后通过实例化然后通过NetworkServer.Spawn生成。整个一代被称为[命令]
持有levelgenerator脚本的游戏对象具有既不支持服务器也不支持本地玩家权限的网络身份。
在播放器脚本中,我有一些代码可以将播放器中的光线投射到鼠标位置,并且它可以正常工作,但是游戏对象的破坏会被破坏。当玩家按下鼠标左键并且光线投射击中一个块时,我会破坏命中对象,在块的2D数组中添加与块的点值相同的点数,然后将数组中的该块设置为null。当主机中断一个块时,客户端可以看到发生了这种情况,但是当客户端中断一个块时,客户端显然可以看到它中断但主机无法看到来自该客户端的更改。这可能没有关系,但是抛出一个错误,说块的数组为空,即使我通过findobjectoftype直接找到了levelgenerator。但是,这不会发生在主机上。这些地图是相同的,所以networkserver.spawn正在工作,但是我遇到了同步块以及它们是否坏了的问题。
块是预制件,并且都具有网络标识,既没有选中框,也没有网络转换,网络发送速率为0,因为它们在生成时没有移动。
所以是的,我无法弄清楚这一点。任何帮助表示赞赏。感谢。
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;
}
}
}
}
播放器:
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;
}
}
}
}
}
答案 0 :(得分:1)
很简单,您永远不会从客户端发送网络消息来告诉服务器销毁该块。当您销毁服务器上的块时,它会自动告诉所有客户端也摆脱该块(尽管您应该使用NetworkServer.Destroy()
,Destroy()
也足够好了) 。另一方面,客户端没有权限或方式告诉其他客户块已被破坏。
您需要做的是从客户端向服务器发送消息以销毁该块,然后服务器可以告诉所有客户端该块应该被销毁。
这可以通过Command call从客户端到服务器来完成。不是销毁块,而是使用要销毁的块从客户端向服务器发送消息(因为块GameObject具有网络标识,您可以将GameObjects作为参数传递)。
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;
}
}
然后添加您的Cmd函数(将在服务器上运行)。
[Command]
void CmdBreakBlock(GameObject block)
{
NetworkServer.Destroy(block);
}
马上你应该注意到一个问题。从客户端想要打破一个块到它实际发生的时间之间有一点延迟。如果客户端在服务器销毁之前再次访问该块并将消息发送回客户端,那么我们将遇到很多问题(客户端将发送另一条消息来销毁现在的空块)。
那么为什么不直接破坏客户端上的块并告诉服务器也要破坏它们的版本呢?当服务器销毁它并向该客户端发送消息时,客户端不知道应该销毁什么,因为它们已经销毁了它。
不幸的是,使用高级API,我知道并没有很多干净的解决方案。你最好的办法就是在服务器上实际销毁时试图破坏它的客户端上的对象停用,但这有点像一个简洁的解决方案。
不幸的是,即便如此,这还不足以解决这个问题。就像你提到的那样,客户端的块矩阵为空的问题。这是有道理的,因为客户端版本的LevelGenerator
永远不会运行,所以让我解释一下原因。
如果您拥有一个具有网络身份但没有权限的对象,那么Cmd呼叫将无法为任何客户端工作。没有权限,命令只能从服务器发送,因此服务器上运行的唯一LevelGenerator
脚本。
这可能是一件好事,但好像每个客户端和服务器的所有LevelGenerator都运行一样,他们所要做的就是告诉服务器多次在服务器上运行地图生成脚本;仍然没有任何事情会发生在您的客户身上(除了会有更多的块游戏对象在彼此之上产生)。
此时我会说你有三种选择:
OnSerialize
和OnDeserialize
功能,这个功能并不好玩,所以我不确定是否推荐它。我推荐的最后一个,你可以尝试这样:
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;
}
很抱歉,如果我没有多大帮助,我会更习惯低级API,但如果您有任何疑问,我会很乐意尝试回答它们!