客户端运行时[Command]特性方法CmdSetBlock未触发求助
核心问题分析
Command调用权限缺失
Mirror的[Command]方法仅允许**拥有网络权限(Authority)**的对象调用。如果BuildingsGrid挂载在场景对象而非玩家对象上,客户端没有该对象的操作权限,Command会直接被忽略。主机模式下本地兼具客户端和服务器身份,所以能正常执行,但纯客户端无法触发。输入判断位置错误
服务器无法获取客户端的输入状态(比如Input.GetMouseButton(0)),你把点击判断放在了Command方法内部,服务器永远检测不到客户端的点击操作,导致后续逻辑完全不执行。本地对象跨网络访问无效
followingBlock是客户端本地实例化的预览对象,服务器上不存在这个引用,在Command方法里访问followingBlock会直接返回null,触发不了后续的实例化逻辑。数组越界风险
IsPlaceTaken方法中worldPosition.x + x可能超出gridSize.x的范围,触发数组索引越界错误,直接中断代码执行,导致Command没被调用。
分步修复代码
1. 确保Command调用权限
把BuildingsGrid脚本挂载到带有NetworkIdentity组件且勾选Local Player Authority的玩家对象上,或者给挂载该脚本的对象添加NetworkIdentity并通过代码赋予客户端权限(推荐前者)。
2. 调整输入判断逻辑
将鼠标点击判断移到客户端Update中,仅在点击时调用Command,避免无效命令请求:
private void Update() { if (!isLocalPlayer) return; Vector2 mousePosition = mainCamera.ScreenToWorldPoint(Input.mousePosition); int x = Mathf.RoundToInt(mousePosition.x); int y = Mathf.RoundToInt(mousePosition.y); Vector2Int worldPosition = new Vector2Int(x, y); bool availeable = true; if (worldPosition.x < 0 || worldPosition.x >= gridSize.x) availeable = false; if (worldPosition.y < 0 || worldPosition.y >= gridSize.y) availeable = false; if (availeable && IsPlaceTaken(worldPosition)) availeable = false; if (availeable && followingBlock != null && money < followingBlock.GetComponent<Building>().Cost) availeable = false; SpawnFollowBlock(worldPosition); if (followingBlock != null) { Debug.Log("FollowingBlock != null"); ChooseBlock(); // 仅在左键按下时调用放置命令 if (Input.GetMouseButtonDown(0) && availeable) { CmdSetBlock(worldPosition, chooseCounter); // 客户端本地销毁预览对象 Destroy(followingBlock); followingBlock = null; } // 仅在右键按下时调用销毁命令 if (Input.GetMouseButtonDown(1)) { CmdBlocksDestroyer(worldPosition); } followingBlock.transform.position = new Vector3(worldPosition.x, worldPosition.y); } }
3. 修改Command方法逻辑
移除对本地followingBlock的依赖,通过参数传递方块类型,在服务器端完成实例化与同步:
[Command] private void CmdSetBlock(Vector2Int mousePosition, int blockType) { Debug.Log("SetBlock"); // 服务器端二次校验位置合法性(防止客户端作弊) if (mousePosition.x < 0 || mousePosition.x >= gridSize.x || mousePosition.y < 0 || mousePosition.y >= gridSize.y) return; if (IsPlaceTaken(mousePosition)) return; GameObject prefabToSpawn = blockType == 1 ? ground : brickWall; Building buildingData = prefabToSpawn.GetComponent<Building>(); // 服务器端校验金钱(防止客户端作弊) if (money < buildingData.Cost) return; // 服务器实例化并同步到所有客户端 GameObject settedBlock = Instantiate(prefabToSpawn, new Vector3(mousePosition.x, mousePosition.y), Quaternion.identity); NetworkServer.Spawn(settedBlock); blocks[mousePosition.x, mousePosition.y] = settedBlock; money -= buildingData.Cost; }
4. 修复数组越界问题
修改IsPlaceTaken方法,先校验整体范围是否超出网格:
private bool IsPlaceTaken(Vector2Int worldPosition) { int size = objectSizes[0, 0]; // 先判断整体放置范围是否超出网格边界 if (worldPosition.x + size > gridSize.x || worldPosition.y + size > gridSize.y) return true; for (int x = 0; x < size; x++) { for (int y = 0; y < size; y++) { if (blocks[worldPosition.x + x, worldPosition.y + y] != null) return true; } } return false; }
5. 同步金钱变量
给money添加[SyncVar]特性,确保客户端与服务器的金钱状态一致:
[SyncVar] [SerializeField] private int money;
6. 修正销毁Command的逻辑
移除对客户端输入的依赖,在服务器端完成校验与操作:
[Command] private void CmdBlocksDestroyer(Vector2Int worldPosition) { if (worldPosition.x < 0 || worldPosition.x >= gridSize.x || worldPosition.y < 0 || worldPosition.y >= gridSize.y) return; if (blocks[worldPosition.x, worldPosition.y] != null) { Building buildingData = blocks[worldPosition.x, worldPosition.y].GetComponent<Building>(); NetworkServer.Destroy(blocks[worldPosition.x, worldPosition.y].gameObject); money += buildingData.Cost; blocks[worldPosition.x, worldPosition.y] = null; } }
额外注意事项
- 确保
ground和brickWall预制体都添加了NetworkIdentity组件,否则NetworkServer.Spawn无法将它们同步到客户端。 - 客户端本地的
followingBlock仅用于放置预览,不需要同步到服务器,销毁操作在客户端本地处理即可。
内容的提问来源于stack exchange,提问作者Никита Нитоша

