You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

客户端运行时[Command]特性方法CmdSetBlock未触发求助

问题排查与修复方案

核心问题分析

  1. Command调用权限缺失
    Mirror的[Command]方法仅允许**拥有网络权限(Authority)**的对象调用。如果BuildingsGrid挂载在场景对象而非玩家对象上,客户端没有该对象的操作权限,Command会直接被忽略。主机模式下本地兼具客户端和服务器身份,所以能正常执行,但纯客户端无法触发。

  2. 输入判断位置错误
    服务器无法获取客户端的输入状态(比如Input.GetMouseButton(0)),你把点击判断放在了Command方法内部,服务器永远检测不到客户端的点击操作,导致后续逻辑完全不执行。

  3. 本地对象跨网络访问无效
    followingBlock是客户端本地实例化的预览对象,服务器上不存在这个引用,在Command方法里访问followingBlock会直接返回null,触发不了后续的实例化逻辑。

  4. 数组越界风险
    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,提问作者Никита Нитоша

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 18:45:59