Skip to content

双端事件系统 (DualEndEvent) ​

概述 ​

WasiCore 框架提供了双端事件系统(DualEndEvent),这是一种简化的跨端事件通信机制。它基于触发器模式,让事件可以在客户端和服务端之间自动转发,开发者无需手动处理消息序列化、网络传输等底层细节。

双端事件是三层客户端-服务端通信机制中最简单的一层,适合入门用户和图形化触发编辑器使用。

> 玩家文本不会自动过滤:DualEndEvent 只负责序列化和传输。开发聊天、昵称、留言等功能时,应把客户端提交与服务端确认分开,在服务端调用 Game.FilterSensitiveWords 后再向客户端发布过滤后的文本。详见敏感词过滤系统。

三层通信机制定位对比 ​

WasiCore 框架提供三种客户端-服务端通信机制,面向不同层次的用户需求:

> 注意:跨服务器频道系统(ServerChannelSystem)是服务器间通信(Server ↔ Server),不属于此处的三层体系。如果你需要在同一局游戏内的客户端和服务端之间通信,请使用以下三者之一。

特性DualEndEvent(双端事件)TypedMessage<T>ProtoCustomMessage
定位初级 - 触发器模式中级 - 结构化消息高级 - 原始二进制
适用用户初学者、图形化编辑器用户中级代码开发者需要极致性能/定制化的高级用户
编程模式事件定义 + Publish/Subscribe消息类 + Handler注册手动序列化 + 事件监听
图形化支持可在触发编辑器中直接构建需要代码需要代码
序列化自动(内部使用 JSON)自动(JSON)手动(任意格式)
类型安全编译时检查编译时检查无(运行时检查)
学习成本低中高
灵活性一般高最高
性能一般(内部基于 TypedMessage)中等(JSON 开销)最高(自定义二进制)
使用场景游戏逻辑事件、简单交互复杂数据交换、状态同步高频位置同步、自定义协议

如何选择? ​

┌─ 是否使用图形化触发编辑器?
│   └─ 是 → DualEndEvent
│
├─ 是否需要发送结构化数据、类型安全的消息?
│   └─ 是 → TypedMessage<T>
│
├─ 是否需要极致性能或自定义二进制协议?
│   └─ 是 → ProtoCustomMessage
│
└─ 不确定 → 从 DualEndEvent 开始,需求增长后再升级

核心概念 ​

工作原理 ​

双端事件系统将「触发器事件」的概念扩展到跨端通信。事件在一端发布后,会自动序列化、通过网络传输、在对端反序列化并触发注册的触发器:

客户端                                    服务端
┌──────────────┐                    ┌──────────────┐
│ Publish(evt) │ ──── 网络 ────→   │ 自动触发      │
│              │                    │ Player 触发器 │
│              │                    │ Game 触发器   │
└──────────────┘                    └──────────────┘

服务端                                    客户端
┌──────────────┐                    ┌──────────────┐
│ Publish(evt) │ ──── 网络 ────→   │ 自动触发      │
│              │                    │ Game 触发器   │
└──────────────┘                    └──────────────┘

核心组件 ​

  • IDualEndEvent<TSelf> — 双端事件标记接口,所有双端事件必须实现
  • DualEndEvent — 静态发布器类,提供 Publish / PublishAsync 方法
  • Trigger<TEvent> — 标准触发器,用于监听双端事件

基本用法 ​

1. 定义双端事件 ​

csharp
// 双端事件必须实现 IDualEndEvent<TSelf>
// Sender 属性由框架自动填充(客户端发送时,服务端会自动设置为发送者玩家)
public class PlayerChatEvent : IDualEndEvent<PlayerChatEvent>
{
    public Player? Sender { get; set; }
    public string Message { get; set; } = "";
    public string Channel { get; set; } = "global";
}

public class GameNotifyEvent : IDualEndEvent<GameNotifyEvent>
{
    public Player? Sender { get; set; }
    public string Title { get; set; } = "";
    public string Content { get; set; } = "";
    public int Priority { get; set; }
}

跨端传递的事件数据必须写成公开 get; set; 属性。双端事件内部使用框架默认 JSON 配置,普通字段不会参与序列化;图形化触发编辑器本身只会构造属性,纯代码定义事件时尤其要注意这一点。

2. 发布事件 ​

csharp
// 客户端和服务端使用完全相同的代码发布事件
DualEndEvent.Publish(new PlayerChatEvent
{
    Message = "你好!",
    Channel = "global"
});

发布行为根据端不同自动处理:

  • 客户端调用:事件发送到服务端,服务端自动设置 Sender 为发送者玩家
  • 服务端调用:事件广播到所有客户端

3. 注册触发器监听事件 ​

csharp
public class ChatSystem : IGameClass
{
    public static void OnRegisterGameClass()
    {
        Game.OnGameTriggerInitialization += OnGameTriggerInitialization;
    }

