主题
双端事件系统 (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
);
#endifAPI 参考
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<TSelf> 接口
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);
};相关文档
- 触发器系统 — 双端事件基于的触发器基础设施
- 强类型消息系统 (TypedMessage) — 中级代码用户的结构化消息方案
- 消息传递系统 (ProtoCustomMessage) — 高级用户的原始二进制通信方案
- 常见开发陷阱 — 包含事件注册时机等常见错误