Общие пакеты
Редактор, рантайм и ваш собственный код собраны из одних и тех же пакетов, и несколько из них доступны вам. Какие именно — зависит от того, где выполняется ваш код.
| Пакет | Что даёт | В скрипте | В плагине | В расширении-компоненте |
|---|---|---|---|---|
@was/ecs | сущности, компоненты, мир | ✓ | ✓ | ✓ |
@was/engine | классы компонентов | ✓ | ✓ | ✓ |
@was/signals | систему реактивности | — | ✓ | ✓ |
@was/svdt | схемы и валидацию | ✓ | ✓ | — |
@was/utils | небольшие помощники | ✓ | ✓ | — |
@was/ui | собственную библиотеку компонентов редактора | — | ✓ | ✓ |
@was/icons | набор иконок редактора | — | ✓ | ✓ |
react | React 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-помощники,
стоящие за проверкой загрузок.
Дальше: Глоссарий