Типизированная сеть
У Reflex-модуля две отдельные среды. Клиент не может напрямую вызвать серверную функцию, а Reflex — напрямую обновить клиентский интерфейс. Сетевой канал передаёт между ними небольшие сообщения. TypeScript проверяет имя и объявленную форму данных, но на границе всё равно действуют правила транспорта.
Обычная схема: клиент сообщает о намерении, Reflex принимает игровое решение, после чего возвращает подтверждённое состояние клиенту.
Опишите один протокол
Заголовок раздела «Опишите один протокол»Храните типы сообщений в src/shared/, чтобы обе стороны импортировали один и
тот же контракт:
export type ClientToServer = { setEnabled: { enabled: boolean }; selectTarget: { agentId: number | undefined };};
export type ServerToClient = { stateChanged: { enabled: boolean; changedAtTick: number }; rejected: { reason: string };};Ключи — имена сообщений, значения — их данные. Неизвестное имя, пропущенное поле или поле неправильного типа становятся ошибкой до запуска игры.
Подключите оба направления
Заголовок раздела «Подключите оба направления»Первый generic всегда описывает то, что текущий файл отправляет, второй — то, что он принимает. Поэтому на сервере их порядок меняется:
import { defineNetwork } from "@killscript/sdk/client";import type { ClientToServer, ServerToClient } from "../shared/messages";
export const coreNetwork = defineNetwork<ClientToServer, ServerToClient>("core");import { defineNetwork } from "@killscript/sdk/server";import type { ClientToServer, ServerToClient } from "../shared/messages";
export const coreNetwork = defineNetwork<ServerToClient, ClientToServer>("core");defineNetwork() должен инициализировать const на верхнем уровне файла. На
обеих сторонах укажите одинаковое пространство имён. Оно позволяет нескольким
независимым протоколам использовать одинаковое имя вроде stateChanged без
конфликта.
Имя сообщения в каждом вызове send() и on() должно быть строковым литералом
прямо в месте вызова. Для динамического выбора сначала сделайте ветвление, а не
передавайте каналу имя из переменной.
Отправьте намерение и верните результат
Заголовок раздела «Отправьте намерение и верните результат»// Клиентский компонентlet confirmedEnabled = false;
coreNetwork.on("stateChanged", ({ enabled }) => { confirmedEnabled = enabled; updateStatusLabel(enabled);});
controls.toggle.onPressed(() => { coreNetwork.send("setEnabled", { enabled: !confirmedEnabled, });});// Серверный компонентlet enabled = false;
coreNetwork.on("setEnabled", (message) => { enabled = message.enabled; applyServerState(enabled);
coreNetwork.send("stateChanged", { enabled, changedAtTick: Time.Tick, });});Клиент просит; Reflex решает и применяет; клиент показывает подтверждённый результат. Не добавляйте круговой запрос к клиенту для решения, которое Reflex способен принять по серверному состоянию самостоятельно.
Зачем сообщения объединяются
Заголовок раздела «Зачем сообщения объединяются»Проверка игры показала: из нескольких Network.SendTable, вызванных в одном
тике симуляции, нативный транспорт сохраняет только один. SDK складывает все
вызовы send() этого тика в одну упорядоченную пачку. Получатель раскрывает её
и вызывает каждый подходящий обработчик в исходном порядке.
coreNetwork.send("stateChanged", firstState);coreNetwork.send("stateChanged", secondState);coreNetwork.send("stateChanged", finalState);// Получатель увидит все три сообщения в этом порядке.Несколько defineNetwork() используют один нативный приёмник и одну исходящую
пачку. Дополнительную очередь вокруг них строить не нужно.
Правила для данных
Заголовок раздела «Правила для данных»Сообщение может содержать строки, числа, boolean, вложенные обычные объекты и
плотные массивы из этих значений. Поле объекта со значением undefined
нативная сериализация пропускает, а undefined внутри массива типы отклоняют.
Защита SDK также отклоняет функции и нативные объекты с методами, например
Agent и Vector3.
Не передавайте никакие нативные userdata, в том числе структуры только с данными, внешне похожие на обычный TypeScript-объект. Структурная система типов не умеет распознать каждую такую обёртку. Вместо неё передайте стабильные простые поля: ID сущности или компоненты вектора.
on() регистрирует обработчик на время жизни всего модуля и намеренно не
возвращает подписку. Проверяйте активность компонента внутри обработчика или
перенаправляйте сообщение в его текущее состояние.
Как выбрать владельца логики, описано в разделе Клиент и Reflex.