news 2026/9/16 20:07:03

ET 框架 ConditionExpr 条件表达式解析器实战:从 Luban 配置到行为树运行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ET 框架 ConditionExpr 条件表达式解析器实战:从 Luban 配置到行为树运行

ET 框架 ConditionExpr 条件表达式解析器实战:从 Luban 配置到行为树运行

【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET

导读

ConditionExpr(cn.etetet.conditionexpr)是 ET 框架中把 Excel/Luban 表格里填写的条件表达式字符串自动编译为行为树(Behavior Tree)节点的解析器与运行器,专门解决"进入副本条件、领取奖励条件、任务接取条件、功能开启条件"这类策划可配置的条件判断需求。读完本文你将掌握:条件表达式的完整语法(比较运算、逻辑运算、错误码绑定、多 owner 变量与专用节点参数)、它在 Luban bean 中的配置方式、底层"词法分析 → 语法分析 → 行为树生成"的编译链路,以及运行期通过BTEnv执行并返回错误码的调用范式。


一、包结构与定位

cn.etetet.conditionexpr位于 Packages/cn.etetet.conditionexpr,只依赖已声明的三个底层包:behaviortreeunitnumeric。它不重复造轮子,通用逻辑节点(&&/||/!)直接复用cn.etetet.behaviortree中的BTSequenceBTSelectorBTNot,自己只定义条件叶子节点。

目录约定:

Scripts/Model/Share 表达式模型、编译器(Lexer/Parser)、BT 节点数据、包元数据 Scripts/Hotfix/Share 运行时 Handler(BTNumericCompareHandler、ConditionRootHandler) Scripts/Hotfix/Test 本包测试用例 Scripts/Model/Test 仅测试使用的 ConditionExpr 模型节点 Luban/Config/Base ConditionExpr Luban bean 定义(__beans__.xlsx)

核心源码文件与职责:

文件职责
ConditionExprLexer.cs词法分析,把表达式字符串切成 Token
ConditionExprParser.cs语法分析,递归下降构建行为树
ConditionExprCompiler.cs对外编译入口,Lexer + Parser的封装
ConditionVariableRegistry.cs变量名 → 节点类型 的注册表
BTNumericCompare.cs普通数值比较节点
ConditionExpr.Partial.csLuban bean 的EndInit编译钩子

二、Luban 配置字段与填写方式

2.1 Bean 定义

本包在Luban/Config/Base/__beans__.xlsx中定义了一个 Luban bean:

ET.ConditionExpr

字段含义:

字段类型含义
Exprstring条件表达式
ErrorCodeint叶子条件没有显式错误码时使用的默认错误码
Descstring策划备注,不参与运行时判断

2.2 在业务表中引用

任意业务表可以直接声明该 bean 类型的字段:

##var EnterCondition ##type ET.ConditionExpr ##comment 进入条件

填写示例:

Expr = (HP >= 10 : 10001 || MP >= 100 : 10002) && Speed > 0 : 10003 ErrorCode = 9999 Desc = 进入副本条件

2.3 编译时机:EndInit

配置对象创建时,Luban 生成的类会调用ConditionExpr.EndInit()。该 partial 方法(见 ConditionExpr.Partial.cs)把字符串交给编译器:

partial void EndInit() { this.Root = ConditionExprCompiler.Compile(this.Expr, this.ErrorCode); }

编译结果保存在ConditionExpr.Root(类型为ConditionRoot)。也就是说,配置加载完成的那一刻,表达式就已经被编译成行为树,运行期不需要再做字符串解析,性能开销只在加载阶段发生一次。

三、表达式语法完整说明

3.1 比较运算

> >= < <= == !=

对应枚举ConditionCompareOp(见 ConditionCompareOp.cs):Greater=1, GreaterEqual=2, Less=3, LessEqual=4, Equal=5, NotEqual=6

3.2 逻辑运算

&& || ! ()

运算优先级(与 C 系语言一致):

! > && > ||

建议复杂条件显式加括号,例如:

(HP >= 10 : 10001 || MP >= 100 : 10002) && Speed > 0 : 10003

3.3 错误码绑定

每个叶子条件可以用冒号绑定显式错误码:

HP >= 10 : 10001

如果叶子条件没写: errorCode,则使用 bean 上的默认ET.ConditionExpr.ErrorCode

HP >= 10

编译时默认错误码参数来自Compile(expr, defaultErrorCode)的第二个参数。

3.4 多 owner 语法:OwnerKey.Variable

BTEnv中存在多个 owner 时,用点号指定从哪个 owner 读取变量:

