Action 生命周期(完整参考)
本文是
wait_action_result/wait_expectation与识别层src/core/detector/的完整使用参考。development.md中的「Action 生命周期」一节是本文摘要,细节以本文为准。
目录
- 1. 它解决什么问题
- 2. 分层模型
- 3. 快速上手
- 4. 识别层:统一判据
- 5. 编排层:
wait_action_result - 6.
wait_expectation - 7. 典型用法配方
- 8. 契约与陷阱
- 9. 什么时候不该用它
- 10. 真实落地样例
- 相关文档
1. 它解决什么问题
游戏里大量操作是同一个形状:看到某个东西 → 动它 → 确认预期结果出现了。 具体拆开是五个环节:
| 代号 | 环节 | 语义 |
|---|---|---|
| A | 前置条件 | 识别函数判断当前状态是否满足;未满足则不动作,或按重试机制处理 |
| Action | 主动作 | 一次点击 / 按键 / 切页 / 移动 |
| C | 结果验证 | 动作后识别预期结果是否出现;命中即成功,未命中则等待/重试/重执行 |
| B | 持续条件 | 可选。B 持续命中时重复附加动作;未命中立即停止 |
| Extra | 附加动作 | 可选。持续阶段重复执行的动作,每次之后继续查 C |
这套逻辑由两个 API 覆盖:wait_action_result(完整生命周期)与 wait_expectation(只等结果)。
2. 分层模型
识别源与生命周期是两个正交维度,因此分成两层——否则参数会爆炸,且 A/C 无法使用不同识别源。
┌─────────────────────────────────────────────────────────┐
│ 编排层 runtime_mixin.py │
│ wait_action_result / wait_expectation │
│ 职责:「什么时候做、做几次、算不算成功」 │
└────────────────────────┬────────────────────────────────┘
│ 只依赖 Detector.detect(frame)
┌────────────────────────┴────────────────────────────────┐
│ 识别层 src/core/detector/ │
│ Hit + Detector 协议 + 5 个适配器 + 3 个组合器 │
│ 职责:「这一帧有没有」 │
└────────────────────────┬────────────────────────────────┘
│ 包装
┌────────────────────────┴────────────────────────────────┐
│ 既有原语 find_feature / find_one / yolo_detect / │
│ ocr / find_button │
└─────────────────────────────────────────────────────────┘
统一判据协议只有一条:
Detector.detect(frame) -> Hit | None
必须接受外部传入的 frame,不能自己调 next_frame()——否则一次判定内会多次截图,时序错乱。
3. 快速上手
from src.core.detector import TemplateDetector
from src.data.feature_list import FeatureList
# A ≠ C:看到宝箱图标 → 点击 → 等解锁界面出现
ok = self.wait_action_result(
condition=TemplateDetector(FeatureList.treasure_icon), # A
action=lambda hit: self.click(hit.box), # Action
expect=TemplateDetector(FeatureList.unlock_ui), # C
time_out=5,
expect_time_out=1.5,
max_attempts=2,
)
if not ok:
self.log_info("解锁界面没出现")
三个要点:
hit是Hit,hit.box可直接喂self.click();- 判据不需要手动
attach,编排入口会代劳; - 返回
bool,True= 生命周期成功。
4. 识别层:统一判据
4.1 Hit:统一命中表示
四类识别源原始返回值各不相同(Box | None / list[Box] / …),统一归一成 Hit:
@dataclass
class Hit:
box: Box # 可直接 click() 的目标框(唯一必需字段)
confidence: float = 1.0
source: str = "" # 识别源标识,见 SOURCE_* 常量
text: str = "" # OCR 命中文本;非 OCR 源为空串
raw: object = None # 原始结果对象,见下
metrics: dict = field(default_factory=dict)
| 字段 | 说明 |
|---|---|
box |
唯一必需。帧坐标,可直接 click() / draw_boxes() |
confidence |
0~1;未知时 1.0 |
source |
SOURCE_TEMPLATE / SOURCE_YOLO / SOURCE_OCR / SOURCE_BUTTON / SOURCE_PREDICATE;取反判据为原判据名 |
text |
OCR 命中的文字,可直接用于日志或分支判断 |
raw |
原始结果:OCR 是 list[Box],模板/按钮是 Box。需要识别器特有字段时从这里取,避免归一化时丢信息 |
metrics |
预留的指标字典 |
Hit.__bool__ 恒为 True —— 未命中时 detect() 返回的是 None,不是空 Hit。所以 if hit: 是可靠的。
hit = TemplateDetector(FeatureList.main_ui).detect(frame)
if hit: # None 才是未命中
self.click(hit.box)
4.2 五个适配器(含全部参数)
TemplateDetector —— 模板匹配
包装 find_feature / find_one。
TemplateDetector(
feature, # 必填。FeatureList 成员或字符串
box=None, # 搜索区域。None 时沿用 coco 标注位置 ±variance
horizontal_variance=0.0,
vertical_variance=0.0,
threshold=0, # 匹配阈值;0 = 框架默认
target_height=0, # 目标缩放高度;0 = 不缩放
mask_function=None, # 掩膜函数,如 make_hsv_isolator(...)
use_gray_scale=False,
canny_lower=0,
canny_higher=0,
name=None, # 判据名;缺省用特征名
use_find_one=False, # True 走 find_one,False 走 find_feature
)
⚠️ box 的默认行为是陷阱:find_feature 不传 box 时只搜 coco 标注位置 ± variance
(DEFAULT_VARIANCE = 0.05)。会移动的图标必须显式传 box,否则永远搜不到。
⚠️ horizontal_variance / vertical_variance 只在使用 find_feature 时生效:
传 0 会在调用处被替换为 DEFAULT_VARIANCE(0.05),即「用默认方差」而不是「零方差」;
走 use_find_one=True 时两个参数原样透传(0 就是 0)。
use_find_one 的选择:
find_feature(默认) |
find_one |
|
|---|---|---|
| 特征列表 | ❌ 不支持 | ✅ 任一命中即可 |
| 帧缓存 | ✅ 支持 Frame 对象 |
— |
YoloDetector —— YOLO 检测
包装 yolo_detect。
YoloDetector(
name, # 必填。目标名或名称列表
box=None, # 裁剪区域;同时作为 pick="center" 的中心基准
conf=0.7, # 置信度阈值
model_key=None, # 指定模型键;None 时由框架按首个 name 解析
pick=PICK_MAX_CONF, # 多候选选择策略
task_name=None, # 判据名;缺省由 name 推导(多个用 _ 连接)
)
pick 策略(PICK_* 常量):
| 常量 | 值 | 语义 |
|---|---|---|
PICK_MAX_CONF |
max_conf |
默认。置信度最高 |
PICK_FIRST |
first |
框架返回顺序的第一个(yolo_detect 按 conf 降序,故接近 max_conf) |
PICK_CENTER |
center |
离搜索区域中心最近;无 box 时退化到画面中心 |
PICK_LEFTMOST |
leftmost |
最左(min(b.x)) |
PICK_TOPMOST |
topmost |
最上(min(b.y)) |
非法 pick 在构造时即抛 ValueError,不会静默取错候选。
OcrDetector —— OCR 文本匹配
包装 ocr。
OcrDetector(
match, # 必填。文本 / 列表 / 正则 / 正则列表
box=None, # 识别区域;与 x/y/to_x/to_y 二选一
x=0, y=0, to_x=1, to_y=1, # 相对区域坐标
threshold=0, # OCR 置信度;0 = 框架默认
lib="default", # OCR 引擎名
target_height=0, # 识别前缩放高度
pick=PICK_FIRST, # 多候选选择策略
name=None, # 判据名;缺省用 match 的可读形式
)
⚠️ OCR 的 pick 默认是 PICK_FIRST,与 YOLO 的 PICK_MAX_CONF 不同。
原因是 ocr() 返回的是按 y 排序的列表(ok/task/task.py:818),
所以 first 实际含义是「最上面那个」。这个差异是刻意的。
pick 支持的策略与 YOLO 相同(first / max_conf / center / leftmost / topmost)。
box 与相对坐标等价,后者更短:
OcrDetector("确认", box=self.box_of_screen(0.55, 0.58, 0.68, 0.66))
OcrDetector("确认", x=0.55, y=0.58, to_x=0.68, to_y=0.66) # 等价
命中时 Hit.text 带出识别到的文字:
hit = OcrDetector("确认", box=area).detect(frame)
self.log_info(f"识别到「{hit.text}」") # hit.raw 是整个 list[Box]
⚠️ ocr() 内部自带调试框(emit_draw_box("ocr"+name, ...)),
所以用 wait_action_result(draw=True) 时不要对 OCR 判据重复绘制。
ButtonDetectorAdapter —— 固定 Box 按钮检测
包装 find_button(中央文本带,无 OCR)。
ButtonDetectorAdapter(
box, # 必填。按钮区域,必须贴合按钮
thresholds=None, # ButtonThresholds 完整阈值
text_hsv=None, # 文字颜色区间
backdrop_hsv=None, # 底色区间;传入即默认开启底色校验
require_backdrop=None, # 显式指定是否校验底色
name="button",
)
⚠️ box 必须贴合按钮——Box 远大于按钮时文本带相对过薄,会被形状判定拒绝。
用法见 development.md 的「固定 Box 按钮检测」一节(含 ButtonThresholds 预设)。
PredicateDetector —— 任意判据兜底
把一段自有逻辑包成判据。
PredicateDetector(
predicate, # 必填。frame -> Box | Hit | bool | None
box=None, # predicate 只返回布尔时,用它提供点击目标
name="predicate",
source=SOURCE_PREDICATE,
)
调用方的返回值决定行为:
| predicate 返回 | 结果 |
|---|---|
Hit |
原样返回 |
Box |
包成 Hit |
True(且给了 box) |
用 box 造 Hit |
True(未给 box) |
返回 None —— 没有可点的坐标 |
False / None |
返回 None |
这也是写「条带还在不在」这类自定义判定的入口:
PredicateDetector(
lambda frame: not self._band_present(frame, band),
box=band,
name=f"band_gone_y{band.y}",
)
4.3 三个组合器
| 组合器 | 签名 | 语义 |
|---|---|---|
MultiBoxDetector |
(detectors, name=None) |
按序取首个命中。detectors 为空抛 ValueError |
FirstHitDetector |
同上 | MultiBoxDetector 的语义别名(多个不同目标的优先级选择时更可读) |
InvertedDetector |
(detector, box=None, name=None) |
取反:原判据未命中即为命中 |
BlindPointDetector |
(x, y, name="blind_point") |
恒命中,返回指定坐标 |
MultiBoxDetector 可混用不同识别源,按列表顺序回退,第一个命中即返回:
MultiBoxDetector([
TemplateDetector(FeatureList.treasure_icon), # 先试模板
YoloDetector("chest", box=roi, conf=0.7), # 再试 YOLO
OcrDetector("开启", box=text_area), # 最后试 OCR
BlindPointDetector(900, 540), # 都搜不到时盲点一次
], name="entry")
⚠️ InvertedDetector 必须显式给 box——点击目标无法从取反判据推出。
未给 box 时永不命中(返回 None)。
⚠️ 取反命中时 Hit.source 是原判据名(如 not_band_gone),
因为 source 只在 Hit 上、detector 本身不持有它。
FirstHitDetector 与 MultiBoxDetector 行为完全相同,选哪个只看可读性:
- 判据是「同一目标的多个区域」→
MultiBoxDetector - 判据是「多个不同目标的优先级选择」→
FirstHitDetector
4.4 判据的 attach 与宿主绑定
适配器需要宿主任务才能工作(detect 内部要调 self._task.find_feature(...))。
编排入口会自动绑定,你不需要手动 attach:
# 直接构造后传进去即可
self.wait_action_result(
condition=TemplateDetector(FeatureList.main_ui),
action=lambda hit: self.click(hit.box),
)
自动绑定由 RuntimeMixin._resolve_detector 完成,它:
- 若判据有
attach且尚未绑定,调用attach(self); - 若是组合器(有
detectors属性),递归处理内部判据; - 若对象没有
detect(frame)方法,抛TypeError。
所以自定义判据只要满足 detect(frame) -> Hit | None 就能直接用,
甚至不必继承任何基类——这是协议(Protocol)而非基类继承。
5. 编排层:wait_action_result
5.1 三阶段流程
┌─ 阶段一:等条件 ────────────────────────────────┐
│ condition 为 None ? │
│ 是 → 跳过等待,直接执行动作 │
│ condition 在 time_out 内命中? │
│ 否 → raise_if_not_found ? 抛异常 : 返回 None │
│ 是 → 继续(settle_time > 0 时要求持续成立) │
└────────────────────┬────────────────────────────┘
↓
┌─ 阶段二:动作 + 验证(最多 max_attempts 轮)──────┐
│ 重取一帧确定目标(防位移) │
│ action_delay → action(hit) → after_sleep │
│ expect is None ? → 返回 True │
│ wait_expectation(expect) 命中 ? → 返回 expect Hit│
│ 未命中 → 下一轮 │
└────────────────────┬────────────────────────────┘
↓
┌─ 阶段三:持续阶段(需 while_condition 且 repeat_action)┐
│ B 未命中 → 立即 break │
│ repeat_interval → repeat_action(hit_b) │
│ 查 expect → 命中则返回 expect Hit │
│ 受 time_out / max_repeat 约束 │
└────────────────────┬────────────────────────────┘
↓
返回 None
5.2 完整参数表
wait_action_result(
action, # 必填。action(hit: Hit) -> bool | None 或 action() -> bool | None
condition=None, # 前置条件判据 A,默认 None(不等条件直接执行)
expect=None, # 预期结果判据 C。None = 动作成功即返回
time_out=5.0, # 等 condition 的总超时;也约束持续阶段
settle_time=0.0, # condition 需持续成立的秒数
max_attempts=1, # 动作最多执行次数(含首次);< 1 会被抬到 1
action_delay=0.0, # 每次动作前的延时
after_sleep=0.0, # 每次动作后的延时
expect_time_out=1.0, # 每次验证 expect 的等待时间
expect_settle_time=0.0, # expect 需持续成立的秒数
while_condition=None, # 持续条件判据 B
repeat_action=None, # 持续阶段重复执行的动作
repeat_interval=0.0, # 持续阶段每轮动作的前置延时
max_repeat=0, # 持续阶段动作次数上限;0 = 只受 time_out 约束
draw=False, # 是否画调试框
raise_if_not_found=False, # 条件未命中时是否抛 WaitFailedException
) -> Hit | bool | None
返回值:
- 命中
expect时返回其Hit对象(包含box、confidence、text、raw等,且bool(hit) == True); expect=None且动作成功执行时返回True;- 条件未命中、超时或验证失败时返回
None。
| 参数 | 默认 | 说明 |
|---|---|---|
condition |
None |
前置条件判据 A;默认 None 表示不等条件直接执行动作 |
time_out |
5.0 |
两处生效:阶段一等 A 的超时、阶段三的总时限 |
settle_time |
0.0 |
0 = 命中一次即可。非 0 时是「每次判定都要求连续成立够时长」 |
max_attempts |
1 |
1 = 不重试。expect 未通过时才进入下一轮 |
expect_time_out |
1.0 |
每次验证 C 的窗口,不宜太大(否则整体变慢) |
max_repeat |
0 |
0 = 不限次数,只受 time_out 约束 |
draw |
False |
绿框 = 动作目标(A),蓝框 = 持续目标(B) |
5.3 动作回调签名
action 与 repeat_action 兼容 0 参数与 1 参数签名(接收当前命中对象,若 condition=None 则 hit 为 None):
action(hit: Hit | None) -> bool | None # 或 action() -> bool | None
repeat_action(hit: Hit) -> None # 或 repeat_action() -> None
因为返回值不用,多语句回调写成 lambda 时用元组技巧:
action=lambda hit: (self.log_info(f"点击「{hit.text}」"), self.click(hit.box)),
复杂逻辑建议定义独立方法,可读性更好:
def _do_unlock(self, hit):
self.log_info(f"点击条带 y={hit.box.y}")
self.click(hit.box)
self.wait_action_result(condition=..., action=self._do_unlock, ...)
6. wait_expectation
只用 C 时(只等一个结果)用它——不要用 wait_action_result 包一层空动作。
wait_expectation(
expectation, # 必填。判据
time_out=1.0, # 最长等待时间
settle_time=0.0, # 需持续成立的秒数
raise_if_not_found=False, # 未命中时是否抛 WaitFailedException
) -> Hit | None
它是所有「动作之后等结果」的唯一收口点——wait_action_result 的阶段二/三内部都调它,
以保证 settle 语义一致。
# 等主界面出现
hit = self.wait_expectation(TemplateDetector(FeatureList.main_ui), time_out=2.0)
if hit:
self.log_info("已进入主界面")
# 等条带稳定消失(要求连续 0.35 秒都不见)
gone = self.wait_expectation(
PredicateDetector(lambda f: not self._band_present(f, band), box=band),
time_out=3.0,
settle_time=0.35,
)
7. 典型用法配方
7.1 等到就点(最简)
self.wait_action_result(
condition=TemplateDetector(FeatureList.skip_dialog),
action=lambda hit: self.click(hit.box),
expect=None, # 点完即成功
time_out=3,
)
7.2 点完等结果出现(A ≠ C)
self.wait_action_result(
condition=TemplateDetector(FeatureList.treasure_icon),
action=lambda hit: self.click(hit.box),
expect=TemplateDetector(FeatureList.unlock_ui),
time_out=5,
expect_time_out=1.5,
max_attempts=2, # 第一次没开成再点一次
)
7.3 点完等元素消失
box = self.box_of_screen(...)
self.wait_action_result(
condition=TemplateDetector(FeatureList.confirm_button, box=box),
action=lambda hit: self.click(hit.box),
expect=InvertedDetector(
TemplateDetector(FeatureList.confirm_button, box=box),
box=box, # 取反必须给 box
),
time_out=5,
expect_settle_time=0.3, # 消失要稳定一点
)
7.4 用 OCR 文本
self.wait_action_result(
condition=OcrDetector("确认", box=self.box_of_screen(0.55, 0.58, 0.68, 0.66)),
action=lambda hit: (self.log_info(f"点击「{hit.text}」"), self.click(hit.box)),
expect=None,
time_out=3,
)
7.5 B 成立期间重复操作
progress = TemplateDetector(FeatureList.progress_bar, box=bar_box)
self.wait_action_result(
condition=progress, # A:进度条出现
action=lambda hit: self.click(hit.box, down_time=0.5), # 开始长按
expect=InvertedDetector(progress, box=bar_box), # C:进度条消失 = 完成
while_condition=progress, # B:进度条还在
repeat_action=lambda hit: self.send_key("space"), # 附加动作
repeat_interval=0.3,
max_repeat=20,
time_out=60,
)
7.6 多区域 / 多识别源回退
MultiBoxDetector([
TemplateDetector(FeatureList.entry_a),
TemplateDetector(FeatureList.entry_b, box=region_b),
OcrDetector("进入", box=text_area),
])
7.7 条件与预期用不同识别源
self.wait_action_result(
condition=OcrDetector("可领取", box=button_area), # A:OCR 判断按钮文字
action=lambda hit: self.click(hit.box),
expect=TemplateDetector(FeatureList.reward_icon), # C:模板判断奖励图标
time_out=5,
)
8. 契约与陷阱
expect=None 表示「动作成功即返回」
这是显式声明「该动作没有可验证的结果」,不是「不验证」也不是「一定失败」。
阶段二执行完 action 立即返回 True。
⚠️ 如果你其实有结果可验,别偷懒省掉——那正是这个 API 存在的意义。
settle_time 的语义是「每次判定都要连续成立」
不是「总共等 N 秒」。wait_until 同语义。默认 0。
⚠️ 非 0 会显著增加耗时——历史上给 find_button 加过 settle 导致性能问题被回滚。
只在「等 UI 稳定下来再确认」时开(如「消失确认」)。
⚠️ 本项目 src/config.py 设了 "wait_until_settle_time": 0,
所以框架侧的 settle_time=-1 在本项目等价于 0。但它是用户可配置项,改成非 0 就会分叉。
计时用 active_time()
暂停感知——脚本暂停期间不消耗 time_out。这受 BaseGameTask 覆写。
条件命中与动作之间会重取一帧
fresh = condition.detect(self.next_frame())
target = fresh if fresh is not None else hit
原因:settle_time > 0 时动作发生在条件命中之后若干毫秒,会动的目标那时可能已位移。
取不到则沿用旧命中值,不因单帧抖动放弃。
draw=True 的调试框
- 绿 = 动作目标(A),蓝 = 持续目标(B)
- 框 4 秒过期,长循环里会重复绘制
- ⚠️ OCR 判据不要重复画——
ocr()内部自带调试框
返回 False 的含义
阶段三也走完仍未成功。调用方据此决定重试、跳过或记录失败,
不要在里面再写一层 while True——重试逻辑已经由 max_attempts / time_out 表达了。
判据必须接受外部 frame
自定义判据若自己调 next_frame(),一次判定内会多次截图导致时序错乱。
必须写成 detect(self, frame),用传入的 frame。
9. 什么时候不该用它
| 场景 | 该用什么 |
|---|---|
| 只等一个结果出现/消失 | wait_expectation |
| 只需要「等到就点」 | wait_click_feature / wait_click_ocr / wait_click_box |
| 一次性查一下有没有 | TemplateDetector(...).detect(frame) 直接调 |
| 就是要固定延时 | self.sleep(n)(但优先用视觉等待) |
| A 未命中要做恢复动作 | safe_back 之类,或 wait_action_result + raise_if_not_found |
⚠️ 等待只应发生在「即将点击的那一次」。给「查找」类函数加 settle_time 会让每帧都变慢。
10. 真实落地样例
TreasureUnlockTask._click_band(src/tasks/trigger/treasure_unlock_task.py)
点条带后用「稳定消失」确认生效——只用到 C,所以走 wait_expectation:
def _click_band(self, band: Box):
"""点击条带,并用「稳定消失」确认生效。"""
self.click(band)
gone = self.wait_expectation(
PredicateDetector(
lambda frame: not self._band_present(frame, band),
box=band,
name=f"band_gone_y{band.y}",
),
time_out=float(self.config.get("_消失确认超时(秒)", 3.0)),
settle_time=float(self.config.get("_消失确认时长(秒)", 0.35)),
)
if not gone:
self._on_click_failed(band)
return
self._active_bands.remove(band)
self.log_info(f"条带 y={band.y} 已消失,剩余 {len(self._active_bands)} 条")
要点:
PredicateDetector承载「条带还在不在」这段自有逻辑(用presence()而非find(), 因为钥匙压在条带上会把它切成两段);settle_time=0.35要求连续 0.35 秒都不见才算消失——这正是 settle 该用的地方;- 超时与时长都是
_前缀隐藏配置项。
相关文档
| 文档 | 内容 |
|---|---|
development.md |
目录职责、各检测器实现细节、i18n、配置迁移 |
quickstart.md |
从源码运行、开发环境搭建 |
tests/test_action_lifecycle.py |
58 例行为固化,改 API 时会被它挡住 |