计划事件(daScript、Quirrel、C++、Net)

计划事件的优势

  • 事件在各种脚本语言(daScript、 Quirrel)。

  • 它们既可在本地传输,也可通过网络传输。

  • 事件可以实时修改,无需重启游戏。

  • 事件具有严格的验证结构,所有字段在 Quirrel 中均可见,并可作为实例访问,例如 中可见,并可作为实例访问,如 evt.someField

  • 事件结构的完整运行时信息(反射)可用。

  • 如果需要,还提供 C++ API 支持来处理这些事件。

声明事件

每个游戏目录都包含一个事件声明文件,文件格式为 events_<game_name>.das(例如 events_cuisine_royale.das)。事件声明由注释和事件结构描述组成。 注释指定了事件是unicast 还是 broadcast,以及网络路由(如果需要),这将在下文介绍。

示例: events_<game>.das

[event(unicast)]
struct CmdCreateMapPoint
  x: float
  z: float

Note

文件 events_<game_name>.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<ecs::event_type_t, str_hash_fnv1("EventOnSeatOwnersChanged")>).

发送事件 (服务器-到-服务器, 客户端-到-客户端)

  • 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::Objectecs::IntListecs::FloatListecs::Point3Listecs::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

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<T> (直接访问参数值)。

  • daScript: C++ 的所有函数在 daScript 的 ecs 模块中也可用(如 events_db_getFieldsCount)。例如,ImGui 中的 Events DB 窗口使用了此 API,请参阅 <project_name>/prog/scripts/game/es/imgui/ecs_events_db.das

  • Quirrel: 调用::log(evt)时,可获得详细的事件打印输出,其中输出了所有事件字段。还提供了支持反射的 API,如下所示:

示例: describe_event.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_<game>.das

[cpp_event(unicast, with_scheme)]
struct EventOnPlayerDash
  from: float3
  to: float3

实用程序 <game>/scripts/genDasevents.bat 将为该事件生成 .h.cpp 文件(当前位于 prog/game/dasEvents.h/cpp)。

Quirrel Stubs/C++ 代码生成

要自动生成 Quirrel Stubs和 C++ 代码,请运行批处理文件 <game>/scripts/genDasevents.bat。如果批处理文件不起作用,请在 <project_name>/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<SomeNetMsg, rcptf::TARGET_ENTITY>.

  • entity_team – 向玩家的英雄和团队发送事件。

    • 等同于C++中的: &rcptf::entity_team<SomeNetMsg, rcptf::TARGET_ENTITY>.

  • possessed_and_spectated – 将事件发送给玩家和正在观看的观众。

    • 等同于C++中的: &rcptf::possessed_and_spectated.

  • possessed_and_spectated_player – 与 possessed_and_spectated 类似,但目标是玩家而不是英雄。

    • 等同于C++中的: &rcptf::possessed_and_spectated_player.

在 daScript 中,过滤器是一个返回 array<net::IConnection> 的函数,为了与 C++ 术语保持一致,我们将其称为 “过滤器”。

Squirrel 中的过滤器

在 Squirrel 和 daScript 中,事件发送方法都有一个可选参数,可以传递连接 ID 数组(即 int 数组)。下面是在 Squirrel 中实现过滤器的示例:

示例: sq_filter.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

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. 所有枚举的构造函数都将出现在 <game_prog>/sq_globals/dasenums.nut

工具

  • ecs.dump_events - 该控制台命令会打印所有事件、其模式和模式哈希值。如果客户端和服务器事件不匹配,可在两者上运行此命令以比较输出结果(日志中已包含分析所需的所有信息)。

  • 此外,游戏中还有一个包含详细事件信息的窗口:打开ImGui menu (F2) ▸ WindowECSEvents db

Events db

FAQ

我有一个 C++ 网络事件,希望将其声明移至 daScript,同时保留 C++ 中的事件。 (例如: ECS_REGISTER_NET_EVENT(EventUserMarkDisabled, net::Er::Unicast, net::ROUTING_SERVER_TO_CLIENT, (&rcptf::entity_ctrl_conn<EventUserMarkDisabledNetMsg, rcptf::TARGET_ENTITY>));

在 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 载入前在系统中注册。每个游戏都有一个初始化脚本(例如,<game>_init.das)。在此初始化脚本中加载包含事件的脚本,以解决错误。


genDasevents.bat 显示编译错误,无法运行。

重建编译器。

See also

更多信息,请参阅 daScript for VSCode 插件.