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

Unity Netcode for GameObjects:RPC命令模式实现求助

Unity Netcode for GameObjects 命令模式RPC传输解决方案

方案一:基于INetworkSerializable的类型标识传输

问题根源

抽象类Command无法被实例化,Netcode反序列化时会尝试创建抽象类实例,从而抛出MissingMethodException。解决思路是让每个命令子类单独实现序列化逻辑,同时传输类型标识区分不同命令。

实现步骤

  1. 修改抽象Command类,定义类型标识接口,无需继承INetworkSerializable;
  2. 每个命令子类实现INetworkSerializable,并提供公共无参构造函数(Netcode反序列化必需);
  3. 发送RPC时先传输命令类型标识,再传输序列化后的命令数据;服务端根据标识创建对应子类实例并反序列化数据。

代码示例

抽象Command类

public abstract class Command
{
    // 每个命令子类对应唯一类型ID
    public abstract string CommandTypeId { get; }
    public abstract void Execute();
}

具体命令类

public class ConcreteCommand : Command, INetworkSerializable
{
    // Netcode支持的可序列化字段
    public int TargetId;
    public string ActionName;

    // 必须提供公共无参构造函数
    public ConcreteCommand() { }

    // 业务构造函数,用于客户端创建命令实例
    public ConcreteCommand(int targetId, string actionName)
    {
        TargetId = targetId;
        ActionName = actionName;
    }

    public override string CommandTypeId => "ConcreteCommand";

    public override void Execute()
    {
        Debug.Log($"执行ConcreteCommand:目标ID {TargetId},动作 {ActionName}");
    }

    // 实现Netcode序列化逻辑
    public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    {
        serializer.SerializeValue(ref TargetId);
        serializer.SerializeValue(ref ActionName);
    }
}

RPC处理逻辑(NetworkBehaviour子类中)

public class CommandHandler : NetworkBehaviour
{
    // 注册命令工厂,服务端和客户端需同步注册
    private readonly Dictionary<string, Func<Command>> _commandFactories = new();

    private void Awake()
    {
        _commandFactories.Add("ConcreteCommand", () => new ConcreteCommand());
        // 可添加更多命令类型...
    }

    // 客户端调用此方法发送命令到服务端
    public void SendCommandToServer(Command command)
    {
        if (!IsClient) return;
        SendCommandRpc(command.CommandTypeId, SerializeCommand(command));
    }

    // 将命令序列化为字节数组
    private byte[] SerializeCommand(Command command)
    {
        if (command is not INetworkSerializable serializableCmd)
            throw new ArgumentException("命令必须实现INetworkSerializable接口");
        
        using var writer = new NetworkWriter();
        serializableCmd.NetworkSerialize(writer);
        return writer.ToArray();
    }

    // 将字节数组反序列化为命令实例
    private Command DeserializeCommand(string commandTypeId, byte[] data)
    {
        if (!_commandFactories.TryGetValue(commandTypeId, out var factory))
            throw new KeyNotFoundException($"未知命令类型:{commandTypeId}");
        
        var command = factory();
        if (command is not INetworkSerializable serializableCmd)
            throw new InvalidOperationException("命令未实现INetworkSerializable接口");
        
        using var reader = new NetworkReader(data);
        serializableCmd.NetworkSerialize(reader);
        return command;
    }

    // 客户端调用、服务端接收的RPC方法
    [Rpc(SendTo.Server)]
    private void SendCommandRpc(string commandTypeId, byte[] commandData, RpcParams rpcParams = default)
    {
        if (!IsServer) return;
        var command = DeserializeCommand(commandTypeId, commandData);
        command.Execute();
    }
}

方案二:反射+参数序列化实现

适用场景

不想为每个命令编写序列化代码的场景,核心是传输命令完整类型名和序列化后的参数数据,服务端通过反射重建实例并执行。

实现步骤

  1. 确保所有命令类提供公共无参构造函数;
  2. 客户端发送RPC时,传输命令的完整类型名和序列化后的字段数据(如JSON格式);
  3. 服务端根据类型名反射创建实例,反序列化数据并调用Execute方法。

代码示例

命令类结构

public abstract class Command
{
    public abstract void Execute();
}

public class ConcreteCommand : Command
{
    public int TargetId;
    public string ActionName;

    // 必需的无参构造函数
    public ConcreteCommand() { }

    // 业务构造函数
    public ConcreteCommand(int targetId, string actionName)
    {
        TargetId = targetId;
        ActionName = actionName;
    }

    public override void Execute()
    {
        Debug.Log($"执行ConcreteCommand:目标ID {TargetId},动作 {ActionName}");
    }
}

RPC处理逻辑

public class CommandHandler : NetworkBehaviour
{
    // 客户端调用发送命令
    public void SendCommandToServer(Command command)
    {
        if (!IsClient) return;
        // 获取命令的完整类型名(包含命名空间)
        string typeName = command.GetType().AssemblyQualifiedName;
        // 用JsonUtility序列化命令字段
        string jsonData = JsonUtility.ToJson(command);
        SendCommandRpc(typeName, jsonData);
    }

    [Rpc(SendTo.Server)]
    private void SendCommandRpc(string typeName, string jsonData, RpcParams rpcParams = default)
    {
        if (!IsServer) return;
        // 根据类型名获取Type
        Type commandType = Type.GetType(typeName);
        if (commandType == null)
        {
            Debug.LogError($"未知命令类型:{typeName}");
            return;
        }
        // 反射创建命令实例
        Command command = Activator.CreateInstance(commandType) as Command;
        if (command == null)
        {
            Debug.LogError($"无法创建命令实例:{typeName}");
            return;
        }
        // 反序列化JSON数据到实例
        JsonUtility.FromJsonOverwrite(jsonData, command);
        // 执行命令
        command.Execute();
    }
}

注意事项

  • 方案一中,类型ID可改用枚举替代字符串,减少拼写错误并提升效率;
  • 方案二中,JsonUtility仅支持public字段或带[SerializeField]的私有字段,复杂类型(如Dictionary)需改用Newtonsoft.Json(需导入Unity官方包)或自定义Netcode序列化;
  • 服务端与客户端的命令类结构必须完全一致(命名空间、字段名、类型),否则反序列化会失败;
  • 所有命令类必须提供无参构造函数,Netcode反序列化和反射创建实例均依赖此构造函数。

内容的提问来源于stack exchange,提问作者Page

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 15:27:46