UI と入力
キャンバス上のボタンや選択カードをクリック可能にし、ホバーで自動的にハイライトします。ホットキーの登録、地面のクリックによる位置指定、マウスが指している場所の取得、ローカルプレイヤーが何を選択しているかの把握ができます。クリック、ホットキー、スキル使用、チャット全文、プレイヤーの退出は、すべてイベントストリームに入ります。
キャンバス で描いたものが、クリックできるようになりました。ランタイムがゲームウィンドウの入力を横取りし、外部プログラムは次のことができます。
| 機能 | 概要 | ゲームに届くか |
|---|---|---|
| クリック可能なキャンバス要素 | ボタン、選択カード、パネル:クリックで 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("レベルアップ!報酬を 1 つ選ぼう", [("筋力 +5", "打たれ強くなる"), ("攻撃速度 +20%", "もっと戦える"), ("オオカミを召喚", "仲間が 1 体増える")],
pause=True, on_pick=lambda g, i: give(g, i)) # 画面中央にカードを 1 列表示。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 と ゲームプレイ MOD のランナーは毎ティック呼んでいます。コールバックを指定していないクリックは ui.clicks に入ります。コールバック内で例外が起きてもログに記録されるだけで、ほかのコールバックやイベントには影響しません。
キャンバスの低レベル API も直接使えます: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 4 文字コード、.b レベル、.value クールダウン秒数、.x .y 詠唱地点 |
message | 画面のメッセージ枠に 1 件表示された | .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() は別のカーソルを持つもう 1 つの読み口で、ゲームのヒント(「Farm がもっと必要です」「そこには建設できません」)もここに入ります。Bot を書くときにこれを使えば、コマンドがなぜ実行されなかったのかがわかります。
他の言語から使う
- ゲートウェイ:
ui.button、ui.choice、ui.hotkey、ui.mouse、ui.cursorなどのメソッドは、ゲートウェイ でも同じ名前で使えます。リモートからはコールバック関数を渡せないので、クリックとホットキーはイベントのプッシュから受け取ります(ui.clickイベントにはkeyが付きます)。 - 共有メモリに直接書き込む:まずセマンティックコマンド
input_enable(W3P オペコード 74)を送ると、ランタイムが入力の横取りを始めます。入力ブロックLocal\War3Input_<pid>に、あなたはホットキー表とマウスのオン・オフを書き込み、ランタイムはマウス位置、指している地面の地点、ホバー中の要素を書き戻します。キャンバス要素のフラグビット0x40は「インタラクティブ」を表します。レイアウトは W3P プロトコル を参照してください。
複数のプログラムで同時に使う
MOD、Farsight、MCP、ゲートウェイの各セッションが、1 つの試合に同時にボタンを置いたりホットキーを登録したりしても、互いに干渉しません。
- 各プログラムは自分のホットキーと地面クリックのオン・オフを登録し、SDK が全員の分を 1 つの表にまとめてランタイムに渡します。同じキーは 1 件だけ残し、イベントは全員に送られ、各プログラムはキーで自分のホットキーを見分けます。
ui.close()が取り下げるのは自分の分だけで、最後のプログラムがいなくなったときにウィンドウの入力を返します。- プログラムが強制終了され、後片付けが間に合わなかった場合:ランタイムは 2 秒ごとに確認し、登録したプログラムがすべて終了していれば、残されたホットキーと地面クリックの横取りを消します。それらが描いたボタンもクリックを横取りしなくなります。
実測
2026-09-25、テストインスタンスでの実機検証 16/16:
- ボタンをクリック →
ui.click+ コールバック。ランタイムの横取りカウンタが +1(ゲームにはこのクリックが届いていない)。ボタンの外をクリックしても発火しない。 - F6 →
hotkey。地面をクリック →mouse.world(握りつぶされる)。 - パラディンを 1 体出して選択 →
selection.changed、g.selection()と一致。Divine Shield を使う →spell.cast('AHds', 1, 35.0)。 - マップのテキスト →
message。チャット →message、.chatから発言者と内容を解析。 - コンピューターを敗北判定 →
player.left。試合を終了 →game.ended。
人がマウスでボタンをクリックしたときや、ホバー時のハイライトも 1 つずつ目で確認しました。
制約と注意点
- 位置は実際のマウスで決まる:ゲーム自身がシステムカーソルから位置を読むので、ホバーや
cursor()は実際のマウスを反映します。横取りするのはボタンやキーの入力だけです。 - マウスカーソルの下に描かれる:Warcraft はカーソルを画面の一部として毎フレーム描き込みます。キャンバスも頭上の吹き出しも、ゲームがカーソルを描く手順の直前に描かれます。そのためゲームの UI の上に重なり、カーソルがさらにその上に重なります。そのフレームでカーソルが描かれない(非表示やカットシーン中の)ときだけ、最後の手順で描く方式に戻ります。
- システムのスケーリング:自分でテストを書き、ウィンドウメッセージでクリックを送る場合、DPI を認識しないプロセスが送った座標はシステムによって拡大されます(150% スケーリングで実測 ×1.5)。テストプログラムでは先に DPI 対応を宣言してください。人による実際のクリックには影響しません。
- 初回はフォントのウォームアップが必要で、約 1 秒かかります。その間はボタンがまだ描かれていないので、クリックできません。
- 試合中でないときは地面のクリックを報告しない:メインメニューとリザルト画面では、
mouse.worldは発行せず、握りつぶしもしません。 - 押した時点で握りつぶした 1 回は、離す前にほかのプログラムに切り替えたり、マウスをウィンドウの外へドラッグしたりした場合もリセットされます。次にボタンを離す操作まで握りつぶしてしまうことはありません。
- 1.27 にはゲームの UI フレームを新しく作る関数がありません(1.31 から)。ここでのボタンやカードはすべてランタイムが描いているので、見た目は自由ですが、ゲーム自身のメニュー階層には現れません。