这是一个简单且易用的条件组分词器,提供了c#、c++、python等多种语言的实现和示例,如果你的项目需要支持复杂的条件组合和分层嵌套机制,那么它将非常适合你
简易的条件组分词器
https://github.com/YaoXuanZhi/condition_group_tokenizer
前言
为了说明这个工具库怎么用,这里以一个游戏项目为例,在这类项目中,条件配置通常会被高频修改,一个通用且可维护性强的条件支持机制在此显得尤为重要。
而在理想情况下,该模块的开发者只需在新增条件类型时才维护一次,至于怎么组合这个条件配置,全由策划折腾才对
需求分析
假设该游戏项目采用csv文件作为游戏数值配置,现在策划童鞋设计了一个功能开启需求,并在需求文档里罗列了一些配置示例,如下所示:
系统功能名 |
开启条件 |
武器系统 |
玩家等级达到5级 |
坐骑系统 |
玩家等级达到15级 |
天赋系统 |
武器系统升到10级【且】开启了坐骑系统 |
聊天系统 |
玩家等级达到30级【且】充值过 |
公会系统 |
玩家等级达到100级【或】累计登录30天 |
收到这个需求之后,我们直接面临以下问题:
- 怎么设计配置表?
- 多种条件组合配置得怎么设计,如何尽可能做到条件类型复用呢?
- 如果后续策划需要修改具体的条件组合,怎么尽可能减少程序的维护工作量?
总不能我在忙着其它开发任务,然后策划童鞋改个已有条件类型的配置还得找上我来配合着改代码吧
- 如果再发散下思维,成就模块、任务模块也会用到类似的东西,能不能将这套条件机制提供给其它系统模块复用呢?
只需要维护一次条件类型变动就能为多个模块提供了机制支持,岂不省时省力
配置表设计
将罗列的开启条件进行整理,如下所示:
配置格式设计如下:Type-Param1-Param2-Param3
Type(条件类型) |
Desc(描述) |
Template(模板) |
Example(示例) |
is_system_open |
系统A是否开启 |
is_system_open-system_id |
武器系统是否开启:is_system_open-weapon_sys |
system_level |
系统A达到B等级 |
system_level-system_id-need_level |
武器系统是否达到10级: system_level-weapon_sys-10 |
player_level |
玩家达到A等级 |
player_level-need_level |
玩家是否达到10级:player_level-10 |
acc_charge_total |
累计充值总额达到A |
acc_charge_total-need_total |
是否累计充值总额已达1000元:acc_charge_total-1000 |
acc_login_days |
累计登录天数达到A |
acc_login_days-need_days |
是否累计登录了30天:acc_login_days-30 |
上述已经将配置示例上的条件内容共性部分拆解成一个个条件类型了,那么怎么表述它们组合时的逻辑关系呢,在此,引入了另外两个特殊符号,如下所示:
符号 |
作用 |
&& |
完成条件A 和条件B 的且运算 |
|| |
完成条件A 和条件B 的或运算 |
现在,我们终于可以配置复杂的条件内容了,如下所示:
- 描述:
玩家等级达到30级【且】充值过
==> 配置项:player_level-30 && acc_charge_total-1
- 描述:
玩家等级达到100级【或】累计登录30天
==> 配置项:player_level-100 || acc_login_days-30
最终的配置表格式如下:
系统功能名 |
注释 |
开启条件 |
武器系统 |
玩家等级达到5级 |
player_level-5 |
坐骑系统 |
玩家等级达到15级 |
player_level-15 |
天赋系统 |
武器系统升到10级【且】开启了坐骑系统 |
system_level-weapon_sys-10 && is_system_open-mount_sys |
聊天系统 |
玩家等级达到30级【且】充值过 |
player_level-30 && acc_charge_total-1 |
公会系统 |
玩家等级达到100级【或】累计登录30天 |
player_level-100 || acc_login_days-30 |
老司机应该能很快意识到,上述的条件配置内容已经很像编程语言的条件表达式了,比如C++里面的if statement,与之相比我们还差了一个小括号()
的功能和逻辑运算符:非
的支持,不过非
一般用不上,可以酌情考虑要不要搞,有了小括号()
的支持后,我们还加上了条件嵌套机制,如下所示:
- 描述:
(武器系统升至50级 【且】锻造系统升至30级) 或 玩家充值达到10000元
- 配置项:
(system_level-weapon_sys-50 && system_level-mount_sys-30) || acc_charge_total-10000
给这类复杂条件起个名字,叫做条件组
,因为每个小括号包裹住的条件配置都被视为一个条件组,如果条件配置没有小括号,可以视为其最外层有个隐藏的小括号,同策划童鞋沟通好,达成共识
程序设计
既然配置表、配套测试配置都就位了,那接下来就是条件模块的功能设计啦,它需要将这些字符串解析成一个个条件类型,并且进行各种逻辑判断,返回最终判断结果
程序实现步骤拆解:
- 实现一个Tokenizer:解析这个配置项文本,依据其设计好的分割规则,拆分成一个Token数组,然后提取里面的
条件块
和逻辑运算符
和层级关系
,举个简单例子:将 system_level-weapon_sys-50 && system_level-mount_sys-30
解析成一个数组:system_level-weapon_sys-50
、&&
、system_level-mount_sys-30
- 将ConditionToken进行解析并返回结果:将上述例子里的
system_level-weapon_sys-50
、system_level-mount_sys-30
进一步解析,并且调用各个系统的数据判断该条件的结果,将结果结合&&
逻辑运算符得出最终结果,可以简化成 result1 && result2
功能实现
参考condition_group_tokenizer 里的Demo示例,重载ProxyCondition()
方法,直接进入到步骤2即可
这里贴了Python版的测试用例,其它语言依葫芦画瓢就行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
|
# coding=utf-8
from condition_group_tokenizer import ConditionGroupTokenizer
import unittest
class PlayerFakeDataComponent:
'''
伪造玩家数据
'''
def __init__(self) -> None:
self.buildFakeDatas()
def buildFakeDatas(self):
'''
测试用例可以重载该函数
'''
# 已开启的系统数据
self.systemDatas = {
"weapon_sys" : {"level":39},
"mount_sys" : {"level":31},
}
# 累计登录天数
self.accLoginDays = 3
# 累计充值金额
self.accChargeTotal = 3
# 玩家等级
self.level = 3
class PlayerConditionComponent(ConditionGroupTokenizer):
"""
玩家条件组件
"""
def __init__(self, playerDataComponent:PlayerFakeDataComponent) -> None:
super().__init__()
self.playerDataComponent = playerDataComponent
def proxyCondition(self, source:str, isPromt:bool) -> bool:
elements = source.split("-")
atomType = elements[0]
params = elements[1:]
result = self.checkConditionAtom(atomType, params)
if isPromt:
print(f"判断:{atomType}:{params} 结果为:{result}")
return result
def checkConditionAtom(self, atomType, params) -> bool:
"""
根据条件类型来逐个判断
"""
if atomType == "is_system_open":
systemId = params[0]
return systemId in self.playerDataComponent.systemDatas
elif atomType == "system_level":
systemId = params[0]
needSystemLevel = int(params[1])
if systemId in self.playerDataComponent.systemDatas:
systemInfo = self.playerDataComponent.systemDatas[systemId]
return systemInfo["level"] >= needSystemLevel
return False
elif atomType == "player_level":
needPlayerLevel = int(params[0])
return self.playerDataComponent.level >= needPlayerLevel
elif atomType == "acc_charge_total":
needAccTotal = int(params[0])
return self.playerDataComponent.accChargeTotal >= needAccTotal
elif atomType == "acc_login_days":
needAccDays = int(params[0])
return self.playerDataComponent.accLoginDays >= needAccDays
else:
assert False, f"条件类型:{atomType} 还没支持,请完善相关条件判断逻辑"
return False
class TestConditionGroupComponent(unittest.TestCase):
def __init__(self, methodName: str = "且运算逻辑") -> None:
super().__init__(methodName)
def test_and_operator_success(self):
'''
测试用例-&&运算符:成功
'''
class TestDataComponent(PlayerFakeDataComponent):
def buildFakeDatas(self):
# 已开启的系统数据
self.systemDatas = {
"weapon_sys" : {"level":50},
"mount_sys" : {"level":60},
}
# 累计登录天数
self.accLoginDays = 3
# 累计充值金额
self.accChargeTotal = 3
# 玩家等级
self.level = 3
source = "system_level-weapon_sys-50 && system_level-mount_sys-30"
dataComponent = TestDataComponent()
conditionComponent = PlayerConditionComponent(dataComponent)
result = conditionComponent.directCheck(source, False)
self.assertTrue(result, f"判断:{source} 结果为:{result}")
def test_and_operator_fail(self):
'''
测试用例-&&运算符:失败
'''
class TestDataComponent(PlayerFakeDataComponent):
def buildFakeDatas(self):
'''
伪造玩家数据
'''
# 已开启的系统数据
self.systemDatas = {
"weapon_sys" : {"level":50},
"mount_sys" : {"level":29},
}
# 累计登录天数
self.accLoginDays = 3
# 累计充值金额
self.accChargeTotal = 3
# 玩家等级
self.level = 3
source = "system_level-weapon_sys-50 && system_level-mount_sys-30"
dataComponent = TestDataComponent()
conditionComponent = PlayerConditionComponent(dataComponent)
result = conditionComponent.directCheck(source, False)
self.assertFalse(result, f"判断:{source} 结果为:{result}")
def test_or_operator_success(self):
'''
测试用例-||运算符:成功
'''
class TestDataComponent(PlayerFakeDataComponent):
def buildFakeDatas(self):
'''
伪造玩家数据
'''
# 已开启的系统数据
self.systemDatas = {
"weapon_sys" : {"level":50},
"mount_sys" : {"level":29},
}
# 累计登录天数
self.accLoginDays = 12
# 累计充值金额
self.accChargeTotal = 3
# 玩家等级
self.level = 3
source = "is_system_open-pet_system || (acc_charge_total-100 || acc_login_days-10) || player_level-5"
dataComponent = TestDataComponent()
conditionComponent = PlayerConditionComponent(dataComponent)
result = conditionComponent.directCheck(source, False)
self.assertTrue(result, f"判断:{source} 结果为:{result}")
def test_or_operator_fail(self):
'''
测试用例-||运算符:失败
'''
class TestDataComponent(PlayerFakeDataComponent):
def buildFakeDatas(self):
'''
伪造玩家数据
'''
# 已开启的系统数据
self.systemDatas = {
"weapon_sys" : {"level":50},
"mount_sys" : {"level":29},
}
# 累计登录天数
self.accLoginDays = 3
# 累计充值金额
self.accChargeTotal = 3
# 玩家等级
self.level = 3
source = "is_system_open-pet_system || (acc_charge_total-100 || acc_login_days-10) || player_level-5"
dataComponent = TestDataComponent()
conditionComponent = PlayerConditionComponent(dataComponent)
result = conditionComponent.directCheck(source, False)
self.assertFalse(result, f"判断:{source} 结果为:{result}")
if __name__ == '__main__':
unittest.main()
|
结尾
在游戏项目里,我们仅需实现一次通用条件组件模块,然后在奖励发放、任务触发&完成、成就事件达成等业务模块上,都调用这个通用组件来返回条件判断结果,就能大大减少重复配置设计了
当然,在集成到项目的过程中,还得完善配置格式合法化检测、条件判断套娃检测等机制,这里就不展开了
后记
- 为UE定制一个条件组插件,时间充足的话,还可以支持上DataTable
参考资料