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

Общие пакеты

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

ПакетЧто даётВ скриптеВ плагинеВ расширении-компоненте
@was/ecsсущности, компоненты, мир
@was/engineклассы компонентов
@was/signalsсистему реактивности
@was/svdtсхемы и валидацию
@was/utilsнебольшие помощники
@was/uiсобственную библиотеку компонентов редактора
@was/iconsнабор иконок редактора
reactReact 19
Скрипты получают реактивность через ctx.effect

Скрипт не может импортировать пакет signals напрямую: ctx.effect — тот же механизм, но со сроком жизни, которым управляют за вас, так что эффект умирает вместе со своим объектом, а не утекает.

@was/signals — система реактивности

Это то, на чём построена вся платформа: значения, которые знают, кто их читает, так что изменение обновляет ровно то, что от него зависело, и ничего больше.

import { signal, computed, effect, batch, untracked } from '@was/signals';

const score = signal(0);
const doubled = computed(() => score.value * 2);

const stop = effect(() => {
render(score.value); // перезапускается только при изменении score
});

batch(() => {
score.value += 1;
score.value += 1; // эффекты выполняются один раз, а не два
});

stop();
ФункцияЧто делает
signal(v)значение, за которым можно следить; читается и пишется .value
computed(fn)производное значение, пересчитываемое, только когда меняется читаемое им
effect(fn)выполняется сейчас и снова при изменении прочитанного; возвращает функцию остановки
batch(fn)сгруппировать записи, чтобы наблюдатели сработали один раз в конце
untracked(fn)прочитать, не становясь зависимостью
ref(obj)сделать реактивным целый объект, до самых листьев
snapshot(obj)обычная, нереактивная копия
raw(obj)нижележащий объект, без отслеживания
readonly(obj)представление, в которое нельзя писать

Для React-панелей есть спутник с useSignal, useComputed, useSignalEffect и useLiveSignal, так что компонент перерисовывается от сигнала без всякой обвязки.

Почему это важно, даже если вы никогда это не импортируете

Именно это заставляет редактор и рантайм вести себя так, как они ведут: ничто не опрашивается, ничто не перерисовывается на всякий случай, а панель обновляется потому, что изменились данные, а не потому, что ей кто-то велел. → Почему этот движок

@was/ui — компоненты редактора

Панели плагинов и расширения-компоненты можно собирать из той же библиотеки, которой пользуется редактор, поэтому они выглядят родными, а не как встроенная веб-страница. Около тридцати компонентов:

Accordion · Avatar · Button · Card · Checkbox · Chip · CloseButton
DimensionInput · Draggable · Dropdown · Flag · Icon · IconButton · Input
Menu · Modal · Outside · Panel · Popover · Portal · Render · Scroll
Search · Section · Segmented · Select · Skeleton · Switcher · Tabs
Toast · Toolbar · Tooltip

Плюс @was/icons для набора иконок.

Таблица стилей собрана заранее

Панели используют фиксированный набор утилитарных классов, поставляемых с библиотекой. Произвольных значений вроде text-[13px] в нём нет — для размеров вне шкалы используйте инлайновый style.

@was/ecs — модель мира

Entity, Component, Space. Плагин читает и меняет сцену ровно тем же API, которым движок пользуется внутри; отдельного, урезанного «API для плагинов» не существует.

Space — мир

ВызовВозвращает
getEntity(id)одну сущность или ничего
hasEntity(id)существует ли она
getEntities()все сущности
queryEntities(A, B, …)каждую сущность, несущую все эти компоненты
makeQueryEntities(A, B, …)тот же запрос, заранее собранный, для многократного использования
createEntity(id?, components?)новую сущность, добавленную в мир
addEntity(…e) · removeEntity(…e)положить одну внутрь или вынуть
getSystem(S) · hasSystem(S)добраться до системы
execute()выполнить один кадр
Тянуться стоит именно к queryEntities

За ним стоит индекс, поэтому спросить «всё, у чего есть свет и transform» — дёшево. getEntities() — не то же самое: он отдаёт вам весь мир и заставляет фильтровать.

Entity — вещь в мире

ВызовЧто делает
getComponent(Type)один компонент или null
getComponents(A, B)сразу несколько, в этом порядке
hasComponent(A, B)несёт ли она их все
addComponent(…c)добавить; добавление уже имеющегося типа игнорируется
removeComponent(…c)убрать — по классу или экземпляру
component(fn)выполнить fn для каждого компонента, сейчас и в будущем
clone() · clean()скопировать или раздеть догола
.id · .componentsеё id и её компоненты по типам

Component — данные

ВызовЧто делает
.x или get('x')прочитать поле; внутри эффекта это ещё и подписывает на него
$dataвсё целиком как обычный объект
$rawDataхранимый объект, без подписки — только для чтения
update({ … })единственный способ писать
updateAt(path, value)записать глубоко внутрь большого компонента, не перевалидируя всё
version(field?)счётчик, тикающий при изменении, — чтобы зависеть от поля, не читая его
reset(data?)вернуть к значениям по умолчанию
clone()копия
version() — тот самый трюк, на котором держится производительность больших списков

Чтение большого значения подписывает вас на каждый лист внутри него. Зависимость от счётчика версии вместо этого означает, что вы слышите, что оно изменилось, не наблюдая за всем содержимым. Именно так панель переживает список из двадцати тысяч элементов.

@was/engine — классы компонентов

Каждый тип компонента как класс: TransformComponent, MaterialComponent, RigidBodyComponent и остальные. Вы импортируете класс и передаёте его в getComponent, hasComponent или query.

Полный список со всеми полями: справочник компонентов.

@was/svdt — схемы

Слой валидации, которым описываются компоненты. Компилируемый, а не интерпретируемый, — поэтому разбор не стоит ничего на пути анимации.

import { s, compile } from '@was/svdt';

const schema = s.object({ speed: s.f64(1), name: s.string('') });
const codec = compile(schema); // компилировать один раз, при объявлении, а не на каждый вызов
const value = codec.parse(input);

Как описать форму

Числа: s.f32 s.f64 s.i8 s.u8 s.i16 s.u16 s.i32 s.u32, каждое принимает значение по умолчанию. Скаляры: s.bool, s.string, s.color, s.literal, s.enum, s.unknown, s.ref. Векторы: s.vec2, s.vec3, s.mat4. Составные: s.object, s.variant, s.array, s.record, s.union, s.preprocess, s.lazy.

Цепочкой к любому из них: .default(v), .optional(), .nullable(), .min(n), .max(n), .int().

Что даёт скомпилированный кодек

ВызовЧто делает
parse(input)проверить и нормализовать, бросая исключение на плохом входе
safeParse(input)то же, но возвращая успех или ошибку вместо исключения
parseAt(path, v)проверить одно поле, не трогая остальные
equals(a, b)глубокое сравнение, сгенерированное под эту форму
diff(a, b)что изменилось
apply(target, p)применить разницу
invert(p)развернуть её — основа отмены действий
pack · unpackв компактную двоичную форму и обратно

А для разбора самой схемы, а не данных: introspect, keys, requiredKeys.

@was/utils

Мелочи, которыми пользуются все: pick, omit, assign, keys, values, entries, capitalize, basename, extname, dispose (склеить функции очистки вместе) и MIME-помощники, стоящие за проверкой загрузок.


Дальше: Глоссарий