Unit1.HP > 0 || Unit2.MP < 100

约束:

  • OwnerKey.Variable只允许一层点号Unit1.A.B这类多级引用会在编译期报错condition variable reference invalid(见 ConditionExprParser.cs);
  • OwnerKey必须是调用方通过BTEnv.AddEntity(key, owner)传入的 owner key;
  • 目标节点必须声明public string OwnerKey字段,否则编译时报错condition node owner key field not found
  • 当前BTNumericCompareHandler按现有NumericComponent契约读取Unit,即要求env.GetEntity<Unit>(node.OwnerKey)取到的实体带NumericComponent

3.5 专用节点参数语法:NodeName(Param1, Param2)

专用条件节点支持括号传字符串参数:

Friend1(HP) > 0 Friend2(HP, MP) < 100

规则:

  • Friend1Friend2是通过ConditionVariableAttribute注册的节点名;
  • HPMP按文本原样写入节点的public string[] Params字段;
  • 只有声明了Params字段(类型必须是string[])的节点才能使用括号参数;
  • 数值节点(BTNumericCompare)没有Params字段,所以HP(Friend1)这种写法会在编译期报错condition node params field not found(测试用例 Conditionexpr_Compile_Test.cs 专门验证了这一点);
  • OwnerKey.Variable与括号参数不能混用于同一专用节点,测试用例验证Unit1.Friend1(HP) > 0会因目标节点缺少OwnerKey字段而报错。

四、编译原理:从字符串到行为树

编译入口是 ConditionExprCompiler.cs:

public static ConditionRoot Compile(string expr, int defaultErrorCode = 1) { ConditionExprLexer lexer = new(expr); List<ConditionToken> tokens = lexer.Tokenize(); ConditionExprParser parser = new(tokens, defaultErrorCode); return parser.Parse(); }

4.1 词法分析(Lexer)

ConditionExprLexer.cs 逐字符扫描,产出 Token 序列(类型定义见 ConditionToken.cs):

  • 标识符:以字母或_开头,可含字母、数字、_..被包含进标识符以便支持Unit1.HP这种 owner 引用);
  • 数字:支持long解析,也支持负数字面量(-后跟数字);解析失败抛condition number parse error
  • 运算符>/>=</<==后必须跟=组成==!/!=&&||():,=&|后面没有配对字符时抛condition token error
  • 空白:全部跳过,所以表达式里可以随意加空格;
  • 扫描结束追加一个End结束符。

4.2 语法分析(Parser)

ConditionExprParser.cs 采用递归下降 + 优先级分层

ParseExpression → ParseOr → ParseAnd → ParseUnary → ParsePrimary → ParseCompare
  • ParseOr处理||,生成BTSelector
  • ParseAnd处理&&,生成BTSequence
  • ParseUnary处理!,生成BTNot(默认错误码取 defaultErrorCode,可被:覆盖);
  • ParsePrimary处理括号(支持对括号组整体应用: errorCode)与叶子比较;
  • ParseCompare解析变量 [OwnerKey.] [括号参数] 比较符 数值 [: errorCode],通过ConditionVariableRegistry查变量对应的节点类型,反射实例化节点并填充字段。

值得一提的优化:AddSequenceChild/AddSelectorChild会把连续的A && B && C摊平成单层BTSequence,避免嵌套行为树过深(见 ConditionExprParser.cs)。

4.3 字段填充与校验

编译期对节点字段做强校验

  • OwnerKey字段必须存在且类型为string,否则抛condition node owner key field must be string
  • Params字段必须存在且类型为string[],否则抛condition node params field must be string[]
  • 错误码超出int范围抛condition error code out of range
  • 括号组错误码只允许作用在叶子节点或!节点上,否则抛condition group error code only support leaf or not node

从源码结构看,这套校验把大量"配置写错"的问题提前到编译期暴露,而不是拖到运行期才出错。

五、变量注册机制

5.1 普通数值变量

普通数值变量来自NumericType枚举。ConditionVariableRegistry.Awake()(见 ConditionVariableRegistry.cs)会:

  1. 遍历NumericType的全部枚举值,把每个枚举名注册为变量,节点类型指向BTNumericCompare
    HP -> BTNumericCompare, NumericType.HP MP -> BTNumericCompare, NumericType.MP Speed -> BTNumericCompare, NumericType.Speed
  2. 通过CodeTypes.Instance.GetTypes(typeof(ConditionVariableAttribute))扫描所有标记了[ConditionVariable("...")]的类,注册专用节点。

因此表达式中的变量名必须和NumericType常量名一致

