13674307070 发表于 2026-8-20 08:31:32

行会系统不大,但 VV 引擎的 "前后拦截" 设计用得很地道

一、触发体系:先拦一刀,再通知一声
行会系统的触发是整个模块最有设计感的部分。几乎每一个关键操作都有两个触发 —— 操作前一个,操作后一个。

创建行会有 OnBeforeCreateGuild 和 OnCreateGuild,加入行会有 OnBeforeJoinGuild 和 OnJoinGuild,退出行会有 OnBeforeLeaveGuild 和 OnLeaveGuild,解散行会有 OnBeforeDisbandGuild 和 OnDisbandGuild。

这两个触发的职责完全不同。

Before 系列是拦截器,返回 true 允许操作,返回 false 阻止操作。你可以在创建行会前检查玩家等级够不够、金币够不够、有没有违规词汇,不满足就 return false,行会就建不成。加入行会前可以检查对方行会是不是满员、玩家有没有被拉黑、是不是在冷却期内,不满足就拦下来。退出前可以检查是不是在行会战期间、是不是有未完成的行会任务,阻止玩家关键时刻跑路。

After 系列是通知器,操作已经完成了,告诉你一声,你可以做后续处理。创建成功后给会长发个奖励、全服公告一下;加入成功后给新成员发个欢迎邮件、加个行会 buff;退出成功后清理玩家的行会称号、扣除行会贡献;解散成功后给所有成员发个通知、退会费时返还。

这种 "前拦截 + 后通知" 的设计,把 "要不要让这件事发生" 和 "这件事发生之后做什么" 彻底分开了。Before 管决策,After 管副作用。两个触发各司其职,逻辑不会混在一起。

这个设计的好处在实际开发中非常明显。比如你要加一个 "创建行会需要消耗 100 万金币" 的功能,只需要在 OnBeforeCreateGuild 里判断金币够不够,不够就 return false,够就先扣掉再 return true。创建成功后的公告、奖励逻辑写在 OnCreateGuild 里,两边互不干扰。以后要改创建条件,只动 Before;要改创建后的奖励,只动 After。

还有两个特殊的触发: guildapplybefore 是请求行会联盟前的拦截, guildchiefdelmember 是掌门踢人前的拦截。这两个只有 Before 没有 After,说明联盟请求和踢人这两个操作,引擎更关注的是 "让不让做",而不是 "做完之后通知"。毕竟联盟请求之后还有对方同意的流程,踢人之后成员退出会走 OnLeaveGuild 的流程,不需要单独的 After 通知。

整个触发体系看下来,最大的感受是:引擎把所有可能需要自定义规则的节点都暴露出来了。你想在任何一个环节加条件、加逻辑,都有对应的触发可以挂。不需要改引擎,不需要写复杂的钩子,在触发函数里写你的逻辑就行。这就是事件驱动架构的好处。

二、行会对象:一切操作的核心
和人物系统里的玩家对象、物品系统里的物品对象一样,行会系统里也有一个核心对象 —— 行会对象。几乎所有操作都围绕它展开。

获取行会对象有几种方式。 GetMyGuild 传玩家对象,返回这个玩家所在的行会对象,没有行会返回字符串 "0"。 FindGuild 可以按行会 ID 或行会名称搜索,返回对应的行会对象。 GetAllGuild 直接返回所有行会对象的列表,做排行榜、全服行会统计的时候用。

拿到行会对象之后, GetGuildInfo 可以读各种信息:行会 ID、名称、公告、成员名单、掌门人名称、副掌门人名称、人数上限。一个接口搞定所有基础信息的读取,靠索引区分要什么。 SetGuildInfo 可以改公告,目前只支持改公告这一项,其他信息的修改有专门的接口。

这里有个设计细节值得注意: GetGuildInfo 的成员名单返回的是 table,其他字段返回的是 string。同一个接口返回不同类型,用的时候要注意 —— 拿成员名单的时候要按 table 遍历,拿名称公告的时候按字符串用。如果不注意,把成员名单当字符串处理,或者把名称当 table 遍历,就会出问题。

