Эту тему может дополнить видео:
ECS evolved in Defold | Part 2 | Архетипы, Чанки, Запросы, Системы
[YouTube]
[VK Видео]
В прошлом уроке мы узнали как создавать фрагменты, как присваивать компонентам значения, как создавать сущность, как получать значения компонентов.
В этом уроке мы создадим простую систему движения, а затем применим эту систему к выбранной сущности:

Прежде чем начать, я хочу, чтобы вы уяснили, что ECS — это прежде всего работа с данными и мы можем работать с данными, сущностями, компонентами, даже не видя их на наших экранах.
Системы обрабатывают данные из компонентов массово, без привязки к объектно-ориентированным методам, что позволяет работать с ними чисто на уровне памяти и запросов, независимо от визуализации. Сущности и компоненты существуют в “невидимой” архитектуре движка, например, в Defold можно хранить их в таблицах Lua и обновлять системами без немедленного рендера.
В вашей игре может быть множество игровых объектов = сущностей.
Допустим, у вас имеется сущность — Шар.
У этой сущности есть компоненты:
- позиция
- скорость
Эта сущность умеет двигаться вправо — т.е существует система, которая двигает его вправо.
Допустим, у нас множество сущностей с другими различными компонентами.
И чтобы эта система движения не распространяла своё действие на все сущности, нужен фильтр, по которому система поймёт с какими компонентами и с какой сущностью нужно работать, т.е к какой сущности с присущей ей компонентами нужно применить код.
Система не знает про конкретный Шар
↓
Фильтр находит всех подходящих Шаров
↓
Система применяет код к релевантным данным
[мир всех сущностей] → [Фильтр] → [Чанк: только Шары] → [Система: обработать всех]
1000 ↓ 5 Шаров движение
сущностей position+velocity
Здесь стоит сказать о том, что существует неявное пространство(мир), в котором находятся все сущности, компоненты, системы.
Что такое архетип?
Архетип — это уникальное сочетание типов компонентов (наборы фрагментов). Ключ для чанков.
Что такое чанк?
Чанк — это таблица, которая хранит вместе список сущностей и массивы компонентов для фиксированного набора фрагментов.
Аналогия
Вы находитесь в библиотеке и в этой библиотеке множество шкафов с книгами.
Чанком являются шкафы с книгами.
Этикектой к шкафу являются архетипы.
Книги — это сущности.
Например:
Этикетка к шкафу #1 "Игровая разработка. Defold" (архетип — набор фрагментов).
Шкаф(чанк) содержит книги(сущности): "Игровая разработка. Defold(фрагменты)".
Этикетка к шкафу #2 "Игровая разработка. Figma. Photoshop" (архетип — набор фрагментов).
Шкаф(чанк) содержит книги(сущности): "Игровая разработка. Figma. Photoshop(фрагменты)".
Этикетка к шкафу #3 "Программирование. Java" (архетип — набор фрагментов).
Шкаф(чанк) содержит книги(сущности): "Программирование. Java(фрагменты)".
В коде это выглядело бы вот так:
local game_dev, defold = evolved.id(2)
local chunk1 = evolved.chunk(game_dev, defold) ← Шкаф #1
local book1 = evolved.id() ← Книга #1
evolved.set(book1, game_dev, true) ← Положили в шкаф
evolved.set(book1, defold, "tutorial") ← + фрагмент Defold
Вернёмся к шару
Когда вы вызываете :spawn() — сущность попадает в архетип (группу с position+velocity) и становится частью мира и чанка по этому архетипу.
local ball = evolved.builder()
:set(position, vmath.vector3(0,0,0))
:set(velocity, vmath.vector3(100,0,0))
:spawn() -- ← добавляется в глобальный мир автоматически
Системы видят мир через фильтры — запросы
evolved.process(system) → сканирует ВЕСЬ мир
↓
фильтр query → только нужные сущности
↓
чанк данных → массив для обработки
Мир существует один на весь скрипт/игру. Все :spawn() добавляют в него сущности, все системы через него же их фильтруют.
Что такое запросы?
Запросы (queries) — это фильтры для поиска сущностей по компонентам.
Запрос = "дай мне всех сущностей с этими компонентами"
local movement_query = evolved.builder()
:include(self.position, self.velocity) -- ← фильтр
:build()
Анатомия запроса:
evolved.INCLUDES = {position, velocity} -- ДОЛЖНЫ быть
evolved.EXCLUDES = {ui_tag} -- НЕ должны быть (опционально)
:build() -- создаёт запрос
Зачем нужны запросы?
БЕЗ запроса: система сканирует ВСЕ 1000 сущностей → 990 проверок впустую
С запросом: система получает ГОТОВЫЙ список из 10 Шаров
Что такое системы?
Системы — это просто сущности с особыми фрагментами QUERY и EXECUTE, которые выполняют логику обработки по запросам.
Давайте создадим систему для движения нашей сущности шара по диагонали вправо с фрагментами: {позиция}, {скорость} и {url игрового объекта}.
Код в init():
-- Создаём компоненты
self.position = evolved.id()
self.velocity = evolved.id()
self.go_url = evolved.id()
-- Создаём сущность игрока
self.player_e = evolved.builder()
:set(self.position, vmath.vector3(0, 0, 0))
:set(self.velocity, vmath.vector3(150, 150, 0))
:set(self.go_url, factory.create("#factory", vmath.vector3(0, 0, 0)))
:spawn()
Стоит отметить, что я создаю игровой объект с помощью фабрики и в компонент self.go_url сохраняю ссылку на созданный игровой объект.
Моя структура коллекции выглядит так:

