# 计划事件(daScript、Quirrel、C++、Net) ## 计划事件的优势 - 事件在各种脚本语言(daScript、 Quirrel)。 - 它们既可在本地传输,也可通过网络传输。 - 事件可以实时修改,无需重启游戏。 - 事件具有严格的验证结构,所有字段在 Quirrel 中均可见,并可作为实例访问,例如 中可见,并可作为实例访问,如 `evt.someField`。 - 事件结构的完整运行时信息(反射)可用。 - 如果需要,还提供 C++ API 支持来处理这些事件。 ## 声明事件 每个游戏目录都包含一个事件声明文件,文件格式为 `events_.das`(例如 `events_cuisine_royale.das`)。事件声明由注释和事件结构描述组成。 注释指定了事件是`unicast` 还是 `broadcast`,以及网络路由(如果需要),这将在下文介绍。 **示例:** `events_.das` ``` [event(unicast)] struct CmdCreateMapPoint x: float z: float ``` ```{note} 文件 `events_.das` 中的所有事件都会先于 Quirrel 加载,以便在 Quirrel 中进行访问。因此,为 Quirrel 声明的事件应放在该文件中。虽然对其他事件来说不是强制性的,但为了保持一致性,建议使用此方法。 ``` ## 创建事件 - **daScript:** 事件的创建与任何常规实例结构一样,例如,`[[RqUseAbility ability_type=ability_type]]`。 - **Quirrel:** 这里,严格的验证确保不包含印刷错误或无关字段,`RqUseAbility({ability_type=“ultimate”})`。 ## 订阅事件 - **daScript:** 使用 `on_event=RqUseAbility`,或在系统中明确设置第一个参数的类型(例如,`evt: RqUseAbility...`)。 - **Quirrel:** 使用 `local {OnAbilityCanceled} = require("dasevents")... ::ecs.register_es("ability_canceling_es", { [OnAbilityCanceled] = @( evt, eid, comp) ::dlog(evt.ability_type)`. - **C++:** 可以通过订阅事件名称来监听事件,例如 `ECS_ON_EVENT(eastl::integral_constant)`. ## 发送事件 (服务器-到-服务器, 客户端-到-客户端) - **daScript:** 使用标准的 `sendEvent`, `broadcastEvent`. - **Quirrel:** 类似的使用 `::ecs.g_entity_mgr.sendEvent`, `::ecs.g_entity_mgr.broadcastEvent`. ## 通过网络发送事件 声明事件时,指定路由以确定其网络路径,例如 `[event(unicast, routing=ROUTING_SERVER_TO_CLIENT)]`, `ROUTING_CLIENT_TO_SERVER`,或 `ROUTING_CLIENT_CONTROLLED_ENTITY_TO_SERVER`. - **daScript:** 使用 `require net ... send_net_event(eid, evt)` 或 `broadcast_net_event(evt)`. - **Quirrel:** 使用 `local {CmdBlinkMarker, sendNetEvent, broadcastNetEvent} = require("dasevents") ... sendNetEvent(eid, CmdBlinkMarker()) ... broadcastNetEvent(CmdBlinkMarker(...))`. ### 网络协议版本 所有声明的网络事件都会影响协议版本。如果服务器和客户端版本不匹配,客户端将断开会话。 对于脚本事件 (`[event]`),可以通过将某些事件排除在协议计算之外来控制这种行为。在不匹配的情况下,这可能会导致屏幕错误或无通知。 - `event(... net_liable=strict ...)` – 事件参与协议版本控制;任何不匹配都会触发断开(默认行为)。 - `event(... net_liable=logerr ...)` – 该事件不会影响协议版本;如果发生不匹配,则会记录日志错误。 - `event(... net_liable=ignore ...)` – 该事件不影响协议版本;如果发生不匹配,则记录日志警告 (`logwarn`)。 C++ 事件遵循类似的逻辑,但使用 `NET_PROTO_VERSION` 常量和网络 C++ 事件计数,无一例外。 ## 事件版本 可以为事件指定一个明确的版本。默认情况下,所有事件都被设置为版本 `0`。在使用 `BitStream` 时,版本是必需的,它有助于在流内容发生重大变化时调整协议。 **示例:** `code.das` ``` [event(broadcast, version=1)] struct TestEvent {} ``` ## 发送容器(离线和在线) 动态数组/容器可与事件一起发送。目前,支持的类型有 `ecs::Object`、`ecs::IntList`、`ecs::FloatList`、`ecs::Point3List` 和 `ecs::Point4List`。 下面是一个从 daScript 发送此类事件的示例: **示例:** `code.das` ``` [event(broadcast)] struct TestEvent str : string i : int obj : ecs::Object const? ... using() <| $(var obj : Object) obj |> set("foo", 1) broadcastEvent([[TestEvent str="test event", i = 42, obj=ecs_addr(obj)]]) ``` ```{important} 1. 事件中的所有容器类型都以指针形式存储。 2. 在事件中发送容器时,请使用辅助函数 `ecs_addr(container)`。 ``` 从 Quirrel 发送事件的过程与此类似: **示例:** `code.nut` ```nut let {CompObject} = require("ecs") let {TestEvent, broadcastNetEvent} = require("dasevents") ... let obj = CompObject() obj["foo"] = 1 broadcastNetEvent(TestEvent({str="test event", i=42, obj=obj})) ``` ```{important} - 事件中的任何 `ecs::Object` 都会自动包含一个名为 `fromconnid` 的字段,该字段存储了发送者的连接 ID(在客户端,它总是 `0`,表示服务器;在服务器端,它保存了实际的连接编号)。 - 如果容器内容发生重大变化,最好指定一个事件版本(例如,`[event(...version=1)]`)。这将确保使用过时版本的客户端或服务器不再支持该事件。 ``` ## 发送BitStream 与容器类似,原始数据流(`BitStream`)也可以在事件中发送。发送 `BitStream` 时,必须指定事件版本。 ## 反射 事件拥有精确的模式,可在运行时访问,并可从任何脚本或 C++ 代码中检索。 - **C++:** 所有事件结构信息都存储在 `ecs::EventsDB` 中,它提供了各种方法,如 `getEventScheme`, `hasEventScheme`, `getFieldsCount` (参数计数), `getFieldOffset` (字段偏移), `getFieldName` (字段名称), `findFieldIndex` (字段索引)和 `getEventFieldValue` (直接访问参数值)。 - **daScript:** C++ 的所有函数在 daScript 的 `ecs` 模块中也可用(如 `events_db_getFieldsCount`)。例如,*ImGui* 中的 **Events DB** 窗口使用了此 API,请参阅 `/prog/scripts/game/es/imgui/ecs_events_db.das`。 - **Quirrel:** 调用`::log(evt)`时,可获得详细的事件打印输出,其中输出了所有事件字段。还提供了支持反射的 API,如下所示: **示例:** `describe_event.nut` ```nut local function describeEvent(evt) { if (evt == null) { ::dlog("null event") return } local eventType = evt.getType() local eventId = ::ecs.g_entity_mgr.getEventsDB().findEvent(eventType) local hasScheme = ::ecs.g_entity_mgr.getEventsDB().hasEventScheme(eventId) if (!hasScheme) { ::dlog($"event without scheme #{eventType}") return } local fieldsCount = ::ecs.g_entity_mgr.getEventsDB().getFieldsCount(eventId) ::dlog($"Event {eventType} fields count #{fieldsCount}") for (local i = 0; i < fieldsCount; i++) { local name = ::ecs.g_entity_mgr.getEventsDB().getFieldName(eventId, i) local type = ::ecs.g_entity_mgr.getEventsDB().getFieldType(eventId, i) local offset = ::ecs.g_entity_mgr.getEventsDB().getFieldOffset(eventId, i) local value = ::ecs.g_entity_mgr.getEventsDB().getEventFieldValue(evt, eventId, i) ::dlog($"field #{i} {name} <{type}> offset={offset} = '{value}'") } } ``` ## C++ 事件 (cpp_event) 除动态事件外,还可以声明 C++ 事件,并为其生成 C++ 代码和 SQ 绑定。在 Quirrel 中,处理这些事件与处理 daScript 中的标准事件相同。 在声明 C++ 事件时,需要使用 `with_scheme` 参数。这是必须的,因为某些事件由于限制(字段必须是基本的 ECS 类型或兼容容器)而无法转换为方案型事件。 **示例:** `events_.das` ``` [cpp_event(unicast, with_scheme)] struct EventOnPlayerDash from: float3 to: float3 ``` 实用程序 `/scripts/genDasevents.bat` 将为该事件生成 `.h` 和 `.cpp` 文件(当前位于 `prog/game/dasEvents.h/cpp`)。 ## Quirrel Stubs/C++ 代码生成 要自动生成 Quirrel Stubs和 C++ 代码,请运行批处理文件 `/scripts/genDasevents.bat`。如果批处理文件不起作用,请在 `/prog/aot` 中运行 `jam -sPlatform=win64 -sCheckedContainers=yes` 手动构建 daScript 编译器。 ## 过滤器 您可以使用过滤器管理服务器端 das-events 的收件人列表。这有助于锁定特定群体,例如只针对球员或球员所在球队。发送事件时,将过滤器指定为附加参数。例如,`send_net_event(eid, [[EnableSpectator]], target_entity_conn(eid))`。目前支持以下过滤器: - `broadcast` (default) – 发送给所有收件人。 - 等同于C++中的: `&net::broadcast_rcptf`. - `target_entity_conn` – 只向玩家发送事件(接收事件的 `eid` 必须是玩家的英雄或玩家 `eid`)。 - 等同于C++中的: `&rcptf::entity_ctrl_conn`. - `entity_team` – 向玩家的英雄和团队发送事件。 - 等同于C++中的: `&rcptf::entity_team`. - `possessed_and_spectated` – 将事件发送给玩家和正在观看的观众。 - 等同于C++中的: `&rcptf::possessed_and_spectated`. - `possessed_and_spectated_player` – 与 `possessed_and_spectated` 类似,但目标是玩家而不是英雄。 - 等同于C++中的: `&rcptf::possessed_and_spectated_player`. 在 daScript 中,过滤器是一个返回 `array` 的函数,为了与 C++ 术语保持一致,我们将其称为 “过滤器”。 ## Squirrel 中的过滤器 在 Squirrel 和 daScript 中,事件发送方法都有一个可选参数,可以传递连接 ID 数组(即 `int` 数组)。下面是在 Squirrel 中实现过滤器的示例: **示例:** `sq_filter.nut` ```nut local filtered_by_team_query = ecs.SqQuery("filtered_by_team_query", {comps_ro=[["team", ecs.TYPE_INT], ["connid",ecs.TYPE_INT]], comps_rq=["player"], comps_no=["playerIsBot"]}) local function filter_connids_by_team(team){ local connids = [] filtered_by_team_query.perform(function(eid, comp){ connids.append(comp["connid"]) },"and(ne(connid,{0}), eq(team,{1}))".subst(INVALID_CONNECTION_ID, team)) return connids } ``` 下面是使用该过滤器发送事件的示例: **示例:** `sq_send_event.nut` ```nut sendNetEvent(eid, RequestNextRespawnEntity({memberEid=eid}), filter_connids_by_team(target_team)) ``` ## cpp_event 中的过滤器 当使用 `filter=` 参数和上述过滤器之一注释 `cpp_event` 时,代码生成过程将生成包含上述括号中指定过滤器的 C++ 代码。 ## 事件发送可靠性 默认情况下,所有事件的发送可靠性级别均为 `RELIABLE_ORDERED`。可以使用 `reliability` 参数对此进行修改。可用的可靠性级别包括 - `UNRELIABLE` - `UNRELIABLE_SEQUENCED` - `RELIABLE_ORDERED` - `RELIABLE_UNORDERED` ## 枚举 如果您需要在两种脚本中都使用枚举类型,就无需用 C++ 编写并为每种语言分别绑定。现在,`genDasevents.bat`工具支持直接从 daScript 生成带有枚举的 Squirrel 代码。 **请按照以下步骤操作:** 1. 在需要的地方用 daScript 定义枚举(最好用单独的文件,以便在生成代码时轻松解析)。 2. 用 `[export_enum]` 注解明确标记枚举。 3. 使用 `--module scripts/file_with_enum.das` 参数为 `genDasevents.bat` 添加文件路径。 4. 运行 `genDasevents.bat`。 5. 所有枚举的构造函数都将出现在 `/sq_globals/dasenums.nut`。 ## 工具 - `ecs.dump_events` - 该控制台命令会打印所有事件、其模式和模式哈希值。如果客户端和服务器事件不匹配,可在两者上运行此命令以比较输出结果(日志中已包含分析所需的所有信息)。 - 此外,游戏中还有一个包含详细事件信息的窗口:打开**ImGui menu** (`F2`) ▸ **Window** ▸ **ECS** ▸ **Events db**。 Events db
## FAQ ###### 我有一个 C++ 网络事件,希望将其声明移至 daScript,同时保留 C++ 中的事件。 (例如: `ECS_REGISTER_NET_EVENT(EventUserMarkDisabled, net::Er::Unicast, net::ROUTING_SERVER_TO_CLIENT, (&rcptf::entity_ctrl_conn));` 在 daScript 中使用 `[cpp_event(unicast, with_scheme, routing=ROUTING_SERVER_TO_CLIENT, filter=direct_connection)]` 注解定义事件,然后运行 `genDasevents.bat`。这将生成存根,事件将在 `.h` 和 `.cpp` 文件中出现或更新。(cpp_event + with_scheme` 激活代码生成)。 --- ###### 我有一个 C++ 网络事件,想把它完全移到脚本中(在 C++ 中没有必要)。 按照上述相同步骤操作,但要使用 `[event(unicast,routing=ROUTING_SERVER_TO_CLIENT)]`。将所有 `sendEvent` 调用改为 `send_net_event/sendNetEvent`,并将类似 `target_entity_conn(eid)` 这样的过滤函数调用作为最后参数传递。运行 `genDasevents.bat` 仍然需要运行 `genDasevents.bat` 来生成存根。 --- ###### 我有一个基于脚本的事件,需要将它移植到 C++。 只需将事件注解从 `event` 更改为 `cpp_event`。然后,将所有 `send_net_event` 调用替换为标准的 `sendEvent` 调用。运行 `genDasevents.bat` 生成存根和 C++ 代码。 --- ###### 我添加了一个事件,但在 Squirrel 中看到以下错误:`[E] daRg: the index 'CmdHeroSpeech' does not exist` 请确保事件已在 Quirrel 载入前在系统中注册。每个游戏都有一个初始化脚本(例如,`_init.das`)。在此初始化脚本中加载包含事件的脚本,以解决错误。 --- ###### `genDasevents.bat` 显示编译错误,无法运行。 重建编译器。 ```{seealso} 更多信息,请参阅 [daScript for VSCode 插件](https://marketplace.visualstudio.com/items?itemName=profelis.dascript-plugin). ```