行会对象的设计思路和玩家对象、物品对象是一致的:先拿到对象,再对对象操作。这种面向对象的风格比 "每次操作都传行会名" 要清晰得多,也避免了重复查找。你拿到一次行会对象,可以连续读它的名称、公告、成员数,不用每次都重新搜索。

实际用的时候要注意判空。 GetMyGuild 没行会返回 "0", FindGuild 搜不到应该也是返回空或 "0"。拿到对象之后先判断是不是有效的,再去调 GetGuildInfo,不然对一个无效对象读信息会报错。

三、成员管理:进、出、职位、人数
行会的核心是人,成员管理是行会系统最基础的功能。

创建 用 BuildGuild,传玩家和行会名。这个接口比较简单,创建后玩家自动成为会长。但创建的条件(比如需要什么道具、多少金币)引擎没有内置,需要你自己在 OnBeforeCreateGuild 里判断。这意味着引擎只提供 "创建" 这个动作,规则全部由脚本定义。这种设计很灵活 —— 有的版本建会要祖玛头像,有的要 100 万金币,有的要达到一定等级,全部在触发里自己写,引擎不限制。

加入 用 AddGuildMember,传玩家对象和行会名。 退出 用 DelGuildMember,支持传玩家对象或玩家名字,用 type 参数区分。这两个接口都是直接操作,不需要对方同意 —— 脚本调用就生效。如果要做 "申请 - 审批" 的加入流程,需要自己在脚本层实现,引擎只提供最终的加入和退出动作。

职位 用 SetPlayGuildLevel 和 GetPlayGuildLevel,职位分五档:会长、副会长、成员 1、成员 2、成员 3。这个职位体系比较简单,只有五个等级,没有更多的自定义职位。对大部分版本来说够用了,但如果要做更复杂的行会架构(比如长老、精英、新人这种多等级),就得自己用变量来记录,引擎的职位体系支持不了。

人数 有两个接口: GetGuildMemberCount 查当前成员数, ChangeGuildMemberLimit 改人数上限,支持加减等于三种操作。人数上限可以动态调整这个设计很实用 —— 比如行会升级后增加人数上限,或者活动期间临时扩容,都可以用这个接口实现,不需要固定在配置里。

成员管理这一块的设计比较中规中矩,该有的功能都有,但不算特别丰富。职位等级少、没有自定义职位、加入退出需要自己做审批流程,这些都是需要脚本层补充的地方。但反过来说,引擎只提供最基础的操作,把规则和流程交给脚本,这种 "引擎做机制,脚本做规则" 的思路是对的 —— 每个版本的行会规则都不一样,引擎定死了反而不灵活。

四、行会关系:战争与联盟
行会之间的关系是行会玩法的核心,VV 引擎提供了战争和联盟两套关系。

行会战 用 SetGuildWar 发起,传两个行会名和时间(分钟),返回是否成功。 IsWarGuild 判断两个行会是否处于宣战状态,支持传行会对象或行会名称。

联盟 用 guildapplybefore 触发做申请前的拦截,但文档里没有看到发起联盟的接口,可能联盟的发起走的是游戏内的传统操作(比如掌门面对面申请),脚本只提供申请前的拦截和关系查询。 IsAllyGuild 判断两个行会是否结盟,同样支持对象或名称。

关系查询接口支持传对象也支持传名称,这个细节很贴心。有的场景你已经拿到了行会对象,直接传对象就行,不用再转成名称;有的场景你只有行会名字,直接传名字也能查。两种方式都支持,不用做额外的转换。

行会战有时间限制,这个设计很合理 —— 无限期的行会战争容易变成死仇,有时间限制的话到期自动结束,想继续打再宣战。做活动的时候也方便,比如 "沙巴克争夺战期间全服行会自由宣战",活动结束自动恢复。

行会关系这块的接口不多,但核心功能覆盖了。宣战、查战、查盟,三个接口搞定行会之间的关系判断。做行会 PK 的时候,先 IsWarGuild 判断是不是敌对,再决定能不能攻击;做组队辅助的时候,先 IsAllyGuild 判断是不是联盟,再决定能不能加血加 buff。这些都是高频调用的接口。

五、行会信息:公告、改名与基础数据
行会的基础信息管理比较简单。

