Архитектурные паттерны
Хорошая архитектура модуля в основном отвечает на два вопроса:
Компонент — самостоятельная возможность модуля со своим состоянием и обработчиками: например HUD, свободная камера или маркер позиции.
Хорошая архитектура отвечает на два вопроса:
- какому компоненту принадлежит это состояние или обработчик?
- когда принадлежащий ему ресурс должен перестать существовать?
Компилятор уже объединяет файлы и изолирует имена. Тяжёлый фреймворк,
контейнер зависимостей или один огромный main.ts не нужны.
Один файл — один цельный компонент
Заголовок раздела «Один файл — один цельный компонент»Небольшой всегда активный компонент регистрируется на верхнем уровне:
import { logger } from "@killscript/sdk/client";
const log = logger("DamageHint");
Agents.OnLocalPlayerReceivedDamage((event) => { NotificationController.ShowHint( `Received ${event.Damage} damage`, 1.5, ); log.debug(`Damage tick: ${event.DamageTick}`);});client/main.ts только импортирует файл. Состояние, нужное одному компоненту,
остаётся в этом же файле и не экспортируется.
Scope для включаемого компонента
Заголовок раздела «Scope для включаемого компонента»Всё созданное при включении должно исчезнуть при выключении. Scope делает владение явным:
import { Keyboard, type ClientScope, defineControls, scheduler, scope, visuals,} from "@killscript/sdk/client";
const controls = defineControls("positionMarker", { toggle: Keyboard.F6,});
let active: ClientScope | undefined;
controls.toggle.onPressed(() => { if (active !== undefined) { active.dispose(); active = undefined; return; }
const local = Agents.GetLocalAgent(); if (local === undefined) return;
const next = scope(); const marker = next.world(visuals.overlay({ position: local.Movement.Position, size: new Vector3(0.6, 0.02, 0.6), color: new Color(0, 1, 1, 0.5), })); if (marker === undefined) { next.dispose(); return; }
next.using(scheduler.every(0.25, () => { const current = Agents.GetLocalAgent(); if (current !== undefined) { marker.SetPosition(current.Movement.Position); } })); active = next;});Переменная отвечает, включён ли компонент. Scope отвечает, что нужно очистить при выключении. Порядок очистки и серверный вариант описаны в разделе Scope и жизненный цикл компонента.
Разделяйте контракты, а не владение ресурсами
Заголовок раздела «Разделяйте контракты, а не владение ресурсами»В shared-файлах находятся данные, имеющие одинаковый смысл в обеих средах:
export interface MarkerState { readonly enabled: boolean; readonly x: number; readonly y: number; readonly z: number;}Состояние UI принадлежит клиенту. Состояние серверного решения принадлежит
Reflex. Передавайте простые значения через
типизированный сетевой канал, а не
пытайтесь разделить Agent, обработчик или изменяемую переменную модуля.
Прямой поток данных
Заголовок раздела «Прямой поток данных»Большинству функций достаточно таких схем:
управление → состояние компонента → клиентское отображениеcontrol → сетевое намерение → Reflex → сетевой результат → отображениеигровое событие → состояние компонента → отображение или логНе создавайте глобальную шину событий, когда прямая функция или одно типизированное сетевое сообщение показывают связь понятнее.
Практические правила
Заголовок раздела «Практические правила»- точка входа остаётся списком импортов включённых компонентов;
- настройки и управление верхнего уровня находятся рядом со своим владельцем;
- переменная остаётся локальной, пока она действительно не нужна другому файлу;
- динамические подписки, таймеры и визуальные объекты принадлежат scope;
- владелец — клиент или Reflex — виден по папке и импорту SDK;
- если вспомогательный метод SDK не улучшает вызов, используется типизированный API игры;
- компонент удаляется вместе с одним импортом из точки входа и принадлежащими ему файлами.
Если файл уже обслуживает несколько несвязанных controls, независимых состояний или разных циклов очистки, разделите его по поведению. Категории вроде «все обработчики» и «все переменные» обычно только прячут владельца.