Будь ласка, використовуйте цей ідентифікатор, щоб цитувати або посилатися на цей матеріал:
https://er.chdtu.edu.ua/handle/ChSTU/9090| Назва: | Структурна оптимізація програмного забезпечення мобільних ігор жанру Roguelike на основі рушія Unity |
| Автори: | Олексюк, Вадим Володимирович Чуплий, Максим Андрійович |
| Ключові слова: | Unity;C#;entity;component;system;framework;archetype;migrations |
| Дата публікації: | 2024 |
| Короткий огляд (реферат): | Чуплий Максим Андрійович; - Структурна оптимізація програмного забезпечення мобільних ігор жанру Roguelike на основі рушія Unity; - 121 Інженерія програмного забезпечення; - Черкаський державний технологічний університет; - Черкаси, 2024; - Кваліфікаційна робота присвячена дослідженню структурної оптимізації ігор на Unity жанру Roguelike. Метою є дослідження, розробка та проектування. Основна ідея роботи полягає в дослідженні нового підходу для оптимізації структурних змін для сутності в Entity Component System архітектурі, в результаті дослідження було виконано: 1 Створена гіпотеза та підтверджена експериментально. 2 Реалізована Entity Component System архітектура у виді фреймворку на основі дослідження 3 Створено програмний інтерфейс для простої адаптації нових розробників у фреймворк Робота складається з чотирьох основних розділів, додатків та списку використаних інформаційних джерел. |
| URI (Уніфікований ідентифікатор ресурсу): | https://er.chdtu.edu.ua/handle/ChSTU/9090 |
| Розташовується у зібраннях: | 121 Інженерія програмного забезпечення (Інженерія програмного забезпечення) |
Файли цього матеріалу:
| Файл | Опис | Розмір | Формат | |
|---|---|---|---|---|
| КРМ_Чуплий Максим_Магістр.pdf Restricted Access | 2.57 MB | Adobe PDF | Переглянути/Відкрити Запит копії |
Усі матеріали в архіві електронних ресурсів захищено авторським правом, усі права збережено.
Extracted text
МІНІСТЕРСТВО ОСВІТИ І НАУКИ УКРАЇНИ
ЧЕРКАСЬКИЙ ДЕРЖАВНИЙ ТЕХНОЛОГІЧНИЙ УНІВЕРСИТЕТ
Факультет інформаційних технологій і систем
Кафедра програмного забезпечення автоматизованих систем
ПОЯСНЮВАЛЬНА ЗАПИСКА
до кваліфікаційної роботи
«магістра»
освітній рівень
на тему: Структурна оптимізація програмного забезпечення мобільних ігор
жанру Roguelike на основі рушія Unity
Виконав: магістрант, групи МПЗ-2304
Спеціальності
121 «Інженерія програмного забезпечення»
(шифр і назва напряму підготовки)
Студент Чуплий М.А.
(прізвище та ініціали)
Керівник Олексюк В.В.
(прізвище та ініціали)
Рецензент
(прізвище та ініціали)
Черкаси 2024
Черкаський державний технологічний університет
повне найменування вищого навчального закладу
Факультет інформаційних технологій і систем
Кафедра програмного забезпечення автоматизованих систем
Освітній рівень магістр
Спеціальність 121 «Інженерія програмного забезпечення»
Освітня програма Інженерія програмного забезпечення
ЗАТВЕРДЖУЮ
Зав. кафедри ПЗАС, професор
____________________Голуб С. В.
«___» _______________ 2024 року
З А В Д А Н Н Я
НА КВАЛІФІКАЦІЙНУ РОБОТУ СТУДЕНТУ
Чуплий Максим Андрійович
(прізвище, ім’я, по батькові)
1. Тему проекту (роботи) Структурна оптимізація програмного забезпечення мобільних ігор
жанру Roguelike на основі рушія Unity
Керівник проекту (роботи) Олексюк Вадим Володимирович к.т.н., доктор філософії, PhD
(прізвище, ім’я , по батькові, науковий ступінь, вчене звання)
Затверджені наказом Черкаського державного технологічного університету від « 10 » жовтня
2024 року
2. Строк подання студентом проекту (роботи) 10 червня 2023 р.
3. Вхідні дані до проекту (роботи) стандарти програмного забезпечення; процеси управління;
вимоги до проекту; календарне планування проекту; управління ризиками проекту;
управління ресурсами
4. Зміст розрахунково-пояснювальної записки (перелік питань, які потрібно розробити)
Вступ; Існуючі методи та засоби розвʼязання поставлених завдань;
Проектування програмного забезпечення системи;
Розробка та тестування програмного забезпечення;
Реалізація та впровадження результатів;
Висновок;
Список використаних джерел;
Додаток А. Специфікація
Додаток Б. Лістинг програми Додаток
Додаток В. Інструкція користувача Додаток
Додаток Г. Графічні матеріали
5. Перелік графічного матеріалу (з точним зазначенням обов’язкових робіт проекту);
Діаграма класів;
Діаграма пакетів;
Діаграма компонентів;
Діаграма розгортання;
Діаграма діяльності;
Діаграма послідовності;
Діаграма комунікації;
Структурна схема системи;
Функціональна схема системи;
6. Консультанти розділів проекту (роботи)
Прізвище, ініціали та посади Підпис, дата
Розділ
консультанта Завдання видав Завдання прийняв
1
2
3
7. Дата видачі завдання 24 червня 2023 р.
КАЛЕНДАРНИЙ ПЛАН
Строк
виконання
№
Назва етапів випускної роботи етапів Примітки
п/п
випускної
роботи
1
2
3
4
1
2
3
4
5
1
2
3
4
5
6
Студент _____________________ Чуплий М. А.
(підпис) (прізвище та ініціали)
Керівник проекту (роботи) _____________________ Олексюк В. В.
(підпис) (прізвище та ініціали)
АНОТАЦІЯ
- Чуплий Максим Андрійович;
- Структурна оптимізація програмного забезпечення мобільних
ігор жанру Roguelike на основі рушія Unity;
- 121 Інженерія програмного забезпечення;
- Черкаський державний технологічний університет;
- Черкаси, 2024;
- Кваліфікаційна робота присвячена дослідженню структурної
оптимізації ігор на Unity жанру Roguelike. Метою є дослідження, розробка та
проектування. Основна ідея роботи полягає в дослідженні нового підходу для
оптимізації структурних змін для сутності в Entity Component System
архітектурі, в результаті дослідження було виконано:
1 Створена гіпотеза та підтверджена експериментально.
2 Реалізована Entity Component System архітектура у виді
фреймворку на основі дослідження
3 Створено програмний інтерфейс для простої адаптації нових
розробників у фреймворк
Робота складається з чотирьох основних розділів, додатків та списку
використаних інформаційних джерел.
Ключові сліва
Unity, C#, ECS, entity, component, system, framework, archetype, migrations.
ANNOTATION
- Chuplyi Maksym
- Structural optimization of game software genre Roguelike based
on Unity;
- 121 Software engineering;
- Cherkasy State Technological University;
- Cherkasy, 2024;
- The qualification work is dedicated to the study of structural
optimization of Unity-based Roguelike games. The objective is research,
development, and design. The main idea of the work lies in exploring a new approach
to optimizing structural changes for entities in an Entity-Component-System (ECS)
architecture. As a result of the research, the following was achieved::
1 A hypothesis was formulated and experimentally confirmed.
2 An ECS architecture was implemented as a framework based on the conducted
research.
A software interface was developed to facilitate the adaptation of new
developers to the framework.
The paper consists of four main sections, appendices and a list of references.
Keywords
Unity, C#, ECS, entity, component, system, framework, archetype, migrations.
ЗМІСТ
ПЕРЕЛІК УМОВНИХ СКОРОЧЕНЬ 4
ВСТУП 5
РОЗДІЛ 1 8
ІСНУЮЧІ МЕТОДИ ТА ЗАСОБИ РОЗВ’ЯЗАННЯ ПОСТАВЛЕНИХ ЗАВДАНЬ 8
1.1 Актуальні проблеми, що виникають в процесі розробки 8
1.2 Методи та засоби, які вже використовуються для вирішення проблем під час
розробки 9
1.3 Огляд існуючих аналогів 11
1.4 Конкретизація завдань роботи 12
РОЗДІЛ 2 14
ТЕОРЕТИЧНІ ТА ЕКСПЕРИМЕНТАЛЬНІ ДОСЛІДЖЕННЯ 14
2.1 Дослідження структури ECS систем 14
2.2 Теоретичні дослідження 17
2.3 Експериментальні дослідження 19
2.4 Висновки до розділу 23
РОЗДІЛ 3 25
ПРОЕКТУВАННЯ ПРОГРАМНОГО ЗАБЕЗПЕЧЕННЯ СИСТЕМИ 25
3.1 Моделювання предметної області 25
3.1.1 Предметна область моделювання. Модель предметної області. Словник
предметної області 25
3.1.2 Елементи моделювання предметної області 26
3.1.3 Робоча область моделювання 27
ЧДТУ 2423 23.010 ПЗ
Зм Лист № документа Підпис Дата
Розроб. Чуплий М. А. Літ. Лист Листів.
Керівник Олексюк В. В.. Структурна оптимізація [н] 2 150
програмного ФІТІС,
Н. контр. Півень О. Б. забезпечення мобільних Кафедра ПЗАС,
Затв. Голуб С. В. ігор жанру Roguelike на
основі рушія Unity МПЗ-2304
3.2 Формування та аналіз вимог 28
3.2.1 Формування вимог до програмного забезпечення 28
3.2.1.1 Первинні і детальні вимоги 28
3.2.1.2 Функціональні та нефункціональні вимоги 29
3.2.2 Формування вимог за допомогою діаграми прецедентів 31
3.3.2 Діаграми пакетів 38
3.4 Архітектурне проектування 39
3.4.1 Діаграма компонентів 39
3.4.2 Розгортання програмної системи на апаратних засобах. Діаграма
розгортання 40
3.5 Моделювання поведінки системи 41
3.5.1 Діаграма діяльності 41
3.5.2 Діаграма послідовності 45
3.5.3 Діаграма комунікації 49
3.5.4 Діаграма скінченного автомату 50
РОЗДІЛ 4 53
РОЗРОБКА ТА ТЕСТУВАННЯ ПРОГРАМНОГО ЗАБЕЗПЕЧЕННЯ 53
4.1 Розробка програмного комплексу 53
4.1.1 Обґрунтування вибору засобів реалізації 53
4.1.2 Опис структурної (функціональної) схеми 54
4.1.3 Опис логічної схеми системи 55
4.1.4 Розробка бази даних 58
4.1.5 Розробка програмного інтерфейсу користувача 58
4.1.6 Опис розробки програмних компонентів 60
4.2 Тестування системи 61
4.2.1 Модульне тестування 61
3.2.2 Інтеграційне тестування 65
4.2.3 Системне тестування 66
4.2.4 Приймальне тестування 67
4.3 Приклади впровадження програмного комплексу. 68
4.3.1 Розгортання системи 68
ВИСНОВОК 70
СПИСОК ВИКОРИСТАНИХ ДЖЕРЕЛ 72
ЧДТУ 242323.010 ПЗ
ЧДТУ 242323.010 МПЗ
ПЕРЕЛІК УМОВНИХ СКОРОЧЕНЬ
Unity - ігровий двигун
ECS - шаблон проектування сутність-компонент-система
ООП - об’єктно орієнтоване програмування
КОП - компонентно орієнтоване програмування
Migration - перехід сутності між архетипами
Archetype - архетип сутності
Event - подія
IL2CPP - компілятор коду
Benchmark - етап тестування програмного забезпечення на
використання ресурсів процесора
API - інтерфейс програмного забезпечення (application
programming interface)
4
ЧДТУ 242323.010 МПЗ
ВСТУП
Актуальність теми: Розробка мобільних ігор у жанрі Roguelike набуває
популярності завдяки унікальній комбінації динамічного ігрового процесу та
процедурно згенерованого контенту. Водночас ефективність роботи ігрового
програмного забезпечення на мобільних платформах залишається однією з
ключових технічних проблем, враховуючи обмеження апаратних ресурсів.
Використання власного ECS-рішення, побудованого на базі архетипів, дозволяє
оптимізувати структуру та продуктивність ігрових систем, знижуючи
навантаження на процесор і пам’ять. Таким чином, дослідження є актуальним з
огляду на зростаючі вимоги до продуктивності мобільних ігор і потребує в
ефективних інструментах їх оптимізації.
Мета і завдання досліджень: розробка та впровадження структурно
оптимізованого ECS-рішення на базі архетипів для мобільних ігор жанру
Roguelike з метою підвищення їх продуктивності на платформах з обмеженими
ресурсами. Завдання дослідження:
1 Провести аналіз сучасних підходів до побудови ECS у мобільних
іграх.
2 Розробити архітектуру власного ECS-рішення, засновану на
архетипах, враховуючи специфіку жанру Roguelike.
3 Оптимізувати алгоритми управління компонентами та обробки
систем для збільшення швидкості виконання.
4 Оцінити ефективність розробленого підходу в порівнянні з
існуючими рішеннями.
Об’єкт дослідження: Процеси оптимізації програмного забезпечення
мобільних ігор жанру Roguelike, спрямовані на забезпечення високої
продуктивності та ефективного використання апаратних ресурсів.
Предмет дослідження: Методи та інструменти побудови ECS-рішень на
базі архетипів, які дозволяють підвищити продуктивність мобільних ігор жанру
Roguelike та прискорити розробку.
5
ЧДТУ 242323.010 МПЗ
Методи дослідження: Аналіз і порівняння - вивчення існуючих
ECS-підходів і рішень для мобільних ігор; Моделювання - розробка структурної
моделі ECS-рішення, заснованого на архетипах; Гіпотеза і експеримент -
реалізація та тестування нового підходу зміни архетипів у сутності; Емпіричний
аналіз - порівняння продуктивності розробленого рішення з альтернативними
підходами; Системний підхід - інтеграція ECS-рішення в ігровий процес з
урахуванням особливостей жанру Roguelike.
Наукова новизна отриманих результатів:
1 Вперше запропоновано метод збереження компонентів у масиві
незалежно від архетипу, що дозволяє зберігати в архетипі лише індекси
компонентів для кожної сутності. Це суттєво знижує витрати на міграцію
сутностей між архетипами під час структурних змін.
2 Удосконалено підхід до розробки ECS-систем для мобільних ігор
шляхом поєднання оптимізації швидкодії та поліпшення зручності розробки.
3 Отримано подальший розвиток методів структурної оптимізації
програмного забезпечення мобільних ігор жанру Roguelike, зокрема завдяки
новим способам збереження даних у ECS.
Практичне значення отриманих результатів:
1 Запропонований підхід до зберігання компонентів дозволяє
підвищити продуктивність ECS-систем, що є критичним для мобільних
платформ із обмеженими ресурсами.
2 Оптимізована ECS-архітектура спрощує процес розробки ігор в
жанрі Roguelike. Це сприяє швидшому створенню ігрових механік.
3 Результати дослідження можуть бути використані як базис для
побудови ECS-систем у комерційних ігрових проектах, а також у навчальних і
дослідницьких цілях.
Особистий внесок автора: Особисто запропоновано концепцію
збереження компонентів у спільному масиві та використання індексів для
зв’язку з архетипами, що дозволяє мінімізувати витрати на обробку структурних
змін; Реалізовано прототип ECS-системи на основі запропонованого підходу,
6
ЧДТУ 242323.010 МПЗ
оптимізованої для роботи на рушії Unity з використанням IL2CPP; Проведено
тестування продуктивності створеної ECS-системи на мобільних пристроях, що
підтвердило її ефективність для застосування у мобільних іграх; Узагальнено
результати досліджень та підготовлено практичні рекомендації для
використання розроблених методів у розробці ігор.
7
ЧДТУ 242323.010 МПЗ
РОЗДІЛ 1
ІСНУЮЧІ МЕТОДИ ТА ЗАСОБИ РОЗВ’ЯЗАННЯ ПОСТАВЛЕНИХ
ЗАВДАНЬ
1.1 Актуальні проблеми, що виникають в процесі розробки
Roguelike-ігри є унікальним жанром, який характеризується глибокою
взаємодією між сутностями та складністю механік. Ці особливості створюють
низку технічних проблем, які розробникам потрібно вирішувати.
Обробка великої кількості сутностей в Roguelike-іграх зазвичай
включають велику кількість активних об’єктів — героїв, ворогів, предметів,
пасток тощо. В ООП-підходах кожна сутність зазвичай представлена як об’єкт
класу, що включає дані та логіку. Це створює такі проблеми:
- Велика кількість викликів методів для обробки логіки кожної
сутності, що призводить до зниження продуктивності [1].
- Надмірне споживання пам’яті, адже кожен об’єкт має власну
накладну пам’ять для метаданих [1].
- Складність масштабування, адже розширення ієрархії класів для
нових сутностей часто призводить до дублювання коду [1].
У Roguelike-іграх присутня велика динаміка змін, характеристики
сутностей (здоров’я, атака, статусні ефекти) змінюються динамічно. Наприклад:
- Герої можуть отримувати нові навички або ефекти від предметів.
- Вороги адаптуються до дій гравця.
В ООП це реалізується через наслідування та використання шаблонів
проектування (Decorator, Strategy). Проте такі рішення:
- Ускладнюють підтримку коду, адже виникають довгі ланцюжки
наслідування [2].
- Мають обмежену продуктивність через необхідність часто
створювати або видаляти об’єкти під час гри [2].
Також у Roguelike-іграх швидкий цикл розробки та тестування є критично
важливим.
8
ЧДТУ 242323.010 МПЗ
- Класичний підхід ООП, що вимагає створення нових класів та
реалізації залежностей для кожної нової механіки, уповільнює цей процес.
- Додатково, велика кількість залежностей між об'єктами ускладнює
внесення змін і підвищує ризик появи помилок.
Багато розробників надають перевагу фреймворкам або інструментам, які
спрощують процес прототипування, але їх інтеграція із великими іграми також
створює технічні виклики.
1.2 Методи та засоби, які вже використовуються для вирішення
проблем під час розробки
Розробка Roguelike-ігор, враховуючи специфіку жанру та складність
механік, потребує використання ефективних методів і технологій, які
допомагають подолати основні проблеми, зокрема оптимізації,
масштабованості, та гнучкості коду. Для цих цілей активно використовуються
різноманітні підходи, технології та інструменти.
Компонентно-орієнтовані архітектури (ECS): Одним з найбільш
популярних підходів для вирішення проблем масштабованості та
продуктивності в ігровій розробці є ECS (Entity-Component-System). ECS є
архітектурним шаблоном, який дозволяє сутностям (Entities) бути незалежними
від компонентів (Components), що визначають їх характеристики та поведінку.
Логіка взаємодії сутностей реалізується у вигляді систем (Systems), які
обробляють групи компонентів.
ECS вирішує низку проблем, зокрема:
Продуктивність: Дозволяє зберігати дані компонентів у масивах, що
підвищує ефективність обробки за рахунок кращої локальності даних і менших
накладних витрат на пам’ять.
Масштабованість: ECS дозволяє ефективно додавати нові механіки та
сутності без змін в основному коді, що дозволяє розробникам швидко
прототипувати нові ідеї та механіки.
9
ЧДТУ 242323.010 МПЗ
Гнучкість: Оскільки сутності і компоненти не мають тісної прив’язки,
вони можуть легко комбінуватися та модифікуватися без потреби в
переписуванні існуючого коду.
У сучасних ігрових двигунах, таких як Unity (з використанням DOTS -
Data-Oriented Technology Stack), ECS активно використовується для підвищення
продуктивності та гнучкості. Наприклад, Unity має власну реалізацію ECS, яка
дозволяє розробникам організовувати дані та обробку у вигляді компонентів,
знижуючи накладні витрати на створення нових об’єктів та взаємодію з ними
[3].
Data-Oriented Design (DOD) - це ще один підхід, що використовується для
оптимізації продуктивності в ігрових системах. Основною ідеєю є орієнтація на
структуру даних, а не на поведінку об'єктів, як у традиційному ООП. Це
дозволяє зменшити кількість кеш-пропусків, що є важливим для
високопродуктивних ігор. DOD зосереджується на оптимізації пам’яті, а саме
дані організовуються так, щоб мінімізувати доступ до кешу і зменшити
накладні витрати, замість того, щоб зберігати об'єкти в окремих структурах з
великою кількістю методів, дані організовуються у вигляді масивів, що
підвищує продуктивність при їх обробці [4].
Цей підхід часто використовується у поєднанні з ECS, оскільки обидва
методи доповнюють один одного, дозволяючи реалізовувати швидкодіючі та
масштабовані системи в іграх, таких як Roguelike [3].
Для того, щоб полегшити процес розробки та зменшити складність коду,
розробники Roguelike-ігор часто використовують модульні фреймворки та
шаблони проектування:
Шаблон "Система-Компонент" дозволяє розподіляти гру на окремі
частини, що реалізують конкретну логіку гри, такі як система руху, бойова
система, інвентар тощо. Це зменшує зв'язність коду та полегшує його підтримку
[2].
Шаблон "Стратегія" застосовується для зміни алгоритмів, наприклад, для
адаптації поведінки ворогів до дій гравця [2].
10
ЧДТУ 242323.010 МПЗ
Шаблон "Фабрика" допомагає створювати об'єкти в залежності від умов у
грі, що дозволяє уникнути дублювання коду при створенні нових типів
предметів або ворогів [2].
Ці патерни дозволяють зменшити складність розробки та прискорити
процес прототипування, а також полегшують масштабування та внесення змін у
вже існуючу гру [2].
1.3 Огляд існуючих аналогів
Для розробки ігор, зокрема Roguelike-ігор, існує чимало систем і рішень,
які базуються на патерні Entity-Component-System (ECS). Це архітектурне
рішення дозволяє створювати продуктивні та масштабовані ігри. У цьому
підрозділі буде розглянуто найпоширеніші ECS-реалізації - Morpeh, Unity DOTS
і Entitas - а також проаналізовано їх відмінності та архітектурні підходи.
Morpeh - це ECS-фреймворк, створений для Unity. Він відзначається
простотою в інтеграції та використанні, зберігаючи при цьому високу
продуктивність. Morpeh реалізує архетипи ECS, де компоненти зберігаються у
розріджених структурах даних, таких як словники, а архетипи
використовуються лише для фільтрації. Це дозволяє динамічно змінювати набір
компонентів у сутності без значних накладних витрат. Такий підхід зручний для
ситуацій, коли склад сутностей часто змінюється, адже не потребує
реструктуризації великих блоків пам’яті. Однак, він може поступатися
архетипними ECS у продуктивності при масштабних обчисленнях, оскільки
розріджені дані менш ефективні для кешування [5].
Unity DOTS (Data-Oriented Technology Stack) представляє архетипну ECS.
У цій реалізації сутності з однаковими наборами компонентів групуються в
архетипи. Кожен архетип є компактною структурою даних, у якій компоненти
зберігаються в масивах, що дозволяє ефективно працювати з великими
обсягами даних завдяки оптимальному використанню кешу процесора. Такий
підхід забезпечує високу продуктивність у сценаріях, де є великий обсяг
сутностей із стабільними наборами компонентів. Проте зміна архетипу сутності
11
ЧДТУ 242323.010 МПЗ
- наприклад, додавання або видалення компонента - потребує переміщення
сутності між архетипами, що може спричинити певні накладні витрати [3].
Entitas реалізує sparse-based ECS. Цей фреймворк був одним із перших
популярних рішень для Unity. Він підтримує концепцію пулів, які дозволяють
оптимізувати створення та знищення сутностей. Компоненти у Entitas
зберігаються у вигляді масивів індексів, що спрощує їхню обробку, але водночас
вимагає використання додаткових структур для відстеження змін у
компонентах. Entitas популярний завдяки простоті API та наявності великої
спільноти користувачів, проте його продуктивність залежить від способу
використання, оскільки деякі задачі вимагають ручного управління структурами
даних [6].
Основна різниця між архетипними та розрідженими (sparse-based) ECS
полягає в організації та доступі до даних. У архетипній ECS сутності
групуються в архетипи, що дозволяє досягти високої продуктивності через
мінімізацію кеш-промахів і покращення локальності даних. Однак, така система
є менш гнучкою в ситуаціях із часто змінюваними наборами компонентів.
Розріджені ECS, у свою чергу, забезпечують більшу гнучкість, оскільки зміна
набору компонентів не потребує перебудови архетипів, але мають нижчу
продуктивність через погану локальність даних [7].
Таким чином, вибір між цими підходами залежить від специфіки проєкту.
Для Roguelike-ігор, де частота змін наборів компонентів сутностей може бути
високою, важливими є реальна продуктивність і оптимізація пам’яті, поєднання
цих підходів може стати найкращим варіантом. Це дозволяє використовувати
сильні сторони кожного методу в залежності від вимог до конкретних частин
гри.
1.4 Конкретизація завдань роботи
Дослідження у галузі оптимізації архітектури ECS для Roguelike-ігор є
актуальним через постійне зростання вимог до продуктивності та гнучкості
програмного забезпечення, особливо на мобільних платформах. Проблеми,
пов’язані з ефективною організацією даних та зменшенням накладних витрат
12
ЧДТУ 242323.010 МПЗ
під час структурних змін у сутностях, є критичними для ігор цього жанру, де
геймплей передбачає часті зміни стану сутностей.
Основною метою цього дослідження є розробка нового підходу до
збереження компонентів, який дозволить значно пришвидшити процес переходу
сутностей між архетипами під час додавання або видалення компонентів,
оскільки замість переміщення всього компоненту переноситься лише його
індекс.
Впровадження такого підходу забезпечить підвищення продуктивності,
знизить накладні витрати на пам’ять і збільшить гнучкость ECS-систем,
роблячи їх більш пристосованих для використання в умовах, характерних для
Roguelike-ігор. Проведення дослідження також сприятиме розширенню
теоретичної бази та практичних методів оптимізації ігрових рушіїв, що
базуються на ECS-архітектурі.
13
ЧДТУ 242323.010 МПЗ
РОЗДІЛ 2
ТЕОРЕТИЧНІ ТА ЕКСПЕРИМЕНТАЛЬНІ ДОСЛІДЖЕННЯ
2.1 Дослідження структури ECS систем
ECS (Entity-Component-System) архітектура є основою для багатьох
сучасних ігрових рушіїв і програмних систем, оскільки дозволяє ефективно
організувати логіку і дані, що використовуються в процесі роботи. Основна
мета ECS - забезпечити модульність, гнучкість і високу продуктивність за
рахунок розділення даних і логіки. Це особливо важливо для Roguelike-ігор, де
структура сутностей часто змінюється, а взаємодії між компонентами можуть
бути надзвичайно динамічними.
ECS-архітектури класифікуються на дві основні категорії: архетипні та
спарссетні. В архетипних системах сутності групуються за архетипами -
наборами компонентів, які вони містять (рис 2.1). Кожен архетип має власні
масиви для зберігання даних, що дозволяє оптимізувати доступ до компонентів
за рахунок лінійної адресації. Натомість спарсcетні системи використовують
розріджені структури даних, які дозволяють зберігати компоненти незалежно
від належності сутностей до груп (рис 2.2). Це забезпечує більшу гнучкість у
випадках, коли структура компонентів сутності часто змінюється, але може
погіршувати продуктивність через розрідженість даних у пам’яті [8].
Переваги архетипних ECS включають швидкий доступ до даних і високу
продуктивність під час обробки систем, оскільки компоненти однієї групи
зазвичай розташовані в пам’яті послідовно. Однак така структура ускладнює
модифікацію компонентів, оскільки зміна архетипу сутності вимагає
переміщення всіх її даних до нового архетипу. Спарссетні ECS, у свою чергу,
дозволяють уникнути значних витрат на переміщення даних, зберігаючи тільки
посилання або індекси, але можуть створювати додаткові накладні витрати на
пошук і обробку розріджених даних [8].
14
ЧДТУ 242323.010 МПЗ
Рис. 2.1 - Візуальне представлення архетипів
Рис. 2.2 - Візуальне представлення спарсетів
15
ЧДТУ 242323.010 МПЗ
Нижче представлена порівняльна таблиця 2.3 основних характеристик
архетипних і спарсетних ECS:
Таблиця 2.4
Різниця між різними ECS архітектурами
Характеристика Архетипна ECS Спарсетна ECS
Організація даних Сутності згруповані в архетипи Компоненти зберігаються
однаковими наборами компоненті розріджених структурах, незалеж
від архетипу.
Продуктивність доступВисока завдяки локалізації дан Низька через розкиданість даних
для групи сутностей. пам’яті.
Зміна структурВимагає переміщення даних м Просте додавання/видален
сутності архетипами. компонентів.
Гнучкість Менш гнучка, підходить д Гнучка, легко адаптується до змін.
систем із заздалегідь визначеним
структурами.
Кеш-ефективність Висока, оскільки дані зберігають Низька через фрагментованість
компактно. пам'яті.
Складність реалізації Складна через потребу у підтрим Відносно проста, достатньо кіль
архетипів і переходів. масивів.
Масштабованість Підходить для великих систем Зручна для динамічних систем
фіксованими наборамчисленними змінами компонентів.
компонентів.
Головними перевагами ECS є гнучкість у моделюванні складних систем,
підвищення продуктивності завдяки оптимізації доступу до пам'яті та
паралельної обробки, а також модульність, яка полегшує тестування та розвиток
коду. Недоліками є: ускладненість структури коду, висока вартість
16
ЧДТУ 242323.010 МПЗ
переналаштування для команд, які раніше не працювали з ECS, і потенційні
накладні витрати у випадках, коли гнучкість не є критичною [8].
Для цього дослідження ECS розглядається як система, що дозволяє
ефективно керувати великою кількістю сутностей із змінною структурою.
Постановка задачі включає оптимізацію зберігання даних компонентів у
масивах таким чином, щоб зменшити затримки при переходах сутностей між
архетипами. Використовуючи системний підхід, ECS моделюється як
багатофакторна система, де основною метою є знаходження раціонального
компромісу між продуктивністю, гнучкістю та обсягом використаних ресурсів.
2.2 Теоретичні дослідження
Ефективна організація переходу сутностей між архетипами є ключовим
завданням ECS. Основна проблема полягає в копіюванні даних, яке виконується
під час переміщення сутності до нового архетипу після зміни її набору
компонентів. Обсяг даних, що копіюється, прямо залежить від розмірів
компонентів [9]. Наприклад, якщо розмір одного компонента становить 64
байти, а сутність має 10 компонентів, то загальний обсяг даних для копіювання
дорівнює:
64 байтів * 10 компонентів = 640 байтів
Для 1000 сутностей з такими ж характеристиками, що змінюють свої
архетипи одночасно, загальний обсяг копіювання зростає до 640 КБ. Уявімо
ситуацію, коли у системі є складні компоненти, наприклад матриці розміром
4х4 (64 байти) або структуровані об'єкти розміром 256 байтів. Тоді для однієї
сутності з 10 такими компонентами обсяг копіювання досягне:
256 байтів * 10 компонентів = 2560 байтів
Для систем із частими структурними змінами це значно уповільнює
роботу, оскільки операції копіювання займають значну частину часу, особливо
при обробці великих наборів даних [3].
Пропонується рішення, яке полягає в тому, щоб зберігати компоненти
сутностей у глобальному масиві, а в архетипах зберігати лише індекси цих
компонентів. У цьому підході зміна архетипу сутності зводиться до оновлення
17
ЧДТУ 242323.010 МПЗ
невеликого масиву індексів. Наприклад, якщо розмір індексу дорівнює 4 байти
(тип int), то для сутності з 10 компонентами загальний обсяг даних для
копіювання складатиме:
4 байта * 10 компонентів = 40 байтів
Це значно швидше, ніж копіювання повних компонентів, особливо для
систем із великими або складними компонентами. Наприклад, замість
копіювання 2560 байтів даних для кожної сутності копіюється лише 40 байтів,
що дає прискорення в десятки разів.
Зберігання і пошук архетипів є ще однією критичною проблемою.
Традиційний підхід використовує граф архетипів, у якому вузли представляють
архетипи, а ребра - можливі переходи між ними при додаванні або видаленні
компонентів. Пошук нового архетипу в такій структурі має складність O(d), де d
- кількість ребер, що відповідають можливим змінам компонентів. У системах із
великою кількістю компонентів, наприклад 32, кількість можливих переходів
може досягати 2 в степені 32, що робить графи великими і важкими для
навігації [10].
Замість графу пропонується використовувати хеш-функцію, яка створює
унікальний ключ для кожного набору компонентів. Наприклад, для бітової
маски компонентів (наприклад, 32-бітної) хеш-функція працює за O(1),
забезпечуючи миттєвий доступ до архетипу. Це значно скорочує час пошуку,
особливо при роботі з динамічними системами.
Хешування також уникає проблем масштабованості, притаманних графам,
оскільки архетипи можуть зберігатися в хеш-таблиці без необхідності
відслідковувати складні зв'язки між вузлами. Наприклад, для архетипів із 1024
можливими комбінаціями компонентів граф потребував би обробки великої
кількості ребер, тоді як хеш дозволяє знайти архетип за однією операцією.
Порівняльний аналіз зображено на таблці 2.4:
18
ЧДТУ 242323.010 МПЗ
Таблиця 2.4
Порівняльний аналіз графу та хешу архетипів
Критерій Граф архетипів Хеш архетипів
Час пошуку архетипу O(d), де O(1)
d – кількість ребер
Масштабованість Погана для великої Висока, незалежно від
кількості компонентів кількості компонентів
Таким чином, використання індексів компонентів і хеш-функцій для
архетипів забезпечує суттєві переваги з точки зору продуктивності та
масштабованості. Це дозволяє ECS працювати ефективно навіть у системах із
великою кількістю компонентів і частими структурними змінами сутностей.
2.3 Експериментальні дослідження
Розроблена дослідницька програмна система, побудована на основі нової
архітектури ECS (Entity-Component-System), має на меті забезпечити високу
продуктивність і гнучкість при роботі зі складними системами, зокрема у
мобільних іграх жанру Roguelike. Модель системи реалізована з використанням
концепції глобального масиву для зберігання компонентів та хешування
архетипів, що дозволяє зменшити час переходу сутностей між архетипами.
Оптимізована структура ECS представлена на рисунку 2.5:
19
ЧДТУ 242323.010 МПЗ
Рис. 2.5 - Структура ECS з використанням нового підходу до збереження
компонентів
Принципи дії та елементи моделі:
Сутності (Entities): ідентифікатори об’єктів, що взаємодіють із системою.
Компоненти (Components): дані, що описують властивості або стан
сутностей. Вони зберігаються у глобальному масиві, що дозволяє уникати
дублювання та спрощує управління пам’яттю.
Архетипи (Archetypes): визначають набір компонентів, притаманних групі
сутностей. У кожному архетипі зберігаються лише індекси компонентів у
глобальному масиві, що знижує обсяг даних, які потрібно копіювати.
Для моделювання роботи системи застосовувались імітаційні методи
моделювання.
Ефективність системи оцінювалась у кількох аспектах: середній час
переходу сутності до нового архетипу порівнювався з традиційними ECS, де
компоненти копіюються, також аналізувалась різниця швидкості переходу
сутності при збільшенні розміру компонентів.
Оскільки Unity використовує мову C# для написання скриптів, то для
проведення експериментальних тестів продуктивності була застосована
20
ЧДТУ 242323.010 МПЗ
бібліотека BenchmarkDotNet. Цей інструмент дозволяє точно вимірювати
виконання різних ділянок коду в контрольованих умовах, надаючи об’єктивні
дані щодо продуктивності запропонованих оптимізацій [11].
Також, експериментальні дослідження проводились на комп'ютері із
наступними характеристиками:
ОS: Windows 11 (10.0.22631.4460/23H2/2023Update/SunValley3)
CPU: 12th Gen Intel Core i5-12450H, 1 CPU, 12 logical and 8 physical cores
RAM: 16 GB DDR4
Експеримент 1, порівняння швидкості зміни архетипу, для 10 компонентів
розміром 8 байт (рис 2.6)
Рис. 2.6 - Графік різниці швидкості виконання для сутності із компонентом
розміром 8 байт
Експеримент 2, порівняння швидкості зміни архетипу, для 10 компонентів
розміром 16 байт (рис 2.7)
21
ЧДТУ 242323.010 МПЗ
Рис. 2.7 - Графік різниці швидкості виконання для сутності із компонентом
розміром 16 байт
Експеримент 3, порівняння швидкості зміни архетипу, для 10 компонентів
розміром 32 байта (рис 2.8)
Рис. 2.8 - Графік різниці швидкості виконання для сутності із компонентом
розміром 32 байта
На основі проведених досліджень побудовано графік залежності часу
переходу сутностей від кількості компонентів. Графік підтверджує, що
22
ЧДТУ 242323.010 МПЗ
розроблена система демонструє лінійний ріст часу, тоді як традиційна ECS має
квадратичний ріст через обсяги копіювання компонентів.
Рис. 2.9 - Графік росту збільшення часу виконання при збільшенні розміру
компонентів
2.4 Висновки до розділу
Результати експериментальних досліджень підтвердили високу
ефективність запропонованої ECS із використанням глобального масиву для
зберігання компонентів та хешування архетипів. Основною перевагою такої
архітектури є значне зниження витрат часу на операції зміни архетипів, які є
критичними у динамічних системах, таких як мобільні ігри жанру Roguelike. У
традиційних ECS перехід сутності між архетипами вимагає копіювання всіх
компонентів, що спричиняє квадратичний ріст часу виконання при збільшенні
кількості чи розміру компонентів. У новій системі копіюються лише індекси
компонентів у глобальному масиві, що забезпечує лінійний ріст часу виконання.
Експерименти з різними розмірами компонентів (8, 16, 32 байти)
підтвердили, що продуктивність системи не залежить від збільшення розміру
даних, у той час як традиційні ECS демонструють значне уповільнення. Це
робить нову ECS архітектуру придатною для складних ігор із багатофакторною
взаємодією між сутностями та компонентами.
23
ЧДТУ 242323.010 МПЗ
Крім того, оптимізація з використанням глобального масиву для
компонентів має такі переваги:
1 Уніфіковане управління пам’яттю спрощує обробку великої кількості
даних.
2 Уникнення дублювання компонентів зменшує витрати на пам’ять, що є
особливо важливим у мобільних додатках із обмеженими ресурсами.
3 Зменшення обсягу операцій копіювання підвищує продуктивність і
забезпечує стабільну роботу навіть у складних сценаріях із великим числом
сутностей.
Запропонована структурна оптимізація має потенціал для широкого
застосування у розробці мобільних ігор, жанру Roguelike. Подальші
дослідження можуть бути спрямовані на розширення функціональності
системи, зокрема на інтеграцію механізмів адаптивного розподілу ресурсів, що
ще більше підвищить її ефективність у складних динамічних середовищах.
24
ЧДТУ 242323.010 МПЗ
РОЗДІЛ 3
ПРОЕКТУВАННЯ ПРОГРАМНОГО ЗАБЕЗПЕЧЕННЯ СИСТЕМИ
3.1 Моделювання предметної області
3.1.1 Предметна область моделювання. Модель предметної області.
Словник предметної області
Entity-Component-System (ECS) - це архітектура програмного
забезпечення, що дозволяє створювати гнучкі, модульні й високопродуктивні
системи [12]. У рамках моделювання ECS предметна область включає ключові
об’єкти, такі як світ (World), сутності (Entities), компоненти (Components),
архетипи (Archetypes), події (Events), фільтри (Filters), а також ієрархічні
відносини "батько-дитина" (Parent-Child).
Світ (World): головна структура ECS, що координує всі взаємодії між
сутностями, компонентами, архетипами та системами. Світ зберігає всі
сутності, обробляє події, забезпечує виконання систем і дозволяє керувати
глобальним станом гри або симуляції. У ньому також відбувається створення та
видалення сутностей, а також керування їх компонентами.
Сутності (Entities): унікальні ідентифікатори об’єктів ігрового світу, таких
як персонажі, вороги чи предмети. Сутності самі по собі не містять даних, але
асоціюються з компонентами, які визначають їх властивості та поведінку.
Компоненти (Components): прості структури, що зберігають дані,
наприклад позицію, здоров’я або тип анімації. Запропонована ECS оптимізує
зберігання компонентів через глобальний масив, незалежний від архетипів.
Архетипи (Archetypes): групи, які визначають набір компонентів, спільних
для певного класу сутностей. У запропонованій ECS хешування архетипів
дозволяє швидко визначати потрібний архетип для сутності, що оптимізує час
переходу між архетипами при зміні компонентів.
Фільтри (Filters): механізм для вибірки сутностей, що відповідають
заданому набору компонентів. Фільтри дозволяють швидко знайти групи
сутностей, які потребують обробки в певній системі. Наприклад, система руху
може працювати лише з сутностями, що мають компоненти Position та Velocity.
25
ЧДТУ 242323.010 МПЗ
Події (Events): спосіб взаємодії між сутностями або системами. Події
передають інформацію про певні події, такі як зміна стану, зіткнення чи
використання предмета.
Ієрархічні відносини (Parent-Child): дозволяють сутностям формувати
ієрархії. Наприклад, персонаж може бути "батьком" для предмета зброї або
броні. Ця структура полегшує трансформації, наприклад, переміщення "дитини"
разом із "батьком".
Словник предметної області:
Світ (World): основна структура, що об’єднує всі сутності, компоненти,
архетипи та системи.
Сутність (Entity): унікальний ідентифікатор об’єкта ігрового світу.
Компонент (Component): структура даних, що описує властивості
сутності.
Архетип (Archetype): група сутностей із однаковим набором компонентів.
Фільтр (Filter): інструмент для вибірки сутностей за компонентами.
Подія (Event): механізм передачі інформації між сутностями чи
системами.
Відносини батько-дитина (Parent-Child): ієрархічний зв’язок між
сутностями.
Запропонована ECS модель спрямована на забезпечення високої
продуктивності, гнучкості та масштабованості. Завдяки оптимізації зберігання
компонентів, механізмам фільтрації сутностей і використанню подій, система
ефективно працює у середовищах із високою динамікою, таких як мобільні ігри
в жанрі Roguelike.
3.1.2 Елементи моделювання предметної області
Програмне забезпечення Entity Component System фреймворк для ігор
жанру Roguelike буде розроблений на мові програмування C# та оптимізовани
для Il2Cpp - скриптового бекенду ігрового двигуна Unity. Для побудови моделі
предметної області будуть застосовані діаграми мови UML, що дозволяє
візуалізувати структуру та логіку роботи системи.
26
ЧДТУ 242323.010 МПЗ
Під час проєктування будуть використані основні типи UML-діаграм, які
забезпечать детальне моделювання предметної області [13] [22]. Загальна
структура ECS буде представлена за допомогою цих діаграм, що дозволить
описати ключові об’єкти системи, такі як World, Entity, Component, Archetype, а
також їх взаємодії через фільтри, події та ієрархічні зв’язки.
3.1.3 Робоча область моделювання
Архітектура ECS, оптимізована для мобільних ігор жанру Roguelike,
структурована таким чином, щоб забезпечити ефективне збереження та обробку
даних, що є основою всього ігрового процесу. Основні сутності системи
забезпечують гнучкість у моделюванні предметної області та оптимізують
взаємодії між елементами гри.
World є головною сутністю, яка зберігає всі інші елементи ECS, зокрема
сутності, компоненти та архетипи. Вона також відповідає за глобальне
управління фільтрами, ієрархічними зв’язками (Parent-Child) та обробкою подій.
Entity представляє ігровий об’єкт і є унікальним ідентифікатором, який
зв’язує компоненти з архетипами. Перехід сутності між архетипами
відбувається динамічно, забезпечуючи ефективне оновлення її стану.
Component містить дані, що описують властивості сутності, наприклад,
здоров’я, швидкість або позицію. Всі компоненти зберігаються у зовнішньому
масиві, що дозволяє уникнути дублювання даних і мінімізує витрати на
копіювання під час переходу між архетипами.
Archetype зберігає структуру компонентів і список сутностей, які
відповідають цій структурі. Архетип містить лише індекси компонентів у
глобальному масиві, що значно знижує накладні витрати на збереження даних.
Filter забезпечує вибірку сутностей на основі їхніх компонентів. Це
дозволяє швидко знаходити всі сутності, які відповідають певним умовам,
необхідним для виконання ігрової логіки.
Event забезпечує механізм передачі повідомлень між сутностями,
дозволяючи реалізувати, наприклад, виклики подій атаки або збору предметів.
27
ЧДТУ 242323.010 МПЗ
EntityRelation зв’язки створюють ієрархічну структуру між сутностями,
наприклад, коли персонаж утримує предмети або вороги групуються навколо
цілі.
На рисунку 3.1 наведена модель основних компонентів системи, яка
ілюструє взаємодії між компонентами ECS, забезпечуючи зручну структуру гри.
Рис 3.1 Модель структури Entity Component System
3.2 Формування та аналіз вимог
3.2.1 Формування вимог до програмного забезпечення
3.2.1.1 Первинні і детальні вимоги
ПЕРВИННІ ВИМОГИ:
1 Фреймворк повинен забезпечувати можливість створення,
видалення та модифікації сутностей та їхніх компонентів.
2 Кожна сутність повинна підтримувати динамічне додавання та
видалення компонентів без втрати даних.
3 Архетипи повинні забезпечувати швидке фільтрування сутностей на
основі набору їхніх компонентів.
4 Фреймворк повинен надавати механізм для отримання даних про
сутності.
28
ЧДТУ 242323.010 МПЗ
5 Фреймворк має підтримувати Parent-Child зв’язки між сутностями,
наприклад, персонажами та предметами.
6 Повинна бути підтримка подій
ДЕТАЛЬНІ ВИМОГИ:
1.1 Фреймворк повинен забезпечувати створення сутностей із базовими
атрибутами.
1.2 Видалення сутностей повинно бути ефективним і не порушувати
цілісність даних інших сутностей.
2.1 Додавання/видалення компонентів до сутностей не повинно
викликати значного уповільнення роботи системи.
3.1 Архетипи повинні автоматично оновлюватися при зміні набору
компонентів сутності.
3.2 Фільтри повинні забезпечувати вибір сутностей на основі точного
збігу компонентів.
4.1 Глобальний масив компонентів має бути оптимізований для
зменшення обсягу пам’яті та часу доступу.
5.1 Кожна сутність може бути прив’язана до батьківської сутності.
5.2 При видаленні батьківської сутності, всі дочірні сутності повинні
також бути видалені.
3.2.1.2 Функціональні та нефункціональні вимоги
Функціональні вимоги
1. Створення, видалення та модифікація сутностей та компонентів
1.1. Фреймворк повинен забезпечувати створення сутностей із базовими
атрибутами.
1.2. Видалення сутностей повинно бути ефективним і не порушувати
цілісність даних інших сутностей.
1.3. Модифікація компонентів має відбуватися з мінімальною затримкою.
2. Динамічне управління компонентами сутностей
2.1. Фреймворк повинен забезпечувати динамічне додавання/видалення
компонентів до сутностей.
29
ЧДТУ 242323.010 МПЗ
2.2. Додавання/видалення компонентів не повинно викликати значного
уповільнення роботи системи.
3. Архетипи та фільтрування сутностей
3.1. Архетипи повинні автоматично оновлюватися при зміні набору
компонентів сутності.
3.2. Фільтри повинні забезпечувати вибір сутностей на основі точного
збігу компонентів.
4. Отримання даних про сутності
4.1. Фреймворк має надавати механізм для отримання даних про сутності.
4.2. Глобальний масив компонентів повинен бути доступним для
швидкого зчитування та модифікації.
5. Підтримка Parent-Child зв’язків між сутностями
5.1. Кожна сутність може бути прив’язана до батьківської сутності.
5.2. При видаленні батьківської сутності, всі дочірні сутності повинні
також бути видалені.
6. Події
6.1. Фреймворк повинен підтримувати механізм обробки подій між
сутностями.
Нефункціональні вимоги:
1. Продуктивність
1.1. Операції створення, видалення та модифікації сутностей повинні
виконуватися із затримкою, не більшою за логарифмічну залежність від
кількості сутностей.
1.2. Додавання/видалення компонентів повинно мати складність не більш
ніж O(1) у більшості випадків.
2. Ефективність пам’яті
2.1. Глобальний масив компонентів має бути оптимізований для
зменшення обсягу пам’яті.
2.2. Архетипи повинні використовувати мінімальну кількість пам’яті для
зберігання індексів компонентів.
30
ЧДТУ 242323.010 МПЗ
3. Масштабованість
3.1. Фреймворк повинен підтримувати обробку великої кількості
сутностей (до мільйонів).
4. Надійність
4.1. Видалення сутностей не повинно призводити до пошкодження інших
даних у системі.
4.2. Обробка подій повинна гарантувати, що жодна подія не буде втрачена.
5. Зручність використання
5.1. Інтерфейс API має бути інтуїтивним для розробників і надавати чітку
документацію.
5.2. Система повинна легко інтегруватися з ігровими рушіями, такими як
Unity.
3.2.2 Формування вимог за допомогою діаграми прецедентів
Діаграми прецедентів будуть зроблені на основі мови UML.
В роботі існує два види акторів:
1 Користувач - той хто користується фреймворком.
2 Система - фільтрує/змінює дані.
На рисунку 3.2 зображено ключові функціональні можливості
фреймворку ECS (Entity Component System), згруповані за основними
підсистемами.
Підсистема "Entity Management" забезпечує базову роботу з сутностями.
Це включає створення, видалення та модифікацію сутностей, а також
додавання, видалення та модифікацію їхніх компонентів. Видалення сутностей
має зв'язок з видаленням компонентів через відношення включення, оскільки
компоненти автоматично видаляються разом із сутністю.
"Archetype & Filtering" відповідає за підтримку архетипів і фільтрацію
сутностей. При додаванні або видаленні компонентів архетипи автоматично
31
ЧДТУ 242323.010 МПЗ
оновлюються, що представлено через зв’язок «тригер». Фільтрація сутностей на
основі архетипів надає можливість швидкого пошуку сутностей із конкретними
наборами компонентів.
У підсистемі "Data Access" реалізовано функції доступу до даних. Вона
дозволяє користувачу отримувати дані про конкретні сутності та звертатися до
глобальних масивів компонентів для оптимального управління пам’яттю.
"Parent-Child Relations" додає підтримку ієрархії між сутностями.
Користувач може встановлювати батьківські зв’язки між сутностями, а
видалення сутності автоматично поширюється на всі дочірні сутності, що
показано через розширення до видалення.
Нарешті, підсистема "Event System" забезпечує обробку подій. Вона
працює з подіями, дозволяючи ефективно обробляти взаємодії між сутностями,
а також обробляє специфічні події, пов'язані з сутностями.
Користувач (Framework User) взаємодіє з фреймворком через операції
створення, модифікації та доступу до даних.
Система (System) фільтрує сутності та події.
32
ЧДТУ 242323.010 МПЗ
Рис. 3.2 - Діаграма прецедентів використання фреймворку
3.3 Проектування логічної структури програмного комплексу
3.3.1 Діаграми класів
На рисунку 3.3 зображено діаграму класів, яка має:
1 World - це основний клас ECS фреймворку, який агрегує всі
компоненти, архетипи, фільтри, події, аспекти, міграції та системи:
ー AddComponent(entityID, component) і RemoveComponent(entityID,
component) - методи для додавання та видалення компонентів у сутностей.
33
ЧДТУ 242323.010 МПЗ
ー archetypes, componentRows, filters, events, migrations, aspects, systems
- колекції об'єктів різних класів, що зберігаються всередині World.
ー Зв'язок агрегація між World і цими класами означає, що всі ці класи
існують незалежно, але є частиною світу.
2 Archetype - клас, що представляє групу сутностей, які мають
однаковий набір компонентів:
ー entityID - ідентифікатор сутності.
ー components - список компонентів, які належать сутності в архетипі.
ー AddEntity(entityID) і RemoveEntity(entityID) - методи для додавання
та видалення сутностей в архетипі.
ー GetComponents(entityID) - повертає список компонентів для
сутності.
ー Зв'язок агрегація з Entity і Component означає, що архетип зберігає
сутності та їх компоненти.
3 ComponentRow - клас, що зберігає компоненти, організовані за
їхніми ідентифікаторами (ID):
ー components - колекція компонентів, прив'язаних до компонентів за їх
ID.
ー GetComponent(componentID) - метод для отримання компонента за
його ID.
ー SetComponent(componentID, component) - метод для встановлення
значення компонента.
ー Зв'язок агрегація з Component показує, що цей клас зберігає
компоненти.
4 Filter - клас для фільтрації сутностей за їх компонентами:
ー componentMask - бітмаска, яка представляє компоненти, що
використовуються для фільтрації.
ー Match(entityID) - метод для перевірки, чи відповідає сутність
фільтру.
34
ЧДТУ 242323.010 МПЗ
ー Зв'язок делегація з Archetype показує, що фільтр може виконувати
дії через архетипи для пошуку сутностей.
5 Event - клас, який зберігає і відправляє дані подій:
ー eventData - колекція подій за їх ID.
ー SendEvent(eventID, data) - метод для відправки події з передачею
даних.
ー Зв'язок делегація з World показує, що події обробляються через світ.
6 Migration - клас, що обробляє зміни компонентів у сутностей:
ー AddEntity(entityID, components) і RemoveEntity(entityID) - методи
для додавання та видалення сутностей з їх компонентами.
ー ApplyChanges() - метод для застосування змін до сутностей.
ー changedEntities - список сутностей, у яких були змінені компоненти.
ー Зв'язок делегація з Entity показує, що зміни в сутностях
обробляються через клас міграції.
7 Aspect - клас, що представляє групу компонентів, які необхідні для
виконання певної функції:
ー requiredComponents - список необхідних компонентів.
ー Matches(entityID) - метод для перевірки, чи відповідає сутність
аспекту.
ー Зв'язок агрегація з Component показує, що аспекти залежать від
компонентів для визначення своїх властивостей.
8 Entity - клас, що представляє сутність в ECS фреймворку:
ー entityID - ідентифікатор сутності.
ー archetype - архетип, до якого належить сутність.
ー GetComponent(componentID) - метод для отримання компонента за
його ID.
ー SetComponent(componentID, component) - метод для встановлення
компонента.
ー Зв'язок агрегація з Archetype і Component показує, що сутність
належить до архетипу і має компоненти.
35
ЧДТУ 242323.010 МПЗ
9 Component - базовий клас для компонентів:
ー data - дані компонента (можуть бути будь-якого типу).
ー Зв'язок агрегація з іншими класами, такими як ComponentRow,
Archetype, та Aspect, показує, що компоненти є основною частиною сутностей
та аспектів.
10 System - клас для оновлення стану світу ECS:
ー Update(world: World) - метод, що викликається для обробки логіки
ігрового світу під час оновлення.
ー Зв'язок делегація з World показує, що системи взаємодіють зі світом,
щоб змінювати його стан.
11 Query - клас, який дозволяє виконувати запити для пошуку
сутностей за фільтрами:
ー filter - фільтр, який використовується для запиту.
ー Execute(world: World) - метод для виконання запиту і отримання
списку сутностей.
ー Зв'язок композиція з Filter означає, що фільтр є невід'ємною
частиною запиту.
12 ComponentManager - додатковий клас для управління компонентами
в різних частинах системи:
ー AddComponent(entityID, component) і RemoveComponent(entityID,
component) - методи для додавання та видалення компонентів у сутностей.
ー GetComponent(entityID, componentID) - метод для отримання
компонента у сутності.
36
ЧДТУ 242323.010 МПЗ
37
ЧДТУ 242323.010 МПЗ
Рис. 3.3 - Діаграма класів
3.3.2 Діаграми пакетів
На рисунку 3.4 зображено діаграму пакетів. Пакети розділені по функціям
фреймворку:
Пакет JaECS.Collections - містить структури даних та колекції які
використовуються в архетипах для оптимізації структури збереження
компонентів сутності.
Пакет JaECS.Archetype - містить дані про сутності.
Пакет JaECS.Relation - містить дані про зв’язки Батько-Дитина між
сутностями.
Пакет JaECS.Entity - містить внутрішні дані про сутність, а саме її
ідентифікатори.
Пакет JaECS.Filter - містить фільтри для фільтрації сутностей в системах і
їх майбутньої обробки.
Пакет JaECS.Systems - представляє системи в яких відбувається логіка з
використанням фільтрів для отримання потрібних сутностей.
Пакет JaECS.Components - має функціонал для роботи з компонентами на
сутності.
Рис. 3.4 – Діаграма пакетів фреймворку
38
ЧДТУ 242323.010 МПЗ
3.4 Архітектурне проектування
3.4.1 Діаграма компонентів
На рисунку 3.14 зображена діаграма компонентів та представлена
структура ECS-фреймворку, яка демонструє основні компоненти системи та їх
взаємодії. Всі компоненти згруповані в межах пакету "ECS Framework", що
підкреслює їхню приналежність до єдиної системи.
Центральним компонентом є World, який координує роботу всіх інших
підсистем фреймворку. Він складається з кількох ключових модулів:
World Core - ядро системи, яке забезпечує взаємодію між основними
модулями: менеджером архетипів (Archetype Manager), системою подій (Event
System) і системою фільтрації (Filter System).
Archetype Manager - управляє архетипами, містить логіку для роботи з
Archetype і виконує операції міграції компонентів за допомогою модуля
Migration. Кожен архетип зберігає дані у вигляді ComponentRow, що є
оптимізованою структурою для збереження компонентів.
Filter System - забезпечує функціональність фільтрації сутностей. Для
цього використовується Filter, який дозволяє знаходити архетипи, що
відповідають конкретним критеріям.
Event System - відповідає за обробку подій у фреймворку. Цей модуль
працює з подіями, представленими компонентом Event, дозволяючи обробляти і
передавати їх.
Migration - виконує складні операції зі зміни архетипів і компонентів,
працюючи безпосередньо з Archetype та ComponentRow.
Кожен з компонентів має чіткі зв’язки та залежності, що відображено на
діаграмі:
Ядро (World Core) використовує всі ключові модулі системи.
Archetype Manager керує архетипами та ініціює процеси міграції.
Query System оперує фільтрами, які застосовуються до архетипів для
швидкої вибірки сутностей.
Event System управляє подіями через спеціалізований компонент.
39
ЧДТУ 242323.010 МПЗ
Migration змінює структуру даних компонентів і архетипів, забезпечуючи
гнучкість системи.
Ця структура забезпечує розділення відповідальностей між
компонентами, модульність і оптимізацію роботи з даними в ECS.
Рис. 3.14 – Діаграма компонентів ECS фреймворку
3.4.2 Розгортання програмної системи на апаратних засобах. Діаграма
розгортання
На рисунку 3.15 представлена діаграма розгортання. Діаграма розгортання
дозволяє зрозуміти, що програмна система підключається як зовнішня
бібліотека до проекту користувача фреймворку в якому він реалізує логіку своєї
системи.
40
ЧДТУ 242323.010 МПЗ
Рис. 3.15 – Діаграма розгортання
3.5 Моделювання поведінки системи
3.5.1 Діаграма діяльності
На рисунку 3.16 зображено діаграму діяльності додавання компонента в
якій для кожної сутності перевіряється наявність відповідного архетипу, якщо
архетип не знайдено, створюється новий.
41
ЧДТУ 242323.010 МПЗ
Рис. 3.16 – Діаграма діяльності: Додавання компонента до сутності
На рисунку 3.17 зображено діаграму видалення компонента в архетипах,
cистема ітерує по сутностях для видалення компонентів, для кожної сутності
архетип оновлюється або залишається без змін, якщо архетип стає порожнім,
він видаляється.
42
ЧДТУ 242323.010 МПЗ
Рис. 3.17 – Діаграма діяльності: Видалення компонента в архетипах
На рисунку 3.18 зображено діаграму діяльності міграції сутностей - для
кожної сутності з оновленими компонентами перевіряється, чи потрібна
міграція, якщо міграція необхідна, сутність переміщується до нового архетипу,
в іншому випадку міграція пропускається.
43
ЧДТУ 242323.010 МПЗ
Рис. 3.18 – Діаграма діяльності: Міграція сутностей
На рисунку 3.19 зображено діаграму діальності для фільтрації сутностей - у
циклі кожна сутність перевіряється на відповідність фільтру, а саме по його
бітовій масці, якщо сутність відповідає, вона додається до списку результатів з
якими буде працювати користувач.
44
ЧДТУ 242323.010 МПЗ
Рис. 3.19 - Діаграма діяльності: Запит сутностей по фільтруванню компонентів
3.5.2 Діаграма послідовності
Діаграма додавання компонента яка зображена на рисунку 3.20 містить
наступну логіку:
1 Client викликає метод AddComponent(entityID, component) у класі World.
2 World перевіряє, чи існує архетип для вказаної сутності, викликавши
метод у класі Archetype.
Якщо архетип існує:
3 Archetype викликає метод Add для ComponentRow, щоб додати нові дані
компонента.
Якщо архетип не існує:
3 World викликає метод для створення нового архетипу.
45
ЧДТУ 242323.010 МПЗ
4 Після цього Archetype також додає дані компонента до ComponentRow.
5 Після успішного додавання компонента, World повертає повідомлення
про успіх назад до Client.
Рис. 3.20 - Діаграма послідовності: Додавання компонента
Діаграма видалення компонента яка зображена на рисунку 3.21 містить
наступну логіку:
1 Client викликає метод RemoveComponent(entityID, component) у класі
World.
2 World перевіряє, чи існує архетип для вказаної сутності.
Якщо архетип існує:
3 Archetype викликає метод для видалення даних компонента з
ComponentRow.
4 Після видалення даних, World перевіряє, чи архетип став порожнім.
5 Якщо архетип порожній, World видаляє архетип.
46
ЧДТУ 242323.010 МПЗ
6 Якщо архетип не існує, World повертає повідомлення про те, що
компонент не знайдено.
7 Після успішного видалення, World повертає повідомлення про успіх
назад до Client.
Рис. 3.21 - Діаграма послідовності: Видалення компонента
Діаграма міграції сутностей яка зображена на рисунку 3.22 містить
наступну логіку:
1 Client вносить зміни до компонентів сутності, викликаючи метод у класі
World.
2 World передає запит до класу Migration, додаючи сутність до черги
міграції.
3 Migration перевіряє, чи потрібен новий архетип для сутності:
4 Якщо новий архетип потрібен, Migration викликає метод для створення
нового архетипу.
5 Потім Migration переміщує сутність до нового архетипу.
47
ЧДТУ 242323.010 МПЗ
6 Якщо новий архетип не потрібен, Migration повідомляє World, що
міграцію виконувати не потрібно.
7 Після завершення міграції, Migration повідомляє World, що процес
завершено.
8 World повертає повідомлення про успіх назад до Client.
Рис. 3.22 - Діаграма послідовності: Міграція сутностей
Діаграма фільтрації сутностей яка зображена на рисунку 3.23 містить
наступну логіку:
1 Client відправляє запит на виконання фільтру через метод
ExecuteQuery(filter) у класі World.
2 World передає фільтр до Filter, щоб застосувати його до всіх сутностей.
3 Filter перевіряє архетипи для відповідності фільтру:
4 Якщо знайдено сутності, що відповідають фільтру, Archetype повертає
їх назад до World.
5 Якщо жодна сутність не відповідає фільтру, Archetype повертає
порожній результат.
48
ЧДТУ 242323.010 МПЗ
6 World повертає результати запиту назад до Client.
Рис. 3.23 - Діаграма послідовності: фільтрація сутностей
3.5.3 Діаграма комунікації
На рисунку 3.24 зображена діаграма комунікацій. Вона представляє
комунікацію користувача фреймворку із частинами його API. Користувач, що
користується фреймворком повинен реалізувати абстракцію SystemBase, після
чого він отримає доступ до внутрішнього API: SendEvent, BuildFilter,
Get/Add/Remove Component. Доступу до наступного API, а саме зміну архетипу
сутності та отримання сутності напряму - користувач не має, і не потребує, всі
ці маніпуляції фреймворк виконує автоматично.
49
ЧДТУ 242323.010 МПЗ
Рис. 3.24 - Діаграма комунікації сценарію підключення до лобі
3.5.4 Діаграма скінченного автомату
На рисунку 3.25 зображену діаграму скінченного автомату ECS
фреймворку, автомат є ієрархічним та містить внутрішні стани:
1 WorldState:
Основний стан ECS фреймворку, коли система в активному стані.
Idle - система в стані очікування, без активних операцій.
AddingComponent - стан додавання компонентів:
CheckingArchetype - перевіряється, чи існує архетип для сутності.
AddingToArchetype - додавання компонента до існуючого архетипу.
50
ЧДТУ 242323.010 МПЗ
CreatingNewArchetype - створення нового архетипу, якщо підходящий
архетип не знайдено.
RemovingComponent - стан видалення компонентів:
CheckingArchetype - перевірка архетипу для видалення компонента.
RemovingFromArchetype - видалення компонента з архетипу.
QueryingEntities - стан виконання запиту фільтрації:
ApplyingFilter - застосування фільтра для пошуку сутностей.
ReturningEntities - повернення результатів запиту.
2 MigrationState:
Цей стан активується, коли змінюються компоненти сутності:
ApplyingChanges - застосування змін компонентів до сутностей.
ValidatingEntities - перевірка, чи потрібно мігрувати сутність до іншого
архетипу.
MovingEntities - міграція сутності до нового архетипу.
3 EventState:
Активується, коли спрацьовує подія:
HandlingEvent - обробка події.
UpdatingWorld - оновлення світу на основі обробленої події.
Опис переходів:
1 Початковий стан: система переходить до Idle.
2 Стан Idle може переходити до різних станів в залежності від
операції (додавання, видалення компонентів, запит).
51
ЧДТУ 242323.010 МПЗ
3 При зміні компонентів сутностей система переходить до
MigrationState.
4 Події активують EventState, а аспекти активують AspectState.
Рис. 3.25 - Діаграма станів фреймворку
52
ЧДТУ 242323.010 МПЗ
РОЗДІЛ 4
РОЗРОБКА ТА ТЕСТУВАННЯ ПРОГРАМНОГО ЗАБЕЗПЕЧЕННЯ
4.1 Розробка програмного комплексу
4.1.1 Обґрунтування вибору засобів реалізації
Вибір програмних та апаратних засобів для реалізації проєкту
здійснювався з урахуванням вимог до продуктивності, функціональності та
оптимізації, а також специфіки задач структурної оптимізації ECS для
мобільних ігор. Основним середовищем розробки обрано Unity, який є одним із
провідних ігрових рушіїв. Unity забезпечує широкий спектр можливостей для
створення 2D та 3D ігор, надаючи інструменти для ефективної роботи з
мобільними платформами. Використання цього рушія дає змогу інтегрувати
системи ECS безпосередньо в ігровий процес, скорочуючи час розробки [14].
Основною мовою програмування для реалізації було обрано C#. Ця мова,
що активно підтримується Unity, забезпечує високу швидкість виконання,
зручність використання та багатий набір інструментів для роботи з пам’яттю.
Завдяки підтримці C# у середовищі Unity можливо інтегрувати складні
структури даних і алгоритми, необхідні для ефективної роботи ECS [15].
Для зберігання даних використано внутрішні механізми Unity. Це
дозволяє оптимально керувати ресурсами і уникати зовнішніх залежностей, що
особливо важливо для мобільних ігор. Збереження прогресу реалізується через
PlayerPrefs і JSON-серіалізацію, що забезпечує швидкий доступ до даних і
стабільну роботу навіть на пристроях із обмеженими ресурсами [16].
Апаратне середовище для розробки проєкту включає сучасні комп’ютери
з потужними процесорами, достатньою кількістю оперативної пам’яті та
SSD-накопичувачами, що забезпечує швидку компіляцію та ефективну роботу з
проєктом. Проєкт орієнтовано на мобільні пристрої середнього та високого
рівня продуктивності, що відповідає сучасним вимогам до ігрової індустрії.
Для забезпечення оптимізації процесу розробки та тестування
використовуються профілювальні інструменти, такі як вбудований
профілювальник Unity та DotTrace Diagnoser [17]. Це дозволяє ефективно
53
ЧДТУ 242323.010 МПЗ
аналізувати та покращувати продуктивність ECS. Таким чином, вибір засобів
розробки був обґрунтований потребами високопродуктивної та масштабованої
реалізації ECS, що є важливим для розробки мобільних ігор у жанрі Roguelike.
4.1.2 Опис структурної (функціональної) схеми
На рисунку 4.1 зображено функціональну схему процесу фільтрації
сутностей і отримання списку подій. У розробленій ECS цей функціонал
виконується за допомогою узгоджених кроків, що включають взаємодію між
користувачем, внутрішніми механізмами фреймворку та базою даних.
У випадку фільтрації сутностей користувач через спеціальний механізм
створює запит із критеріями, на основі яких необхідно виконати фільтрацію.
Цей механізм обробляє запит і надсилає його у внутрішню систему світу ECS.
Світ звертається до бази даних, у якій зберігаються всі сутності та їхні
компоненти. На основі заданих критеріїв виконується пошук і створюється
список відповідних сутностей. Отримані дані записуються в дебаг інформацію
для подальшого аналізу та надсилаються користувачу у вигляді готового списку.
Процес отримання списку подій працює аналогічно. Користувач через
спеціальний механізм формує запит для отримання подій, які відбулися в
системі. Цей запит обробляється та передається в систему світу. Світ
звертається до бази даних, у якій зберігаються події. Дані про події, що
відповідають критеріям, обираються, записуються в дебаг інформацію та
відправляються назад користувачу як готовий список.
Обидва процеси ілюструють структуровану взаємодію між користувачем і
системою, використання централізованої бази даних для зберігання сутностей і
подій, а також забезпечення можливості аналізу через запис дебаг інформації.
Такий підхід забезпечує модульність, прозорість роботи та можливість
розширення функціональності ECS.
54
ЧДТУ 242323.010 МПЗ
Рис. 4.1 - Функціональна схема фреймворку
4.1.3 Опис логічної схеми системи
Процес функціонування системи ECS побудований на основі циклічного
управління станами сутностей, їх компонентів та взаємодій через події. Логічна
структура включає в себе основні операції: створення сутностей, управління
компонентами, обробка подій, фільтрація сутностей та передача результатів
користувачу. Послідовність дій, яка відбувається у системі, організована
наступним чином:
1 Ініціалізація системи. При запуску створюється об'єкт світу ECS, що
виступає як центральний елемент управління всіма процесами. Світ ініціалізує
базу даних, яка містить інформацію про архетипи, компоненти, фільтри та події.
2 Додавання сутностей та компонентів. Користувач через API системи
може створювати нові сутності. При створенні сутності система генерує
55
ЧДТУ 242323.010 МПЗ
унікальний ідентифікатор та додає запис про сутність до бази даних. За
необхідності, до сутності додаються компоненти, що автоматично оновлює її
архетип.
3 Фільтрація сутностей. При запиті фільтрації користувач задає критерії, за
якими відбираються сутності. Механізм фільтрації аналізує ці критерії, визначає
архетипи, що відповідають умовам, та виконує пошук у базі даних. Результат
зберігається у тимчасовій структурі для доступу.
4 Обробка подій. Події, створені сутностями або системами, записуються у
чергу подій. Світ обробляє ці події у рамках спеціального циклу [18],
модифікуючи стан сутностей чи викликаючи необхідні обробники. Дані про
події можуть бути записані у дебаг-інформацію.
5 Робота з батьківсько-дочірніми зв’язками. Якщо сутність прив'язується до
іншої як дочірня, система зберігає відповідну ієрархію. Видалення батьківської
сутності автоматично ініціалізує видалення всіх дочірніх сутностей.
6 Доступ до даних. Запити користувача на отримання даних (фільтри,
компоненти, події) виконуються через відповідні модулі системи. Дані з бази
обробляються, форматуються та надсилаються користувачу.
7 Завершення роботи. При зупинці системи ECS виконується очищення
пам’яті, щоб уникнути її витоку.
На рисунку 4.2 зображено алгоритм роботи фреймворку
56
ЧДТУ 242323.010 МПЗ
Рис. 4.2 - Блок-схема логічної роботи фреймворку
57
ЧДТУ 242323.010 МПЗ
4.1.4 Розробка бази даних
База даних у системі ECS є невід'ємною частиною, яка виконує роль
центрального сховища для даних про сутності, компоненти, архетипи, події та
інші об'єкти. Вона забезпечує доступ до даних усіх функціональних модулів
системи, мінімізуючи надмірність і дозволяючи швидко оперувати інформацією.
Процес розробки бази даних включає три етапи: концептуальне, логічне
та фізичне моделювання. На першому етапі визначаються ключові об'єкти
системи та зв’язки між ними, що дозволяє створити базове уявлення про
структуру даних із точки зору предметної області. Логічне моделювання
деталізує концептуальну модель, додаючи формалізовані вимоги до даних і
описуючи їх у вигляді сутностей, атрибутів і зв'язків між ними. На
завершальному етапі фізичне моделювання перетворює логічну модель у
структуру таблиць бази даних, включаючи визначення ключів, зв'язків та
індексів [19].
Для зберігання даних про сутності передбачена таблиця, що містить
унікальні ідентифікатори та посилання на пов’язані компоненти. Дані про
компоненти зберігаються в окремій таблиці, яка включає інформацію про їх тип
та значення. Архетипи представлені у вигляді таблиці, що відображає зв'язок
між сутностями та наборами їхніх компонентів. Черга подій реалізована у
вигляді таблиці, яка дозволяє асинхронно обробляти повідомлення між
системами. Для забезпечення вибірки сутностей за заданими критеріями
передбачені таблиці фільтрів, що зберігають правила фільтрації.
У якості моделі даних обрана реляційна структура, що забезпечує
зрозумілість і зручність організації даних. Реляційна модель дозволяє легко
адаптувати структуру бази під змінні вимоги системи та ефективно працювати з
великим обсягом даних. Для оптимізації доступу до даних і зменшення
навантаження на базу застосовуються індексація та механізми кешування.
4.1.5 Розробка програмного інтерфейсу користувача
Проектування правильного API є одним із найважливіших аспектів
розробки програмного забезпечення, оскільки саме API забезпечує взаємодію
58
ЧДТУ 242323.010 МПЗ
користувачів із системою та визначає її зручність, продуктивність і гнучкість.
Добре спроектоване API спрощує процес розробки, полегшує інтеграцію та
розширення системи, а також мінімізує ризик виникнення помилок під час
роботи з нею [20].
Одним із ключових принципів створення API є зрозумілість і простота.
API повинно бути інтуїтивно зрозумілим для розробників, що використовують
його. Це означає, що методи та функції повинні мати чіткі назви, які
відображають їх призначення, а структура повинна бути логічною і
передбачуваною. Наявність якісної документації, яка пояснює функціональність
кожного методу, також відіграє вирішальну роль у зручності використання API.
Розроблене API ECS Framework забезпечує гнучку та високопродуктивну
взаємодію розробників із основними компонентами системи. API орієнтоване
на спрощення роботи з сутностями, компонентами, архетипами та фільтрами, а
також підтримує ієрархічні зв’язки та події.
Основні можливості API:
1 Управління сутностями:
Користувач може створювати, видаляти та отримувати інформацію про
сутності. Метод CreateEntity дозволяє створювати сутності з базовими або
попередньо визначеними компонентами. Метод DestroyEntity видаляє сутність,
при цьому забезпечується автоматичне очищення її даних. API також дозволяє
перевіряти стан сутності за допомогою методу IsEntityAlive.
2 Управління компонентами:
API підтримує додавання, видалення та модифікацію компонентів у
сутностях. Наприклад, методи AddComponent і RemoveComponent оновлюють
набір компонентів у сутності, викликаючи відповідне оновлення архетипу.
Компоненти зберігаються у глобальному масиві, що забезпечує ефективність
доступу та зменшення дублювання даних.
3 Архетипи та фільтрування:
Функції API дозволяють автоматично оновлювати архетипи при зміні
набору компонентів сутності. Користувач може створювати фільтри через
59
ЧДТУ 242323.010 МПЗ
GetFilterBuilder, які відбирають сутності на основі заданих умов. Це полегшує
ітерацію через сутності, що мають певні набори компонентів.
4 Ієрархічні зв’язки:
API надає можливість створювати відносини "батько-дитина" між
сутностями. Методи AddChild та RemoveChild забезпечують додавання дочірніх
сутностей до батьківських і видалення відповідних зв’язків. Видалення
батьківської сутності автоматично ініціює видалення її дочірніх сутностей, що
значно спрощує керування складними структурами.
5. Система подій:
Користувач може створювати та обробляти події, використовуючи функції
AddEvent та EventFilter.
4.1.6 Опис розробки програмних компонентів
В розробленій ECS системі для мобільного ігрового додатку реалізовано
ряд модулів, які забезпечують ефективну організацію та управління сутностями,
їх компонентами, а також обробку подій. Основною точкою входу є клас, що
відповідає за ініціалізацію всієї системи та взаємодію між її модулями.
World - є основною точкою доступу до всіх сутностей, архетипів та
компонентів в системі. Цей компонент виконує роль головного керівника в
процесі взаємодії між іншими компонентами. Він взаємодіє з модулями для
створення, модифікації та видалення сутностей, а також керує фільтрацією за
допомогою архетипів і компонентів.
Entity - клас, що представляє сутність в ECS системі. Кожна сутність це
ідентифікатор для масиву компонентів. Цей клас також містить логіку для
підтримки стану сутності в світі, а саме ідентифікатор архетипу та його індекс.
Component - компонент є складовою частиною сутності, що зберігає дані
та надає функціональність. Кожен компонент є типізованим і має конкретне
призначення, наприклад, для здоров’я персонажа, інвентарю або шкідливих
ефектів.
ArchetypeManager - відповідає за управління архетипами, які визначають
структуру та типи компонентів, що належать до кожної сутності. Архетипи
60
ЧДТУ 242323.010 МПЗ
дозволяють оптимізувати запити на фільтрацію сутностей, що мають певні
компоненти.
Migration - компонент, що відповідає за зміну компонентів сутностей в
процесі гри. Наприклад, якщо сутність повинна отримати новий компонент або
змінити існуючий, цей компонент відповідає за перенесення даних між
архетипами або компонентами.
Filter - клас, який забезпечує фільтрацію сутностей за набором
компонентів. Запит на фільтрацію може бути виконаний через інтерфейс
користувача, що дозволяє вибрати необхідні критерії для вибору сутностей, що
задовольняють умови фільтрації.
Event System - система подій, яка забезпечує механізм асинхронної
обробки подій. Кожен тип події може бути оброблений окремим обробником,
який змінює стан гри або сутностей на основі події. Цей компонент також
взаємодіє з іншими модулями для зміни стану гри чи об’єктів.
Parent-Child Relations - клас, який реалізує підтримку батьківсько-дочірніх
зв’язків між сутностями. Наприклад, персонаж може бути батьком для
предметів, які він несе. Видалення батьківської сутності автоматично ініціює
видалення дочірніх сутностей.
4.2 Тестування системи
4.2.1 Модульне тестування
Написання модульних тестів було виконано в спеціальному для цього
модулі та за допомогою інструментів для тестування, а саме:
NUnit - це бібліотека для тестування програмного забезпечення для мови
програмування С#. Вона надає розширений функціонал для написання та
виконання тестів одиниць (unit tests) та інтеграційних тестів для проектів,
розроблених з використанням Unity. [21]
61
ЧДТУ 242323.010 МПЗ
Модульні тести розроблялись згідно з методологією AAA [22], що умовно
розділяє тест на три основні етапи: Arrange - підготовка даних для тестування;
Act - виконання бізнес-логіки тестованої системи; Assert - перевірка результатів.
Рис. 4.3 - Уривок коду ініціалізації тестів
Тести для модуля створення сутностей реалізовані в файлі EntitiesTests.cs,
та представлені нижче на рисунках 4.4-4.8:
Рис. 4.4 - Тестування функції видалення пустих сутностей
62
ЧДТУ 242323.010 МПЗ
Рис. 4.5 - Тестування видалення компонентів при видаленні сутностей
Рис. 4.6 - Метод для тестування функції отримання сутності по ідентифікатору
63
ЧДТУ 242323.010 МПЗ
Рис. 4.7 - Метод для перевикористання ідентифікаторів сутності
Рис. 4.8 - Методи для створення і видалення батьківських зв’язків
64
ЧДТУ 242323.010 МПЗ
3.2.2 Інтеграційне тестування
Інтеграційне тестування в цьому проекті було спрямоване на перевірку
взаємодії між різними компонентами ECS системи, а також на забезпечення
коректної роботи всіх підсистем при їх спільному використанні. Метою
тестування було виявлення помилок, які можуть виникнути під час інтеграції
різних частин системи, і перевірка того, як окремі компоненти взаємодіють
один з одним у реальних умовах роботи системи.
Тестування почалося з перевірки базових функціональних компонентів,
таких як створення, модифікація та видалення сутностей, додавання та
видалення компонентів, а також перевірка фільтрації сутностей за допомогою
архетипів. Кожен з цих процесів був протестований окремо для того, щоб
переконатися, що зміни в одній частині системи не призводять до
непередбачених помилок в іншій. Для цього використовувалися автоматизовані
тести, які запускалися в середовищі, що імітує реальні умови роботи системи.
Під час інтеграційного тестування особлива увага приділялася взаємодії
між компонентами, що відповідають за архетипи та фільтрацію сутностей.
Тести перевіряли, чи правильно система обробляє запити на фільтрацію
сутностей і чи коректно архетипи оновлюються в разі додавання або видалення
компонентів з сутностей. Важливою частиною тестування було забезпечення
того, щоб система не створювала помилок або невідповідностей між
архетипами та сутностями при зміні їхнього складу.
Іншою важливою частиною інтеграційного тестування була перевірка
механізму підтримки батьківсько-дочірніх зв’язків між сутностями. Це
включало тестування правильності створення, зміни та видалення дочірніх
сутностей при зміні стану батьківської сутності.
Для тестування були використані як юніт-тести, так і інтеграційні тести,
які дозволяють перевірити взаємодію кількох компонентів системи. Результати
тестів демонстрували, що в основному система стабільно працює в умовах
інтеграції різних частин, хоча було виявлено кілька проблем, пов’язаних з
65
ЧДТУ 242323.010 МПЗ
оптимізацією фільтрації сутностей при великих обсягах даних. Ці проблеми
були виправлені в процесі рефакторингу.
У результаті інтеграційного тестування було підтверджено, що всі
компоненти системи правильно взаємодіють між собою, і система здатна
обробляти різноманітні сценарії взаємодії сутностей, подій та архетипів, що
робить систему стабільною та готовою до використання в реальних умовах.
4.2.3 Системне тестування
Системне тестування є важливою частиною процесу забезпечення якості
програмного забезпечення, яке перевіряє, чи відповідає система вимогам
замовника, і чи правильно функціонує її структура та компоненти. Після
успішного завершення інтеграційного тестування, яке забезпечує перевірку
взаємодії між модулями, було проведено системне тестування всієї ECS системи
в цілому.
Для проведення системного тестування було створено спеціалізований
код, що імітує роботу гри та охоплює основні функції системи. Тестовий код
містив реалізацію ключових функціональностей ECS системи, зокрема
створення, видалення, додавання та видалення компонентів, а також фільтрацію
сутностей. Ці функції є основними для коректного функціонування ігрової
системи та перевірялись на відповідність вимогам, що були висунуті до
системи.
Створення сутностей:
Тестувальник перевірив можливість створення сутностей в системі через
відповідні методи та переконався, що кожна сутність може бути створена з
базовими атрибутами та збережена в базі даних. Під час тестування також
перевірялося, що після створення сутності з нею можна виконувати
різноманітні операції, такі як додавання та видалення компонентів.
Видалення сутностей:
Тестування видалення сутностей включало перевірку того, що при
видаленні сутності всі її компоненти та дані також коректно видаляються без
пошкодження даних інших сутностей в системі. Також була перевірена
66
ЧДТУ 242323.010 МПЗ
здатність системи зберігати цілісність даних, навіть якщо одночасно
видаляються кілька сутностей.
Додавання та видалення компонентів:
Тестувальник перевіряв можливість динамічного додавання та видалення
компонентів до сутностей без втрати існуючих даних. Для цього були
розроблені тести, які перевіряли правильність додавання нових компонентів до
сутностей, а також механізми їхнього видалення. Були перевірені як прості
компоненти, так і компоненти, які взаємодіють з іншими частинами системи
(наприклад, компоненти, що відповідають за взаємодію з фізикою чи AI).
Фільтрація сутностей:
Також тестувалося коректне виконання фільтрації сутностей на основі
наявних компонентів. Для цього було створено набір запитів на фільтрацію, які
відбирали сутності за конкретними критеріями (наприклад, наявність
компонента здоров’я або обраного предмета). Перевірялося, що система
правильно обробляє запити та видає точні результати відповідно до
запитуваного набору компонентів.
Результати тестування:
Усі тестовані функції пройшли перевірку на відповідність вимогам. Під
час тестування не було виявлено критичних помилок, і система показала свою
здатність коректно працювати з усіма основними функціональними вимогами,
зокрема з управлінням сутностями та їх компонентами, а також з фільтрацією
сутностей.
Системне тестування продемонструвало, що система функціонує згідно з
технічними вимогами, і не було виявлено серйозних недоліків, які б могли
вплинути на її стабільність чи продуктивність в реальних умовах.
4.2.4 Приймальне тестування
Процес приймального тестування включає декілька критичних етапів, що
допоможуть забезпечити якість та функціональність мобільного ігрового
додатку.
67
ЧДТУ 242323.010 МПЗ
На початку, планування тестування визначає стратегію його проведення.
Це охоплює розробку тестових сценаріїв, які включають в себе різні аспекти
додатку, а також створення критеріїв оцінки та тестових кейсів.
Після цього, підготовка тестових середовищ готує необхідні ресурси для
виконання тестування. Це може включати встановлення додатку на відповідні
пристрої, створення локальних мереж або налаштування точок доступу Wi-Fi.
Виконання тестових сценаріїв включає в себе програвання запланованих
сценаріїв, що тестують різні аспекти додатку, включаючи його
функціональність, взаємодію між гравцями, графіку та звукові ефекти. Також
проводиться перевірка стабільності та продуктивності додатку.
Помилки та некоректне функціонування додатку виявляють під час
тестування, і їх фіксують для подальшого виправлення. Відстеження
виправлень дозволяє переконатися, що виявлені проблеми були вирішені і
додаток працює належним чином.
Остаточна оцінка приймального тестування заснована на результатів
тестування та враховує вимоги, визначені у постановці завдання. Ця оцінка
може включати аспекти, такі як функціональність, стабільність, продуктивність
та користувацький досвід. Завдяки цьому процесу приймального тестування
забезпечується висока якість та задоволення користувачів від мобільного
ігрового додатку.
4.3 Приклади впровадження програмного комплексу.
4.3.1 Розгортання системи
Проект був розроблений як open source, що дозволяє будь-кому вносити
зміни або додавати новий функціонал відповідно до своїх потреб. Завдяки
такому підходу, система може бути адаптована до різних умов і проектів, що
робить її універсальним інструментом для розробників. Всі зміни та оновлення
коду публікуються у відкритому репозиторії на GitHub, що дає можливість
спільноті активно брати участь у розвитку та покращенні фреймворку.
Для того, щоб розгорнути систему та почати її використання в власному
проекті, необхідно завантажити останню актуальну версію бібліотеки у вигляді
68
ЧДТУ 242323.010 МПЗ
DLL з репозиторію на GitHub. Після цього бібліотеку потрібно підключити до
свого проекту. Інструкція по підключенню доступна в документації
репозиторію та детально описує всі кроки, починаючи з підготовки середовища
розробки до налаштування фреймворку.
Далі, дотримуючись покрокових інструкцій у документації, розробники
можуть почати створювати свої власні проекти на базі цього фреймворку. Вся
необхідна інформація щодо конфігурації, налаштувань та прикладів
використання надається для полегшення впровадження системи в різноманітні
типи проектів. Документація також містить приклади коду для демонстрації
основних функціональних можливостей фреймворку, що дозволяє швидко
освоїти систему та ефективно інтегрувати її в існуючі проекти.
Таким чином, для розгортання системи достатньо декількох простих
кроків, що робить фреймворк легким у використанні та швидким у
впровадженні, дозволяючи розробникам зосередитись на реалізації
специфічних бізнес-логік і функціоналу без необхідності створювати базову
інфраструктуру з нуля.
69
ЧДТУ 242323.010 МПЗ
ВИСНОВОК
Було проведено дослідження та розробка структурної оптимізації
програмного забезпечення для мобільних ігор у жанрі Roguelike, зокрема на
основі розробленого фреймворку ECS (Entity-Component-System). Система була
спроектована для оптимізації обробки даних та взаємодії сутностей, що
дозволяє значно покращити ефективність роботи додатків у реальному часі.
Враховуючи тенденції розвитку індустрії та вимоги до швидкості обробки
даних, розробка такого фреймворку є актуальною як для наукових, так і для
практичних застосувань.
Методи вирішення наукової задачі включали аналіз існуючих
архітектурних підходів до розробки ECS систем, а також впровадження
оптимізацій для зменшення часу доступу до даних та витрат пам’яті. На основі
цих методів був створений власний фреймворк, який дозволяє ефективно
управляти сутностями, компонентами та архетипами. Застосування цієї
архітектури дозволило значно знизити навантаження на систему при обробці
великої кількості сутностей, що важливо для мобільних платформ з
обмеженими ресурсами. Рішення, запропоновані в роботі, перевищують
традиційні підходи до роботи з даними в грі та надають гнучкість у розробці
складних ігрових систем.
Практичний аналіз показав, що розроблений фреймворк на основі ECS
значно покращує масштабованість і продуктивність, зокрема в умовах
обмежених ресурсів мобільних пристроїв. Отримані результати підтвердили
ефективність запропонованих рішень у порівнянні з іншими методами
організації даних, такими як використання складних структур даних або
традиційні об'єктно-орієнтовані підходи. Розроблений фреймворк може бути
успішно використаний у мобільних іграх та інших реальних додатках, що
вимагають високої ефективності обробки даних.
Для подальших досліджень та вдосконалення системи можна
рекомендувати реалізацію додаткових механізмів для автоматичного
масштабування та адаптації до різних платформ. Це дозволить забезпечити
70
ЧДТУ 242323.010 МПЗ
більш гнучке та ефективне використання ресурсів в залежності від специфічних
вимог і можливостей пристроїв, а також удосконалення механізмів для обробки
подій у реальному часі.
Отже, була створена гіпотеза та підтверджена теоретично і
експериментально, на основі дослідження спроектовано, розроблено та
протестовано програмне забезпечення у виді ECS фреймворку, що оптимізує
структуру мобільних ігор жанру Roguelike.
71
ЧДТУ 242323.010 МПЗ
СПИСОК ВИКОРИСТАНИХ ДЖЕРЕЛ
1 Martin, R. C. Clean Code: A Handbook of Agile Software Craftsmanship.
Prentice Hall; July, 2009; Language: English ISBN-0-13-235088-2:
2008024750
2 Gamma, E., Helm, R., Johnson, R., & Vlissides, J. Design Patterns: Elements
of Reusable Object-Oriented Software; March, 2009; Language: English
ISBN-0-201-63361-2: 2008024750
3 Unity Technologies. (2023). Unity DOTS Documentation. - [Електронний
ресурс] - Режим доступу: https://unity.com/dots
4 Richard Fabian, Data-Oriented Design - [Електронний ресурс] - Режим
доступу: https://www.dataorienteddesign.com/dodmain/
5 Morpeh - [Електронний ресурс] - Режим доступу:
https://github.com/scellecs/morpeh
6 Entitas - [Електронний ресурс] - Режим доступу:
https://github.com/sschmid/Entitas/wiki
7 ECS - типы и проблемы - [Електронний ресурс] - Режим доступу:
https://spiiin.github.io/blog/1601029690/
8 ECS: under the hood - [Електронний ресурс] - Режим доступу:
https://habr.com/ru/articles/651921/
9 Structure types (C# reference) - [Електронний ресурс] - Режим доступу:
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-type
s/struct
10 Sander Mertens, Building an ECS #2: Archetypes and Vectorization -
[Електронний ресурс] - Режим доступу:
https://ajmmertens.medium.com/building-an-ecs-2-archetypes-and-vectorizatio
n-fe21690805f9
11 BenchmarkDotNet - [Електронний ресурс] - Режим доступу:
https://benchmarkdotnet.org/
12 Entity Component System - [Електронний ресурс] - Режим доступу:
https://en.wikipedia.org/wiki/Entity_component_system
72
ЧДТУ 242323.010 МПЗ
13 Rosenberg D., Scott K. Application of object modeling using UML and case
analysis / D. Rosenberg, K. Scott. - Per. from English. – M.: DMK Press, 2002.
– 160 p. Publisher: Addison Wesley First Edition June 14, 2001 ISBN:
0-201-73039-1, 176 pages
14 Unity 3D - [Електронний ресурс] - Режим доступу:
https://docs.unity3d.com/Manual/index.html
15 C# language documentation - [Електронний ресурс] - Режим доступу:
https://learn.microsoft.com/en-us/dotnet/csharp/
16 JSON - [Електронний ресурс] - Режим доступу:
https://www.json.org/json-en.html
17 DotTrace - [Електронний ресурс] - Режим доступу:
https://www.jetbrains.com/profiler/
18 Robert Nystrom. Game Programming Patterns - [Електронний ресурс] -
Режим доступу: https://gameprogrammingpatterns.com/
19 Sander Mertens, Why it is time to start thinking of games as databases -
[Електронний ресурс] - Режим доступу:
https://ajmmertens.medium.com/why-it-is-time-to-start-thinking-of-games-as-d
atabases-e7971da33ac3
20 A Guidance Framework for Designing a Great API - [Електронний ресурс]
- Режим доступу:
https://henrylee1.gitbooks.io/note/content/gartner/design-api.html
21 NUnit - [Електронний ресурс] - Режим доступу: https://nunit.org/
22 Vladimir Khorikov. Unit-testing. (2020) Language: English 320 pages ISBN:
9781617296277
23 Методичні рекомендації до підготовки кваліфікаційної роботи магістра
для здобувачів вищої освіти другого (магістерського) рівня зі
спеціальності 121 «Інженерія програмного забезпечення» усіх форм
навчання [Текст] /Укл.: Голуб С.В., Данченко О.Б., Півень О.Б. ; М-во
освіти і науки України, Черкас, держ. технол. ун-т. - Черкаси : ЧДТУ, 2022.
- 104 с.
73
ДОДАТОК А
ЗАТВЕРДЖЕНО:
Зав. кафедри ПЗАС
проф. Голуб С. В.
___________________________
СТРУКТУРНА ОПТИМІЗАЦІЯ ПРОГРАМНОГО ЗАБЕЗПЕЧЕННЯ
МОБІЛЬНИХ ІГОР ЖАНРУ ROGUELIKE НА ОСНОВІ РУШІЯ UNITY
Специфікація
482.ЧДТУ.242323-010 ПЗ
Листів 1
Розробник ________________ Чуплий М. А.
Керівник ________________ Олексюк В.В.
Н. Контроль ________________ Півень О.Б.
Черкаси 2024
Позначення Найменування Примітка
Документація
482.ЧДТУ.242323-010 12-01 Лістинг програми
482.ЧДТУ.242323-010 34-01 Інструкція користувача
482.ЧДТУ.242323-010 90-01 Графічні матеріали
75
ДОДАТОК Б
СТРУКТУРНА ОПТИМІЗАЦІЯ ПРОГРАМНОГО ЗАБЕЗПЕЧЕННЯ
МОБІЛЬНИХ ІГОР ЖАНРУ ROGUELIKE НА ОСНОВІ РУШІЯ UNITY
Лістинг програми
482.ЧДТУ.242323 12-01
Листів 59
Розробник ________________ Чуплий М. А.
Черкаси 2024
482.ЧДТУ.242323-010 12-01
Файл World.cs
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using JaECS.Collections;
namespace JaECS
{
#if ENABLE_IL2CPP
using Unity.IL2CPP.CompilerServices;
[Il2CppSetOption(Option.NullChecks, false)]
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]
[Il2CppSetOption(Option.DivideByZeroChecks, false)]
#endif
public sealed partial class World
{
internal byte Id;
internal byte Gen;
internal WorldConfig Config;
#region Entities
internal EntityRef[] Entities;
internal EntityArchetypeData[] EntitiesData;
internal int NextNewEntityId;
internal BitMask AliveEntitesMask;
internal int[] RecycledEntitiesIndex;
internal int RecycledEntitiesCount;
#endregion
#region Archetypes
internal Pool<Archetype> ArchetypesPool;
internal BitmaskChunksHashMap ArchetypesIdsLookup;
internal Archetype[] Archetypes;
#endregion
#region Migration
internal Migrations Migrations;
#endregion
77
482.ЧДТУ.242323-010 12-01
#region Components
internal IntHashMap ComponentsIdsLookup;
internal Row[] ComponentRows;
internal Dictionary<FilterMask, ArchetypeFilter> CachedFilters;
internal Dictionary<(int eventId, FilterMask mask), EventFilter> CachedEventFilters;
internal Dictionary<(FilterMask parentFilterMask, FilterMask childFilterMask),
RelationFilter> CachedRelationFilters;
internal int IterationDepth;
#endregion
#region Events
internal Dictionary<Type, Event> EventsLookup;
internal DynamicArray<Event> Events;
#endregion
private World() { }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static World Create(WorldConfig config = default)
{
config ??= WorldConfig.Default;
var world = new World();
for (var i = Static.Worlds.Length - 1; i >= 0; i--)
{
if (Static.Worlds[i] == null)
{
Static.Worlds[i] = world;
Static.Gens[i]++;
goto Initialize;
}
}
Array.Resize(ref Static.Worlds, Static.Worlds.Length + 1);
Array.Resize(ref Static.Gens, Static.Gens.Length + 1);
Static.Worlds[^1] = world;
Initialize:
world.Config = config;
world.ComponentRows = new Row[4];
78
482.ЧДТУ.242323-010 12-01
world.ComponentsIdsLookup = new IntHashMap(4);
world.CachedEventFilters = new Dictionary<(int, FilterMask), EventFilter>(4);
world.CachedRelationFilters = new Dictionary<(FilterMask, FilterMask), RelationFilter>();
world.CachedFilters = new Dictionary<FilterMask, ArchetypeFilter>();
world.EntitiesData = new EntityArchetypeData[world.Config.InitialEntitiesCount];
world.Entities = new EntityRef[world.Config.InitialEntitiesCount];
world.AliveEntitesMask = new BitMask(world.Config.InitialEntitiesCount);
world.RecycledEntitiesIndex = new int[world.Config.InitialEntitiesCount];
world.Migrations = new Migrations(world);
world.ArchetypesPool = new Pool<Archetype>(5);
world.ArchetypesIdsLookup = new
BitmaskChunksHashMap(world.Config.InitialArchetypesCount);
world.Archetypes = new Archetype[world.Config.InitialArchetypesCount];
var archetype = new Archetype(world);
archetype.Mask = new BitMask(BitMask.MinBitsCount);
archetype.Init(0);
world.ArchetypesIdsLookup.Add(ref archetype.MaskChunks, out var index);
archetype.Id = index;
world.Archetypes[index] = archetype;
world.EventsLookup = new Dictionary<Type, Event>();
world.Events = new DynamicArray<Event>(1);
world.Parents = new SparseSet<Parent>(world.Config.InitialEntitiesCount);
world.Childs = new SparseSet<Child>(world.Config.InitialEntitiesCount);
world.Disposed = false;
return world;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
Static.Worlds[Id] = null;
foreach (var value in ArchetypesIdsLookup)
{
Archetypes[value].Dispose();
}
foreach (var value in RelationsIdsLookup)
{
Relations[value].Dispose();
}
79
482.ЧДТУ.242323-010 12-01
ComponentRows = null;
ComponentsIdsLookup = null;
Config = null;
CachedFilters = null;
EntitiesData = null;
Entities = null;
AliveEntitesMask = default;
RecycledEntitiesIndex = null;
NextNewEntityId = 0;
RecycledEntitiesCount = 0;
Migrations = null;
ArchetypesPool = null;
ArchetypesIdsLookup = null;
EventsLookup = null;
Events = null;
Disposed = true;
}
public bool Disposed { get; private set; }
internal int ComponentsInWorld
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ComponentsIdsLookup.Count;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Archetype GetRecycleArchetype() => Archetypes[0];
}
public class WorldConfig
{
public static WorldConfig Default = new WorldConfig();
public int InitialEntitiesCount = 512;
public int InitialMigrationSize = 10;
public int InitialArchetypesCount = 50;
public int ArchetypeEntitiesInitialCount = 25;
public int NewArchetypeFiltersInitialCount = 25;
80
482.ЧДТУ.242323-010 12-01
public int ComponentRowInitialSize = 5;
}
#if ENABLE_IL2CPP
[Il2CppEagerStaticClassConstruction]
[Il2CppSetOption(Option.NullChecks, false)]
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]
[Il2CppSetOption(Option.DivideByZeroChecks, false)]
#endif
internal static class Static
{
public static World[] Worlds;
public static byte[] Gens;
static Static()
{
Worlds = new World[1];
Gens = new byte[1];
}
}
}
Файл World.Entities.cs
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace JaECS
{
public partial class World
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsEntityAlive(in EntityRef entityRef) => entityRef.Gen > 0 &&
Entities[entityRef.Id].Gen == entityRef.Gen;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Row[] GetComponentsAlloc(in EntityRef entity)
{
if (!IsEntityAlive(entity))
{
throw new InvalidOperationException("Trying to get info from dead entity, it is not
allowed");
}
ref var archetypeData = ref GetEntityArchetypeData(entity.Id);
var archetype = Archetypes[archetypeData.ArchetypeId];
var componentsIds = archetype.ComponentsIds;
var components = new Row[archetype.ComponentsCount];
81
482.ЧДТУ.242323-010 12-01
for (var i = 0; i < archetype.ComponentsCount; i++)
{
components[i] = ComponentRows[componentsIds[i]];
}
return components;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int ComponentsCount(in EntityRef entityRef)
{
if (!IsEntityAlive(entityRef))
{
throw new InvalidOperationException("Trying to get info from dead entity, it is not
allowed");
}
ref var archetypeData = ref GetEntityArchetypeData(entityRef.Id);
var componentsCount = Archetypes[archetypeData.ArchetypeId].ComponentsCount;
if (!Migrations.Has(entityRef.Id))
{
ref var migration = ref Migrations.Get(entityRef.Id);
componentsCount += (migration.AddedComponentsCount -
migration.RemovedComponentsCount);
}
return componentsCount;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T1 GetComponent<T1>(in EntityRef entityRef)
where T1 : struct, IComponentData
{
return ref (ComponentRows[ComponentsIdsLookup.GetIndex(Row<T1>.TypeId)] as
Row<T1>).Get(entityRef.Id);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HasComponent<T1>(in EntityRef entityRef)
where T1 : struct, IComponentData
{
if (!IsEntityAlive(entityRef))
{
throw new InvalidOperationException("Trying to get info from dead entity, it is not
allowed");
}
82
482.ЧДТУ.242323-010 12-01
if (!ComponentsIdsLookup.TryGet(Row<T1>.TypeId, out var index))
{
return false;
}
return ComponentRows[index]!.Has(entityRef.Id);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddComponent<T1>(in EntityRef entityRef, T1 value1)
where T1 : struct, IComponentData
{
if (!IsEntityAlive(entityRef))
{
throw new InvalidOperationException("Trying to modify dead entity, it is not allowed");
}
var entityId = entityRef.Id;
ref var entityData = ref GetEntityArchetypeData(entityId);
var componentRow = ResolveComponentRow<T1>();
var archetype = Archetypes[entityData.ArchetypeId];
var offset = archetype.GetOffset(componentRow!.Id);
componentRow.AddComponentInternal(offset, entityId, ref entityData, ref
archetype.EntitiesKeys) = value1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddComponent<T1, T2>(in EntityRef entityRef, T1 value1, T2 value2)
where T1 : struct, IComponentData
where T2 : struct, IComponentData
{
if (!IsEntityAlive(entityRef))
{
throw new InvalidOperationException("Trying to modify dead entity, it is not allowed");
}
var entityId = entityRef.Id;
ref var entityData = ref EntitiesData[entityRef.Id];
var componentRow1 = ResolveComponentRow<T1>();
var componentRow2 = ResolveComponentRow<T2>();
var archetype = Archetypes[entityData.ArchetypeId];
83
482.ЧДТУ.242323-010 12-01
var offset1 = archetype.GetOffset(componentRow1!.Id);
var offset2 = archetype.GetOffset(componentRow2!.Id);
componentRow1.AddComponentInternal(offset1, entityId, ref entityData, ref
archetype.EntitiesKeys) = value1;
componentRow2.AddComponentInternal(offset2, entityId, ref entityData, ref
archetype.EntitiesKeys) = value2;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddComponent<T1, T2, T3>(in EntityRef entityRef, T1 value1, T2 value2, T3
value3)
where T1 : struct, IComponentData
where T2 : struct, IComponentData
where T3 : struct, IComponentData
{
if (!IsEntityAlive(entityRef))
{
throw new InvalidOperationException("Trying to modify dead entity, it is not allowed");
}
var entityId = entityRef.Id;
ref var entityData = ref GetEntityArchetypeData(entityId);
var componentRow1 = ResolveComponentRow<T1>();
var componentRow2 = ResolveComponentRow<T2>();
var componentRow3 = ResolveComponentRow<T3>();
var archetype = Archetypes[entityData.ArchetypeId];
var offset1 = archetype.GetOffset(componentRow1!.Id);
var offset2 = archetype.GetOffset(componentRow2!.Id);
var offset3 = archetype.GetOffset(componentRow3!.Id);
componentRow1.AddComponentInternal(offset1, entityId, ref entityData, ref
archetype.EntitiesKeys) = value1;
componentRow2.AddComponentInternal(offset2, entityId, ref entityData, ref
archetype.EntitiesKeys) = value2;
componentRow3.AddComponentInternal(offset3, entityId, ref entityData, ref
archetype.EntitiesKeys) = value3;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddComponent<T1, T2, T3, T4>(in EntityRef entityRef, T1 value1, T2 value2, T3
value3, T4 value4)
where T1 : struct, IComponentData
where T2 : struct, IComponentData
where T3 : struct, IComponentData
where T4 : struct, IComponentData
84
482.ЧДТУ.242323-010 12-01
{
if (!IsEntityAlive(entityRef))
{
throw new InvalidOperationException("Trying to modify dead entity, it is not allowed");
}
var entityId = entityRef.Id;
ref var entityData = ref GetEntityArchetypeData(entityId);
var componentRow1 = ResolveComponentRow<T1>();
var componentRow2 = ResolveComponentRow<T2>();
var componentRow3 = ResolveComponentRow<T3>();
var componentRow4 = ResolveComponentRow<T4>();
var archetype = Archetypes[entityData.ArchetypeId];
var offset1 = archetype.GetOffset(componentRow1!.Id);
var offset2 = archetype.GetOffset(componentRow2!.Id);
var offset3 = archetype.GetOffset(componentRow3!.Id);
var offset4 = archetype.GetOffset(componentRow4!.Id);
componentRow1.AddComponentInternal(offset1, entityId, ref entityData, ref
archetype.EntitiesKeys) = value1;
componentRow2.AddComponentInternal(offset2, entityId, ref entityData, ref
archetype.EntitiesKeys) = value2;
componentRow3.AddComponentInternal(offset3, entityId, ref entityData, ref
archetype.EntitiesKeys) = value3;
componentRow4.AddComponentInternal(offset4, entityId, ref entityData, ref
archetype.EntitiesKeys) = value4;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddComponent<T1, T2, T3, T4, T5>(in EntityRef entityRef, T1 value1, T2
value2, T3 value3, T4 value4, T5 value5)
where T1 : struct, IComponentData
where T2 : struct, IComponentData
where T3 : struct, IComponentData
where T4 : struct, IComponentData
where T5 : struct, IComponentData
{
if (!IsEntityAlive(entityRef))
{
throw new InvalidOperationException("Trying to modify dead entity, it is not allowed");
}
var entityId = entityRef.Id;
ref var entityData = ref GetEntityArchetypeData(entityId);
var componentRow1 = ResolveComponentRow<T1>();
var componentRow2 = ResolveComponentRow<T2>();
85
482.ЧДТУ.242323-010 12-01
var componentRow3 = ResolveComponentRow<T3>();
var componentRow4 = ResolveComponentRow<T4>();
var componentRow5 = ResolveComponentRow<T5>();
var archetype = Archetypes[entityData.ArchetypeId];
var offset1 = archetype.GetOffset(componentRow1!.Id);
var offset2 = archetype.GetOffset(componentRow2!.Id);
var offset3 = archetype.GetOffset(componentRow3!.Id);
var offset4 = archetype.GetOffset(componentRow4!.Id);
var offset5 = archetype.GetOffset(componentRow5!.Id);
componentRow1.AddComponentInternal(offset1, entityId, ref entityData, ref
archetype.EntitiesKeys) = value1;
componentRow2.AddComponentInternal(offset2, entityId, ref entityData, ref
archetype.EntitiesKeys) = value2;
componentRow3.AddComponentInternal(offset3, entityId, ref entityData, ref
archetype.EntitiesKeys) = value3;
componentRow4.AddComponentInternal(offset4, entityId, ref entityData, ref
archetype.EntitiesKeys) = value4;
componentRow5.AddComponentInternal(offset5, entityId, ref entityData, ref
archetype.EntitiesKeys) = value5;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveComponent<T1>(in EntityRef entityRef)
where T1 : struct, IComponentData
{
if (!IsEntityAlive(entityRef))
{
throw new InvalidOperationException("Trying to modify dead entity, it is not allowed");
}
var entityId = entityRef.Id;
ref var entityData = ref GetEntityArchetypeData(entityId);
var componentRow =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T1>.TypeId)] as Row<T1>;
var archetype = Archetypes[entityData.ArchetypeId];
var offset = archetype.GetOffset(componentRow.Id);
componentRow.RemoveComponentInternal(offset, entityId, ref entityData, ref
archetype.EntitiesKeys);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveComponent<T1, T2>(in EntityRef entityRef)
86
482.ЧДТУ.242323-010 12-01
where T1 : struct, IComponentData
where T2 : struct, IComponentData
{
if (!IsEntityAlive(entityRef))
{
throw new InvalidOperationException("Trying to modify dead entity, it is not allowed");
}
var entityId = entityRef.Id;
ref var entityData = ref GetEntityArchetypeData(entityId);
var componentRow1 =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T1>.TypeId)] as Row<T1>;
var componentRow2 =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T2>.TypeId)] as Row<T2>;
var archetype = Archetypes[entityData.ArchetypeId];
var offset1 = archetype.GetOffset(componentRow1!.Id);
var offset2 = archetype.GetOffset(componentRow2!.Id);
componentRow1.RemoveComponentInternal(offset1, entityId, ref entityData, ref
archetype.EntitiesKeys);
componentRow2.RemoveComponentInternal(offset2, entityId, ref entityData, ref
archetype.EntitiesKeys);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveComponent<T1, T2, T3>(in EntityRef entityRef)
where T1 : struct, IComponentData
where T2 : struct, IComponentData
where T3 : struct, IComponentData
{
if (!IsEntityAlive(entityRef))
{
throw new InvalidOperationException("Trying to modify dead entity, it is not allowed");
}
var entityId = entityRef.Id;
ref var entityData = ref GetEntityArchetypeData(entityId);
var componentRow1 =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T1>.TypeId)] as Row<T1>;
var componentRow2 =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T2>.TypeId)] as Row<T2>;
var componentRow3 =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T3>.TypeId)] as Row<T3>;
var archetype = Archetypes[entityData.ArchetypeId];
87
482.ЧДТУ.242323-010 12-01
var offset1 = archetype.GetOffset(componentRow1!.Id);
var offset2 = archetype.GetOffset(componentRow2!.Id);
var offset3 = archetype.GetOffset(componentRow3!.Id);
componentRow1.RemoveComponentInternal(offset1, entityId, ref entityData, ref
archetype.EntitiesKeys);
componentRow2.RemoveComponentInternal(offset2, entityId, ref entityData, ref
archetype.EntitiesKeys);
componentRow3.RemoveComponentInternal(offset3, entityId, ref entityData, ref
archetype.EntitiesKeys);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveComponent<T1, T2, T3, T4>(in EntityRef entityRef)
where T1 : struct, IComponentData
where T2 : struct, IComponentData
where T3 : struct, IComponentData
where T4 : struct, IComponentData
{
if (!IsEntityAlive(entityRef))
{
throw new InvalidOperationException("Trying to modify dead entity, it is not allowed");
}
var entityId = entityRef.Id;
ref var entityData = ref GetEntityArchetypeData(entityId);
var componentRow1 =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T1>.TypeId)] as Row<T1>;
var componentRow2 =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T2>.TypeId)] as Row<T2>;
var componentRow3 =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T3>.TypeId)] as Row<T3>;
var componentRow4 =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T4>.TypeId)] as Row<T4>;
var archetype = Archetypes[entityData.ArchetypeId];
var offset1 = archetype.GetOffset(componentRow1!.Id);
var offset2 = archetype.GetOffset(componentRow2!.Id);
var offset3 = archetype.GetOffset(componentRow3!.Id);
var offset4 = archetype.GetOffset(componentRow4!.Id);
componentRow1.RemoveComponentInternal(offset1, entityId, ref entityData, ref
archetype.EntitiesKeys);
componentRow2.RemoveComponentInternal(offset2, entityId, ref entityData, ref
archetype.EntitiesKeys);
componentRow3.RemoveComponentInternal(offset3, entityId, ref entityData, ref
archetype.EntitiesKeys);
88
482.ЧДТУ.242323-010 12-01
componentRow4.RemoveComponentInternal(offset4, entityId, ref entityData, ref
archetype.EntitiesKeys);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveComponent<T1, T2, T3, T4, T5>(in EntityRef entityRef)
where T1 : struct, IComponentData
where T2 : struct, IComponentData
where T3 : struct, IComponentData
where T4 : struct, IComponentData
where T5 : struct, IComponentData
{
if (!IsEntityAlive(entityRef))
{
throw new InvalidOperationException("Trying to modify dead entity, it is not allowed");
}
var entityId = entityRef.Id;
ref var entityData = ref GetEntityArchetypeData(entityId);
var componentRow1 =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T1>.TypeId)] as Row<T1>;
var componentRow2 =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T2>.TypeId)] as Row<T2>;
var componentRow3 =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T3>.TypeId)] as Row<T3>;
var componentRow4 =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T4>.TypeId)] as Row<T4>;
var componentRow5 =
ComponentRows[ComponentsIdsLookup.GetIndex(Row<T5>.TypeId)] as Row<T5>;
var archetype = Archetypes[entityData.ArchetypeId];
var offset1 = archetype.GetOffset(componentRow1!.Id);
var offset2 = archetype.GetOffset(componentRow2!.Id);
var offset3 = archetype.GetOffset(componentRow3!.Id);
var offset4 = archetype.GetOffset(componentRow4!.Id);
var offset5 = archetype.GetOffset(componentRow5!.Id);
componentRow1.RemoveComponentInternal(offset1, entityId, ref entityData, ref
archetype.EntitiesKeys);
componentRow2.RemoveComponentInternal(offset2, entityId, ref entityData, ref
archetype.EntitiesKeys);
componentRow3.RemoveComponentInternal(offset3, entityId, ref entityData, ref
archetype.EntitiesKeys);
componentRow4.RemoveComponentInternal(offset4, entityId, ref entityData, ref
archetype.EntitiesKeys);
componentRow5.RemoveComponentInternal(offset5, entityId, ref entityData, ref
archetype.EntitiesKeys);
89
482.ЧДТУ.242323-010 12-01
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public EntityRef CreateCopy(in EntityRef entityRef)
{
if (!IsEntityAlive(entityRef))
{
throw new InvalidOperationException("Trying to modify dead entity, it is not allowed");
}
var newEntity = CreateEntityInternal();
var newEntityId = newEntity.Id;
ref var entityData = ref GetEntityArchetypeData(entityRef.Id);
var archetype = Archetypes[entityData.ArchetypeId];
archetype.MoveFrom(newEntityId, ref GetEntityArchetypeData(newEntityId),
GetRecycleArchetype());
for (var i = archetype.ComponentsCount - 1; i >= 0; i--)
{
var component = ComponentRows[archetype.ComponentsIds[i]];
if (!component.CanCopy)
{
continue;
}
var offset = archetype.GetOffset(component.Id);
var idx = component.Copy(offset, ref entityData, ref archetype.EntitiesKeys);
Migrations.GetOrCreate(newEntityId).AddComponent(component.Id, idx,
component.Hash);
}
return newEntity;
}
}
}
Файл World.PublicAPI.cs
using System;
using System.Runtime.CompilerServices;
namespace JaECS
{
public partial class World
{
public int AliveEntitiesCount => NextNewEntityId - RecycledEntitiesCount;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public EntityRef CreateEntity<T>(T value)
90
482.ЧДТУ.242323-010 12-01
where T : struct, IComponentData
{
ref var entity = ref CreateEntityInternal();
GetComponentRow<T>().Add(entity.Id) = value;
return entity;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public EntityRef CreateEntity()
{
ref var entity = ref CreateEntityInternal();
Migrations.GetOrCreate(entity.Id);
return entity;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public EntityRef GetEntityRef(int id)
{
if (!TryGetEntity(id, out var entityRef))
{
throw new InvalidOperationException($"Entity {{ id: {id} }} - is not alive in the
world!");
}
return entityRef;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetEntity(int id, out EntityRef entityRef)
{
if (!AliveEntitesMask.GetBit(id))
{
entityRef = default;
return false;
}
entityRef = GetEntityInternal(id);
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void DestroyEntity(in EntityRef entityRef)
{
if (!IsEntityAlive(entityRef))
{
throw new InvalidOperationException($"Entity {{ id: {entityRef.Id} }} - is not alive in
the world!");
}
91
482.ЧДТУ.242323-010 12-01
DestroyEntityInternal(entityRef.Id);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ResolveComponentRow<T>(out Row<T> row)
where T : struct, IComponentData
{
row = ResolveComponentRow<T>();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Row<T> ResolveComponentRow<T>()
where T : struct, IComponentData
{
if (ComponentsIdsLookup.TryGet(Row<T>.TypeId, out var index))
{
return ComponentRows[index] as Row<T>;
}
return CreateRow<T>();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ResizeComponents(int newSize)
{
Array.Resize(ref ComponentRows, newSize);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Row<T> CreateRow<T>()
where T : struct, IComponentData
{
var prevCapacity = ComponentsIdsLookup.Capacity;
if (!ComponentsIdsLookup.Add(Row<T>.TypeId, out var index))
throw new InvalidOperationException($"World already has componentRow with same id:
{Row<T>.TypeId}");
if (prevCapacity != ComponentsIdsLookup.Capacity)
{
ResizeComponents(ComponentsIdsLookup.Capacity);
}
var componentRow = new Row<T>(this, index);
ComponentRows[index] = componentRow;
var oldSize = GetRecycleArchetype().ComponentIdToOffset.Length;
GetRecycleArchetype().ComponentIdToOffset.Resize(ComponentsIdsLookup.Capacity);
92
482.ЧДТУ.242323-010 12-01
GetRecycleArchetype().ComponentIdToOffset.Fill(oldSize,
GetRecycleArchetype().ComponentIdToOffset.Length - oldSize, 0);
Migrations.MigrationMask.Resize(ComponentsInWorld);
foreach (var archetypeId in ArchetypesIdsLookup)
{
Archetypes[archetypeId].ComponentIdToOffset.Resize(ComponentsInWorld);
Archetypes[archetypeId].ComponentIdToOffset[ComponentsInWorld - 1] = 0;
}
return componentRow;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Row<T> GetComponentRow<T>()
where T : struct, IComponentData
{
return ComponentRows[ComponentsIdsLookup.GetIndex(Row<T>.TypeId)] as Row<T>;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ForceMigrate() => Migrations.Process();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void EndFrame()
{
Migrations.Process();
for (var i = Events.Count() - 1; i >= 0; i--)
{
Events.GetRef(i).FrameEnd();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Event<TEventData> GetEvent<TEventData>()
where TEventData : struct, IEventData
{
if (EventsLookup.TryGetValue(typeof(TEventData), out var eventData))
{
return (Event<TEventData>)eventData;
}
var @event = new Event<TEventData>(Events.Count());
EventsLookup.Add(typeof(TEventData), @event);
Events.Add(@event);
return @event;
}
93
482.ЧДТУ.242323-010 12-01
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void GetAspectFactory<TAspect>(out AspectFactory<TAspect> aspectFactory)
where TAspect : struct, IAspect
{
aspectFactory = new AspectFactory<TAspect>(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddComponentAddedCallback<T>(Row<T>.ComponentCallback callback)
where T : struct, IComponentData
{
GetComponentRow<T>().ComponentAdded += callback;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveComponentAddedCallback<T>(Row<T>.ComponentCallback callback)
where T : struct, IComponentData
{
GetComponentRow<T>().ComponentAdded -= callback;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddComponentRemovedCallback<T>(Row<T>.ComponentCallback callback)
where T : struct, IComponentData
{
GetComponentRow<T>().ComponentRemoved += callback;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveComponentRemovedCallback<T>(Row<T>.ComponentCallback
callback)
where T : struct, IComponentData
{
GetComponentRow<T>().ComponentRemoved -= callback;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public FilterBuilderSelector GetFilterBuilder()
{
return new FilterBuilderSelector(this);
}
}
}
Файл World.Relations.cs
using System;
using System.Runtime.CompilerServices;
94
482.ЧДТУ.242323-010 12-01
using JaECS.Collections;
namespace JaECS
{
public partial class World
{
internal SparseSet<Parent> Parents;
internal SparseSet<Child> Childs;
internal ChildsGroup[] ChildsGroups = new ChildsGroup[1];
internal int ChildsGroupsCount;
internal int[] RecycledChildsGroups = new int[1];
internal int RecycledChildsGroupsCount;
internal LongHashMap RelationsIdsLookup = new LongHashMap(4);
internal EntitiesRelation[] Relations = new EntitiesRelation[4];
internal Pool<EntitiesRelation> RelationsPool = new Pool<EntitiesRelation>(1);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ref Parent GetParent(in Child child)
{
return ref Parents.GetRef(child.ParentId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ref Parent GetOrCreateParent(int parentId)
{
if (!Parents.Has(parentId))
{
ref var parent = ref Parents.Set(parentId);
parent.FirstChildsGroup = -1;
parent.LastChildsGroup = -1;
95
482.ЧДТУ.242323-010 12-01
parent.ChildsCount = 0;
return ref parent;
}
return ref Parents.GetRef(parentId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void DestroyRelations(int entityId)
{
if (Parents.Has(entityId))
{
ref var parent = ref Parents.GetRef(entityId);
var relationGroupId = parent.FirstChildsGroup;
while (relationGroupId != -1)
{
ref var relationGroup = ref ChildsGroups[relationGroupId];
relationGroupId = relationGroup.NextRelationGroup;
var childId = relationGroup.FirstChild;
while (childId != -1)
{
ref var child = ref Childs.GetRef(childId);
var entityIdToDestroy = childId;
childId = child.NextChild;
DestroyEntityInternal(entityIdToDestroy);
}
}
}
96
482.ЧДТУ.242323-010 12-01
if (Childs.Has(entityId))
{
ref var child = ref Childs.GetRef(entityId);
RemoveChildInternal(child.ParentId, entityId);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int ChildsCount(EntityRef parent)
{
var parentId = parent.Id;
return !Parents.Has(parentId) ? 0 : Parents.GetRef(parentId).ChildsCount;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddChild(in EntityRef parentEntity, in EntityRef childEntity)
{
var parentId = parentEntity.Id;
var childId = childEntity.Id;
if (parentId == childId)
{
throw new InvalidOperationException("Entity cannot be child of self");
}
if (Childs.Has(childId))
{
throw new InvalidOperationException("Entity already has parent");
}
var parentArchetype = GetEntityArchetypeData(parentId).ArchetypeId;
var childArchetype = GetEntityArchetypeData(childId).ArchetypeId;
97
482.ЧДТУ.242323-010 12-01
var relation = GetOrCreateRelation(parentArchetype, childArchetype);
relation.AddChild(parentId, childId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveChild(in EntityRef parentEntity, in EntityRef childEntity)
{
var parentId = parentEntity.Id;
var childId = childEntity.Id;
RemoveChildInternal(parentId, childId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveParent(in EntityRef childEntity)
{
var childId = childEntity.Id;
if (!Childs.Has(childId))
{
return;
}
ref var child = ref Childs.GetRef(childId);
RemoveChildInternal(child.ParentId, childId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ChildsFilter GetChildsFilterOf(in EntityRef parent, ArchetypeFilter archetypeFilter)
{
var parentId = parent.Id;
if (!Parents.Has(parentId))
{
return new ChildsFilter()
98
482.ЧДТУ.242323-010 12-01
{
World = this,
Parent = new Parent()
{
FirstChildsGroup = -1
}
};
}
return new ChildsFilter()
{
World = this,
Parent = Parents.GetRef(parentId),
ParentId = parentId,
ArchetypeFilter = archetypeFilter
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void RemoveChildInternal(int parentId, int childId)
{
if (!Childs.Has(childId))
{
throw new InvalidOperationException($"Entity with id: {childId} is not child");
}
ref var child = ref Childs.GetRef(childId);
if (child.ParentId == parentId)
{
var relation = GetCurrentRelation(ref child, childId);
relation.RemoveChild(child.ParentId, childId);
99
482.ЧДТУ.242323-010 12-01
ref var parent = ref Parents.GetRef(parentId);
if (parent.ChildsCount == 0)
{
Parents.Remove(parentId);
}
if (relation.FirstRelationId == -1)
{
CleanupRelation(relation);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void UpdateRelation(int entityId, Archetype prevArchetype)
{
if (Parents.Has(entityId))
{
var parentArchetype = GetEntityArchetypeData(entityId).ArchetypeId;
ref var parent = ref Parents.GetRef(entityId);
var relationGroupId = parent.FirstChildsGroup;
while (relationGroupId != -1)
{
ref var relationGroup = ref ChildsGroups[relationGroupId];
relationGroupId = relationGroup.NextRelationGroup;
var childArchetype = Relations[relationGroup.RelationId].ChildArchetype;
var relation = Relations[relationGroup.RelationId];
relation.RemoveChildsGroup(ref relationGroup);
if (relation.FirstRelationId == -1)
100
482.ЧДТУ.242323-010 12-01
{
CleanupRelation(relation);
}
relation = GetOrCreateRelation(parentArchetype, childArchetype.Id);
relation.AddChildsGroup(ref relationGroup);
}
}
if (Childs.Has(entityId))
{
ref var child = ref Childs.GetRef(entityId);
var parentId = child.ParentId;
if (TryGetRelation(GetEntityArchetypeData(parentId).ArchetypeId, prevArchetype.Id,
out var relation))
{
relation.RemoveChild(parentId, entityId);
if (relation.FirstRelationId == -1)
{
CleanupRelation(relation);
}
}
var parentArchetype = GetEntityArchetypeData(child.ParentId).ArchetypeId;
var childArchetype = GetEntityArchetypeData(entityId).ArchetypeId;
relation = GetOrCreateRelation(parentArchetype, childArchetype);
relation.AddChild(parentId, entityId);
}
}
101
482.ЧДТУ.242323-010 12-01
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private EntitiesRelation CreateRelation(long relationKey, int parentArchetypeId, int
childArchetypeId)
{
var relation = RelationsPool.Get();
if (relation == null)
{
relation = new EntitiesRelation(this, RelationsPool.Count(), parentArchetypeId,
childArchetypeId);
}
relation.Key = relationKey;
relation.ParentArchetype = Archetypes[parentArchetypeId];
relation.ChildArchetype = Archetypes[childArchetypeId];
for (var i = relation.ChildArchetype.ComponentsCount - 1; i >= 0; i--)
{
var filters =
ComponentRows[relation.ChildArchetype.ComponentsIds[i]].RelationFilters;
for (var j = filters.Count() - 1; j >= 0; j--)
{
filters.Get(j).TryAddRelation(relation);
}
}
RelationsIdsLookup.Add(relationKey, out var index);
if (Relations.Length != RelationsIdsLookup.Capacity)
{
Array.Resize(ref Relations, RelationsIdsLookup.Capacity);
}
102
482.ЧДТУ.242323-010 12-01
relation.Id = index;
Relations[index] = relation;
return relation;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void CleanupRelation(EntitiesRelation relation)
{
RelationsPool.Return(relation);
RelationsIdsLookup.Remove(relation.Key, out var slotIndex);
Relations[slotIndex] = null;
relation.Reset();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryGetRelation(int parentArchetypeId, int childArchetypeId, out EntitiesRelation
relation)
{
if (RelationsIdsLookup.TryGet(((long)parentArchetypeId << 32) | (uint)childArchetypeId,
out var id))
{
relation = Relations[id];
return true;
}
relation = null;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private EntitiesRelation GetOrCreateRelation(int parentArchetypeId, int childArchetypeId)
103
482.ЧДТУ.242323-010 12-01
{
var relationKey = ((long)parentArchetypeId << 32) | (uint)childArchetypeId;
if (RelationsIdsLookup.TryGet(relationKey, out var relationIdx))
{
return Relations[relationIdx];
}
return CreateRelation(relationKey, parentArchetypeId, childArchetypeId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private EntitiesRelation GetCurrentRelation(ref Child child, int childId)
{
var parentArchetypeId = GetEntityArchetypeData(child.ParentId).ArchetypeId;
var childArchetypeId = GetEntityArchetypeData(childId).ArchetypeId;
var relationKey = ((long)parentArchetypeId << 32) | (uint)childArchetypeId;
return Relations[RelationsIdsLookup.GetIndex(relationKey)];
}
}
}
Файл Archetype.cs
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using JaECS.Collections;
namespace JaECS
{
#if ENABLE_IL2CPP
104
482.ЧДТУ.242323-010 12-01
using Unity.IL2CPP.CompilerServices;
[Il2CppSetOption(Option.NullChecks, false)]
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]
[Il2CppSetOption(Option.DivideByZeroChecks, false)]
[Il2CppEagerStaticClassConstruction]
#endif
public sealed class Archetype
{
internal readonly World World;
internal int Id;
internal BitMask Mask;
internal BitMaskChunks MaskChunks;
internal ArchetypeFilter[] Filters;
internal int FiltersCount;
internal IntUnsafeArray ComponentsIds;
internal UshortUnsafeArray ComponentIdToOffset;
internal IntUnsafeArray EntitiesKeys;
internal int EntityKeySize;
internal int NextIdx;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Archetype(World world)
{
World = world;
Filters = new ArchetypeFilter[world.Config.NewArchetypeFiltersInitialCount];
EntityKeySize = 1;
MaskChunks = new BitMaskChunks(1);
ComponentIdToOffset = new UshortUnsafeArray(world.ComponentsInWorld);
105
482.ЧДТУ.242323-010 12-01
ComponentIdToOffset.Fill(0);
}
public int ComponentsCount
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ComponentsIds.Length;
}
public int NextKeyStep
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => EntityKeySize;
}
public int Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => NextIdx / EntityKeySize;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public World GetWorld()
{
return World;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IntUnsafeArray GetKeys()
{
return EntitiesKeys.ToCopyRef(NextIdx);
}
106
482.ЧДТУ.242323-010 12-01
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Entities GetEntities()
{
return new Entities(World.Entities, GetKeys(), EntityKeySize);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Row<T>.Components GetComponents<T>()
where T : struct, IComponentData
{
var row = World.GetComponentRow(Row<T>.TypeId) as Row<T>;
return new Row<T>.Components(row, GetKeys(), GetOffset(row.Id));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Init(int newHash)
{
MaskChunks.Init(ref Mask, newHash);
ComponentsIds = ComponentsUtils.CreateComponentsIdsFromMask(ref Mask);
if (ComponentIdToOffset.Length < World.ComponentsInWorld)
{
var oldSize = ComponentIdToOffset.Length;
ComponentIdToOffset.Resize(World.ComponentsInWorld);
ComponentIdToOffset.Fill(oldSize, ComponentIdToOffset.Length - oldSize, 0);
}
for (var i = ComponentsIds.Length - 1; i >= 0; i--)
{
ComponentIdToOffset[ComponentsIds[i]] = (ushort)(i + 1);
}
EntityKeySize = ComponentsIds.Length + 1;
107
482.ЧДТУ.242323-010 12-01
EntitiesKeys = new IntUnsafeArray(EntityKeySize *
World.Config.ArchetypeEntitiesInitialCount);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void MoveFrom(int entityId, ref EntityArchetypeData archetypeData, Archetype from)
{
var entityIdx = archetypeData.ArchetypeDataIdx;
var newIdx = NextIdx;
if (EntitiesKeys.Length <= newIdx)
{
EntitiesKeys.Resize(EntitiesKeys.Length << 1);
}
var newArchetypeEntitiesData = EntitiesKeys;
var fromArchetypeEntitiesData = from.EntitiesKeys;
newArchetypeEntitiesData[newIdx] = entityId;
var fromComponentsIds = from.ComponentsIds;
for (var i = from.ComponentsCount - 1; i >= 0; i--)
{
var id = fromComponentsIds[i];
var offset = ComponentIdToOffset[id];
if (offset == 0)
{
continue;
}
newArchetypeEntitiesData[newIdx + offset] = fromArchetypeEntitiesData[entityIdx + i +
1];
108
482.ЧДТУ.242323-010 12-01
}
from.Remove(entityIdx);
archetypeData.ArchetypeId = Id;
archetypeData.ArchetypeDataIdx = newIdx;
NextIdx += EntityKeySize;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Remove(int entityIdx)
{
NextIdx -= EntityKeySize;
MemoryMarshal.CreateSpan(ref EntitiesKeys[NextIdx], EntityKeySize)
.CopyTo(MemoryMarshal.CreateSpan(ref EntitiesKeys[entityIdx], EntityKeySize));
World.EntitiesData[EntitiesKeys[entityIdx]].ArchetypeDataIdx = entityIdx;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void SetComponentIdxForEntity(int entityDataArchetypeIdx, int componentId, int
idx)
{
EntitiesKeys[entityDataArchetypeIdx + ComponentIdToOffset[componentId]] = idx;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void SetEmpty(int id, ref EntityArchetypeData archetypeData)
{
if (EntitiesKeys.Length <= id)
{
EntitiesKeys.Resize(EntitiesKeys.Length << 1);
}
109
482.ЧДТУ.242323-010 12-01
archetypeData.ArchetypeId = Id;
archetypeData.ArchetypeDataIdx = NextIdx;
EntitiesKeys[NextIdx] = id;
NextIdx += EntityKeySize;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void AddFilter(ArchetypeFilter archetypeFilter)
{
if (Filters.Length == FiltersCount)
{
ResizeFilters(FiltersCount << 1);
}
Filters[FiltersCount] = archetypeFilter;
FiltersCount++;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ResizeFilters(int newSize)
{
Array.Resize(ref Filters, newSize);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Reset()
{
for (var i = FiltersCount - 1; i >= 0; i--)
{
Filters[i].RemoveArchetype(Id);
}
110
482.ЧДТУ.242323-010 12-01
FiltersCount = 0;
for (var i = ComponentsCount - 1; i >= 0; i--)
{
ComponentIdToOffset[ComponentsIds[i]] = 0;
}
NextIdx = 0;
ComponentsIds.Free();
EntitiesKeys.Free();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal int GetOffset(int id)
{
return ComponentIdToOffset[id];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Dispose()
{
ComponentsIds.Free();
EntitiesKeys.Free();
ComponentIdToOffset.Free();
}
}
}
Файл Filter.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
111
482.ЧДТУ.242323-010 12-01
using JaECS.Collections;
namespace JaECS
{
#if ENABLE_IL2CPP
using Unity.IL2CPP.CompilerServices;
[Il2CppSetOption(Option.NullChecks, false)]
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]
[Il2CppSetOption(Option.DivideByZeroChecks, false)]
#endif
public class ArchetypeFilter
{
internal World World;
internal Archetype[] Archetypes;
internal IntHashMap ArchetypeIdsLookup;
internal BitMaskChunks IncludeBitmask;
internal BitMaskChunks ExcludeBitmask;
internal int FirstComponentId;
public ArchetypeFilter(World world, BitMask includeMask, BitMask excludeMask, int
firstComponentId)
{
World = world;
FirstComponentId = firstComponentId;
IncludeBitmask = new BitMaskChunks(ref includeMask);
ExcludeBitmask = new BitMaskChunks(ref excludeMask);
ArchetypeIdsLookup = new IntHashMap(5);
Archetypes = new Archetype[5];
112
482.ЧДТУ.242323-010 12-01
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetLength()
{
var length = 0;
foreach (var slotIndex in ArchetypeIdsLookup)
{
length += Archetypes[slotIndex].Size;
}
return length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Has(int entityId)
{
return ArchetypeIdsLookup.Has(World.GetEntityArchetypeData(entityId).ArchetypeId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Has(Archetype archetype)
{
return ArchetypeIdsLookup.Has(archetype.Id);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool ArchetypeFit(Archetype archetype)
{
return IncludeBitmask.All(ref archetype.Mask) && !ExcludeBitmask.Any(ref
archetype.Mask);
}
113
482.ЧДТУ.242323-010 12-01
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void TryAddArchetype(Archetype archetype)
{
if (!ArchetypeFit(archetype))
{
return;
}
if(!ArchetypeIdsLookup.Add(archetype.Id, out var slotIndex))
throw new InvalidOperationException($"Filter already has Archetype with same id:
{archetype.Id}");
if (Archetypes.Length != ArchetypeIdsLookup.Capacity)
{
ResizeArchetypes(ArchetypeIdsLookup.Capacity);
}
Archetypes[slotIndex] = archetype;
archetype.AddFilter(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ResizeArchetypes(int size)
{
Array.Resize(ref Archetypes, size);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void RemoveArchetype(int id)
{
ArchetypeIdsLookup.Remove(id, out var slotIndex);
114
482.ЧДТУ.242323-010 12-01
Archetypes[slotIndex] = null;
}
public void Dispose()
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Enumerator GetEnumerator()
{
if (!World.IsMigrationLocked())
{
World.ForceMigrate();
}
var enumerator = default(Enumerator);
enumerator.World = World;
enumerator.Archetypes = Archetypes;
enumerator.MapEnumerator = ArchetypeIdsLookup.GetEnumerator();
World.IterationDepth++;
return enumerator;
}
#if ENABLE_IL2CPP
[Il2CppSetOption(Option.NullChecks, false)]
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]
[Il2CppSetOption(Option.DivideByZeroChecks, false)]
#endif
public struct Enumerator
{
internal World World;
115
482.ЧДТУ.242323-010 12-01
internal Archetype[] Archetypes;
internal IntHashMap.Enumerator MapEnumerator;
public Archetype Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Archetypes[MapEnumerator.Current];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (MapEnumerator.MoveNext())
{
return true;
}
World.IterationDepth--;
return false;
}
}
}
}
Файл ComponentRow.cs
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using JaECS.Collections;
namespace JaECS
{
#if ENABLE_IL2CPP
116
482.ЧДТУ.242323-010 12-01
using Unity.IL2CPP.CompilerServices;
#endif
public interface IComponentData{}
public interface IDisableCopy{}
#if ENABLE_IL2CPP
[Il2CppEagerStaticClassConstruction]
#endif
internal static class Counter
{
private static int _value = 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Increment()
{
return _value++;
}
}
#if ENABLE_IL2CPP
[Il2CppSetOption(Option.NullChecks, false)]
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]
[Il2CppSetOption(Option.DivideByZeroChecks, false)]
#endif
public abstract class Row
{
internal World World;
internal DynamicArray<ArchetypeFilter> Filters = new(1);
internal DynamicArray<RelationFilter> RelationFilters = new(1);
internal int Hash;
117
482.ЧДТУ.242323-010 12-01
internal bool CanCopy;
internal Row(World world, int id, int hash, bool canCopy, Type componentType)
{
World = world;
Id = id;
Hash = hash;
CanCopy = canCopy;
ComponentType = componentType;
}
public int Id
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get;
}
public Type ComponentType
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Has(int entityId)
{
return World.Archetypes[World.EntitiesData[entityId].ArchetypeId].Mask.GetBit(Id);
}
public abstract void SetBoxing(int entityId, object value);
public abstract object GetBoxing(int entityId);
public abstract bool HasVirtual(int entityId);
118
482.ЧДТУ.242323-010 12-01
internal abstract void RemoveInternal(int idx, int entityId);
internal abstract int Copy(int offset, ref EntityArchetypeData archetypeDataFrom, ref
IntUnsafeArray entitiesKeysFrom);
}
#if ENABLE_IL2CPP
[Il2CppSetOption(Option.NullChecks, false)]
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]
[Il2CppSetOption(Option.DivideByZeroChecks, false)]
#endif
public sealed class Row<T> : Row
where T : struct, IComponentData
{
internal static readonly int TypeId;
internal T[] Data;
internal int DataCount;
internal int[] RecycledData;
internal int RecycledCount;
public delegate void ComponentCallback(World world, int entityId, ref T component);
internal ComponentCallback ComponentAdded;
internal ComponentCallback ComponentRemoved;
static Row()
{
TypeId = Counter.Increment();
}
119
482.ЧДТУ.242323-010 12-01
internal Row(World world, int id) : base(world, id,
ComponentsUtils.CalculateHashForId(TypeId), default(T) is IDisableCopy, typeof(T))
{
Data = new T[world.Config.ComponentRowInitialSize];
RecycledData = new int[world.Config.ComponentRowInitialSize];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Components GetComponents(Archetype archetype)
{
return new Components(this, archetype.GetKeys(), archetype.GetOffset(Id));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Components GetComponents(in ChildsOf childs)
{
return new Components(this, childs.Relation.ChildArchetype.GetKeys(),
childs.Relation.ChildArchetype.GetOffset(Id));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T Get(int entityId)
{
ref var data = ref World.EntitiesData[entityId];
var archetype = World.Archetypes[data.ArchetypeId];
return ref Data[archetype.EntitiesKeys[data.ArchetypeDataIdx + archetype.GetOffset(Id)]];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T Add(int entityId)
{
ref var entityData = ref World.GetEntityArchetypeData(entityId);
120
482.ЧДТУ.242323-010 12-01
var archetype = World.Archetypes[entityData.ArchetypeId];
return ref AddComponentInternal(archetype.ComponentIdToOffset[Id], entityId, ref
entityData,
ref archetype.EntitiesKeys);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(int entityId, T value)
{
ref var entityData = ref World.GetEntityArchetypeData(entityId);
var archetype = World.Archetypes[entityData.ArchetypeId];
AddComponentInternal(archetype.ComponentIdToOffset[Id], entityId, ref entityData,
ref archetype.EntitiesKeys) = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Remove(int entityId)
{
ref var entityData = ref World.GetEntityArchetypeData(entityId);
var archetype = World.Archetypes[entityData.ArchetypeId];
RemoveComponentInternal(archetype.ComponentIdToOffset[Id], entityId, ref entityData,
ref archetype.EntitiesKeys);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal T[] GetData()
{
return Data;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ref T AddComponentInternal(int offset, int entityId,
121
482.ЧДТУ.242323-010 12-01
ref EntityArchetypeData entityArchetypeData, ref IntUnsafeArray entitiesData)
{
if (offset == 0)
{
ref var migration = ref World.Migrations.GetOrCreate(entityId);
var addedComponents = migration.AddedComponents;
for (var i = migration.AddedComponentsValues - 2; i >= 0; i -= 2)
{
if (Id != addedComponents[i])
continue;
return ref Data[addedComponents[i + 1]];
}
var idx = GetNewData();
migration.AddComponent(Id, idx, Hash);
ref var data = ref Data[idx];
ComponentAdded?.Invoke(World, entityId, ref data);
return ref data;
}
else
{
if (World.Migrations.Has(entityId))
{
ref var migration = ref World.Migrations.Get(entityId);
var removedComponents = migration.RemovedComponents;
for (var i = migration.RemovedComponentsCount - 1; i >= 0; i -= 1)
{
if (Id != removedComponents[i])
continue;
122
482.ЧДТУ.242323-010 12-01
removedComponents[i] =
removedComponents[migration.RemovedComponentsCount - 1];
migration.RemovedComponentsCount--;
ref var data = ref Data[entitiesData[entityArchetypeData.ArchetypeDataIdx +
offset]];
ComponentAdded?.Invoke(World, entityId, ref data);
return ref Data[entitiesData[entityArchetypeData.ArchetypeDataIdx + offset]];
}
}
return ref Data[entitiesData[entityArchetypeData.ArchetypeDataIdx + offset]];
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void RemoveComponentInternal(int offset, int entityId,
ref EntityArchetypeData entityArchetypeData, ref IntUnsafeArray entitiesData)
{
ref var migration = ref World.Migrations.GetOrCreate(entityId);
if (offset == 0)
{
var addedComponents = migration.AddedComponents;
for (var i = migration.AddedComponentsValues - 2; i >= 0; i -= 2)
{
if (Id != addedComponents[i])
continue;
RemoveData(addedComponents[i + 1], entityId);
addedComponents[i] = addedComponents[migration.AddedComponentsValues - 2];
addedComponents[i + 1] = addedComponents[migration.AddedComponentsValues -
1];
123
482.ЧДТУ.242323-010 12-01
migration.AddedComponentsValues -= 2;
break;
}
return;
}
var removedComponents = migration.RemovedComponents;
for (var i = migration.RemovedComponentsCount - 1; i >= 0; i -= 1)
{
if (Id != removedComponents[i])
continue;
return;
}
RemoveData(entitiesData[entityArchetypeData.ArchetypeDataIdx + offset], entityId);
migration.RemoveComponent(Id, Hash);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetNewData()
{
if (Data.Length <= DataCount)
{
ResizeData();
}
var id = 0;
if (RecycledCount > 0)
{
RecycledCount--;
124
482.ЧДТУ.242323-010 12-01
id = RecycledData[RecycledCount];
DataCount++;
}
else
{
id = DataCount;
DataCount++;
}
return id;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RemoveData(int idx, int entityId)
{
if (RecycledData.Length <= RecycledCount)
{
ResizeRecycle();
}
RecycledData[RecycledCount] = idx;
if (ComponentRemoved != null)
{
var removed = Data[idx];
ComponentRemoved.Invoke(World, entityId, ref removed);
}
Data[idx] = default;
RecycledCount++;
DataCount--;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
125
482.ЧДТУ.242323-010 12-01
private void ResizeRecycle()
{
Array.Resize(ref RecycledData, RecycledData.Length << 1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ResizeData()
{
Array.Resize(ref Data, Data.Length << 1);
}
internal override void RemoveInternal(int idx, int entityId)
{
if (RecycledData.Length <= RecycledCount)
{
ResizeRecycle();
}
RecycledData[RecycledCount] = idx;
if (ComponentRemoved != null)
{
var removed = Data[idx];
ComponentRemoved.Invoke(World, entityId, ref removed);
}
Data[idx] = default;
RecycledCount++;
DataCount--;
}
internal override int Copy(int offset, ref EntityArchetypeData archetypeDataFrom, ref
IntUnsafeArray entitiesKeysFrom)
{
126
482.ЧДТУ.242323-010 12-01
var dataIdx = GetNewData();
Data[dataIdx] = Data[entitiesKeysFrom[archetypeDataFrom.ArchetypeDataIdx + offset]];
return dataIdx;
}
public override void SetBoxing(int entityId, object value)
{
if (!World.TryGetEntity(entityId, out var entity))
{
throw new InvalidOperationException($"Entity: {entity} - is not alive");
}
if (value is T tValue)
{
Add(entityId) = tValue;
}
}
public override object GetBoxing(int entityId)
{
if (!World.TryGetEntity(entityId, out var entity))
{
throw new InvalidOperationException($"Entity: {entity} - is not alive");
}
return Get(entityId);
}
public override bool HasVirtual(int entityId)
{
return Has(entityId);
}
#if ENABLE_IL2CPP
127
482.ЧДТУ.242323-010 12-01
[Il2CppSetOption(Option.NullChecks, false)]
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]
[Il2CppSetOption(Option.DivideByZeroChecks, false)]
#endif
public readonly struct Components
{
private readonly IntUnsafeArray _entitiesData;
private readonly Row<T> _row;
private readonly int _offset;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Components(Row<T> row, IntUnsafeArray entitiesData, int offset)
{
_row = row;
_entitiesData = entitiesData;
_offset = offset;
}
public ref T this[EntityKey key]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref _row.Data[_entitiesData[_offset + key.Value]];
}
}
}
}
Файл EntityRef.cs
using System;
using System.Runtime.CompilerServices;
namespace JaECS
{
128
482.ЧДТУ.242323-010 12-01
#if ENABLE_IL2CPP
using Unity.IL2CPP.CompilerServices;
[Il2CppSetOption(Option.NullChecks, false)]
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]
[Il2CppSetOption(Option.DivideByZeroChecks, false)]
#endif
public readonly struct EntityRef : IEquatable<EntityRef>
{
internal readonly long Value;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal EntityRef(int id, short gen, byte worldId, byte worldGen)
{
Value = ((id & 0xFFFFFFFFL) << 32) | ((gen & 0xFFFFL) << 16) | ((worldId & 0xFFL) <<
8) | (worldGen & 0xFFL);
}
public int Id {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (int)((Value >> 32) & 0xFFFFFFFF);
}
public ushort Gen {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (ushort)((Value >> 16) & 0xFFFF);
}
public int WorldId {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (int)((Value >> 8) & 0xFF);
}
129
482.ЧДТУ.242323-010 12-01
public int WorldGen {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (int)(Value & 0xFF);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(EntityRef other)
{
return Value == other.Value;
}
public override bool Equals(object? obj)
{
return obj is EntityRef other && Equals(other);
}
public override int GetHashCode()
{
return (int)Value;
}
}
public static class EntityExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsAlive(this in EntityRef entityRef)
{
return entityRef.GetWorld().IsEntityAlive(entityRef);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Destroy(this in EntityRef entityRef)
130
482.ЧДТУ.242323-010 12-01
{
entityRef.GetWorld().DestroyEntity(entityRef);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static World GetWorld(this in EntityRef entityRef)
{
#if DEBUG
var entityWorldId = entityRef.WorldId;
if (entityWorldId < 0 || entityWorldId >= Static.Worlds.Length)
{
throw new IndexOutOfRangeException("Entity's world is destroyed or hasn't been
created");
}
var entityWorldGen = entityRef.WorldGen;
if (entityWorldGen != Static.Gens[entityWorldId])
{
throw new InvalidOperationException("Entity's world generation is obsolete; the world
was probably destroyed");
}
#endif
return Static.Worlds[entityRef.WorldId];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void AddChild(this in EntityRef entityRef, in EntityRef child)
{
entityRef.GetWorld().AddChild(entityRef, child);
}
131
482.ЧДТУ.242323-010 12-01
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RemoveChild(this in EntityRef entityRef, in EntityRef child)
{
entityRef.GetWorld().RemoveChild(entityRef, child);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RemoveParent(this in EntityRef entityRef)
{
entityRef.GetWorld().RemoveParent(entityRef);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ChildsFilter GetFilteredChilds(this in EntityRef entityRef, ArchetypeFilter
archetypeFilter)
{
return entityRef.GetWorld().GetChildsFilterOf(entityRef, archetypeFilter);
}
}
}
Файл Migrations.cs
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace JaECS
{
#if ENABLE_IL2CPP
using Unity.IL2CPP.CompilerServices;
[Il2CppSetOption(Option.NullChecks, false)]
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]
132
482.ЧДТУ.242323-010 12-01
[Il2CppSetOption(Option.DivideByZeroChecks, false)]
#endif
public class Migrations
{
private World _world;
internal EntityMigration[] _migrations;
internal int[] _sparse;
private int _migrationsCount;
private EntityMigration _default = default;
private readonly Archetype _nullArchetype;
internal BitMask MigrationMask;
public Migrations(World world)
{
_world = world;
_migrations = new EntityMigration[world.Config.InitialMigrationSize];
for (var i = _migrations.Length - 1; i >= 0; i--)
{
_migrations[i].AddedComponents = new int[12];
_migrations[i].RemovedComponents = new int[6];
}
_sparse = new int[world.Entities.Length];
Array.Fill(_sparse, -1);
_nullArchetype = new Archetype(world);
_nullArchetype.Id = -1;
_nullArchetype.Reset();
_nullArchetype.Dispose();
_nullArchetype.EntityKeySize = 1;
133
482.ЧДТУ.242323-010 12-01
_nullArchetype.NextIdx = 1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Resize(int newSize)
{
var prevSize = _sparse.Length;
Array.Resize(ref _sparse, newSize);
Array.Fill(_sparse, -1, prevSize, newSize - prevSize);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void ResizeMigrations()
{
var oldSize = _migrations.Length;
Array.Resize(ref _migrations, _migrationsCount << 1);
for (var i = _migrations.Length - 1; i >= oldSize; i--)
{
_migrations[i].AddedComponents = new int[12];
_migrations[i].RemovedComponents = new int[6];
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref EntityMigration GetOrCreate(int entityId)
{
ref var idx = ref _sparse[entityId];
if (idx != -1)
{
return ref _migrations[idx];
}
134
482.ЧДТУ.242323-010 12-01
if (_migrations.Length <= _migrationsCount)
{
ResizeMigrations();
}
idx = _migrationsCount;
ref var migration = ref _migrations[_migrationsCount];
migration.EntityId = entityId;
migration.AddedComponentsValues = 0;
migration.RemovedComponentsCount = 0;
migration.Hash = 0;
_migrationsCount++;
return ref migration;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Has(int entityId)
{
return _sparse[entityId] != -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref EntityMigration Get(int entityId)
{
return ref _migrations[_sparse[entityId]];
}
public void Process()
{
if (_migrationsCount <= 0)
{
return;
135
482.ЧДТУ.242323-010 12-01
}
var entityArchetypeData = _world.EntitiesData;
var prevArchetype = _nullArchetype;
var nextArchetype = default(Archetype);
var i = _migrationsCount - 1;
do
{
ref var migrationData = ref _migrations[i];
ref var entityData = ref entityArchetypeData[migrationData.EntityId];
var oldArchetype = _world.Archetypes[entityData.ArchetypeId];
if (migrationData.AddedComponentsValues + migrationData.RemovedComponentsCount
== 0)
{
if (oldArchetype.Id == 0)
{
_world.RecycleEntity(ref _world.GetEntityInternal(migrationData.EntityId));
}
goto Next;
}
var addedComponents = migrationData.AddedComponents;
var removedComponents = migrationData.RemovedComponents;
if (prevArchetype.Id == oldArchetype.Id)
{
for (var j = migrationData.AddedComponentsValues - 2; j >= 0; j -= 2)
{
if (_default.AddedComponents[j] != addedComponents[j])
{
goto FindNewArchetype;
}
136
482.ЧДТУ.242323-010 12-01
}
for (var j = migrationData.RemovedComponentsCount - 1; j >= 0; j--)
{
if (_default.RemovedComponents[j] != removedComponents[j])
{
goto FindNewArchetype;
}
}
goto Migrate;
}
FindNewArchetype:
ClearArchetypeIfEmpty(prevArchetype);
_default = migrationData;
prevArchetype = oldArchetype;
nextArchetype = FindNewArchetype(oldArchetype, ref migrationData);
Migrate:
if (nextArchetype.Id == 0)
{
_world.RecycleEntity(ref _world.GetEntityInternal(migrationData.EntityId));
}
UpdateEntity(nextArchetype, oldArchetype, ref entityData, ref migrationData);
Next:
_migrationsCount--;
137
482.ЧДТУ.242323-010 12-01
_sparse[migrationData.EntityId] = -1;
} while (--i >= 0);
ClearArchetypeIfEmpty(prevArchetype);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateEntity(Archetype nextArchetype, Archetype oldArchetype, ref
EntityArchetypeData entityData, ref EntityMigration migrationData)
{
nextArchetype.MoveFrom(migrationData.EntityId, ref entityData, oldArchetype);
for (var j = migrationData.AddedComponentsValues - 2; j >= 0; j -= 2)
{
nextArchetype.SetComponentIdxForEntity(entityData.ArchetypeDataIdx,
migrationData.AddedComponents[j],
migrationData.AddedComponents[j + 1]);
}
_world.UpdateRelation(migrationData.EntityId, oldArchetype);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ClearArchetypeIfEmpty(Archetype archetype)
{
if (archetype.Size == 0 && archetype.Id != 0)
{
_world.CleanupArchetype(archetype);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
138
482.ЧДТУ.242323-010 12-01
private Archetype FindNewArchetype(Archetype oldArchetype, ref EntityMigration
migrationData)
{
var hash = oldArchetype.MaskChunks.Hash ^ migrationData.Hash;
MemoryMarshal.CreateSpan(ref oldArchetype.Mask.BitArray[0],
MigrationMask.BitArray.Length)
.CopyTo(MemoryMarshal.CreateSpan(ref MigrationMask.BitArray[0],
MigrationMask.BitArray.Length));
MigrationMask.BitsCount = oldArchetype.Mask.BitsCount;
var addedComponents = migrationData.AddedComponents;
var removedComponents = migrationData.RemovedComponents;
for (var j = migrationData.AddedComponentsValues - 2; j >= 0; j -= 2)
{
MigrationMask.SetBit(addedComponents[j]);
}
for (var j = migrationData.RemovedComponentsCount - 1; j >= 0; j--)
{
MigrationMask.ClearBit(removedComponents[j]);
}
if (_world.ArchetypesIdsLookup.TryGet(hash, ref MigrationMask, out var index))
{
return _world.Archetypes[index];
}
return _world.CreateArchetype(MigrationMask, hash);
}
139
482.ЧДТУ.242323-010 12-01
#if ENABLE_IL2CPP
[Il2CppSetOption(Option.NullChecks, false)]
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]
[Il2CppSetOption(Option.DivideByZeroChecks, false)]
#endif
public struct EntityMigration
{
internal int EntityId;
internal int[] AddedComponents;
internal int AddedComponentsValues;
internal int[] RemovedComponents;
internal int RemovedComponentsCount;
internal int Hash;
public int AddedComponentsCount
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => AddedComponentsValues / 2;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public EntityMigration(int entityId, int[] addedComponents,
int addedComponentsValues, int[] removedComponents, int removedComponentsCount)
{
EntityId = entityId;
AddedComponents = addedComponents;
AddedComponentsValues = addedComponentsValues;
RemovedComponents = removedComponents;
RemovedComponentsCount = removedComponentsCount;
Hash = 0;
}
140
482.ЧДТУ.242323-010 12-01
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddComponent(int componentId, int idx, int hash)
{
if (AddedComponents.Length == AddedComponentsValues)
{
ResizeAddedComponents();
}
AddedComponents[AddedComponentsValues] = componentId;
AddedComponents[AddedComponentsValues + 1] = idx;
AddedComponentsValues += 2;
Hash ^= hash;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveComponent(int componentId, int hash)
{
if (RemovedComponents.Length == RemovedComponentsCount)
{
ResizeRemovedComponents();
}
RemovedComponents[RemovedComponentsCount] = componentId;
RemovedComponentsCount++;
Hash ^= hash;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void ResizeAddedComponents()
{
Array.Resize(ref AddedComponents, AddedComponents.Length << 1);
141
482.ЧДТУ.242323-010 12-01
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void ResizeRemovedComponents()
{
Array.Resize(ref RemovedComponents, RemovedComponents.Length << 1);
}
}
}
}
142
ДОДАТОК В
СТРУКТУРНА ОПТИМІЗАЦІЯ ПРОГРАМНОГО ЗАБЕЗПЕЧЕННЯ
МОБІЛЬНИХ ІГОР ЖАНРУ ROGUELIKE НА ОСНОВІ РУШІЯ UNITY
Інструкція користувача
482.ЧДТУ.242323 34-01
Листів 2
Розробник ________________ Чуплий М. А.
Черкаси 2023
482.ЧДТУ.242323-010 34-01
Для початку роботи з фреймворком його потрібно завантажити в вигляді
dll файлів та імпортувати у власний проект. Це можна зробити створивши папку
Plugins в файлах проекту та додати dll файли.
Фреймворк складається із двох dll файлів: JaECS.dll (основний
фреймворк) та JaECS.SourceGen.dll (генератор коду який спрощує роботу з
API).
Далі потрібно створити новий клас і наслідувати його від SystemBase,
додати новий метод та створити наступні атрибути для нього:
[Query] - Атрибут який вказує, що цей метод є методом системи для якого
буде відбуватись фільтрація сутностей за наступними критеріями.
[With] - Атрибут який приймає параметри типу Type[] і вказує, що
фільтрація повинна включити всі сутності із вказаними типами компонентів.
[Without] - Атрибут який приймає параметри типу Type[] і вказує, що
фільтрація повинна виключити всі сутності із вказаними типами компонентів.
В параметрах методу можна додати компоненти які включені в фільтр та
саму сутність яка обробляється.
Приклад отриманого коду:
public partial class ExampleSystem : SystemBase
{
[Query]
[With(typeof(Comp2))]
[Without(typeof(Comp1))]
private void ExampleQuery(ref EntityRef entity, ref Comp2 comp2)
{
// Логіка роботи із даними компонента
}
}
В результаті цю систему можна буде додати в ECSApp, що ввімкне її та
додасть до виконання в ігровому циклі.
144
ДОДАТОК Г
ПРОГРАМНЕ ЗАБЕЗПЕЧЕННЯ КООПЕРАТИВНОГО ІГРОВОГО
МОБІЛЬНОГО ДОДАТКУ НА ОСНОВІ UNITY
Графічні матеріали
482.ЧДТУ.242323 90-01
Листів 9
Розробник ________________ Чуплий М. А.
Черкаси 2023
482.ЧДТУ.242323-010 90-01
Рис. Г.1 - Слайд вступу
146