Skip to content

消息传递系统 (ProtoCustomMessage) ​

ProtoCustomMessage 用于在客户端和服务器之间传递自定义格式的二进制数据。支持双向通信,提供广播、定向发送和条件过滤。

> 定位说明:ProtoCustomMessage 是框架三层跨端通信机制的底层,直接发送字节数组。 > - 更简单? → 使用 双端事件 (DualEndEvent) > - 需要类型安全? → 使用 TypedMessage<T>

核心 API ​

csharp
public readonly partial struct ProtoCustomMessage
{
    public byte[] Message { get; init; }

#if CLIENT
    public void Handle();                    // 接收时自动调用,触发 EventServerMessage
    public bool SendToServer();              // 发送到服务器
#elif SERVER
    public void Handle(Player player);       // 接收时自动调用,触发 EventClientMessage
    public int Broadcast(Func<Player, bool>? filter = null);  // 广播,返回接收人数
    public bool SendTo(Player player, Func<Player, bool>? filter = null);  // 定向发送
#endif
}

基本用法 ​

客户端发送消息到服务器 ​

csharp
#if CLIENT
var data = new { Action = "Move", X = 10.5f, Y = 20.3f };
var json = JsonSerializer.Serialize(data);
var message = new ProtoCustomMessage
{
    Message = Encoding.UTF8.GetBytes(json)
};

if (message.SendToServer())
{
    Game.Logger.LogInformation("消息发送成功");
}
#endif

服务器发送消息到客户端 ​

csharp
#if SERVER
var gameState = new { Wave = 5, TimeRemaining = 120 };
var message = new ProtoCustomMessage
{
    Message = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(gameState))
};

// 广播给所有玩家
int count = message.Broadcast();

// 发送给特定玩家
message.SendTo(targetPlayer);

// 条件广播
message.Broadcast(player => player.IsOnline);
#endif

事件系统集成 ​

客户端接收服务器消息 ​

csharp
#if CLIENT
public class ClientMessageReceiver : IGameClass
{
    public static void OnRegisterGameClass()
    {
        Game.OnGameTriggerInitialization += () =>
        {
            Trigger<EventServerMessage>.Subscribe(OnServerMessage);
        };
    }

    private static async Task<bool> OnServerMessage(object sender, EventServerMessage e)
    {
        try
        {
            var json = Encoding.UTF8.GetString(e.Message);
            var data = JsonSerializer.Deserialize<GameStateUpdate>(json);
            Game.Logger.LogInformation("收到服务器消息: {Type}", data?.Type);
            return true;
        }
        catch (Exception ex)
        {
            Game.Logger.LogError(ex, "处理服务器消息失败");
            return false;
        }
    }
}
#endif

服务器接收客户端消息 ​

csharp
#if SERVER
public class ServerMessageReceiver : IGameClass
{
    public static void OnRegisterGameClass()
    {
        Game.OnGameTriggerInitialization += () =>
        {
            Trigger<EventClientMessage>.Subscribe(OnClientMessage);
        };
    }

    private static async Task<bool> OnClientMessage(object sender, EventClientMessage e)
    {
        var player = e.Player;
        try
        {
            var json = Encoding.UTF8.GetString(e.Message);
            var action = JsonSerializer.Deserialize<PlayerAction>(json);
            Game.Logger.LogInformation("玩家 {PlayerId} 操作: {Action}", player.Id, action?.Action);
            return true;
        }
        catch (Exception ex)
        {
            Game.Logger.LogError(ex, "处理客户端消息失败");
            return false;
        }
    }
}
#endif

消息格式 ​

推荐的信封格式 ​

csharp
public class MessageEnvelope
{
    public string Type { get; set; } = string.Empty;
    public string Version { get; set; } = "1.0";
    public DateTime Timestamp { get; set; } = DateTime.UtcNow;
    public object Data { get; set; } = null!;
}

// 发送
public static void SendTypedMessage<T>(string messageType, T data) where T : class
{
    var envelope = new MessageEnvelope { Type = messageType, Data = data };
    var message = new ProtoCustomMessage
    {
        Message = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(envelope))
    };
    message.SendToServer();
}

// 接收时解析
private static MessageEnvelope? ParseMessage(byte[] messageBytes)
{
    try
    {
        var json = Encoding.UTF8.GetString(messageBytes);
        return JsonSerializer.Deserialize<MessageEnvelope>(json);
    }
    catch (Exception ex)
    {
        Game.Logger.LogError(ex, "消息解析失败");
        return null;
    }
}

序列化格式选择 ​

格式优点缺点适用场景
JSON易调试、跨平台体积大、性能较低开发阶段、低频消息
MessagePack高性能、体积小需额外依赖生产环境、高频消息
自定义二进制最优性能维护成本高性能关键场景

MessagePack 示例 ​

csharp
[MessagePackObject]
public class PlayerAction
{
    [Key(0)]
    public int PlayerId { get; set; }

    [Key(1)]
    public string Action { get; set; } = string.Empty;

    [Key(2)]
    public int Target { get; set; }
}

var bytes = MessagePackSerializer.Serialize(action);
var message = new ProtoCustomMessage { Message = bytes };

最佳实践 ​

消息大小控制 ​

csharp
private const int MAX_MESSAGE_SIZE = 64 * 1024; // 64KB

if (messageBytes.Length > MAX_MESSAGE_SIZE)
{
    Game.Logger.LogWarning("消息过大: {Size} bytes", messageBytes.Length);
    return;
}

重试机制 ​

csharp
public static async Task<bool> SendWithRetry(ProtoCustomMessage message, int maxRetries = 3)
{
    for (int i = 0; i < maxRetries; i++)
    {
        if (message.SendToServer())
        {
            return true;
        }
        Game.Logger.LogWarning("消息发送失败,重试 {Retry}/{Max}", i + 1, maxRetries);
        await Game.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
    }
    Game.Logger.LogError("消息发送失败,已达最大重试次数");
    return false;
}

消息频率控制 ​

csharp
public class MessageRateLimiter
{
    private readonly Dictionary<string, Queue<DateTime>> _messageTimes = new();
    private readonly TimeSpan _timeWindow = TimeSpan.FromSeconds(1);
    private readonly int _maxMessagesPerWindow = 10;

    public bool CanSendMessage(string messageType)
    {
        var now = DateTime.UtcNow;

        if (!_messageTimes.TryGetValue(messageType, out var times))
        {
            times = new Queue<DateTime>();
            _messageTimes[messageType] = times;
        }

        while (times.Count > 0 && now - times.Peek() > _timeWindow)
        {
            times.Dequeue();
        }

        if (times.Count >= _maxMessagesPerWindow)
        {
            return false;
        }

        times.Enqueue(now);
        return true;
    }
}

故障排除 ​

问题可能原因解决方案
SendToServer() 返回 false网络断开、消息过大检查网络状态和消息大小
事件处理器未触发未注册监听器、消息格式错误确认在 OnGameTriggerInitialization 中注册
内存增长未注销事件监听器使用 trigger.Unregister() 清理

相关文档 ​