js13k-ecs
v1.0.0
Published
Tiny <700b entity component system, designed for js13kGames
Maintainers
Readme
js13k-ecs
Микроскопическая объектно-ориентированная Entity Component System созданная специально для конкурса Js13kGames.
Нацелена в первую очередь на достижение следующих целей:
- Легковесность (<700b minzipped)
- Эффективная минификация итогового пользовательсткого кода
- Простое, лаконичное и гибкое API в современном стиле JavaScript
Производительность не является целью данной библиотеки и она ниже чем у других доступных реализаций ECS, но для небольших игр в рамках конкурса ее вполне достаточно. Маленький размер при полнофункциональной поддержке архитектуры ECS покрывает отставание в скорости.
Установка
$ npm i js13k-ecs<script src="https://unpkg.com/js13k-ecs/dist/ecs.umd.js"></script>После этого библиотека может быть найдена в window.ecs.
Использование
import ecs from 'js13k-ecs';
const [registerComponents, createWorld] = ecs;
class Vector {
constructor(x = 0, y = x) {
this.x = x;
this.y = y;
}
}
class Position extends Vector {}
class Velocity extends Vector {}
registerComponents(Position, Velocity);
const world = createWorld();
world.create().add(new Position(), new Velocity(1, 1));
class MovementSystem {
constructor(world) {
const query = world.query(Position, Velocity);
this.update = (delta) => {
query.iterate(([position, velocity]) => {
position.x += velocity.x * delta;
position.y += velocity.y * delta;
});
};
}
}
const pipeline = [new MovementSystem(world)];
let last = performance.now();
const loop = () => {
const now = performance.now();
const delta = now - last;
last = now;
world.update(pipeline, delta);
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);Демонстрацию можно найти в папке example. Пример демонстрирует очень динамичную симуляцию мира с несколькими тысячями сущностей. В качестве иллюстрации повышения производительности в примере также реализовано разбиение пространства с помощью одного компонента, одной системы и утилитарного класса SpaceManager. Live demo
API
registerComponents(Class1, ..., ClassN)
Функция регистрирует классы JavaScript для использования в качестве компонентов. Классы можно регистрировать как одним вызовом с несколькими аргументами, так и отдельными вызовами. Повторная регистрация уже зарегистрированного класса к ошибке не приведет - в этом случае ничего не будет сделано. Ограничений на количество зарегистрированных классов нет. Ни каких требований для классов нет, они могут содержать любые данные и иметь любые методы. Если класс содержит метод destructor, то он будет вызван при удалении компонента из сущности (в том числе при удалении самой сущности) и получит аргументом ссылку на сущность.
createWorld()
Функция создает мир - отдельный контейнер для симуляции. Одновременно может существовать сколько угодно независимых друг от друга миров. Мир является корневым элементом ECS, с его помощью создаются сущности и запускается апдейт.
World
world.create()
Метод создает и возвращает пустую сущность.
world.query(Class1, ..., ClassN)
Метод возвращает набор сущностей, гарантированно содержащих компоненты всех заданных классов. Набор обновляется автоматически и всегда актуален.
Набор имеет свойство length, которое хранит количество сущностей в наборе и метод iterate(iterator), позволяющий перебрать все сущности из набора.
class MovementSystem {
constructor(world) {
const query = world.query(Position, Velocity);
this.update = (delta) => {
const movement = ([position, velocity], entity) => {
position.x += velocity.x * delta;
position.y += velocity.y * delta;
position.rotation += velocity.rotation * delta;
};
query.iterate(movement);
};
}
}В этом примере сущности набора итерируется с помощью функции movement, которая применится для каждой сущности у которой одновременно имеется и компонент Position, и компонент Velocity. Первым аргументом в эту функцию приходит массив компонентов в том же порядке, в котором они заданы при создании набора. Если набор задан одним компонентом, то аргументом придет не массив, а заданный компонент. Вторым аргуметном в эту функцию приходит сама сущность (в данном примере она не используется).
Запросы являются основным способом реализации систем. Обычно запрос создается при создании системы и используется в методе update.
world.update(pipeline, arg1, ..., argN)
Метод последовательно запускает метод update каждой системы из массива систем pipeline, передавая в него заданные параметры. В тоже время, апдейт мира можно произвести и без использования этого метода путем ручного вызова нужных систем. Вот пример, самостоятельно запускающий системы и собирающий информацию о времени выполнения каждой из них:
const pipeline = [new TargetingSystem(ecs), new MovementSystem(ecs)];
let last = performance.now();
const loop = () => {
const now = performance.now();
const delta = now - last;
last = now;
// Запуск систем "из коробки"
// world.update(pipeline, delta);
// Запуск систем в ручную
const statistics = {};
pipeline.forEach((system) => {
const begin = performance.now();
system.update(delta);
statistics[system.constructor.name] = performance.now() - begin;
});
console.log(statistics);
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);В качестве системы может выступать любой класс или объект с методом update, параметры которого библиотекой не регламентируются и определяются самим разработчиком при вызове world.update. Это позволяет гибко настраивать поведение библиотеки в зависимости от нужд приложения. Как правило, этот метод принимает только один аргумент delta, определяющий сколько времени прошло с момента последнего обновления.
world.reset()
Удаляет из мира все сущности и их компоненты.
Entity
Сущности в данной реализации ECS не имеют id, при необходимости передаются по ссылке и несут в себе все необходимые методы для добавления, получения и удаления компонентов. А так же метод для удаления сущности из мира (с удалением всех ее компонентов).
entity.add(component1, ..., componentN)
Метод добавляет компоненты к сущности. Метод ожидает инстансы ранее зарегистрированных классов. В случае, если передаваемый компонент уже присутствует в сущности, то старый компонент будет удален (с вызовом деструктора при его наличии) и заменен новым. Возвращает саму сущность.
entity.get(Class1, ..., ClassN)
Метод возвращает заданные компоненты сущности. Метод ожидает ранее зарегистрированные классы. Если запрошен только один компонент, метод возвратит запрошенный компонент, если запрошено больше одного компонента, метод возвратит массив компонентов в том же порядке. В случае отсутствия у сущности запрошенного компонента возвращается null.
entity.remove(Class1, ..., ClassN)
Метод удаляет компоненты из сущности. Метод ожидает ранее зарегистрированные классы. При наличии у компонента диструктора вызывает его перед удалением. Возвращает саму сущность.
entity.delete()
Метод удаляет сущность из мира, предварительно очистив ее от компонентов с вызовом их деструкторов.
entity.exists
Свойство служит для определения, находится ли сущность в мире или уже удалена из него. Если сущность удалена, возвращает null, иначе возвращает саму сущность. При сохранении ссылкок на сущности этот свойство следует всегда проверять перед операциями над ними. Следует иметь в виду, что такую проверку нет необходимости делать для сущностей, получаемых из запросов - там сущности гарантированно актуальны.
class TargetingSystem {
constructor(world) {
this.query = world.query(Unit);
}
update(delta) {
this.query.iterate((unit) => {
if (!unit.target?.exists) {
// unit.target не задан, либо сущность, на которую указывает таргет потеряла актуальность
// найти новый таргет
// ...
}
});
}
}