公告 用 GetGuildInfo 读, SetGuildInfo 写。目前 SetGuildInfo 只支持改公告这一项,索引 0 就是公告。这个接口的设计预留了扩展空间 —— 以后要加可修改的行会信息,加索引就行,不用新写接口。

改名 用 ChangeGuildName,传玩家对象、原行会名、新行会名。这个接口的设计比较特别,它不是直接返回成功失败,而是走回调 —— changeguildnameok 成功时触发, changeguildnamefail 失败时触发,失败回调还带一个 model 参数告诉你失败原因(参数错误、原行会找不到、名字不合法、新名字重复)。

为什么改名要走回调而不是直接返回?我的理解是:改名涉及到重名检查、合法性过滤这些可能比较耗时的操作,或者需要异步处理,所以用回调的方式通知结果。也可能是因为改名的触发链比较长,要更新很多地方的数据,同步返回不太方便。

不管原因是什么,用的时候要注意:调用 ChangeGuildName 之后不要立刻假设改名成功了,要在 changeguildnameok 回调里做成功后的逻辑(比如发公告、记录日志),在 changeguildnamefail 里处理失败情况(比如提示玩家原因)。如果按同步接口的思路,调用完就往下走,可能会出现 "改名实际失败了但脚本以为成功了" 的 bug。

改名的失败原因分得很细,四种情况各有各的 code,做提示的时候可以根据 model 给玩家不同的提示 ——"名字包含敏感词"" 这个名字已经被占用了 ",比统一提示" 改名失败 " 体验好很多。

六、几个实际使用中的心得
把行会系统过完,结合实际开发场景,总结几个心得。

第一,Before 触发里做扣减要小心。 比如创建行会需要消耗 100 万金币,你在 OnBeforeCreateGuild 里判断金币够,然后扣掉,再 return true。但要注意,如果扣完金币之后、行会创建之前发生了异常(虽然概率很低),金币扣了行会没建成,玩家就亏了。更安全的做法是在 Before 里只判断不扣减,在 After 里扣减。但 After 里扣减又有个问题 ——After 的时候行会已经建好了,如果扣减失败(比如玩家在这一瞬间把金币交易走了),就会出现 "行会建了但没扣钱"。所以这个时序问题要根据实际情况权衡,大部分版本在 Before 里扣减是可以接受的,因为创建行会是瞬时操作,中间出问题的概率极低。

第二,GetMyGuild 的返回值要判空。 没有行会的时候返回字符串 "0",不是 nil 也不是 false。判断的时候要写 if guild and guild ~= "0" then,不能只写 if guild then,因为字符串 "0" 在 Lua 里是真值,会通过判断。这个坑不注意的话,没行会的玩家也会走进行会逻辑,然后报错。

第三,GetGuildInfo 的返回类型要注意。 成员名单返回 table,其他返回 string。用的时候先搞清楚你要的那个索引返回的是什么类型,按对应的方式处理。尤其是成员名单,拿到之后要遍历,不要当字符串拼接。

第四,行会改名是异步的。 这个前面说了,再强调一次 —— 调用 ChangeGuildName 之后不要立刻做后续操作,等回调。成功回调里做成功的事,失败回调里处理失败。按同步接口用会出 bug。

第五,职位体系只有五档,复杂架构要自己扩展。 会长、副会长、三个成员等级,这个体系对简单版本够用,但如果要做元老、精英、新人、荣誉会员这种多等级,就得自己用玩家变量来存职位,引擎的职位接口用不了。做版本规划的时候要提前考虑清楚,别做到一半发现职位不够用,再改就麻烦了。

第六,行会战和联盟关系是全局的。 IsWarGuild 和 IsAllyGuild 判断的是两个行会之间的关系,不是两个玩家之间的。做 PK 判定的时候,先拿两个玩家各自的行会,再判断行会之间的关系。不要直接拿玩家对象去判断,这两个接口要的是行会对象或行会名。


13674307070 发表于 2026-8-20 08:57:31

挠头的500贡献
页: [1]
查看完整版本: 行会系统不大,但 VV 引擎的 "前后拦截" 设计用得很地道