メンタルモデル
スナップショット、コマンド、レシート、イベント、ティック、バッチ。この 6 つの概念を理解すれば、API がなぜこの形なのか、どう書けば速くなるのかが分かります。
スナップショット:読み取り、待ち時間ゼロ
ランタイムは 50 ms ごとにゲームスレッド上でワールド全体を収集し、共有メモリに書き込みます。g.snapshot() で得られるのは、完全で一貫したワールドです。
- 16 のプレイヤースロット:ゴールド、木材、人口、上限、累計採集量、種族。
- 最大 1024 ユニット:型、所有者、座標、HP / マナ(上限を含む)、現在のオーダーとオーダーのターゲット、実際に攻撃している相手(タスクターゲット)、ヒーローのレベル / 経験値 / スキルポイント、各プレイヤーからの可視性。
- 最大 256 件のユニット詳細:12 個のアビリティ(レベル、残りクールダウン)、8 個の buff、6 スロットのインベントリ。
- 地面のアイテム、木(2 秒ごとに更新)、生産テーブル(訓練 / 研究 / 建設 / アップグレードの進捗)、ゲームクロック、ゲーム内時刻。
1 回の読み取りは約 0.4 ms(Python でのパース)で、ゲームスレッドを待ちません。だから、好きなだけ読んでください。g.units()、g.my_army()、g.cooldown()、g.inventory() などの API はすべて同じスナップショットから取り出すので、1 ティックで何回呼んでもほとんどコストはかかりません。
発行周期は g.set_publish_period(ms) で 16 ~ 1000 ミリ秒の範囲で調整できます。1 回の収集はゲームスレッド上で約 0.5 ~ 0.9 ms なので、33 ms でも問題ありません。値はマシン全体で共有され、最後に書き込んだものが有効になります。
コマンド:書き込み、約 1 フレーム
g.move / attack / gather / build / train / cast … はゲームスレッドに渡されて実行されます。ランタイムはゲームスレッドのイベントディスパッチの中で、クライアントが送信したコマンドをまとめて実行します。そのため、コマンド 1 件の待ち時間は約 1 フレームです(イベントがまとまって処理されるタイミングに当たれば約 0.1 ms、そうでなければ次のディスパッチまで待ちます)。
- コマンドにはユニット 1 体またはリストを渡せます。リスト内のユニットには同じフレームでまとめて命令が出ます。
queue='after'を付けると Shift キューになり、今の作業を終えてから実行します。- コマンドのユニットにはスナップショットから取得したオブジェクトをそのまま使えます。SDK はハンドルペアで同一性を確認します(アドレスは新しいユニットに再利用されますが、ハンドルは再利用されません)。
レシート:すべてのコマンドに付く
r = g.build(worker, "hbar", x, y)
if r: # エンジンが受理した
...
else:
r.reason # 'rejected(金不够)'(= ゴールド不足)
r.verdict # 8
r.exec_us # このコマンドがゲームスレッド上で実行に要したマイクロ秒数
レシートは同じフレームの中で読み戻されます:命令前後のユニットのオーダー、エンジン関数の戻り値、実行可否チェックの理由コード。レシートが答えるのは「エンジンがこのコマンドを受理したか、しなかったならなぜか」であり、「最終的に成功したか」には答えません —— 成功したかどうかはスナップショットとイベントで確認します。
すべてのステータスコードと理由コードは レシートと理由コード を参照してください。
イベント:何が起きたか
on_event(g, ev) は毎ティックの on_tick の前に、前回のティック以降のイベントを 1 件ずつ渡します。
| イベント | 意味 |
|---|---|
unit.appeared / unit.died / unit.removed | ユニットの出現、死亡、消滅(金鉱に入る、変換される、死体が朽ちるのも消滅に含まれ、死亡と同じではありません) |
unit.damaged / order.changed / owner.changed | HP の減少、オーダーの変更、所有者の変更 |
hero.levelup | ヒーローのレベルアップ |
item.appeared / item.removed | 地面のアイテムの出現、拾われた・使われた |
damage | エンジンレベル:1 発ごとのダメージ。ダメージ元のユニット、攻撃タイプ、ダメージタイプ、実際に減った HP、アーマー適用前のダメージ |
killed | エンジンレベル:この 1 発でとどめを刺した。倒したユニット付き |
production.done | 訓練 / 研究 / 建設 / アップグレードの完了。4 文字コードとかかったゲーム秒数付き。相手のものも含まれる |
spell.cast | ユニットがスキルを使った:スキルの 4 文字コード、レベル、クールダウン秒数、詠唱地点 |
message | 画面のメッセージ枠に 1 件表示された:ゲームのヒント(「Farm がもっと必要です」)、チャット(.chat に発言者と内容)、システムメッセージ |
selection.changed / player.left | ローカルプレイヤーの選択が変わった / プレイヤーが退出した、または敗北判定で除外された |
game.started / game.ended | 新しい試合の開始 / 試合から退出 |
キャンバスのボタンのクリック、ホットキー、地面のクリックといった入力イベントについては UI と入力 を参照してください。
イベントストリームはグローバルです。相手の生産完了やクリープの死亡も含まれます。ev.owner やユニットのハンドルでフィルタリングしてください。
ティック:Bot のリズム
on_tick はデフォルトで毎秒 5 回(実時間)呼ばれます。1 ティックの所要時間はほぼあなた自身の計算時間だけです:スナップショットは待ち時間ゼロ、コマンドは約 1 フレーム。1 ティックが周期を超えると自動的に後ろにずれ、どんどん溜まっていくことはありません。
- 2 倍速では実時間で待たないでください。 3 ゲーム秒待ちたいなら、
g.clock()が 3 進んだかを見ます。sleep(1.5)は使いません。 on_tickの中でsleepしないでください。 「少し後でやる」必要があるなら、現在のゲーム時刻を記録しておき、次のティックで判定します。
バッチ:数十件のコマンドでも待つのは 1 回
1 ティックで多くのコマンドを出すときは、with g.batch(): で囲みます。
with g.batch():
g.attack(melee, target_a)
g.attack(ranged, target_b)
g.move(wounded, home.x, home.y)
g.cast(hero, "thunderclap")
# ブロック終了時にバッチ全体を送信:同じフレームで実行され、ゲームスレッドを待つのは 1 回だけ
- ブロック内のコマンドは
Pendingを返し、ブロック終了後にレシートになります。ブロック終了前に読むと例外が発生します。 - ブロック内で例外が発生すると、バッチ全体が破棄されます(中途半端なコマンドは、送らないより危険です)。
- 実測(移動 8 件):1 件ずつなら 68 ~ 99 ms、バッチなら 6.5 ~ 10 ms。
同じ考え方はクエリにも使えます:g.can_do_many([(u, code), ...])、g.tech_many([...]) で一度にまとめて問い合わせます。
自分が書いたばかりのものを読む
同じティック内では、スナップショットにはまだ直前に出したコマンドが反映されていません(次の発行で追いつきます)。そのため、2 つのロジックが同じワーカーを取り合うことがあります。片方が Farm を建てに行かせたばかりなのに、もう片方はスナップショットを見て、まだ手が空いていると判断してしまうのです。
g.order_of(u) がこの問題を解決します。スナップショットが追いつくまでは、レシートにある新しいオーダーを優先します。「手が空いているか」の判定には u.order ではなく g.order_of(u) を使ってください。 g.idle_workers() は「このティックで仕事を割り当てられたばかり」のワーカーをすでに除外しています。
レイテンシの段階
| 段階 | 経路 | レイテンシ | 用途 |
|---|---|---|---|
| 0 | プッシュスナップショット + イベントストリーム | 1 回の読み取り約 0.4 ms。データは 50 ms ごとに更新 | すべての「見る」API |
| 1 | ファストレーン | 約 1 フレーム。6 プロセス同時実行時の中央値 0.06 ms | すべてのコマンドとクエリ(SDK のデフォルト) |
| 2 | コントロールチャネル | 20 ~ 40 ms | フォールバック、一部の UI 系操作(ゲーム速度、吹き出し、メッセージ) |
| 3 | ゲートウェイ(WebSocket / JSON) | 段階 1 + 約 1 ms | 任意の言語、ブラウザ、LLM、別のマシン上のプログラム |
API カタログ では、各 API がどの段階を使うかを示しています。