HP >= 10 MP >= 100 Speed > 0 Unit1.HP > 0 Unit2.MP < 100

如果变量没有注册,编译时抛:

condition variable not registered: Xxx

重复注册同一个变量名也会抛异常(duplicate condition variable: Xxx)。

5.2 注册表存放方式

注册结果保存在ConditionVariableRegistry实例字段variableNodeTypes/numericTypes两个字典)中,不使用静态字典。该注册表是一个挂在 ET 生命周期上的Singleton<ConditionVariableRegistry>,符合包 AGENTS 约定的"Attribute 扫描结果必须挂在 ET 生命周期对象上"。

六、运行时执行与错误码语义

6.1 返回值约定

行为树返回值约定:

0 条件成功 非 0 条件失败,值为错误码

6.2 三种逻辑节点的短路语义

BTSequence对应&&

从左到右执行 遇到第一个失败节点,直接返回该节点错误码 全部成功返回 0

BTSelector对应||

从左到右执行 任意子节点成功,返回 0 全部失败时,返回第一个失败节点的错误码

BTNot对应!

子节点失败时,! 成功,返回 0 子节点成功时,! 失败,返回 BTNot.ErrorCode

6.3 示例推演

对表达式(HP >= 10 : 10001 || MP >= 100 : 10002) && Speed > 0 : 10003

HP 满足且 Speed 满足 -> 0 HP 不满足但 MP 满足且 Speed 满足 -> 0 HP 和 MP 都不满足 -> 10001 HP 或 MP 满足但 Speed 不满足 -> 10003

测试用例 Conditionexpr_Run_Test.cs 用真实Unit+NumericComponent验证了这三种返回路径(HP=5/MP=50 → 10001;MP=100 → 0;HP=10/Speed=0 → 10003)。

6.4 数值比较 Handler

运行时,叶子节点由 BTNumericCompareHandler.cs 执行:

Unit owner = env.GetEntity<Unit>(node.OwnerKey); NumericComponent numericComponent = owner.NumericComponent; long left = numericComponent.GetAsLong(node.NumericType); return ConditionCompareHelper.Compare(left, node.Op, node.Value) ? ErrorCode.ERR_Success : node.ErrorCode;

比较逻辑集中在 ConditionCompareHelper.cs 的Compare(long left, ConditionCompareOp op, long right)中,未知操作符抛unknown condition compare op

6.5 Root Handler

ConditionRootHandler.cs 是整棵条件树的入口:空树直接成功;子节点数量大于 1 抛异常;否则把唯一的子节点交给BTDispatcher执行。

七、运行期调用范式

7.1 使用配置对象中的 Root

配置加载完成后,直接用 Luban 对象里的Root

ConditionExpr condition = mapConfig.EnterCondition; BTEnv env = BTEnv.Create(scene, unit.Id); try { env.AddEntity(ConditionExprEnvKeys.Unit, unit); int errorCode = BTHelper.RunTree(condition.Root, env); if (errorCode != ErrorCode.ERR_Success) { return errorCode; } } finally { env.Dispose(); }

ConditionExprEnvKeys.Unit的值为字符串"Unit"(见 ConditionExprEnvKeys.cs)。裸变量(没有 owner 前缀的变量)默认从"Unit"这个 key 读取 owner,所以调用方必须用env.AddEntity(ConditionExprEnvKeys.Unit, unit)注册默认 owner。

7.2 直接编译字符串

也可以在代码里直接编译表达式字符串(跳过 Luban):

ConditionRoot root = ConditionExprCompiler.Compile( "(HP >= 10 : 10001 || MP >= 100 : 10002) && Speed > 0 : 10003", 9999);

7.3 多 owner key 示例

ConditionRoot root = ConditionExprCompiler.Compile( "Unit1.HP > 0 || Unit2.MP < 100", 9999); BTEnv env = BTEnv.Create(scene, unit1.Id); try { env.AddEntity("Unit1", unit1); env.AddEntity("Unit2", unit2); int errorCode = BTHelper.RunTree(root, env); } finally { env.Dispose(); }

带 owner key 的变量会读取表达式前缀对应的 key:Unit1.HP从 key"Unit1"取实体,Unit2.MP从 key"Unit2"取实体。运行测试 Conditionexpr_Run_Test.cs 用两个带NumericComponentUnit实体验证了三条分支路径(双失败 → 20001;仅 HP 分支成功 → 0;仅 MP 分支成功 → 0)。

八、扩展专用变量节点

当某个变量不能从NumericComponent读取时(例如 VIP 等级、好友关系等业务数据),可以通过ConditionVariableAttribute注册专用节点,让它与BTNumericCompare平行工作。