Хорошо, у нас теперь создана сущность с необходимыми компонентами.
Мы вспомним то, что система должна обрабатывать сущности с фрагментами: “позиция”, “скорость”, “url игрового объекта”.
Для этого нам нужно сначала создать запрос, чтобы система могла понимать, к каким сущностям нудно применять код системы.
Давайте напишем код, также в init():
-- Создаём запрос для сущностей с position, velocity, go_url
local movement_query = evolved.builder()
:include(self.position, self.velocity, self.go_url)
:build()
Ещё раз, запрос — это фильтр, который “пропускает” в систему именно те сущности, которые указаны в запросе.
Теперь создадим систему (также в init()):
-- Создаём систему движения
self.movement_system = evolved.builder()
:query(movement_query)
:execute(function (chunk, entity_list, entity_count)
-- Получаем массивы компонентов
local positions = chunk:components(self.position)
local velocities = chunk:components(self.velocity)
local go_urls = chunk:components(self.go_url)
-- Обрабатываем все сущности в чанке
for i = 1, entity_count do
-- Обновляем позицию (используя delta-time из замыкания)
local dt = self.dt or 0
local new_position = vmath.vector3(
positions[i].x + velocities[i].x * dt,
positions[i].y + velocities[i].x * dt,
0
)
-- Обновляем позицию в массиве компонентов
positions[i] = new_position
-- Синхронизируем с Game Object
go.set_position(positions[i], go_urls[i])
end
end)
:build()
Возвращаемся к аналогии с библиотекой.
В каждой библиотеке есть библиотекарь, который выдаёт тебе книги.
Ты приходишь в библиотеку и говоришь ему, мне нужны книги на тему: “Defold. Игровая разработка” — твои слова. Это некий фильтр для него.
Библиотекарь идёт за книгами и пользуется этим фильтром, сравнивая его с этикетками (архетипами).
Он подходит к нужному шкафу и смотрит на этикетки, находит несколько книг с соответствующими этикеткой(архетипу). Приносит тебе несколько книг.
А потом ты уже с этими книгами можешь делать что хочешь.
В вышеприведенном коде система использует изменение позиции для игрового объекта. И для того, чтобы создавалась иллюзия движения мы используем dt.
Предварительно полученную в update().
Может быть полезно:
defolder/forum/lessons/defold_without_pain/delta_time at main · dprogrb/defolder · GitHub
defolder/forum/lessons/defold_without_pain/vectors at main · dprogrb/defolder · GitHub
В update() мы получаем значение dt и запускаем нашу систему, чтобы она работала каждый кадр.
function update(self, dt)
-- Сохраняем dt для использования в системе
self.dt = dt
-- Выполняем систему через evolved.process
evolved.process(self.movement_system)
end
Полный код:
local evolved = require("modules.evolved")
function init(self)
-- Создаём компоненты
self.position = evolved.id()
self.velocity = evolved.id()
self.go_url = evolved.id()
-- Создаём сущность игрока
self.player_e = evolved.builder()
:set(self.position, vmath.vector3(0, 0, 0))
:set(self.velocity, vmath.vector3(150, 150, 0))
:set(self.go_url, factory.create("#factory", vmath.vector3(0, 0, 0)))
:spawn()
-- Создаём запрос для сущностей с position, velocity, go_url
local movement_query = evolved.builder()
:include(self.position, self.velocity, self.go_url)
:build()
-- Создаём систему движения
self.movement_system = evolved.builder()
:query(movement_query)
:execute(function (chunk, entity_list, entity_count)
-- Получаем массивы компонентов
local positions = chunk:components(self.position)
local velocities = chunk:components(self.velocity)
local go_urls = chunk:components(self.go_url)
-- Обрабатываем все сущности в чанке
for i = 1, entity_count do
-- Обновляем позицию (используя delta-time из замыкания)
local dt = self.dt or 0
local new_position = vmath.vector3(
positions[i].x + velocities[i].x * dt,
positions[i].y + velocities[i].x * dt,
0
)
-- Обновляем позицию в массиве компонентов
positions[i] = new_position
-- Синхронизируем с Game Object
go.set_position(positions[i], go_urls[i])
end
end)
:build()
end
function update(self, dt)
-- Сохраняем dt для использования в системе
self.dt = dt
-- Выполняем систему через evolved.process
evolved.process(self.movement_system)
end
На этом всё.
Если остались вопросы, задавайте или в тг или под темой.
Всем спасибо за внимание!