    private static void OnGameTriggerInitialization()
    {
        // 注册到 Game.Instance,监听所有双端聊天事件
        var chatTrigger = new Trigger<PlayerChatEvent>(OnChatEvent);
        chatTrigger.Register(Game.Instance);
    }

    private static async Task<bool> OnChatEvent(object sender, PlayerChatEvent e)
    {
#if SERVER
        // 服务端:验证并记录
        Game.Logger.LogInformation("玩家 {PlayerId} 发送聊天: {Message}",
            e.Sender?.Id, e.Message);
#endif

#if CLIENT
        // 客户端:显示聊天消息
        ShowChatUI(e.Sender?.Name ?? "系统", e.Message);
#endif
        return true;
    }
}

服务端专用 API ​

服务端除了基本的 Publish 广播外,还支持定向发送和条件过滤:

发送给指定玩家 ​

csharp
#if SERVER
// 向特定玩家发送私人通知
DualEndEvent.Publish(targetPlayer, new GameNotifyEvent
{
    Title = "恭喜",
    Content = "你获得了传说装备!",
    Priority = 1
});
#endif

条件过滤广播 ​

csharp
#if SERVER
// 只向 10 级以上的玩家广播
int count = DualEndEvent.Publish(
    new GameNotifyEvent { Title = "Boss 出现", Content = "世界 Boss 已刷新" },
    filter: p => p.Level >= 10
);

Game.Logger.LogInformation("通知已发送给 {Count} 名玩家", count);
#endif

异步发布 ​

csharp
// 所有 Publish 方法都有对应的异步版本
await DualEndEvent.PublishAsync(new PlayerChatEvent { Message = "异步发送" });

#if SERVER
await DualEndEvent.PublishAsync(targetPlayer, new GameNotifyEvent { Title = "异步通知" });
await DualEndEvent.PublishAsync(
    new GameNotifyEvent { Title = "异步广播" },
    filter: p => p.IsOnline
);
#endif

API 参考 ​

DualEndEvent 静态方法 ​

方法可用端说明
Publish<T>(T, bool)双端发布事件。客户端发到服务端,服务端广播到所有客户端
PublishAsync<T>(T, bool)双端异步版本
Publish<T>(Player, T, bool)仅服务端发布事件到指定客户端
PublishAsync<T>(Player, T, bool)仅服务端异步版本
Publish<T>(T, Func<Player, bool>, bool)仅服务端发布事件到过滤后的客户端,返回发送成功的玩家数
PublishAsync<T>(T, Func<Player, bool>, bool)仅服务端异步版本

所有方法的 publishLocally 参数默认为 true,表示事件也会在本地触发。设为 false 则仅发送到对端。

IDualEndEvent&lt;TSelf&gt; 接口 ​

csharp
public interface IDualEndEvent<TSelf> : ITriggerEvent<TSelf>, IDualEndEvent
    where TSelf : IDualEndEvent<TSelf>
{
}

public interface IDualEndEvent
{
    // 由框架自动填充:客户端事件到达服务端时,设置为发送者玩家
    Player? Sender { get; set; }
}

触发器注册规则 ​

端可注册目标说明
客户端Game.Instance只能注册到全局
服务端Game.Instance全局监听所有事件
服务端Player 实例监听来自特定玩家的事件

事件流转详解 ​

客户端 → 服务端 ​

客户端:
  DualEndEvent.Publish(event)
    ├─ publishLocally=true? → 在 Game.Instance 上触发本地触发器
    └─ 序列化 → 通过 TypedMessage 发送到服务端

服务端(收到后自动处理):
  ├─ 反序列化事件
  ├─ 设置 event.Sender = 发送者 Player
  ├─ 在对应 Player 上触发触发器
  └─ 在 Game.Instance 上触发触发器

服务端 → 客户端(广播) ​

服务端:
  DualEndEvent.Publish(event)
    ├─ publishLocally=true? → 在 Game.Instance 上触发本地触发器
    └─ 序列化 → 通过 TypedMessage 广播到所有客户端

客户端(收到后自动处理):
  ├─ 反序列化事件
  └─ 在 Game.Instance 上触发触发器

服务端 → 特定客户端 ​

服务端:
  DualEndEvent.Publish(targetPlayer, event)
    ├─ publishLocally=true?
    │   ├─ 在 targetPlayer 上触发触发器
    │   └─ 在 Game.Instance 上触发触发器
    └─ 序列化 → 通过 TypedMessage 发送到 targetPlayer 的客户端

客户端(收到后自动处理):
  ├─ 反序列化事件
  └─ 在 Game.Instance 上触发触发器

完整示例:简易聊天系统 ​

> 以下代码主要演示 DualEndEvent 的注册和传输结构,不应直接作为生产聊天系统使用。正式聊天流程应避免在客户端先显示未经服务端确认的原文,并在服务端广播前执行敏感词过滤、长度限制、频率限制和权限校验。推荐流程见敏感词过滤系统。

