Перейти к содержимому

Типизированная сеть

У Reflex-модуля две отдельные среды. Клиент не может напрямую вызвать серверную функцию, а Reflex — напрямую обновить клиентский интерфейс. Сетевой канал передаёт между ними небольшие сообщения. TypeScript проверяет имя и объявленную форму данных, но на границе всё равно действуют правила транспорта.

Обычная схема: клиент сообщает о намерении, Reflex принимает игровое решение, после чего возвращает подтверждённое состояние клиенту.

Храните типы сообщений в src/shared/, чтобы обе стороны импортировали один и тот же контракт:

src/shared/messages.ts
export type ClientToServer = {
setEnabled: { enabled: boolean };
selectTarget: { agentId: number | undefined };
};
export type ServerToClient = {
stateChanged: { enabled: boolean; changedAtTick: number };
rejected: { reason: string };
};

Ключи — имена сообщений, значения — их данные. Неизвестное имя, пропущенное поле или поле неправильного типа становятся ошибкой до запуска игры.

Первый generic всегда описывает то, что текущий файл отправляет, второй — то, что он принимает. Поэтому на сервере их порядок меняется:

src/client/network.ts
import { defineNetwork } from "@killscript/sdk/client";
import type { ClientToServer, ServerToClient } from "../shared/messages";
export const coreNetwork =
defineNetwork<ClientToServer, ServerToClient>("core");
src/server/network.ts
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.