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 時會被它擋住 |