8.1 定义一个专用节点

[ConditionVariable("VipLevel")] public class BTVipLevelCompare : BTCondition { public string[] Params; public ConditionCompareOp Op; public long Value; public int ErrorCode; }

8.2 注册规则

专用节点必须继承 BTCondition 如果要接收表达式中的比较符、目标值和错误码,需要声明 Op / Value / ErrorCode public 字段 如果要支持 OwnerKey.Variable 语法,需要声明 OwnerKey public 字段,类型必须是 string 如果要支持 NodeName(Param1, Param2) 语法,需要声明 Params public 字段,类型必须是 string[] 变量名不能和已有 NumericType 或其它 ConditionVariableAttribute 重复 注册结果保存在 ConditionVariableRegistry 实例字段中,不使用静态字典

其中Op/Value/ErrorCode三个字段是通过反射BTNumericCompare的字段名写入的(见 ConditionExprParser.cs 的SetCompareField),因此字段名不能改动;OwnerKeyParams则由专门的SetOwnerKeyField/SetParamsField反射写入并校验类型。

BTNumericCompareBTVipLevelCompare是平行关系,都继承BTCondition。专用节点的 Handler 负责按业务规则取值并比较,与数值 Handler 一样继承ABTHandler<T>即可。

测试用的专用节点 Conditionexpr_ParamCompareNode.cs 放在Scripts/Model/Test而非Scripts/Model/Share,遵循"只给测试使用的节点不要污染共享模型"的包约定。

九、测试与验证

本包测试位于Packages/cn.etetet.conditionexpr/Scripts/Hotfix/Test,共三个测试文件:

  • Conditionexpr_Compile_Test.cs:验证表达式编译结构(Root 子节点数量与类型)、NumericType注册、多 owner 解析、专用节点Params解析、以及两类编译期报错;
  • Conditionexpr_LubanBean_Test.cs:验证 Luban bean 构造后EndInit自动编译,Root非空且叶子节点为BTNumericCompare
  • Conditionexpr_Run_Test.cs:在真实Scene+Unit+NumericComponent上跑行为树,验证错误码返回与多 owner 数值读取。

执行测试:

dotnet build ET.sln Remove-Item ./Logs -Recurse -Force -ErrorAction SilentlyContinue "Test --Name=Conditionexpr" | dotnet ./Bin/ET.App.dll --SceneName=Test

覆盖点汇总:

表达式编译结构 Luban bean EndInit 编译 NumericType 注册到 BTNumericCompare 运行时错误码返回 多 owner key 数值读取 专用节点 Params 参数解析

十、开发约定速查

最后,汇总本包 AGENTS 文档中的关键开发约束,方便后续在包内扩展时遵守:

只依赖已声明的底层包:behaviortree、unit、numeric 不新增静态注册表或静态缓存;Attribute 扫描结果必须挂在 ET 生命周期对象上 通用逻辑节点复用 cn.etetet.behaviortree 中的 BTSequence、BTSelector、BTNot ConditionExpr 自己的节点定义在本包内,不放回 cn.etetet.btnode 只给测试使用的 ConditionExpr 模型节点放在 Scripts/Model/Test OwnerKey.Variable 只允许一层点号,OwnerKey 必须是调用方已通过 BTEnv.AddEntity 传入的 key 专用条件节点支持 NodeName(Param1, Param2) 语法,没有 Params 字段的节点不能使用括号参数

从整体看,ConditionExpr 的定位非常清晰:把策划填写的条件字符串在配置加载期编译为行为树,运行期由 ET 行为树调度器统一执行,错误码即条件失败原因。这种设计既让策划免于写 C# 代码,又让程序侧复用成熟的行为树执行体系,是配置驱动玩法逻辑的一个典型实践。

【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 20:03:01

STM32F407硬件I2C实战:从初始化、DMA传输到总线故障排查

简介&#xff1a;面向 STM32F407 嵌入式开发者的硬件 I2C 通信参考例程&#xff0c;基于 MDK 与 HAL 库编写&#xff0c;适合正在学习 I2C 协议或需要移植外设驱动的入门及中级开发者。代码以一个精简的 C 源文件呈现&#xff0c;集中展示 I2C 外设初始化配置&#xff0c;包括时…

作者头像 李华
网站建设 2026/9/16 20:01:03

Codex 自动化从 0 到 1:模型 Key 走 TaoToken,定时巡检任务照原文建

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 20:00:56

leetcode 速成题单不显示?TaoToken Key 给 Codex 查插件版本

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华