计划事件(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::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
事件中的所有容器类型都以指针形式存储。
在事件中发送容器时,请使用辅助函数
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 参数对此进行修改。可用的可靠性级别包括
UNRELIABLEUNRELIABLE_SEQUENCEDRELIABLE_ORDEREDRELIABLE_UNORDERED
枚举
如果您需要在两种脚本中都使用枚举类型,就无需用 C++ 编写并为每种语言分别绑定。现在,genDasevents.bat工具支持直接从 daScript 生成带有枚举的 Squirrel 代码。
请按照以下步骤操作:
在需要的地方用 daScript 定义枚举(最好用单独的文件,以便在生成代码时轻松解析)。
用
[export_enum]注解明确标记枚举。使用
--module scripts/file_with_enum.das参数为genDasevents.bat添加文件路径。运行
genDasevents.bat。所有枚举的构造函数都将出现在
<game_prog>/sq_globals/dasenums.nut。
工具
ecs.dump_events- 该控制台命令会打印所有事件、其模式和模式哈希值。如果客户端和服务器事件不匹配,可在两者上运行此命令以比较输出结果(日志中已包含分析所需的所有信息)。此外,游戏中还有一个包含详细事件信息的窗口:打开ImGui menu (
F2) ▸ Window ▸ ECS ▸ 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 插件.