csharp
// === 事件定义(双端共用) ===
public class ChatMessageEvent : IDualEndEvent<ChatMessageEvent>
{
    public Player? Sender { get; set; }
    public string Content { get; set; } = "";
    public string Channel { get; set; } = "global";
}

public class SystemAnnouncementEvent : IDualEndEvent<SystemAnnouncementEvent>
{
    public Player? Sender { get; set; }
    public string Title { get; set; } = "";
    public string Message { get; set; } = "";
}

// === 聊天管理器(双端共用注册逻辑) ===
public class ChatManager : IGameClass
{
    public static void OnRegisterGameClass()
    {
        Game.OnGameTriggerInitialization += OnGameTriggerInitialization;
    }

    private static void OnGameTriggerInitialization()
    {
        // 注册聊天事件触发器
        var chatTrigger = new Trigger<ChatMessageEvent>(OnChatMessage);
        chatTrigger.Register(Game.Instance);

        // 注册系统公告触发器
        var announceTrigger = new Trigger<SystemAnnouncementEvent>(OnAnnouncement);
        announceTrigger.Register(Game.Instance);
    }

    private static async Task<bool> OnChatMessage(object sender, ChatMessageEvent e)
    {
#if SERVER
        // 服务端:验证消息合法性
        if (e.Sender == null || string.IsNullOrWhiteSpace(e.Content))
            return false;

        if (e.Content.Length > 200)
        {
            Game.Logger.LogWarning("玩家 {PlayerId} 发送过长消息", e.Sender.Id);
            return false;
        }

        var filterResult = Game.FilterSensitiveWords(e.Content);
        e.Content = filterResult.FilteredText;

        Game.Logger.LogInformation("[聊天] {PlayerName}: {Content}",
            e.Sender.Name, e.Content);
#endif

#if CLIENT
        // 客户端:显示聊天气泡
        DisplayChatBubble(e.Sender?.Name ?? "未知", e.Content, e.Channel);
#endif
        return true;
    }

    private static async Task<bool> OnAnnouncement(object sender, SystemAnnouncementEvent e)
    {
#if CLIENT
        // 客户端:显示系统公告 UI
        ShowAnnouncementBanner(e.Title, e.Message);
#endif
        return true;
    }

    // === 客户端发送聊天 ===
    public static void SendChat(string content, string channel = "global")
    {
        DualEndEvent.Publish(new ChatMessageEvent
        {
            Content = content,
            Channel = channel
        });
    }

    // === 服务端发送系统公告 ===
    public static void SendAnnouncement(string title, string message)
    {
#if SERVER
        DualEndEvent.Publish(new SystemAnnouncementEvent
        {
            Title = title,
            Message = message
        });
#endif
    }
}

注意事项 ​

事件类型要求 ​

  • 事件类必须实现 IDualEndEvent<TSelf> 接口
  • 事件类必须包含 Player? Sender { get; set; } 属性
  • 所有需要跨端传递的事件数据必须是公开 get; set; 属性,不要使用字段;DualEndEvent 使用框架默认 JSON 配置,未开启 IncludeFields
  • 事件属性类型必须支持 JSON 序列化(避免二维数组 [,]、循环引用等)
  • 不要在事件类中放置不可序列化的对象(如 Entity、Unit 等引用类型),使用 ID 替代

性能考量 ​

  • 双端事件内部基于 TypedMessage(JSON 序列化),不适合每帧发送的高频数据
  • 需要高频同步(如位置更新)时,考虑使用属性同步或 ProtoCustomMessage
  • 条件过滤广播会遍历所有在线玩家,玩家数量很多时注意性能

常见错误 ​

csharp
// ❌ 错误:在事件中直接放 Entity 引用
public class BadEvent : IDualEndEvent<BadEvent>
{
    public Player? Sender { get; set; }
    public Entity Target { get; set; }  // Entity 无法 JSON 序列化
}

// ✅ 正确:使用 ID 引用
public class GoodEvent : IDualEndEvent<GoodEvent>
{
    public Player? Sender { get; set; }
    public int TargetEntityId { get; set; }  // 传递 ID,对端通过 ID 查找
}
csharp
// ❌ 错误:用字段承载事件数据
public class BadScoreEvent : IDualEndEvent<BadScoreEvent>
{
    public Player? Sender { get; set; }
    public int Score;  // 框架默认 JSON 配置不会序列化普通字段
}

// ✅ 正确:用属性承载事件数据
public class GoodScoreEvent : IDualEndEvent<GoodScoreEvent>
{
    public Player? Sender { get; set; }
    public int Score { get; set; }
}
csharp
// ❌ 错误:在 OnGameDataInitialization 中注册触发器
Game.OnGameDataInitialization += () =>
{
    var trigger = new Trigger<MyEvent>(OnMyEvent);
    trigger.Register(Game.Instance);
};

// ✅ 正确:在 OnGameTriggerInitialization 中注册触发器
Game.OnGameTriggerInitialization += () =>
{
    var trigger = new Trigger<MyEvent>(OnMyEvent);
    trigger.Register(Game.Instance);
};

相关文档 ​