Документация Основные понятия

Ментальная модель

Снимок, команда, квитанция, событие, тик, пачка. Разберитесь в этих шести понятиях — и станет ясно, почему интерфейсы устроены именно так и как писать быстрый код.

Снимок: чтение без ожидания

Каждые 50 ms рантайм обходит в игровом потоке весь мир и записывает его в общую память. g.snapshot() возвращает полный и согласованный мир:

  • 16 слотов игроков: золото, древесина, пища, лимит, всего добыто, раса;
  • до 1024 юнитов: тип, владелец, координаты, здоровье / мана (с максимумами), текущий приказ и цель приказа, кого юнит реально атакует (текущая цель), уровень героя / опыт / очки навыков, видимость юнита для каждого игрока;
  • до 256 записей с деталями юнитов: 12 способностей (уровень, оставшаяся перезарядка), 8 баффов, 6 ячеек инвентаря;
  • предметы на земле, деревья (обновляются каждые 2 секунды), таблица производства (прогресс найма / исследований / постройки / улучшений), игровые часы, игровое время суток.

Чтение одной копии занимает около 0.4 ms (разбор на Python) и не ждёт игровой поток. Поэтому: читайте сколько угодно. Интерфейсы вроде g.units(), g.my_army(), g.cooldown(), g.inventory() берут данные из одного и того же снимка, и сколько бы раз вы их ни вызвали за тик, это дёшево.

Период публикации настраивается: g.set_publish_period(ms), от 16 до 1000 миллисекунд. Один сбор занимает в игровом потоке около 0.5 ~ 0.9 ms, так что и 33 ms — не проблема. Значение одно на всю машину; действует последнее записанное.

Команда: запись, около одного кадра

g.move / attack / gather / build / train / cast … выполняются в игровом потоке. Рантайм выполняет отправленные клиентом команды пачками внутри диспетчеризации событий игрового потока, поэтому одна команда ждёт около одного кадра (около 0.1 ms, если попадает в серию событий, иначе — до следующей диспетчеризации).

  • Команде можно передать один юнит или список; юниты из списка получают приказ в одном кадре;
  • 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 по одному передаёт вам события, накопившиеся с прошлого тика:

СобытиеЗначение
unit.appeared / unit.died / unit.removedЮнит появился, погиб, исчез (вход в золотой рудник, обращение, разложение трупа — тоже исчезновение, а не смерть)
unit.damaged / order.changed / owner.changedПолучил урон, сменил приказ, сменил владельца
hero.levelupГерой получил уровень
item.appeared / item.removedПредмет на земле появился, подобран или использован
damageУровень движка: каждый удар. Юнит-источник, тип атаки, тип урона, фактически снятое здоровье, урон до учёта брони
killedУровень движка: этот удар добил цель, с указанием убийцы
production.doneНайм / исследование / постройка / улучшение завершены, с четырёхсимвольным кодом и затраченными игровыми секундами. Приходит и для противника
spell.castЮнит применил способность: четырёхсимвольный код способности, уровень, перезарядка в секундах, точка применения
messageВ окне сообщений на экране появилась строка: подсказка игры («Нужно больше ферм»), чат (в .chat — отправитель и текст), системное сообщение
selection.changed / player.leftИзменилось выделение локального игрока / игрок вышел или удалён после поражения
game.started / game.endedНачалась новая партия / выход из матча

События ввода — нажатия кнопок холста, горячие клавиши, клики по земле — описаны в разделе Интерфейс и ввод.

Поток событий глобальный: в нём есть и завершённое производство противника, и гибель крипов. Фильтруйте по ev.owner или по дескриптору юнита.

Тик: ритм бота

По умолчанию on_tick вызывается 5 раз в секунду (по реальному времени). Время одного тика — это практически только ваши собственные вычисления: снимок читается без ожидания, команда занимает около кадра. Если тик не укладывается в период, следующий автоматически сдвигается, и отставание не накапливается.

  • На скорости 2× не ждите по реальному времени. Чтобы подождать 3 игровые секунды, смотрите, когда g.clock() вырастет на 3, а не вызывайте sleep(1.5).
  • Не вызывайте sleep в on_tick. Если нужно «сделать чуть позже», запомните текущее игровое время и проверьте в следующем тике.

Пачка: десятки команд — одно ожидание

Когда за тик нужно отдать много команд, оберните их в 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")
# в конце блока вся пачка отправляется разом: выполняется в одном кадре, игровой поток ждём один раз
  • Команды внутри блока возвращают Pending, который после конца блока превращается в квитанцию; чтение до конца блока выбросит ошибку;
  • если внутри блока выброшено исключение, вся пачка отменяется (половина команд опаснее, чем ни одной);
  • измерено на 8 командах движения: по одной — 68 ~ 99 ms, пачкой — 6.5 ~ 10 ms.

Тот же подход работает и для запросов: g.can_do_many([(u, code), ...]), g.tech_many([...]) спрашивают о многом за раз.

Чтение только что записанного

В пределах одного тика снимок ещё не видит отданных вами команд (он догонит их в следующей публикации). Две части логики могут не поделить одного рабочего: одна только что отправила его строить ферму, а другая по снимку считает, что он всё ещё свободен.

Эту проблему решает g.order_of(u): пока снимок не догнал, он берёт новый приказ из квитанции. Чтобы определить, простаивает ли юнит, используйте g.order_of(u), а не u.order. g.idle_workers() уже исключает тех, кому работу дали в этом тике.

Уровни задержки

УровеньКаналЗадержкаДля чего
0push-снимок + поток событийчтение копии около 0.4 ms; свежие данные каждые 50 msвсе интерфейсы «наблюдения»
1быстрая полосаоколо 1 кадра; медиана 0.06 ms при 6 параллельных процессахвсе команды и запросы (по умолчанию в SDK)
2канал управления20 ~ 40 msзапасной путь и немногие операции с интерфейсом (скорость игры, реплики, сообщения)
3шлюз (WebSocket / JSON)уровень 1 + около 1 msлюбые языки, браузер, LLM, программы на другой машине

В каталоге API для каждого интерфейса указано, через какой уровень он работает.