介面與輸入
畫板上的按鈕、選項卡片能點,滑鼠停在上面會自動醒目顯示;登記熱鍵、點地面選位置、讀取滑鼠指著哪裡、知道本機玩家選取了誰。點擊、熱鍵、放技能、聊天全文、玩家離開,都會進入事件流。
畫板 畫出來的東西,現在能點了。執行環境接管遊戲視窗的輸入,外部程式可以:
| 能力 | 一句話 | 遊戲收得到嗎 |
|---|---|---|
| 可點擊的畫板項目 | 按鈕、選項卡片、面板:點中時發出 ui.click,滑鼠停在上面自動醒目顯示 | 點在按鈕上的那一下收不到 |
| 熱鍵 | 登記 F5、ctrl+shift+Q 這樣的組合,按下時發出 hotkey | 可選擇吞掉(連同它產生的字元) |
| 點地面 | 點在世界上時發出 mouse.world,帶有地面座標 | 可選擇吞掉(「點一個位置放塔」) |
| 滑鼠位置 | 每幀更新:螢幕像素、指著的地面點、懸停的畫板項目 | —— |
| 選取 | 本機玩家選取了誰;一有變化就發出 selection.changed | —— |
全部都是本機輸入 + 本機繪製:不進入指令流,多人局也安全。但如果回呼裡改動了世界(刷單位、改屬性),那還是只能用於單人局。
Python:g.ui
ui = g.ui # 第一次使用時,執行環境接管視窗輸入
ui.button("shop", "買一瓶藥水(50 金)", screen=(40, 300), on_click=lambda g, ev: buy(g))
c = ui.choice("升級了!選一個獎勵", [("力量 +5", "更耐打"), ("攻速 +20%", "輸出更高"), ("召喚狼", "多一個幫手")],
pause=True, on_pick=lambda g, i: give(g, i)) # 螢幕中間一排卡片;pause=True 選擇時遊戲暫停
i = c.wait(timeout=30) # 也可以阻塞等待(期間照樣處理事件,不會遺失)
ui.hotkey("F5", lambda g, ev: g.say(hero, "收到!")) # 預設吞掉
ui.hotkey("ctrl+shift+Q", on_press=..., swallow=False)
ui.mouse(on_click, capture=True, buttons=("left", "right")) # 擷取地面點擊:左鍵、右鍵都回報,並吞掉
xy = ui.pick_point("點地面:塔要蓋在哪?") # 阻塞版:下一次左鍵點地面 -> (x, y);Esc 或逾時 -> None
ui.cursor() # {'screen': (x, y), 'world': (x, y, z) 或 None, 'hover': 'shop'}
ui.toast("第 3 波來了!", seconds=3)
ui.close() # 收起自己的控制項和熱鍵;沒有別的程式在用輸入時才交還視窗輸入
g.close() # 或者整個中斷連線(也可以寫 with Game(...) as g:)
回呼的參數是 (g, ev),在你呼叫 g.events() 時觸發 —— Bot 和玩法模組的執行器每拍都會呼叫。沒有給回呼的點擊會進入 ui.clicks。回呼裡拋出例外只會記入日誌,不影響其他回呼和事件。
也可以直接使用畫板底層:g.canvas.text(..., clickable=True, hover=顏色),點擊從事件流裡接收,ev.key 就是繪製時給的 key。畫可點擊的元素會自動開啟輸入,不必先碰 g.ui。
熱鍵寫法:F1 ~ F24、A ~ Z、0 ~ 9、numpad0 ~ numpad9、space enter esc tab backspace insert delete home end pageup pagedown left up right down,前面可以加上 ctrl+、shift+、alt+。
不帶修飾鍵的字母和數字會和聊天輸入、遊戲快速鍵互相衝突。優先使用 F5 ~ F8 這類遊戲沒占用的鍵,或者組合鍵。
新事件
g.events() 裡多了這些(完整欄位見 W3P 協定):
| kind | 什麼時候 | 便捷欄位 |
|---|---|---|
ui.click | 可互動的畫板項目被點擊 | .key 畫板 key、.button('left' / 'right')、.mods 修飾鍵 |
ui.hover | 滑鼠移入 / 移出畫板項目 | .key(移出時是 None) |
hotkey | 登記的熱鍵被按下 | .key 熱鍵寫法、.mods |
mouse.world | 開啟地面點擊後,點在世界上 | .x .y 地面座標、.button、.value(1 = 被吞掉了) |
selection.changed | 本機玩家的選取變了 | 用 g.selection() 取得單位 |
spell.cast | 單位放了技能(技能開始冷卻) | .spell 四字碼、.b 等級、.value 冷卻秒數、.x .y 施法點 |
message | 螢幕訊息框裡出現一條 | .text 全文、.frame 哪個框、.chat(是聊天時) |
player.left | 玩家離開或被判定落敗移除 | .player |
game.ended | 離開對局 | —— |
聊天和螢幕訊息
玩家在聊天框輸入的文字,直接從 message 事件的 .chat 讀取:
for ev in g.events():
if ev.kind == "message" and ev.chat and ev.chat["text"] == "-follow":
... # ev.chat = {'channel': '所有人', 'sender': '玩家名稱', 'text': '-follow'}
g.messages() 另有一份獨立的游標:遊戲提示(「需要更多的農場」「不能在那裡建造」)也在裡面。寫 Bot 時用它,就知道一條命令為什麼沒做成。
從其他語言使用
- 閘道:
ui.button、ui.choice、ui.hotkey、ui.mouse、ui.cursor這些方法在 閘道 上以同名提供。遠端無法傳入回呼函式,點擊和熱鍵從事件推送裡接收(ui.click事件帶有key)。 - 直接寫入共用記憶體:先送出語意命令
input_enable(W3P 操作碼 74),執行環境開始接管輸入;在輸入區塊Local\War3Input_<pid>裡,你寫入熱鍵表和滑鼠開關,它回寫滑鼠位置、指著的地面點和懸停項目。畫板項目的旗標位元0x40表示「可互動」。配置見 W3P 協定。
好幾個程式同時使用
模組、遠見、MCP、閘道的每個工作階段可能同時在一局遊戲裡放按鈕、登記熱鍵,彼此互不干擾:
- 每個程式登記自己的熱鍵和地面點擊開關,SDK 把大家的合併成一張表交給執行環境。同一個鍵只留一條,事件發給所有人,各自依鍵認出自己的熱鍵;
ui.close()只撤掉自己的,最後一個程式離開了才交還視窗輸入;- 程式被強制結束、來不及收尾:執行環境每 2 秒檢查一次,登記過的程式全都結束了,就清掉它們留下的熱鍵和地面點擊攔截,它們畫的按鈕也不再攔截點擊。
實測
2026-09-25,測試實例上的實機核對 16/16:
- 點按鈕 →
ui.click+ 回呼,執行環境的攔截計數 +1(遊戲沒收到這一下);點在按鈕外面不觸發; - F6 →
hotkey;點地面 →mouse.world(被吞掉); - 刷出一個聖騎士、選取它 →
selection.changed,g.selection()對得上;放神聖護甲 →spell.cast('AHds', 1, 35.0); - 地圖文字 →
message;聊天 →message,.chat解析出發言者和內容; - 判定電腦落敗 →
player.left;結束對局 →game.ended。
真人用滑鼠點按鈕、懸停醒目顯示也逐一看過。
邊界與注意事項
- 位置用的是真實滑鼠:遊戲本身依系統游標讀取位置,所以懸停、
cursor()反映的是真實滑鼠。攔截只管按鍵。 - 畫在滑鼠游標下面:魔獸把游標當作畫面的一部分,每幀畫進去。畫板和頭頂氣泡都在遊戲畫游標那一步之前繪製:蓋住遊戲介面,被游標蓋住。只有這一幀沒畫游標(隱藏或過場動畫)時,才退回到最後一步繪製。
- 系統縮放:自己寫測試、用視窗訊息投遞點擊時,未宣告 DPI 感知的行程送過去的座標會被系統放大(150% 縮放時實測 ×1.5)。測試程式要先宣告 DPI 感知。真人點擊不受影響。
- 第一次要預熱字型,約 1 秒。這段期間按鈕還沒畫出來,點不中。
- 不在對局裡時不回報地面點擊:主選單和結算畫面上
mouse.world不發出,也不吞掉。 - 按下時被吞掉的那一下,放開前切換到別的程式或滑鼠拖出了視窗,也會復位,不會連下一次放開也吞掉。
- 1.27 沒有建立新遊戲介面框架的函式(1.31 才有):這裡的按鈕、卡片都是執行環境畫的,樣式隨你定,但不會出現在遊戲本身的選單層級裡。