Ментальная модель
Снимок, команда, квитанция, событие, тик, пачка. Разберитесь в этих шести понятиях — и станет ясно, почему интерфейсы устроены именно так и как писать быстрый код.
Снимок: чтение без ожидания
Каждые 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() уже исключает тех, кому работу дали в этом тике.
Уровни задержки
| Уровень | Канал | Задержка | Для чего |
|---|---|---|---|
| 0 | push-снимок + поток событий | чтение копии около 0.4 ms; свежие данные каждые 50 ms | все интерфейсы «наблюдения» |
| 1 | быстрая полоса | около 1 кадра; медиана 0.06 ms при 6 параллельных процессах | все команды и запросы (по умолчанию в SDK) |
| 2 | канал управления | 20 ~ 40 ms | запасной путь и немногие операции с интерфейсом (скорость игры, реплики, сообщения) |
| 3 | шлюз (WebSocket / JSON) | уровень 1 + около 1 ms | любые языки, браузер, LLM, программы на другой машине |
В каталоге API для каждого интерфейса указано, через какой уровень он работает.