Первый скрипт почти никогда не работает с первого раза. Плохая новость: kOS не подсвечивает ошибки заранее. Хорошая: у сбоев всего пять-шесть типовых причин, и они узнаются с одного взгляда.
Базовый синтаксис — в kOS: основы, структура больших скриптов — в kOS: продвинутое.
Скрипт «висит» — забыл WAIT
Самая частая ошибка новичка:
UNTIL SHIP:APOAPSIS > 80000 {
PRINT SHIP:APOAPSIS.
}
Цикл крутится внутри одного кадра игры. kOS исполняет отведённое число инструкций, упирается в лимит, и с точки зрения игры процессор просто занят навсегда. Апоцентр при этом не растёт: физика не тикает, пока цикл не отдаст управление.
Лечится одной строкой:
UNTIL SHIP:APOAPSIS > 80000 {
PRINT SHIP:APOAPSIS.
WAIT 0.
}
WAIT 0. означает «отдай кадр игре и продолжи со следующего». Это не пауза, это точка передачи управления. Любой цикл ожидания обязан содержать WAIT — либо WAIT 0., либо осмысленную задержку вроде WAIT 0.1..
Ещё лучше, когда ждёшь одного условия, — вообще без цикла:
WAIT UNTIL SHIP:APOAPSIS > 80000.
Лимит инструкций за кадр
Сколько кода kOS успевает выполнить за один тик, задаётся параметром IPU (instructions per update). В настройках сложности «Оператора» он стоит на 200.
PRINT CONFIG:IPU. // посмотреть текущее
SET CONFIG:IPU TO 500. // поднять из скрипта
| IPU | Когда |
|---|---|
| 50–200 | по умолчанию; хватает на взлёт, циркуляризацию, простые автопилоты |
| 500 | тяжёлая математика в цикле: подбор угла, перебор вариантов |
| 1000+ | почти всегда признак того, что цикл надо переписать, а не разогнать |
Поднимать IPU можно, но помни: это отнимает кадры у игры. Просадка FPS во время работы скрипта — почти всегда следствие большого IPU или цикла без WAIT.
Скрипт душит сам себя
Два эффекта, которые не выглядят как ошибка, но портят управление:
Тяжёлый LOCK при низком IPU. Если в LOCK STEERING стоит функция с векторной математикой, при штатных 200 инструкциях за кадр она не успевает пересчитаться каждый физический тик — руль отстаёт от аппарата. Поднимай на время манёвра:
SET IPU_SAVE TO CONFIG:IPU.
SET CONFIG:IPU TO 800.
// ... полёт ...
SET CONFIG:IPU TO IPU_SAVE.
Лог на каждом тике. Запись строки в файл каждый проход цикла — это тысячи операций ввода-вывода за полёт, и они отъедают у управления больше, чем сам расчёт. Ограничивай частоту:
IF TIME:SECONDS < LOG_NEXT { RETURN. }
SET LOG_NEXT TO TIME:SECONDS + 0.5.
Аппарат не слушается руля
Три причины, в порядке частоты:
1. SET вместо LOCK. SET STEERING TO ... зафиксирует одно вычисленное направление и забудет его обновлять. Руль и тяга — всегда через LOCK.
2. Скрипт закончился, а блокировка осталась. После завершения программы kOS снимает свои LOCK, и аппарат резко теряет заданный курс. Если хочешь оставить его стабилизированным — верни управление явно:
UNLOCK STEERING.
UNLOCK THROTTLE.
SET SHIP:CONTROL:NEUTRALIZE TO TRUE.
SAS ON.
3. Нет связи. Беспилотник без канала до Кербина получает только частичное управление, и скрипт не сможет ни дать тягу, ни поставить манёвр — молча. Прежде чем чинить код, проверь связь: Диагностика связи.
Аппарат виляет и раскачивается
Рулевой менеджер kOS настроен на «среднюю» ракету. Лёгкий зонд с мощными двигателями ориентации он перекручивает, тяжёлую ступень — не докручивает.
SET STEERINGMANAGER:MAXSTOPPINGTIME TO 2. // мягче на лёгких аппаратах
SET STEERINGMANAGER:ROLLCONTROLANGLERANGE TO 5. // не гоняться за креном
MAXSTOPPINGTIME — насколько заранее менеджер начинает тормозить вращение. Больше значение — плавнее и медленнее. Если аппарат «дрожит» вокруг цели, увеличь; если лениво доворачивает — уменьши.
Отдельная классика: раскачка начинается после отделения ступени, когда масса упала втрое, а настройки остались прежними. Пересчитывай их на каждом этапе полёта.
Ошибки парсера
kOS показывает номер строки — с него и начинай. Что встречается чаще всего:
| Сообщение | Что на самом деле |
|---|---|
Expected end of statement |
забыта точка в конце строки |
Undefined variable |
опечатка в имени или переменная объявлена внутри другого блока |
Suffix X not found on object Y |
суффикса нет у этого типа — например SHIP:ETA вместо ETA:APOAPSIS |
Cannot iterate |
пытаешься пройти циклом по не-списку |
Tried to push infinity onto the stack |
деление на ноль в формуле — обычно нулевая тяга или нулевая масса |
Not allowed to SET a name that will clobber the BUILTIN_FUNCTION 'r' |
переменная затирает встроенную функцию |
Точка после каждой команды — не украшение, а разделитель. Больше половины «непонятных» ошибок это она.
Имена, которые нельзя занимать
kOS не даст объявить переменную или параметр с именем встроенной функции или встроенной переменной — скрипт не запустится вовсе:
Not allowed to SET a name that will clobber or hide
the BUILTIN_FUNCTION called 'v'.
Ловушка в том, что это самые естественные имена. На одной библиотеке из девяти модулей мы поймали три штуки подряд:
| Хотелось назвать | Кто занял | Как назвали |
|---|---|---|
r — радиус |
R(), поворот |
rad |
v — вектор скорости |
V(), конструктор вектора |
vel |
alt — высота |
встроенная ALT |
hgt |
path — путь к файлу |
PATH() |
fpath |
Полный список того, что стоит проверять до запуска: односимвольные R, V, Q; переменные ALT, ETA, TIME, BODY, SHIP, STATUS, TARGET, STAGE, THROTTLE, STEERING, SAS, RCS, PANELS, GEAR, LIGHTS, CONFIG, TERMINAL, CORE; функции NODE, LIST, LEX, PATH, MIN, MAX, ROUND, HEADING, LATLNG, VANG, VDOT, VCRS, VXCL, ANGLEAXIS, LOOKDIRUP.
Ошибка вылезает при компиляции, то есть до первой команды двигателю. Поэтому дешевле всего ловить её прогоном модуля на стартовом столе — секунда против потерянной ракеты.
Одна деталь — несколько модулей
PART:GETMODULE("имя") возвращает первый подходящий модуль. Если в детали их два — например, магнитометр и датчик радиации в одной мачте, — второй потеряется молча. Перебирай по индексу:
LOCAL i IS 0.
UNTIL i >= p:MODULES:LENGTH {
IF p:MODULES[i] = "ModuleScienceExperiment" { LOCAL m IS p:GETMODULEBYINDEX(i). ... }
SET i TO i + 1.
}
Печать без мусора
Обычный PRINT прокручивает терминал и через минуту превращает его в кашу. Печатай в фиксированные позиции:
PRINT "Ap: " + ROUND(SHIP:APOAPSIS/1000,1) + " км " AT (0,3).
PRINT "Pe: " + ROUND(SHIP:PERIAPSIS/1000,1) + " км " AT (0,4).
PRINT "Дв: " + ROUND(SHIP:AVAILABLETHRUST,1) + " кН " AT (0,5).
Хвост из пробелов обязателен: строка не стирается, а перезаписывается, и остатки длинного прошлого значения повиснут на экране.
Для разбора после полёта печать бесполезна — терминал не сохраняется. Пиши в файл: Телеметрия: разбирать полёт по числам.
Внешний редактор
Править .ks в игровом терминале — мучение. Архив (том 0:) — это обычная папка:
<KSP>/Ships/Script/
Открывай её любым редактором. kOS перечитывает файл при каждом RUNPATH, перезапуск игры не нужен — сохранил, переключился в игру, запустил заново.
Схема, которая экономит больше всего времени:
- Редактор и игра на двух половинах экрана.
- Быстрое сохранение (F5) перед прогоном.
RUNPATH("0:/ascent.ks").в терминале.- Не полетело — F9, правка, снова с шага 3.
Стрелка вверх в терминале повторяет прошлую команду, так что шаг 3 — это два нажатия.
Отдать эту петлю ассистенту, который сам правит файлы в этой папке, — см. Подключаем ИИ-агентов. А если включить telnet-сервер kOS, тот же ассистент сможет и запускать скрипт, и читать ошибки сам: kOS по telnet: пульт снаружи игры.
Аварийный выход
Три уровня, от мягкого к жёсткому:
| Что | Как | Когда |
|---|---|---|
| Прервать программу | Ctrl+C в окне терминала |
скрипт пошёл не туда, аппарат цел |
| Отобрать управление | пункт Аварийное подавление в меню детали | скрипт держит руль и не отдаёт |
| Откатиться | F9 | уже поздно |
И правило, которое отменяет необходимость во всех трёх: быстрое сохранение перед каждым прогоном.
Защита прямо в коде
Хорошая привычка — не полагаться на свою реакцию, а зашивать пороги в скрипт:
// глушим двигатель, если на подъёме пошли вниз
WHEN SHIP:VERTICALSPEED < -5 AND SHIP:ALTITUDE < 70000 THEN {
LOCK THROTTLE TO 0.
PRINT "АВАРИЯ: падаем на подъёме".
}
// выход по таймауту, чтобы цикл не крутился вечно
SET deadline TO TIME:SECONDS + 600.
UNTIL готово OR TIME:SECONDS > deadline {
WAIT 0.1.
}
Разбор триггеров WHEN/ON — в kOS: продвинутое.
Чек-лист перед первым прогоном
- Точка в конце каждой команды.
WAITвнутри каждого цикла ожидания.- Руль и тяга через
LOCK, а неSET. - В конце —
UNLOCKи возврат управления. - Порог аварийного прерывания есть.
- Быстрое сохранение сделано.
- Аппарат дешёвый.