Please use this identifier to cite or link to this item:
https://er.chdtu.edu.ua/handle/ChSTU/8225| Title: | Структурна оптимізація програмного забезпечення мобільних ігор жанру Roguelike на основі рушія Unity |
| Authors: | Олексюк, Вадим Володимирович Чуплий, Максим Андрійович |
| Keywords: | Unity;C#;ECS;entity;component;system;framework;archetype;migrations |
| Issue Date: | 13-Dec-2024 |
| Abstract: | АНОТАЦІЯ
Чуплий Максим Андрійович;
Структурна оптимізація програмного забезпечення мобільних ігор жанру
Roguelike на основі рушія Unity;
121 Інженерія програмного забезпечення;
Черкаський державний технологічний університет;
Черкаси, 2024;
Тема кваліфікаційної роботи є актуальною через зростаючі вимоги до
високопродуктивного програмного забезпечення для мобільних ігор. У сучасних
умовах обмежених ресурсів мобільних пристроїв важливим є створення
архітектури, яка ефективно обробляє дані та мінімізує використання пам'яті й
процесорного часу.
Мета дослідження: метою роботи є розробка та структурна оптимізація
фреймворку ECS (Entity-Component-System), орієнтованого на мобільні ігри
жанру Roguelike, із застосуванням архітектури архетипів. Основна увага
приділяється підвищенню продуктивності обробки даних, гнучкості та
масштабованості системи.
Методологія дослідження: для досягнення мети роботи використано аналіз
існуючих архітектур ECS і їхньої адаптації для мобільних платформ. Проведено
тестування продуктивності різних підходів обробки даних. У рамках дослідження
реалізовано власний фреймворк ECS із механізмами швидкої індексації
компонентів, переходу між архетипами та інтеграції з рушієм Unity.
Результати дослідження: розроблений фреймворк демонструє значне
скорочення часу обробки даних у порівнянні з традиційними підходами.
Експериментальні результати підтвердили, що використання архітектури
архетипів дозволяє підвищити швидкість обробки та зменшити обсяг
використаної пам’яті, що є критично важливим для мобільних ігор.
Висновки: розроблений фреймворк ECS забезпечує значне підвищення
продуктивності мобільних ігор жанру Roguelike, водночас зберігаючи гнучкість та
масштабованість архітектури. Робота сприяє розвитку інструментів оптимізації
програмного забезпечення для мобільних платформ. ANNOTATION Chuplyi Maksym Structural optimization of game software genre Roguelike based on Unity; 121 Software engineering; Cherkasy State Technological University; Cherkasy, 2024; The topic of the qualification work is relevant due to the growing demand for high-performance software for mobile games. Under modern conditions of limited mobile device resources, it is crucial to create an architecture that processes data efficiently while minimizing memory and CPU usage. Objective of the study: the goal of this work is to develop and structurally optimize an ECS (Entity-Component-System) framework tailored for mobile games in the Roguelike genre, utilizing an archetype-based architecture. The primary focus is on improving data processing performance, flexibility, and system scalability. Research methodology: to achieve the objective, existing ECS architectures were analyzed and adapted for mobile platforms. The performance of various data processing approaches was tested. A custom ECS framework was implemented, featuring mechanisms for fast component indexing, transitions between archetypes, and integration with the Unity engine. Research results: the developed framework demonstrates a significant reduction in data processing time compared to traditional approaches. Experimental results confirmed that using an archetype-based architecture enhances processing speed and reduces memory usage, which is critically important for mobile games. Conclusions: the developed ECS framework significantly improves the performance of mobile games in the Roguelike genre while maintaining the flexibility and scalability of the architecture. This work contributes to the development of optimization tools for mobile platform software. |
| URI: | https://er.chdtu.edu.ua/handle/ChSTU/8225 |
| Appears in Collections: | 121 Інженерія програмного забезпечення (Інженерія програмного забезпечення) |
Files in This Item:
| File | Description | Size | Format | |
|---|---|---|---|---|
| Кваліфікаційна робота магістра_Чуплий Максим Андрійович.pdf Restricted Access | 4.61 MB | Adobe PDF | View/Open Request a copy |
Items in DSpace are protected by copyright, with all rights reserved, unless otherwise indicated.
Extracted text
МІНІСТЕРСТВО ОСВІТИ І НАУКИ УКРАЇНИ ЧЕРКАСЬКИЙ ДЕРЖАВНИЙ ТЕХНОЛОГІЧНИЙ УНІВЕРСИТЕТ Факультет інформаційних технологій і систем Кафедра програмного забезпечення автоматизованих систем ПОЯСНЮВАЛЬНА ЗАПИСКА до кваліфікаційної роботи «магістра» освітній рівень на тему: Структурна оптимізація програмного забезпечення мобільних ігор жанру Roguelike на основі рушія Unity Виконав: магістрант, групи МПЗ-2304 Спеціальності 121 «Інженерія програмного забезпечення» (шифр і назва напряму підготовки) Магістрант Чуплий М.А. (прізвище та ініціали) Керівник Олексюк В.В. (прізвище та ініціали) Рецензент Ключка К. М. (прізвище та ініціали) Черкаси 2024 Черкаський державний технологічний університет повне найменування вищого навчального закладу Факультет інформаційних технологій і систем Кафедра програмного забезпечення автоматизованих систем Освітній рівень магістр Спеціальність 121 «Інженерія програмного забезпечення» Освітня програма Інженерія програмного забезпечення ЗАТВЕРДЖУЮ Зав. кафедри ПЗАС, професор ____________________Голуб С. В. «___» _______________ 2024 року З А В Д А Н Н Я НА КВАЛІФІКАЦІЙНУ РОБОТУ МАГІСТРАНТА Чуплий Максим Андрійович (прізвище, ім’я, по батькові) 1.Тему проекту (роботи) Структурна оптимізація програмного забезпечення мобільних ігор жанру Roguelike на основі рушія Unity Керівник проекту (роботи) Олексюк Вадим Володимирович к.т.н., доктор філософії, PhD (прізвище, ім’я , по батькові, науковий ступінь, вчене звання) Затверджені наказом Черкаського державного технологічного університету від « 7 » жовтень 2024 року №299/04 2. Строк подання магістрантом проекту (роботи) 9 вересня 2024 р. 3. Вхідні дані до проекту (роботи) стандарти програмного забезпечення; процеси управління; вимоги до проекту; календарне планування проекту; управління ризиками проекту; управління ресурсами 4. Зміст розрахунково-пояснювальної записки (перелік питань, які потрібно розробити) Вступ; Існуючі методи та засоби розвʼязання поставлених завдань; Теоретичні та експериментальні дослідження; Проектування програмного забезпечення системи; Розробка та тестування програмного забезпечення; Реалізація та впровадження результатів; Висновок; Список використаних джерел; Додаток А. Специфікація Додаток Б. Лістинг програми Додаток Додаток В. Інструкція користувача Додаток Додаток Г. Графічні матеріали 5. Перелік графічного матеріалу (з точним зазначенням обов’язкових робіт проекту); слайд 1 - вступний слайд; слайд 2 - вступ; слайд 3 - вимоги; слайд 4 - формування гіпотези; слайд 5 - гіпотеза; слайд 6 - експ. дослідження; слайд 7 - експ. дослідження 2; слайд 8- діаграма прецендентів; слайд 9 - лагічна структура; слайд 10 - діаграма пакетів; слайд 11 -діаграма прецендентів; слайд 12 - діаграма діяльності; слайд 13 - діаграма послідовності; слайд 14 - діаграма комунікації; слайд 15 - діаграма станів; слайд 16 - функціональна схема; слайд 17 - логічна схема; слайд 18 - слайд програмний інтерфейс; слайд 19 - тестування; слайд 20 - кінцевий; 6. Консультанти розділів проекту (роботи) Прізвище, ініціали та посади Підпис, дата Розділ консультанта Завдання видав Завдання прийняв 1 2 3 7. Дата видачі завдання 9 вересня 2024 р. КАЛЕНДАРНИЙ ПЛАН Строк виконання № Назва етапів випускної роботи етапів Примітки п/п випускної роботи 1 Постановка задачі 2.09.2024 виконано 2 Підготовка завдання 4.09.2024 виконано 3 Погодження завдання 6.09.2024 виконано 4 Затвердження завдання 8.09.2024 виконано Основна стадія 1 Підбір матеріалів 12.09.2024 виконано 2 Аналіз шляхів вирішення поставленої задачі 16.09.2024 виконано 3 Розрахунок основних параметрів роботи 20.09.2024 виконано 4 Вибір кінцевого варіанту проектного рішення 24.09.2024 виконано 5 Оформлення первісної редакції роботи 28.10.2024 виконано Заключна стадія 1 Узгодження прийнятих проектних рішень з 10.11.2024 виконано керівником 2 Оформлення пояснювальної записки роботи в 19.11.2024 виконано кінцевій редакції 3 Попередній захист роботи 25.11.2024 виконано 4 Затвердження роботи 29.11.2024 5 Рецензування роботи 09.12.2024 6 Захист роботи 12.12.2024 Магістранта _____________________ Чуплий М. А. (підпис) (прізвище та ініціали) Керівник проекту (роботи) _____________________ Олексюк В. В. (підпис) (прізвище та ініціали) АНОТАЦІЯ Чуплий Максим Андрійович; Структурна оптимізація програмного забезпечення мобільних ігор жанру Roguelike на основі рушія Unity; 121 Інженерія програмного забезпечення; Черкаський державний технологічний університет; Черкаси, 2024; Тема кваліфікаційної роботи є актуальною через зростаючі вимоги до високопродуктивного програмного забезпечення для мобільних ігор. У сучасних умовах обмежених ресурсів мобільних пристроїв важливим є створення архітектури, яка ефективно обробляє дані та мінімізує використання пам'яті й процесорного часу. Мета дослідження: метою роботи є розробка та структурна оптимізація фреймворку ECS (Entity-Component-System), орієнтованого на мобільні ігри жанру Roguelike, із застосуванням архітектури архетипів. Основна увага приділяється підвищенню продуктивності обробки даних, гнучкості та масштабованості системи. Методологія дослідження: для досягнення мети роботи використано аналіз існуючих архітектур ECS і їхньої адаптації для мобільних платформ. Проведено тестування продуктивності різних підходів обробки даних. У рамках дослідження реалізовано власний фреймворк ECS із механізмами швидкої індексації компонентів, переходу між архетипами та інтеграції з рушієм Unity. Результати дослідження: розроблений фреймворк демонструє значне скорочення часу обробки даних у порівнянні з традиційними підходами. Експериментальні результати підтвердили, що використання архітектури архетипів дозволяє підвищити швидкість обробки та зменшити обсяг використаної пам’яті, що є критично важливим для мобільних ігор. Висновки: розроблений фреймворк ECS забезпечує значне підвищення продуктивності мобільних ігор жанру Roguelike, водночас зберігаючи гнучкість та масштабованість архітектури. Робота сприяє розвитку інструментів оптимізації програмного забезпечення для мобільних платформ. Ключові сліва 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 topic of the qualification work is relevant due to the growing demand for high-performance software for mobile games. Under modern conditions of limited mobile device resources, it is crucial to create an architecture that processes data efficiently while minimizing memory and CPU usage. Objective of the study: the goal of this work is to develop and structurally optimize an ECS (Entity-Component-System) framework tailored for mobile games in the Roguelike genre, utilizing an archetype-based architecture. The primary focus is on improving data processing performance, flexibility, and system scalability. Research methodology: to achieve the objective, existing ECS architectures were analyzed and adapted for mobile platforms. The performance of various data processing approaches was tested. A custom ECS framework was implemented, featuring mechanisms for fast component indexing, transitions between archetypes, and integration with the Unity engine. Research results: the developed framework demonstrates a significant reduction in data processing time compared to traditional approaches. Experimental results confirmed that using an archetype-based architecture enhances processing speed and reduces memory usage, which is critically important for mobile games. Conclusions: the developed ECS framework significantly improves the performance of mobile games in the Roguelike genre while maintaining the flexibility and scalability of the architecture. This work contributes to the development of optimization tools for mobile platform software. Keywords: Unity, C#, ECS, entity, component, system, framework, archetype, migrations. ЗМІСТ ПЕРЕЛІК УМОВНИХ СКОРОЧЕНЬ.................................................................................4 ВСТУП......................................................................................................................................5 РОЗДІЛ 1 ІСНУЮЧІ МЕТОДИ ТА ЗАСОБИ РОЗВ’ЯЗАННЯ ПОСТАВЛЕНИХ ЗАВДАНЬ................................................................................................................................. 8 1.1 Актуальні проблеми, що виникають в процесі розробки..........................................8 1.2 Методи та засоби, які вже використовуються для вирішення проблем під час розробки............................................................................................................................... 9 1.3 Огляд існуючих аналогів............................................................................................ 11 1.4 Конкретизація завдань роботи................................................................................... 13 Висновок до першого розділу.......................................................................................... 13 РОЗДІЛ 2 ТЕОРЕТИЧНІ ТА ЕКСПЕРИМЕНТАЛЬНІ ДОСЛІДЖЕННЯ............... 15 2.1 Дослідження структури ECS систем......................................................................... 15 2.2 Теоретичні дослідження............................................................................................. 19 2.3 Експериментальні дослідження................................................................................. 23 Висновки до другого розділу........................................................................................... 29 РОЗДІЛ 3 ВПРОВАДЖЕННЯ РЕЗУЛЬТАТІВ ДОСЛІДЖЕНЬ У ПРАКТИКУ ПРОЕКТУВАННЯ ПРОГРАМНОГО ЗАБЕЗПЕЧЕННЯ ІНФОРМАЦІЙНИХ СИСТЕМ................................................................................................................................ 30 3.1 Моделювання предметної області............................................................................. 30 3.1.1 Предметна область моделювання. Модель предметної області. Словник предметної області....................................................................................................... 30 3.1.2 Елементи моделювання предметної області.....................................................31 3.1.3 Робоча область моделювання.............................................................................34 3.2 Формування та аналіз вимог...................................................................................... 35 3.2.1 Формування вимог до програмного забезпечення. Первинні і детальні вимоги. Вимоги замовника і розробника. Функціональні і нефункціональні вимоги........................................................................................................................... 35 3.2.2 Формування вимог за допомогою діаграми прецедентів..................................... 38 ЧДТУ 242323.010 ПЗ Зм Лист № документа Підпис Дата Розроб. Чуплий М. А. Літ. Лист Листів. Керівник Олексюк В. В.. Структурна оптимізація [н] 2 167 програмного Н. контр. Півень О. Б. забезпечення мобільних ФІТІС, Кафедра ПЗАС, Затв. Голуб С. В. ігор жанру Roguelike на основі рушія Unity МПЗ-2304 3.3 Проектування логічної структури програмного комплексу.................................... 40 3.3.1 Діаграми класів................................................................................................... 40 3.3.2 Діаграми пакетів..................................................................................................44 3.4 Архітектурне проектування....................................................................................... 45 3.4.1 Діаграма компонентів......................................................................................... 45 3.4.2 Розгортання програмної системи на апаратних засобах. Діаграма розгортання...................................................................................................................47 3.5 Моделювання поведінки системи..............................................................................48 3.5.1 Діаграма діяльності.............................................................................................48 3.5.2 Діаграма послідовності...................................................................................... 51 3.5.3 Діаграма комунікації...........................................................................................56 3.5.4 Діаграма скінченного автомату..........................................................................57 Висновок до третього розділу..........................................................................................58 РОЗДІЛ 4 РОЗРОБКА ТА ТЕСТУВАННЯ ПРОГРАМНОГО ЗАБЕЗПЕЧЕННЯ... 59 4.1 Розробка програмного комплексу..............................................................................59 4.1.1 Обґрунтування вибору засобів реалізації......................................................... 59 4.1.2 Опис функціональної схеми...............................................................................60 4.1.3 Опис логічної схеми системи.............................................................................61 4.1.4 Розробка бази даних........................................................................................... 64 4.1.5 Розробка програмного інтерфейсу користувача...............................................65 4.1.6 Опис розробки програмних компонентів......................................................... 67 4.2 Тестування системи.................................................................................................... 69 4.2.1 Модульне тестування..........................................................................................69 4.2.2 Інтеграційне тестування..................................................................................... 73 4.2.3 Системне тестування.......................................................................................... 75 4.2.4 Приймальне тестування..................................................................................... 77 4.3 Приклади впровадження програмного комплексу................................................... 79 4.3.1 Розгортання системи...........................................................................................79 Висновок до четвертого розділу...................................................................................... 80 ЗАГАЛЬНІ ВИСНОВКИ.....................................................................................................81 СПИСОК ВИКОРИСТАНИХ ДЖЕРЕЛ..........................................................................83 ДОДАТКИ……………………………………………………...…………………………...87 ЧДТУ 242323.010 ПЗ ЧДТУ 242323.010 ПЗ ПЕРЕЛІК УМОВНИХ СКОРОЧЕНЬ Unity - ігровий двигун ECS - шаблон проектування сутність-компонент-система ООП - об’єктно орієнтоване програмування КОП - компонентно орієнтоване програмування Migration - перехід сутності між архетипами Archetype - архетип сутності Event - подія IL2CPP - компілятор коду Benchmark - етап тестування програмного забезпечення на використання ресурсів процесора API - інтерфейс програмного забезпечення (application programming interface) 4 ЧДТУ 242323.010 ПЗ ВСТУП Актуальність теми: Інженерія програмного забезпечення має на меті розробку мобільних ігор у жанрі Roguelike яка набуває популярності завдяки унікальній комбінації динамічного ігрового процесу та процедурно згенерованого контенту. Водночас ефективність роботи ігрового програмного забезпечення на мобільних платформах залишається однією з ключових технічних проблем, враховуючи обмеження апаратних ресурсів. Використання власного ECS-рішення, побудованого на базі архетипів, дозволяє оптимізувати структуру та продуктивність ігрових систем, знижуючи навантаження на процесор і пам’ять. Таким чином, дослідження є актуальним з огляду на зростаючі вимоги до продуктивності мобільних ігор і потребує в ефективних інструментах їх оптимізації. Зв’язок з науковими програмами та планами, темами: робота має зв’язок із сучасними тенденціями розробки інструментів для мобільних ігор, що спрямовані на підвищення продуктивності та ефективності використання обмежених ресурсів. Результати дослідження можуть бути використані для вдосконалення методів і підходів у розробці ігрових механік, інтеграції ECS-фреймворків із популярними ігровими рушіями, зокрема Unity. Отримані теоретичні та практичні результати відповідають пріоритетним напрямам розвитку інформаційних технологій, і сприяють вирішенню завдань, пов’язаних із структурною оптимізацією програмного забезпечення для мобільних платформ. Мета і завдання дослідження: підвищення ефективності побудови програмного забезпечення шаблону проектування Entity Component System шляхом розробки та впровадження структурно оптимізованого ECS-рішення на базі архетипів для мобільних ігор жанру Roguelike з метою підвищення їх продуктивності на платформах з обмеженими ресурсами. Завдання дослідження: 1 Провести аналіз сучасних підходів до побудови ECS у мобільних іграх. 2 Розробити архітектуру власного ECS-рішення, засновану на архетипах, враховуючи специфіку жанру Roguelike. 5 ЧДТУ 242323.010 ПЗ 3 Оптимізувати алгоритми управління компонентами та обробки систем для збільшення швидкості виконання. 4 Оцінити ефективність розробленого підходу в порівнянні з існуючими рішеннями. Об’єкт дослідження: процес структурної оптимізації програмного забезпечення мобільних ігор жанру Roguelike, спрямовані на забезпечення покращення ефективності структурних змін в сутності в архітектурі на базі Entity Component System. Предмет дослідження: розробка метода побудови ECS-рішень на базі архетипів з використанням індексації, які дозволяють оптимізувати стуктуру архітектурного шаблон Entity Component System для мобільних ігор жанру Roguelike. Методи дослідження: порівняльний аналіз - вивчення існуючих ECS-підходів і рішень для мобільних ігор; моделювання - розробка структурної моделі ECS-рішення, заснованого на архетипах; гіпотеза і експеримент - припущення, реалізація та тестування нового підходу переміщення сутності між архетипами. Наукова новизна отриманих результатів: вперше запропоновано метод збереження компонентів у лінійному масиві даних незалежно від архетипу, який заключається в індексації компонентів, і на відміну від існуючих способів зберігання компонентів в архетипі або в спарсеті, дозволяє прискорити структурні зміни в ECS та знизити витрати на міграцію сутностей між архетипами під час структурних змін. Практичне значення отриманих результатів: 1 Запропонований метод до зберігання компонентів дозволяє підвищити продуктивність ECS-систем, що є критичним для мобільних платформ із обмеженими ресурсами. 2 Оптимізована ECS-архітектура спрощує процес розробки ігор в жанрі Roguelike. Це сприяє швидшому створенню ігрових механік. 6 ЧДТУ 242323.010 ПЗ 3 Результати дослідження можуть бути використані як базис для побудови ECS-систем у комерційних ігрових проектах, а також у навчальних і дослідницьких цілях. Особистий внесок автора: особисто автором запропоновано метод збереження компонентів у окремому від архетипу масиві та використання індексів для зв’язку з архетипами, що дозволяє мінімізувати витрати на обробку структурних змін; Реалізовано прототип ECS-системи на основі запропонованого підходу, оптимізованої для роботи на рушії Unity з використанням IL2CPP; Проведено тестування продуктивності створеної ECS-системи на мобільних пристроях, що підтвердило її ефективність для застосування у мобільних іграх; Узагальнено результати досліджень та підготовлено практичні рекомендації для використання розроблених методів у розробці ігор. 7 ЧДТУ 242323.010 ПЗ РОЗДІЛ 1 ІСНУЮЧІ МЕТОДИ ТА ЗАСОБИ РОЗВ’ЯЗАННЯ ПОСТАВЛЕНИХ ЗАВДАНЬ 1.1 Актуальні проблеми, що виникають в процесі розробки Інженерія програмного забезпечення розглядає проблеми які впливають на розробку Roguelike-ігри які є унікальним жанром, який характеризується глибокою взаємодією між сутностями та складністю механік. Ці особливості створюють низку технічних проблем для інженерії програмного забезпечення, які розробникам потрібно вирішувати. Обробка великої кількості сутностей в Roguelike-іграх зазвичай включають велику кількість активних об’єктів — героїв, ворогів, предметів, пасток тощо. В ООП-підходах кожна сутність зазвичай представлена як об’єкт класу, що включає дані та логіку. Це створює такі проблеми: – велика кількість викликів методів для обробки логіки кожної сутності, що призводить до зниження продуктивності [1]; – надмірне споживання пам’яті, адже кожен об’єкт має власну накладну пам’ять для метаданих [1]; – cкладність масштабування, адже розширення ієрархії класів для нових сутностей часто призводить до дублювання коду [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. У цій реалізації сутності з однаковими наборами компонентів групуються в архетипи. Кожен архетип є компактною структурою даних, у якій компоненти зберігаються в масивах, що дозволяє ефективно працювати з великими обсягами даних завдяки оптимальному використанню кешу процесора. Такий підхід забезпечує високу продуктивність у сценаріях, де є великий обсяг сутностей із стабільними наборами компонентів. Проте зміна архетипу сутності - наприклад, додавання або видалення компонента - потребує переміщення сутності між архетипами, що може спричинити певні накладні витрати [3]. 11 ЧДТУ 242323.010 ПЗ Entitas реалізує sparse-based ECS. Цей фреймворк був одним із перших популярних рішень для Unity. Він підтримує концепцію пулів, які дозволяють оптимізувати створення та знищення сутностей. Компоненти у Entitas зберігаються у вигляді масивів індексів, що спрощує їхню обробку, але водночас вимагає використання додаткових структур для відстеження змін у компонентах. Entitas популярний завдяки простоті API та наявності великої спільноти користувачів, проте його продуктивність залежить від способу використання, оскільки деякі задачі вимагають ручного управління структурами даних [6]. Основна різниця між архетипними та розрідженими (sparse-based) ECS полягає в організації та доступі до даних. У архетипній ECS сутності групуються в архетипи, що дозволяє досягти високої продуктивності через мінімізацію кеш-промахів і покращення локальності даних. Однак, така система є менш гнучкою в ситуаціях із часто змінюваними наборами компонентів. Розріджені ECS, у свою чергу, забезпечують більшу гнучкість, оскільки зміна набору компонентів не потребує перебудови архетипів, але мають нижчу продуктивність через погану локальність даних [7]. Отже, в розглянутих аналогах можна виділити суттєві переваги та недоліки: 1 Morpeh: – спосіб збереження компонентів: хеш-мапи; – переваги: швидкі методи створення/видалення компонентів, просте API, швидке фільтрування сутностей; – недоліки: при наявності колізій в хешмапах швидкість отримання компонентів уповільнюється, що впливає на більшу частину програми. – фреймворк підходить для ігор у яких кількість сутностей не перевищує за 10.000 та часто створюються та видаляються компоненти. 2 Unity DOTS: – спосіб збереження компонентів: архетипи; – переваги: ефективне використання пам’яті та швидке фільтрування сутностей по їх компонентам; 12 ЧДТУ 242323.010 ПЗ – недоліки: повільні методи створення/видалення компонентів, складне API; – фреймворк підходить для ігор з великою кількістю сутностей та компонентів 3 Entitas: – спосіб збереження компонентів: спарсети; – переваги: швидкі методи створення/видалення компонентів, розвинене API яке допомагає вирішувати проблеми в процесі розробки; – недоліки: при збільшенні кількості компонентів та кількості сутностей в програмі квадратично збільшується використання пам’яті; – фреймворк підходить для невеликих ігор, у яких кількість сутностей не перевищує за 10.000 та кількість компонентів за 1.000. 1.4 Конкретизація завдань роботи Дослідження у галузі оптимізації архітектури ECS для Roguelike-ігор є актуальним через постійне зростання вимог до продуктивності та гнучкості програмного забезпечення, особливо на мобільних платформах. Проблеми, пов’язані з ефективною організацією даних та зменшенням накладних витрат під час структурних змін у сутностях, є критичними для ігор цього жанру, де геймплей передбачає часті зміни стану сутностей. Основною метою цього дослідження є розробка нового підходу до збереження компонентів, який дозволить значно пришвидшити процес переходу сутностей між архетипами під час додавання або видалення компонентів, оскільки замість переміщення всього компоненту переноситься лише його індекс. Висновок до першого розділу Отже, впровадження такого підходу забезпечить підвищення продуктивності, знизить накладні витрати на пам’ять і збільшить гнучкість ECS-систем, роблячи їх більш пристосованими для використання в умовах, характерних для Roguelike-ігор. Проведення дослідження також сприятиме 13 ЧДТУ 242323.010 ПЗ розширенню теоретичної бази та практичних методів оптимізації ігрових рушіїв, що базуються на ECS-архітектурі. 14 ЧДТУ 242323.010 ПЗ РОЗДІЛ 2 ТЕОРЕТИЧНІ ТА ЕКСПЕРИМЕНТАЛЬНІ ДОСЛІДЖЕННЯ 2.1 Дослідження структури ECS систем Інженерія програмного забезпечення включає в себе архітектуру ECS (Entity-Component-System) яка є основою для багатьох сучасних ігрових рушіїв і програмних систем, оскільки дозволяє ефективно організувати логіку і дані, що використовуються в процесі роботи. Основна мета ECS - забезпечити модульність, гнучкість і високу продуктивність за рахунок розділення даних і логіки. Це особливо важливо для Roguelike-ігор, де структура сутностей часто змінюється, а взаємодії між компонентами можуть бути надзвичайно динамічними. ECS-архітектури класифікуються на дві основні категорії: архетипні та спарссетні. В архетипних системах сутності групуються за архетипами - наборами компонентів, які вони містять (рис 2.1). Кожен архетип має власні масиви для зберігання даних, що дозволяє оптимізувати доступ до компонентів за рахунок лінійної адресації. Натомість спарсcетні системи використовують розріджені структури даних, які дозволяють зберігати компоненти незалежно від належності сутностей до груп (рис 2.2). Це забезпечує більшу гнучкість у випадках, коли структура компонентів сутності часто змінюється, але може погіршувати продуктивність через розрідженість даних у пам’яті [8]. Переваги архетипних ECS включають швидкий доступ до даних і високу продуктивність під час обробки систем, оскільки компоненти однієї групи зазвичай розташовані в пам’яті послідовно. Однак така структура ускладнює модифікацію компонентів, оскільки зміна архетипу сутності вимагає переміщення всіх її даних до нового архетипу. Спарссетні ECS, у свою чергу, дозволяють уникнути значних витрат на переміщення даних, зберігаючи тільки посилання або індекси, але можуть створювати додаткові накладні витрати на пошук і обробку розріджених даних [8]. 15 ЧДТУ 242323.010 ПЗ Рис. 2.1 - Візуальне представлення архетипів 16 ЧДТУ 242323.010 ПЗ Рис. 2.2 - Візуальне представлення спарсетів 17 ЧДТУ 242323.010 ПЗ Нижче представлена порівняльна таблиця 2.1 основних характеристик архетипних і спарсетних ECS: Таблиця 2.1 Різниця між різними ECS архітектурами Характеристика Архетипна ECS Спарсетна ECS Організація даних Сутності згруповані в Компоненти зберігаються у архетипи з однаковими розріджених структурах, незалежн наборами компонентів. від архетипу. Продуктивність Висока завдяки локалізації Низька через розкиданість даних доступу даних для групи сутностей. у пам’яті. Зміна структури Вимагає переміщення даних Просте додавання/видалення сутності між архетипами. компонентів. Гнучкість Менш гнучка, підходить для Гнучка, легко адаптується до змін. систем із заздалегідь визначеними структурами. Кеш-ефективність Висока, оскільки дані Низька через фрагментованість у зберігаються компактно. пам'яті. Складність Складна через потребу у Відносно проста, достатньо кілька реалізації підтримці архетипів і масивів. переходів. Масштабованість Підходить для великих Зручна для динамічних систем із систем із фіксованими численними змінами компонентів. наборами компонентів. Головними перевагами ECS є гнучкість у моделюванні складних систем, 18 ЧДТУ 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 байтів Для систем із частими структурними змінами це значно уповільнює роботу, оскільки операції копіювання займають значну частину часу, особливо при 19 ЧДТУ 242323.010 ПЗ обробці великих наборів даних [3]. Гіпотеза полягає в тому, щоб зберігати компоненти сутностей у окремому від архетипу масиві, а в архетипах зберігати лише індекси цих компонентів, цей метод збереження компонентів отримав назву - індексація. У цьому методі зміна архетипу сутності зводиться до оновлення невеликого масиву індексів. Наприклад, якщо розмір індексу дорівнює 4 байти (тип int), то для сутності з 10 компонентами загальний обсяг даних для копіювання складатиме: 4 байта · 10 компонентів = 40 байтів Це значно швидше, ніж копіювання повних компонентів, особливо для систем із великими або складними компонентами. Наприклад, замість копіювання 2560 байтів даних для кожної сутності копіюється лише 40 байтів, що дає прискорення в десятки разів. Математичне моделювання задачі по мінімізуванню загального обсягу даних, що копіюється під час переходу сутностей між архетипами в ECS-системі. Дано: Нехай Sc – розмір компонента, а N – кількість сутностей в яких С – кількість компонентів, необхідно знайти таке Vc яке відповідає розміру копіювання, тобто Vc – обсяг даних яке потрібно скопіювати, також маємо Si який дорівнює розміру індексу компонента. Також необхідно знайти такий R який буде показувати таке Vi на Vc яке має оптимальне значення. Тому мінімізаційна мета - мінімізувати V, де V = Vc або V = Vi. Отримали функцію: () = · · · · Де: N— кількість сутностей, які змінюють архетип. C— кількість компонентів у кожної сутності. 20 ЧДТУ 242323.010 ПЗ Sc— розмір одного компонента (в байтах). Si— розмір індексу компонента (в байтах, наприклад, 4 байти для типу int). Необхідно визначити: Vc — загальний обсяг даних для копіювання при використанні прямого копіювання компонентів. Vi— загальний обсяг даних для копіювання при використанні індексів. R — розмір покращення копіювання оптимізованих даних Із вказаної формули виходить, що ми отримали цільову функцію мінімізацію обсягу копіювання: (, ) = () = ( · · , · · ) Довести: Обсяг мінімальних скопійованих даних буде дорівнювати обсягу копіювання індексів, і буде меншим за обсяг скопійованих компонентів. V = Vi, Vi < Vc, V < Vc Доведення №1: Маємо компонент розміром 256 байти, а також індекс який має розмір 4 байти, а також дано 1000 сутностей із 10 компонентами. N = 1000, C = 10, Sc = 256 байти, Si = 4 байти. Обсяг скопійованих даних для компонентів: Vc = 1000 · 10 · 256 = 2,56 МБ 21 ЧДТУ 242323.010 ПЗ Обсяг скопійованих даних для індексів: Vi = 1000 · 10 · 4 = 40 КБ Результат оптимізації: (, ) = (1000 · 10 · 256, 1000 · 10 · 4) (, ) = (2560000, 40000) = (2, 56 , 40 ) = 40 Далі потрібно знайти розмір оптимізації, щоб побачити остаточне () = 1000 · 10 · 256 2560000 1000 · 10 · 4 = 40000 = 64 Доведення №2: Маємо компонент розміром 128 байти, а також індекс який має розмір 4 байти, а також дано 1000 сутностей із 10 компонентами. N = 1000, C = 10, Sc = 128 байти, Si = 4 байти. Обсяг скопійованих даних для компонентів: Vc = 1000 · 10 · 128 = 1,28 МБ Обсяг скопійованих даних для індексів: Vi = 1000 · 10 · 4 = 40 КБ 22 ЧДТУ 242323.010 ПЗ Результат оптимізації: (, ) = (1000 · 10 · 128, 1000 · 10 · 4) (, ) = (1280000, 40000) = (1, 28 , 40 ) = 40 Далі потрібно знайти розмір оптимізації, щоб побачити остаточний виграш оптимізації: () = 1000 · 10 · 128 = 1280000 1000 · 10 · 4 40000 = 32 Висновок: Доведення 1 та 2 математичної моделі показує, що використання індексації компонентів знижує обсяг даних для копіювання. Це рішення є ефективним для ECS-систем. Таким чином, використання індексів компонентів для архетипів забезпечує суттєві переваги з точки зору продуктивності та масштабованості. Це дозволяє ECS працювати ефективно навіть у системах із великою кількістю компонентів і частими структурними змінами сутностей. 2.3 Експериментальні дослідження Базуючись на попередній гіпотезі було вирішено дослідити її експериментально за допомогою імітаційної моделі та тестуванням продуктивності, для цього був розроблений план експерименту в якому потрібно: 1 Створити дослідницьку програмну систему, побудовану на основі нової архітектури ECS (Entity-Component-System), 23 ЧДТУ 242323.010 ПЗ 2 Структура в нової архітектури має на меті забезпечити високу продуктивність і гнучкість при роботі зі складними системами, зокрема у мобільних іграх жанру Roguelike. 3 Модель системи повинна бути реалізована з використанням концепції індексації для зберігання компонентів, що дозволяє зменшити час переходу сутностей між архетипами. 4 Під час тестування продуктивності буде змінюватись розмір компоненту та перевіряти швидкість виконання переміщення компоненту різних розмірів та індексів. Оптимізована структура ECS представлена на рисунку 2.5: Рис. 2.5 - Структура ECS з використанням нового підходу до збереження компонентів 24 ЧДТУ 242323.010 ПЗ Принципи дії та елементи моделі: Сутності (Entities): ідентифікатори об’єктів, що взаємодіють із системою. Компоненти (Components): дані, що описують властивості або стан сутностей. Вони зберігаються у глобальному масиві, що дозволяє уникати дублювання та спрощує управління пам’яттю. Архетипи (Archetypes): визначають набір компонентів, притаманних групі сутностей. У кожному архетипі зберігаються лише індекси компонентів у глобальному масиві, що знижує обсяг даних, які потрібно копіювати. Для моделювання роботи системи застосовувались імітаційні методи моделювання. Ефективність системи оцінювалась у кількох аспектах: середній час переходу сутності до нового архетипу порівнювався з традиційними ECS, де компоненти копіюються, також аналізувалась різниця швидкості переходу сутності при збільшенні розміру компонентів. Оскільки Unity використовує мову C# для написання скриптів, то для проведення експериментальних тестів продуктивності була застосована бібліотека 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) 25 ЧДТУ 242323.010 ПЗ Рис. 2.6 - Графік різниці швидкості виконання для сутності із компонентом розміром 8 байт Експеримент 2, порівняння швидкості зміни архетипу, для 10 компонентів розміром 16 байт (рис 2.7) Рис. 2.7 - Графік різниці швидкості виконання для сутності із компонентом розміром 16 байт 26 ЧДТУ 242323.010 ПЗ Експеримент 3, порівняння швидкості зміни архетипу, для 10 компонентів розміром 32 байта (рис 2.8) Рис. 2.8 - Графік різниці швидкості виконання для сутності із компонентом розміром 32 байта На основі проведених досліджень побудовано графік залежності часу переходу сутностей від кількості компонентів. Графік підтверджує, що розроблена система демонструє лінійний ріст часу, тоді як традиційна ECS має квадратичний ріст через обсяги копіювання компонентів. 27 ЧДТУ 242323.010 ПЗ Рис. 2.9 - Графік росту збільшення часу виконання при збільшенні розміру компонентів 28 ЧДТУ 242323.010 ПЗ Висновки до другого розділу Результати експериментальних досліджень підтвердили власну гіпотезу яка полягає у високій ефективності запропонованої ECS із використанням індексації для зберігання компонентів. Основною перевагою такої архітектури є значне зниження витрат часу на операції зміни архетипів, які є критичними у динамічних системах, таких як мобільні ігри жанру Roguelike. У традиційних ECS перехід сутності між архетипами вимагає копіювання всіх компонентів, що спричиняє квадратичний ріст часу виконання при збільшенні кількості чи розміру компонентів. У новій системі копіюються лише індекси компонентів в архетипі, що забезпечує лінійний ріст часу виконання. Експерименти з різними розмірами компонентів (8, 16, 32 байти) підтвердили, що продуктивність системи не залежить від збільшення розміру даних, у той час як традиційні ECS демонструють значне уповільнення. Це робить нову ECS архітектуру придатною для складних ігор із багатофакторною взаємодією між сутностями та компонентами. Крім того, оптимізація з використанням індексації для компонентів має такі переваги: 1 Уникнення дублювання компонентів зменшує витрати на пам’ять, що є особливо важливим у мобільних додатках із обмеженими ресурсами. 2 Зменшення обсягу операцій копіювання підвищує продуктивність і забезпечує стабільну роботу навіть у складних сценаріях із великим числом сутностей. Запропонована структурна оптимізація має потенціал для широкого застосування у розробці мобільних ігор, жанру Roguelike. Подальші дослідження можуть бути спрямовані на розширення функціональності системи, зокрема на інтеграцію механізмів адаптивного розподілу ресурсів, що ще більше підвищить її ефективність у складних динамічних середовищах. 29 ЧДТУ 242323.010 ПЗ РОЗДІЛ 3 ВПРОВАДЖЕННЯ РЕЗУЛЬТАТІВ ДОСЛІДЖЕНЬ У ПРАКТИКУ ПРОЕКТУВАННЯ ПРОГРАМНОГО ЗАБЕЗПЕЧЕННЯ ІНФОРМАЦІЙНИХ СИСТЕМ 3.1 Моделювання предметної області Розділ присвячений проєткувальній частині проєкту, головна ціль - спроєктувати ECS-фреймворк на основі власної гіпотези, яка полягає в новому способі збереження комопнентів на сутності. В розділі будуть розглянуті вимоги до фреймворку та на основі них, спроектовані на мові UML, діаграми. 3.1.1 Предметна область моделювання. Модель предметної області. Словник предметної області Entity-Component-System (ECS) - це архітектура програмного забезпечення, що дозволяє створювати гнучкі, модульні й високопродуктивні системи [12]. У рамках моделювання ECS предметна область включає ключові об’єкти, такі як світ (World), сутності (Entities), компоненти (Components), архетипи (Archetypes), події (Events), фільтри (Filters), а також ієрархічні відносини "батько-дитина" (Parent-Child). Світ (World): головна структура ECS, що координує всі взаємодії між сутностями, компонентами, архетипами та системами. Світ зберігає всі сутності, обробляє події, забезпечує виконання систем і дозволяє керувати глобальним станом гри або симуляції. У ньому також відбувається створення та видалення сутностей, а також керування їх компонентами. Сутності (Entities): унікальні ідентифікатори об’єктів ігрового світу, таких як персонажі, вороги чи предмети. Сутності самі по собі не містять даних, але асоціюються з компонентами, які визначають їх властивості та поведінку. Компоненти (Components): прості структури, що зберігають дані, наприклад позицію, здоров’я або тип анімації. Запропонована ECS оптимізує зберігання компонентів через глобальний масив, незалежний від архетипів. Архетипи (Archetypes): групи, які визначають набір компонентів, спільних для певного класу сутностей. У запропонованій ECS хешування архетипів 30 ЧДТУ 242323.010 ПЗ дозволяє швидко визначати потрібний архетип для сутності, що оптимізує час переходу між архетипами при зміні компонентів. Фільтри (Filters): механізм для вибірки сутностей, що відповідають заданому набору компонентів. Фільтри дозволяють швидко знайти групи сутностей, які потребують обробки в певній системі. Наприклад, система руху може працювати лише з сутностями, що мають компоненти Position та Velocity. Події (Events): спосіб взаємодії між сутностями або системами. Події передають інформацію про певні події, такі як зміна стану, зіткнення чи використання предмета. Ієрархічні відносини (Parent-Child): дозволяють сутностям формувати ієрархії. Наприклад, персонаж може бути "батьком" для предмета зброї або броні. Ця структура полегшує трансформації, наприклад, переміщення "дитини" разом із "батьком". Словник предметної області: Світ (World): основна структура, що об’єднує всі сутності, компоненти, архетипи та системи. Сутність (Entity): унікальний ідентифікатор об’єкта ігрового світу. Компонент (Component): структура даних, що описує властивості сутності. Архетип (Archetype): група сутностей із однаковим набором компонентів. Фільтр (Filter): інструмент для вибірки сутностей за компонентами. Подія (Event): механізм передачі інформації між сутностями чи системами. Відносини батько-дитина (Parent-Child): ієрархічний зв’язок між сутностями. Запропонована ECS модель спрямована на забезпечення високої продуктивності, гнучкості та масштабованості. Завдяки оптимізації зберігання компонентів, механізмам фільтрації сутностей і використанню подій, система ефективно працює у середовищах із високою динамікою, таких як мобільні ігри в жанрі Roguelike. 3.1.2 Елементи моделювання предметної області Програмне забезпечення Entity Component System фреймворк для ігор жанру 31 ЧДТУ 242323.010 ПЗ Roguelike буде розроблений на мові програмування C# та оптимізовани для Il2Cpp - скриптового бекенду ігрового двигуна Unity. Для побудови моделі предметної області будуть застосовані діаграми мови UML, що дозволяє візуалізувати структуру та логіку роботи системи. Під час проектування будуть використані основні типи UML-діаграм які зображені на рисунку 3.1 та рисунку 3.2, які забезпечать детальне моделювання предметної області [13] [22] [23]. Загальна структура ECS буде представлена за допомогою цих діаграм, що дозволить описати ключові об’єкти системи, такі як World, Entity, Component, Archetype, а також їх взаємодії через фільтри, події та ієрархічні зв’язки. Рис. 3.1 - Основні графічні символи UML [23] 32 ЧДТУ 242323.010 ПЗ Рис. 3.2 - Эднальні елементи UML [23] 33 ЧДТУ 242323.010 ПЗ 3.1.3 Робоча область моделювання Архітектура ECS, оптимізована для мобільних ігор жанру Roguelike, структурована таким чином, щоб забезпечити ефективне збереження та обробку даних, що є основою всього ігрового процесу. Основні сутності системи забезпечують гнучкість у моделюванні предметної області та оптимізують взаємодії між елементами гри. World є головною сутністю, яка зберігає всі інші елементи ECS, зокрема сутності, компоненти та архетипи. Вона також відповідає за глобальне управління фільтрами, ієрархічними зв’язками (Parent-Child) та обробкою подій. Entity представляє ігровий об’єкт і є унікальним ідентифікатором, який зв’язує компоненти з архетипами. Перехід сутності між архетипами відбувається динамічно, забезпечуючи ефективне оновлення її стану. Component містить дані, що описують властивості сутності, наприклад, здоров’я, швидкість або позицію. Всі компоненти зберігаються у зовнішньому масиві, що дозволяє уникнути дублювання даних і мінімізує витрати на копіювання під час переходу між архетипами. Archetype зберігає структуру компонентів і список сутностей, які відповідають цій структурі. Архетип містить лише індекси компонентів у глобальному масиві, що значно знижує накладні витрати на збереження даних. Filter забезпечує вибірку сутностей на основі їхніх компонентів. Це дозволяє швидко знаходити всі сутності, які відповідають певним умовам, необхідним для виконання ігрової логіки. Event забезпечує механізм передачі повідомлень між сутностями, дозволяючи реалізувати, наприклад, виклики подій атаки або збору предметів. EntityRelation зв’язки створюють ієрархічну структуру між сутностями, наприклад, коли персонаж утримує предмети або вороги групуються навколо цілі. На рисунку 3.3 наведена модель основних компонентів системи, яка ілюструє взаємодії між компонентами ECS, забезпечуючи зручну структуру гри. 34 ЧДТУ 242323.010 ПЗ Рис 3.3 Модель структури Entity Component System 3.2 Формування та аналіз вимог В наступних підрозділах було проведено формування вимог, а також їх аналіз. 3.2.1 Формування вимог до програмного забезпечення. Первинні і детальні вимоги. Вимоги замовника і розробника. Функціональні і нефункціональні вимоги. ПЕРВИННІ ВИМОГИ: 1 Фреймворк повинен забезпечувати можливість створення, видалення та модифікації сутностей та їхніх компонентів. 2 Кожна сутність повинна підтримувати динамічне додавання та видалення компонентів без втрати даних. 3 Архетипи повинні забезпечувати швидке фільтрування сутностей на основі набору їхніх компонентів. 35 ЧДТУ 242323.010 ПЗ 4 Фреймворк повинен надавати механізм для отримання даних про сутності. 5 Фреймворк має підтримувати Parent-Child зв’язки між сутностями, наприклад, персонажами та предметами. 6 Повинна бути підтримка подій ДЕТАЛЬНІ ВИМОГИ: 1.1 Фреймворк повинен забезпечувати створення сутностей із базовими атрибутами. 1.2 Видалення сутностей повинно бути ефективним і не порушувати цілісність даних інших сутностей. 2.1 Додавання/видалення компонентів до сутностей не повинно викликати значного уповільнення роботи системи. 3.1 Архетипи повинні автоматично оновлюватися при зміні набору компонентів сутності. 3.2 Фільтри повинні забезпечувати вибір сутностей на основі точного збігу компонентів. 4.1 Глобальний масив компонентів має бути оптимізований для зменшення обсягу пам’яті та часу доступу. 5.1 Кожна сутність може бути прив’язана до батьківської сутності. 5.2 При видаленні батьківської сутності, всі дочірні сутності повинні також бути видалені. Функціональні вимоги 1 Створення, видалення та модифікація сутностей та компонентів 1.1 Фреймворк повинен забезпечувати створення сутностей із базовими атрибутами. 1.2 Видалення сутностей повинно бути ефективним і не порушувати цілісність даних інших сутностей. 1.3 Модифікація компонентів має відбуватися з мінімальною затримкою. 2 Динамічне управління компонентами сутностей 2.1 Фреймворк повинен забезпечувати динамічне додавання/видалення 36 ЧДТУ 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 Глобальний масив компонентів має бути оптимізований для зменшення обсягу пам’яті. 37 ЧДТУ 242323.010 ПЗ 2.2 Архетипи повинні використовувати мінімальну кількість пам’яті для зберігання індексів компонентів. 3 Масштабованість 3.1 Фреймворк повинен підтримувати обробку великої кількості сутностей (до мільйонів). 4 Надійність 4.1 Видалення сутностей не повинно призводити до пошкодження інших даних у системі. 4.2 Обробка подій повинна гарантувати, що жодна подія не буде втрачена. 5 Зручність використання 5.1 Інтерфейс API має бути інтуїтивним для розробників і надавати чітку документацію. 5.2 Система повинна легко інтегруватися з ігровими рушіями, такими як Unity. 3.2.2 Формування вимог за допомогою діаграми прецедентів Діаграми прецедентів будуть зроблені на основі мови UML. В роботі існує два види акторів: 1 Користувач - той хто користується фреймворком. 2 Система - фільтрує/змінює дані. На рисунку 3.4 зображено ключові функціональні можливості фреймворку ECS (Entity Component System), згруповані за основними підсистемами. Підсистема "Entity Management" забезпечує базову роботу з сутностями. Це включає створення, видалення та модифікацію сутностей, а також додавання, видалення та модифікацію їхніх компонентів. Видалення сутностей має зв'язок з видаленням компонентів через відношення включення, оскільки компоненти автоматично видаляються разом із сутністю. "Archetype & Filtering" відповідає за підтримку архетипів і фільтрацію сутностей. При додаванні або видаленні компонентів архетипи автоматично оновлюються, що представлено через зв’язок «тригер». Фільтрація сутностей на 38 ЧДТУ 242323.010 ПЗ основі архетипів надає можливість швидкого пошуку сутностей із конкретними наборами компонентів. У підсистемі "Data Access" реалізовано функції доступу до даних. Вона дозволяє користувачу отримувати дані про конкретні сутності та звертатися до глобальних масивів компонентів для оптимального управління пам’яттю. "Parent-Child Relations" додає підтримку ієрархії між сутностями. Користувач може встановлювати батьківські зв’язки між сутностями, а видалення сутності автоматично поширюється на всі дочірні сутності, що показано через розширення до видалення. Нарешті, підсистема "Event System" забезпечує обробку подій. Вона працює з подіями, дозволяючи ефективно обробляти взаємодії між сутностями, а також обробляє специфічні події, пов'язані з сутностями. Користувач (Framework User) взаємодіє з фреймворком через операції створення, модифікації та доступу до даних. Система (System) фільтрує сутності та події. Рис. 3.4 - Діаграма прецедентів використання фреймворку 39 ЧДТУ 242323.010 ПЗ 3.3 Проектування логічної структури програмного комплексу В цьому розділі розглянемо детально діаграми класів та пакетів. 3.3.1 Діаграми класів На рисунку 3.5 зображено діаграму класів, яка має: 1 World - це основний клас ECS фреймворку, який агрегує всі компоненти, архетипи, фільтри, події, аспекти, міграції та системи: – AddComponent(entityID, component) і RemoveComponent(entityID, component) - методи для додавання та видалення компонентів у сутностей. – 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. 40 ЧДТУ 242323.010 ПЗ – SetComponent(componentID, component) - метод для встановлення значення компонента. – Зв'язок агрегація з Component показує, що цей клас зберігає компоненти. 4 Filter - клас для фільтрації сутностей за їх компонентами: – componentMask - бітмаска, яка представляє компоненти, що використовуються для фільтрації. – Match(entityID) - метод для перевірки, чи відповідає сутність фільтру. – Зв'язок делегація з 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 показує, що аспекти залежать від компонентів для визначення своїх властивостей. 41 ЧДТУ 242323.010 ПЗ 8 Entity - клас, що представляє сутність в ECS фреймворку: – entityID - ідентифікатор сутності. – archetype - архетип, до якого належить сутність. – GetComponent(componentID) - метод для отримання компонента за його ID. – SetComponent(componentID, component) - метод для встановлення компонента. – Зв'язок агрегація з Archetype і Component показує, що сутність належить до архетипу і має компоненти. 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) - методи для додавання та видалення компонентів у 42 ЧДТУ 242323.010 ПЗ сутностей. – GetComponent(entityID, componentID) - метод для отримання компонента у сутності. Рис. 3.5 - Діаграма класів 43 ЧДТУ 242323.010 ПЗ 3.3.2 Діаграми пакетів На рисунку 3.6 зображено діаграму пакетів. Пакети розділені по функціям фреймворку: Пакет JaECS.Collections - містить структури даних та колекції які використовуються в архетипах для оптимізації структури збереження компонентів сутності. Пакет JaECS.Archetype - містить дані про сутності. Пакет JaECS.Relation - містить дані про зв’язки Батько-Дитина між сутностями. Пакет JaECS.Entity - містить внутрішні дані про сутність, а саме її ідентифікатори. Пакет JaECS.Filter - містить фільтри для фільтрації сутностей в системах і їх майбутньої обробки. Пакет JaECS.Systems - представляє системи в яких відбувається логіка з використанням фільтрів для отримання потрібних сутностей. Пакет JaECS.Components - має функціонал для роботи з компонентами на сутності. Рис. 3.6 – Діаграма пакетів фреймворку 44 ЧДТУ 242323.010 ПЗ 3.4 Архітектурне проектування 3.4.1 Діаграма компонентів На рисунку 3.7 зображена діаграма компонентів та представлена структура 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 оперує фільтрами, які застосовуються до архетипів для швидкої вибірки сутностей. 45 ЧДТУ 242323.010 ПЗ Event System управляє подіями через спеціалізований компонент. Migration змінює структуру даних компонентів і архетипів, забезпечуючи гнучкість системи. Ця структура забезпечує розділення відповідальностей між компонентами, модульність і оптимізацію роботи з даними в ECS. Рис. 3.7 – Діаграма компонентів ECS фреймворку 46 ЧДТУ 242323.010 ПЗ 3.4.2 Розгортання програмної системи на апаратних засобах. Діаграма розгортання На рисунку 3.8 представлена діаграма розгортання. Діаграма розгортання дозволяє зрозуміти, що програмна система підключається як зовнішня бібліотека до проекту користувача фреймворку в якому він реалізує логіку своєї системи. Рис. 3.8 – Діаграма розгортання 47 ЧДТУ 242323.010 ПЗ 3.5 Моделювання поведінки системи 3.5.1 Діаграма діяльності На рисунку 3.9 зображено діаграму діяльності додавання компонента в якій для кожної сутності перевіряється наявність відповідного архетипу, якщо архетип не знайдено, створюється новий. Рис. 3.9 – Діаграма діяльності: Додавання компонента до сутності 48 ЧДТУ 242323.010 ПЗ На рисунку 3.10 зображено діаграму видалення компонента в архетипах, cистема ітерує по сутностях для видалення компонентів, для кожної сутності архетип оновлюється або залишається без змін, якщо архетип стає порожнім, він видаляється. Рис. 3.10 – Діаграма діяльності: Видалення компонента в архетипах На рисунку 3.11 зображено діаграму діяльності міграції сутностей - для кожної сутності з оновленими компонентами перевіряється, чи потрібна міграція, 49 ЧДТУ 242323.010 ПЗ якщо міграція необхідна, сутність переміщується до нового архетипу, в іншому випадку міграція пропускається. Рис. 3.11 – Діаграма діяльності: Міграція сутностей На рисунку 3.12 зображено діаграму діальності для фільтрації сутностей - у циклі кожна сутність перевіряється на відповідність фільтру, а саме по його бітовій 50 ЧДТУ 242323.010 ПЗ масці, якщо сутність відповідає, вона додається до списку результатів з якими буде працювати користувач. Рис. 3.12 - Діаграма діяльності: Запит сутностей по фільтруванню компонентів 3.5.2 Діаграма послідовності Діаграма додавання компонента яка зображена на рисунку 3.13 містить наступну логіку 1 Client викликає метод AddComponent(entityID, component) у класі World. 2 World перевіряє, чи існує архетип для вказаної сутності, викликавши метод у класі Archetype. 51 ЧДТУ 242323.010 ПЗ 3 Якщо архетип існує: 4 Archetype викликає метод Add для ComponentRow, щоб додати нові дані компонента. 5 Якщо архетип не існує: 6 World викликає метод для створення нового архетипу. 7 Після цього Archetype також додає дані компонента до ComponentRow. 8 Після успішного додавання компонента, World повертає повідомлення про успіх назад до Client. Рис. 3.13 - Діаграма послідовності: Додавання компонента Діаграма видалення компонента яка зображена на рисунку 3.14 містить наступну логіку: 1 Client викликає метод RemoveComponent(entityID, component) у класі World. 2 World перевіряє, чи існує архетип для вказаної сутності. 3 Якщо архетип існує: 52 ЧДТУ 242323.010 ПЗ 4 Archetype викликає метод для видалення даних компонента з ComponentRow. 5 Після видалення даних, World перевіряє, чи архетип став порожнім. 6 Якщо архетип порожній, World видаляє архетип. 7 Якщо архетип не існує, World повертає повідомлення про те, що компонент не знайдено. 8 Після успішного видалення, World повертає повідомлення про успіх назад до Client. Рис. 3.14 - Діаграма послідовності: Видалення компонента Діаграма міграції сутностей яка зображена на рисунку 3.15 містить наступну логіку: 1 Client вносить зміни до компонентів сутності, викликаючи метод у класі World. 53 ЧДТУ 242323.010 ПЗ 2 World передає запит до класу Migration, додаючи сутність до черги міграції. 3 Migration перевіряє, чи потрібен новий архетип для сутності: 4 Якщо новий архетип потрібен, Migration викликає метод для створення нового архетипу. 5 Потім Migration переміщує сутність до нового архетипу. 6 Якщо новий архетип не потрібен, Migration повідомляє World, що міграцію виконувати не потрібно. 7 Після завершення міграції, Migration повідомляє World, що процес завершено. 8 World повертає повідомлення про успіх назад до Client. Рис. 3.15 - Діаграма послідовності: Міграція сутностей 54 ЧДТУ 242323.010 ПЗ Діаграма фільтрації сутностей яка зображена на рисунку 3.16 містить наступну логіку: 1 Client відправляє запит на виконання фільтру через метод ExecuteQuery(filter) у класі World. 2 World передає фільтр до Filter, щоб застосувати його до всіх сутностей. 3 Filter перевіряє архетипи для відповідності фільтру: 4 Якщо знайдено сутності, що відповідають фільтру, Archetype повертає їх назад до World. 5 Якщо жодна сутність не відповідає фільтру, Archetype повертає порожній результат. 6 World повертає результати запиту назад до Client. Рис. 3.16 - Діаграма послідовності: фільтрація сутностей 55 ЧДТУ 242323.010 ПЗ 3.5.3 Діаграма комунікації На рисунку 3.17 зображена діаграма комунікацій. Вона представляє комунікацію користувача фреймворку із частинами його API. Користувач, що користується фреймворком повинен реалізувати абстракцію SystemBase, після чого він отримає доступ до внутрішнього API: SendEvent, BuildFilter, Get/Add/Remove Component. Доступу до наступного API, а саме зміну архетипу сутності та отримання сутності напряму - користувач не має, і не потребує, всі ці маніпуляції фреймворк виконує автоматично. Рис. 3.17 - Діаграма комунікації використання фреймворку 56 ЧДТУ 242323.010 ПЗ 3.5.4 Діаграма скінченного автомату На рисунку 3.18 зображену діаграму скінченного автомату ECS фреймворку, автомат є ієрархічним та містить внутрішні стани: 1 WorldState: Основний стан ECS фреймворку, коли система в активному стані. Idle - система в стані очікування, без активних операцій. AddingComponent - стан додавання компонентів: CheckingArchetype - перевіряється, чи існує архетип для сутності. AddingToArchetype - додавання компонента до існуючого архетипу. CreatingNewArchetype - створення нового архетипу, якщо підходящий архетип не знайдено. RemovingComponent - стан видалення компонентів: CheckingArchetype - перевірка архетипу для видалення компонента. RemovingFromArchetype - видалення компонента з архетипу. QueryingEntities - стан виконання запиту фільтрації: ApplyingFilter - застосування фільтра для пошуку сутностей. ReturningEntities - повернення результатів запиту. 2 MigrationState: Цей стан активується, коли змінюються компоненти сутності: ApplyingChanges - застосування змін компонентів до сутностей. ValidatingEntities - перевірка, чи потрібно мігрувати сутність до іншого архетипу. MovingEntities - міграція сутності до нового архетипу. 3 EventState: Активується, коли спрацьовує подія: HandlingEvent - обробка події. UpdatingWorld - оновлення світу на основі обробленої події. Опис переходів: 1 Початковий стан: система переходить до Idle. 57 ЧДТУ 242323.010 ПЗ 2 Стан Idle може переходити до різних станів в залежності від операції (додавання, видалення компонентів, запит). 3 При зміні компонентів сутностей система переходить до MigrationState. 4 Події активують EventState, а аспекти активують AspectState. Рис. 3.18 - Діаграма станів фреймворку Висновок до третього розділу Розроблені діаграми спрощують майбутні етапи розробки програмного забезпечення, а саме, на основі вказаних діаграм будуть створені класи та зв’язки між різними модулями фреймворку. 58 ЧДТУ 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-накопичувачами, що забезпечує швидку компіляцію та ефективну роботу з 59 ЧДТУ 242323.010 ПЗ проектом. Проект орієнтовано на мобільні пристрої середнього та високого рівня продуктивності, що відповідає сучасним вимогам до ігрової індустрії. Для забезпечення оптимізації процесу розробки та тестування використовуються профілювальні інструменти, такі як вбудований профілювальник Unity та DotTrace Diagnoser [17]. Це дозволяє ефективно аналізувати та покращувати продуктивність ECS. Таким чином, вибір засобів розробки був обґрунтований потребами високопродуктивної та масштабованої реалізації ECS, що є важливим для розробки мобільних ігор у жанрі Roguelike. 4.1.2 Опис функціональної схеми На рисунку 4.1 зображено функціональну схему процесу фільтрації сутностей і отримання списку подій. У розробленій ECS цей функціонал виконується за допомогою узгоджених кроків, що включають взаємодію між користувачем, внутрішніми механізмами фреймворку та базою даних. У випадку фільтрації сутностей користувач через спеціальний механізм створює запит із критеріями, на основі яких необхідно виконати фільтрацію. Цей механізм обробляє запит і надсилає його у внутрішню систему світу ECS. Світ звертається до бази даних, у якій зберігаються всі сутності та їхні компоненти. На основі заданих критеріїв виконується пошук і створюється список відповідних сутностей. Отримані дані записуються в дебаг інформацію для подальшого аналізу та надсилаються користувачу у вигляді готового списку. Процес отримання списку подій працює аналогічно. Користувач через спеціальний механізм формує запит для отримання подій, які відбулися в системі. Цей запит обробляється та передається в систему світу. Світ звертається до бази даних, у якій зберігаються події. Дані про події, що відповідають критеріям, обираються, записуються в дебаг інформацію та відправляються назад користувачу як готовий список. Обидва процеси ілюструють структуровану взаємодію між користувачем і системою, використання централізованої бази даних для зберігання сутностей і подій, а також забезпечення можливості аналізу через запис дебаг інформації. 60 ЧДТУ 242323.010 ПЗ Такий підхід забезпечує модульність, прозорість роботи та можливість розширення функціональності ECS. Рис. 4.1 - Функціональна схема фреймворку 4.1.3 Опис логічної схеми системи Процес функціонування системи ECS побудований на основі циклічного управління станами сутностей, їх компонентів та взаємодій через події. Логічна структура включає в себе основні операції: створення сутностей, управління компонентами, обробка подій, фільтрація сутностей та передача результатів користувачу. Послідовність дій, яка відбувається у системі, організована наступним чином: 61 ЧДТУ 242323.010 ПЗ 1 Ініціалізація системи. При запуску створюється об'єкт світу ECS, що виступає як центральний елемент управління всіма процесами. Світ ініціалізує базу даних, яка містить інформацію про архетипи, компоненти, фільтри та події. 2 Додавання сутностей та компонентів. Користувач через API системи може створювати нові сутності. При створенні сутності система генерує унікальний ідентифікатор та додає запис про сутність до бази даних. За необхідності, до сутності додаються компоненти, що автоматично оновлює її архетип. 3 Фільтрація сутностей. При запиті фільтрації користувач задає критерії, за якими відбираються сутності. Механізм фільтрації аналізує ці критерії, визначає архетипи, що відповідають умовам, та виконує пошук у базі даних. Результат зберігається у тимчасовій структурі для доступу. 4 Обробка подій. Події, створені сутностями або системами, записуються у чергу подій. Світ обробляє ці події у рамках спеціального циклу [18], модифікуючи стан сутностей чи викликаючи необхідні обробники. Дані про події можуть бути записані у дебаг-інформацію. 5 Робота з батьківсько-дочірніми зв’язками. Якщо сутність прив'язується до іншої як дочірня, система зберігає відповідну ієрархію. Видалення батьківської сутності автоматично ініціалізує видалення всіх дочірніх сутностей. 6 Доступ до даних. Запити користувача на отримання даних (фільтри, компоненти, події) виконуються через відповідні модулі системи. Дані з бази обробляються, форматуються та надсилаються користувачу. 62 ЧДТУ 242323.010 ПЗ 7 Завершення роботи. При зупинці системи ECS виконується очищення пам’яті, щоб уникнути її витоку. На рисунку 4.2 зображено алгоритм роботи фреймворку. Рис. 4.2 - Блок-схема логічної роботи фреймворку 63 ЧДТУ 242323.010 ПЗ 4.1.4 Розробка бази даних У рамках даної роботи використання бази даних не є необхідним, оскільки архітектура ECS (Entity Component System) працює з динамічною структурою даних, яка організовується безпосередньо в пам’яті. Всі сутності, компоненти, архетипи та події обробляються в оперативній пам’яті, що забезпечує високу продуктивність і низьку затримку, критично важливу для ігор реального часу. Однак, якщо в майбутньому виникне потреба зберігати дані для довготривалого використання, наприклад, для збереження прогресу гри, статистики користувачів або історії подій, систему можна легко адаптувати для роботи з базою даних. Для цього може бути використаний ORM (Object-Relational Mapping) фреймворк, який дозволяє інтегрувати базу даних без значних змін в існуючій архітектурі. [19] ORM забезпечує зручність у роботі з базою даних завдяки автоматичному перетворенню об’єктів у реляційні таблиці та навпаки. Це дозволить уникнути ручного написання SQL-запитів і забезпечує підтримку реляційної моделі з її перевагами, такими як зрозумілість і гнучкість. У випадку адаптації ECS до роботи з ORM буде доцільно застосовувати наступні етапи моделювання бази даних: 1 Концептуальна модель даних: аналіз основних понять і об’єктів проблемної області гри, таких як сутності, їх стани та зв’язки. 2 Логічна модель даних: визначення структури даних у вигляді сутностей і відносин між ними, наприклад, таблиць для сутностей та їх компонентів. 3 Фізична модель даних: проектування конкретних таблиць, їх атрибутів і зв’язків, зокрема первинних та зовнішніх ключів, для забезпечення цілісності та ефективності. Таким чином, базу даних можна розглядати як додатковий ресурс для зберігання даних, якщо це необхідно, але її використання не є критичною вимогою для функціонування ECS системи, орієнтованої на ігровий процес. 64 ЧДТУ 242323.010 ПЗ 4.1.5 Розробка програмного інтерфейсу користувача Проектування правильного API є одним із найважливіших аспектів розробки програмного забезпечення, оскільки саме API забезпечує взаємодію користувачів із системою та визначає її зручність, продуктивність і гнучкість. Добре спроектоване API спрощує процес розробки, полегшує інтеграцію та розширення системи, а також мінімізує ризик виникнення помилок під час роботи з нею [20]. Одним із ключових принципів створення API є зрозумілість і простота. API повинно бути інтуїтивно зрозумілим для розробників, що використовують його. Це означає, що методи та функції повинні мати чіткі назви, які відображають їх призначення, а структура повинна бути логічною і передбачуваною. Наявність якісної документації, яка пояснює функціональність кожного методу, також відіграє вирішальну роль у зручності використання API. Розроблене API ECS Framework забезпечує гнучку та високопродуктивну взаємодію розробників із основними компонентами системи. API орієнтоване на спрощення роботи з сутностями, компонентами, архетипами та фільтрами, а також підтримує ієрархічні зв’язки та події. Основні можливості API: 1 Управління сутностями: Користувач може створювати, видаляти та отримувати інформацію про сутності. Метод CreateEntity дозволяє створювати сутності з базовими або попередньо визначеними компонентами. Метод DestroyEntity видаляє сутність, при цьому забезпечується автоматичне очищення її даних. API також дозволяє перевіряти стан сутності за допомогою методу IsEntityAlive як зображено на рисунку 4.3. Рис. 4.3 - Частина програмного інтерфейсу: перевірка існування сутності 65 ЧДТУ 242323.010 ПЗ 2 Управління компонентами: API підтримує додавання, видалення та модифікацію компонентів у сутностях. Наприклад, методи AddComponent і RemoveComponent оновлюють набір компонентів у сутності, викликаючи відповідне оновлення архетипу, як це зображено на рисунку 4.4. Компоненти зберігаються у глобальному масиві, що забезпечує ефективність доступу та зменшення дублювання даних. Рис. 4.4 - Частина програмного інтерфейсу: додавання та видалення компонента 3 Архетипи та фільтрування: Функції API дозволяють автоматично оновлювати архетипи при зміні набору компонентів сутності. Користувач може створювати фільтри через GetFilterBuilder, які відбирають сутності на основі заданих умов. Це полегшує ітерацію через сутності, що мають певні набори компонентів. Рис. 4.5 - Частина програмного інтерфейсу: додавання та видалення компонента 66 ЧДТУ 242323.010 ПЗ 4 Ієрархічні зв’язки: API надає можливість створювати відносини "батько-дитина" між сутностями. Методи AddChild та RemoveChild забезпечують додавання дочірніх сутностей до батьківських і видалення відповідних зв’язків як це зображено на рисунку 4.6. Видалення батьківської сутності автоматично ініціює видалення її дочірніх сутностей, що значно спрощує керування складними структурами. Рис. 4.6 - Частина програмного інтерфейсу: створення зв’язків 5. Система подій: Користувач може створювати та обробляти події, використовуючи функції GetEvent та EventFilter. Рис. 4.7 - Частина програмного інтерфейсу: керування подіями 67 ЧДТУ 242323.010 ПЗ 4.1.6 Опис розробки програмних компонентів В розробленій ECS системі для мобільного ігрового додатку реалізовано ряд модулів, які забезпечують ефективну організацію та управління сутностями, їх компонентами, а також обробку подій. Основною точкою входу є клас, що відповідає за ініціалізацію всієї системи та взаємодію між її модулями. World - є основною точкою доступу до всіх сутностей, архетипів та компонентів в системі. Цей компонент виконує роль головного керівника в процесі взаємодії між іншими компонентами. Він взаємодіє з модулями для створення, модифікації та видалення сутностей, а також керує фільтрацією за допомогою архетипів і компонентів. Entity - клас, що представляє сутність в ECS системі. Кожна сутність це ідентифікатор для масиву компонентів. Цей клас також містить логіку для підтримки стану сутності в світі, а саме ідентифікатор архетипу та його індекс. Component - компонент є складовою частиною сутності, що зберігає дані та надає функціональність. Кожен компонент є типізованим і має конкретне призначення, наприклад, для здоров’я персонажа, інвентарю або шкідливих ефектів. ArchetypeManager - відповідає за управління архетипами, які визначають структуру та типи компонентів, що належать до кожної сутності. Архетипи дозволяють оптимізувати запити на фільтрацію сутностей, що мають певні компоненти. Migration - компонент, що відповідає за зміну компонентів сутностей в процесі гри. Наприклад, якщо сутність повинна отримати новий компонент або змінити існуючий, цей компонент відповідає за перенесення даних між архетипами або компонентами. Filter - клас, який забезпечує фільтрацію сутностей за набором компонентів. Запит на фільтрацію може бути виконаний через інтерфейс користувача, що дозволяє вибрати необхідні критерії для вибору сутностей, що задовольняють умови фільтрації. 68 ЧДТУ 242323.010 ПЗ Event System - система подій, яка забезпечує механізм асинхронної обробки подій. Кожен тип події може бути оброблений окремим обробником, який змінює стан гри або сутностей на основі події. Цей компонент також взаємодіє з іншими модулями для зміни стану гри чи об’єктів. Parent-Child Relations - клас, який реалізує підтримку батьківсько-дочірніх зв’язків між сутностями. Наприклад, персонаж може бути батьком для предметів, які він несе. Видалення батьківської сутності автоматично ініціює видалення дочірніх сутностей. 4.2 Тестування системи 4.2.1 Модульне тестування Написання модульних тестів було виконано в спеціальному для цього модулі та за допомогою інструментів для тестування, а саме: NUnit - це бібліотека для тестування програмного забезпечення для мови програмування С#. Вона надає розширений функціонал для написання та виконання тестів одиниць (unit tests) та інтеграційних тестів для проектів, розроблених з використанням Unity. [21] Модульні тести розроблялись згідно з методологією AAA [22], що умовно розділяє тест на три основні етапи: Arrange - підготовка даних для тестування; Act - виконання бізнес-логіки тестованої системи; Assert - перевірка результатів. Рис. 4.8 - Уривок коду ініціалізації тестів 69 ЧДТУ 242323.010 ПЗ Тести для модуля створення сутностей реалізовані в файлі EntitiesTests.cs, та представлені нижче на рисунках 4.9-4.13: Рис. 4.9 - Тестування функції видалення пустих сутностей 70 ЧДТУ 242323.010 ПЗ Рис. 4.10 - Тестування видалення компонентів при видаленні сутностей Рис. 4.11 - Метод для тестування функції отримання сутності по ідентифікатору 71 ЧДТУ 242323.010 ПЗ Рис. 4.12 - Метод для перевикористання ідентифікаторів сутності Рис. 4.13 - Методи для створення і видалення батьківських зв’язків 72 ЧДТУ 242323.010 ПЗ На таблиці 4.1 представлені фінальні результати тестування. Таблиця 4.1 Результат модульного тестування Ім’я модульного Очікуваний результат Отриманий результат тесту Створення сутності Сутність буде створена Створена сутність Додавання Сутність буде мати 1 Сутність має 1 компонент компонента на компонент сутність Видалення Сутність залишиться без Сутність залишилася без компонента з компоненту компоненту сутності Видалення сутності Сутність буде видалена Сутність видалена Додавання дитини до З’явиться батьківський З’явився батьківський батька зв’язок зв’язок в якому вказана сутність стала дитиною батька 4.2.2 Інтеграційне тестування Інтеграційне тестування було спрямоване на перевірку взаємодії між різними компонентами ECS системи, а також на забезпечення коректної роботи всіх підсистем при їх спільному використанні. Метою тестування було виявлення помилок, які можуть виникнути під час інтеграції різних частин системи, і перевірка того, як окремі компоненти взаємодіють один з одним у реальних умовах роботи системи. Тестування почалося з перевірки базових функціональних компонентів, таких як створення сутності, додавання та видалення компонентів, а також перевірка фільтрації сутностей. Кожен з цих процесів був протестований окремо для того, щоб переконатися, що зміни в одній частині системи не призводять до непередбачених помилок в іншій. Для цього використовувалися автоматизовані тести, які запускалися в середовищі, що імітує реальні умови роботи системи. 73 ЧДТУ 242323.010 ПЗ Під час інтеграційного тестування особлива увага приділялася взаємодії між компонентами, що відповідають за архетипи та фільтрацію сутностей. Тести перевіряли, чи правильно система обробляє запити на фільтрацію сутностей і чи коректно архетипи оновлюються в разі додавання або видалення компонентів з сутностей. Важливою частиною тестування було забезпечення того, щоб система не створювала помилок або невідповідностей між архетипами та сутностями при зміні їхнього складу. Для тестування були використані інтеграційні тести. Результати тестів демонстрували, що в основному система стабільно працює в умовах інтеграції різних частин, хоча було виявлено кілька проблем, пов’язаних з оптимізацією фільтрації сутностей при великих обсягах даних. Ці проблеми були виправлені в процесі рефакторингу. У результаті інтеграційного тестування, яке зображено в таблиці 4.2, було підтверджено, що всі компоненти системи правильно взаємодіють між собою, і система здатна обробляти різноманітні сценарії взаємодії сутностей, подій та архетипів, що робить систему стабільною та готовою до використання в реальних умовах. Таблиця 4.2 Результат інтеграційного тестування Ім’я інтеграційного Очікуваний результат Отриманий результат тесту Створення сутності Сутність буде створена, Створена сутність не була без компонентів при але не додана ні в один додана в жоден фільтр наявності фільтрів з фільт компонентом X Додавання Сутність буде мати 1 Сутність має 1 компонент компонента X на компонент та додана в та додана в фільтри які сутність фільтри які потребують потребують цей компнент цей компонент 74 ЧДТУ 242323.010 ПЗ Продовження таблиці 4.2 Видалення компонента X Сутність залишиться без Сутність залишилася без з сутності компоненту, а фільтри компоненту, а фільтри яким потрібен цей залишилися без цієї компонент ну будуть сутності мати сутності Фільтрування сутностей Отримані сутності в Отримані сутності мають із 3 компонентами результаті фільтрації ці компоненти будуть мати ці компоненти Фільтрування сутностей Отримані сутності в Отримані сутності мають із 2 компонентами результаті фільтрації ці компоненти будуть мати ці компоненти Зміна 3 компонентів Сутність змінить архетип Сутність змінила архетип сутності та з’явиться в потрібних та додалася в потрібні фільтрах фільтри 4.2.3 Системне тестування Системне тестування є важливою частиною процесу забезпечення якості програмного забезпечення, яке перевіряє, чи відповідає система вимогам замовника, і чи правильно функціонує її структура та компоненти. Після успішного завершення інтеграційного тестування, яке забезпечує перевірку взаємодії між модулями, було проведено системне тестування всієї ECS системи в цілому. Для проведення системного тестування було створено спеціалізований код, що імітує роботу гри та охоплює основні функції системи. Тестовий код містив реалізацію ключових функціональностей ECS системи, зокрема створення, видалення, додавання та видалення компонентів, а також фільтрацію сутностей. Ці функції є основними для коректного функціонування ігрової системи та перевірялись на відповідність вимогам, що були висунуті до системи. 75 ЧДТУ 242323.010 ПЗ Створення сутностей: Тестувальник перевірив можливість створення сутностей в системі через відповідні методи та переконався, що кожна сутність може бути створена з базовими атрибутами. Під час тестування також перевірялося, що після створення сутності з нею можна виконувати різноманітні операції, такі як додавання та видалення компонентів. Видалення сутностей: Тестування видалення сутностей включає перевірку того, що при видаленні сутності всі її компоненти та дані також коректно видаляються без пошкодження даних інших сутностей в системі. Також була перевірена здатність системи зберігати цілісність даних, навіть якщо одночасно видаляються кілька сутностей. Додавання та видалення компонентів: Тестувальник перевіряв можливість динамічного додавання та видалення компонентів до сутностей без втрати існуючих даних. Були перевірені як прості компоненти, так і компоненти, які взаємодіють з іншими частинами системи (наприклад, компоненти, що відповідають за взаємодію з фізикою чи AI). Фільтрація сутностей: Також тестувалося коректне виконання фільтрації сутностей на основі наявних компонентів. Для цього було створено набір запитів на фільтрацію, які відбирали сутності за конкретними критеріями (наприклад, наявність компонента здоров’я або обраного предмета). Перевірялося, що система правильно обробляє запити та видає точні результати відповідно до запитуваного набору компонентів. Результати тестування: Усі тестовані функції пройшли перевірку на відповідність вимогам. Під час тестування не було виявлено критичних помилок, і система показала свою 76 ЧДТУ 242323.010 ПЗ здатність коректно працювати з усіма основними функціональними вимогами, зокрема з управлінням сутностями та їх компонентами, а також з фільтрацією сутностей. Системне тестування продемонструвало, що система функціонує згідно з технічними вимогами, і не було виявлено серйозних недоліків, які б могли вплинути на її стабільність чи продуктивність в реальних умовах. 4.2.4 Приймальне тестування Приймальне тестування програмного забезпечення ECS-системи було проведене з метою перевірки її готовності до використання в реальних умовах. Особлива увага приділялася перевірці функціональної відповідності системи поставленим вимогам, її стабільності та продуктивності в умовах моделювання ігрового середовища. Під час приймального тестування були перевірені основні аспекти роботи ECS-системи, включаючи управління сутностями, ефективність архетипної структури, роботу з подіями, взаємодію компонентів системи та швидкість обробки фільтрів. Усі ключові функції системи продемонстрували стабільну та коректну роботу. Зокрема, фільтрація сутностей за компонентами відбувалася швидко, міграція сутностей між архетипами виконувалася без втрати даних, а механізм обробки подій забезпечував правильну реакцію на внутрішньо ігрові зміни. Після завершення тестування було впроваджено ECS-систему в пілотний проект - гра жанру roguelike. У процесі тестування на етапі впровадження підтверджено відповідність системи потребам проекту. На рисунку 4.14 зображено частину структури гри в якій проводилось приймальне тестування. 77 ЧДТУ 242323.010 ПЗ Рис. 4.14 - Частина структури гри для приймального тестування На прикладі реалізації системи продемонстровано її можливості. Зокрема, через інтерфейс системи було виконано запити на отримання списку сутностей за фільтрами, успішно оброблено понад тисячу ігрових подій та продемонстровано динамічне створення і видалення сутностей у процесі гри. Дані, отримані за 78 ЧДТУ 242323.010 ПЗ допомогою інструментів налагодження, відображали коректну обробку всіх сценаріїв. Система отримала рекомендації до впровадження в інших проектах, зокрема в мобільних іграх з подібною архітектурою. Висновок після тестування свідчить про її придатність для масштабування та адаптації до різних умов, що дозволяє значно знизити витрати на розробку та оптимізацію ігрового процесу. 4.3 Приклади впровадження програмного комплексу 4.3.1 Розгортання системи Проект був розроблений як open source, що дозволяє будь-кому вносити зміни або додавати новий функціонал відповідно до своїх потреб. Завдяки такому підходу, система може бути адаптована до різних умов і проектів, що робить її універсальним інструментом для розробників. Всі зміни та оновлення коду публікуються у відкритому репозиторії на GitHub, що дає можливість спільноті активно брати участь у розвитку та покращенні фреймворку. Для того, щоб розгорнути систему та почати її використання в власному проекті, необхідно завантажити останню актуальну версію бібліотеки у вигляді DLL з репозиторію на GitHub. Після цього бібліотеку потрібно підключити до свого проекту. Інструкція по підключенню доступна в документації репозиторію та детально описує всі кроки, починаючи з підготовки середовища розробки до налаштування фреймворку. Далі, дотримуючись покрокових інструкцій у документації, розробники можуть почати створювати свої власні проекти на базі цього фреймворку. Вся необхідна інформація щодо конфігурації, налаштувань та прикладів використання надається для полегшення впровадження системи в різноманітні типи проектів. Документація також містить приклади коду для демонстрації основних функціональних можливостей фреймворку, що дозволяє швидко освоїти систему та ефективно інтегрувати її в існуючі проекти. Таким чином, для розгортання системи достатньо декількох простих кроків, що робить фреймворк легким у використанні та швидким у впровадженні, 79 ЧДТУ 242323.010 ПЗ дозволяючи розробникам зосередитись на реалізації специфічних бізнес-логік і функціоналу без необхідності створювати базову інфраструктуру з нуля. Висновок до четвертого розділу В результаті розробки та тестування програмного забезпечення можна зробити висновок, що розроблений фреймворк відповідає початковим вимогам та виконує потрібні функції, всі частини фреймворку були протестовані, а проблеми усунуті. 80 ЧДТУ 242323.010 ПЗ ЗАГАЛЬНІ ВИСНОВКИ Було проведено дослідження та розробка структурної оптимізації програмного забезпечення для мобільних ігор у жанрі Roguelike, зокрема на основі розробленого фреймворку ECS (Entity-Component-System). Система була спроектована для оптимізації обробки даних та взаємодії сутностей, що дозволяє значно покращити ефективність роботи додатків у реальному часі. Враховуючи тенденції розвитку індустрії та вимоги до швидкості обробки даних, розробка такого фреймворку є актуальною як для наукових, так і для практичних застосувань. Методи вирішення наукової задачі включали аналіз існуючих архітектурних підходів до розробки ECS систем, а також впровадження оптимізацій для зменшення часу доступу до даних та витрат пам’яті. На основі цих методів був створений власний фреймворк, який дозволяє ефективно управляти сутностями, компонентами та архетипами. Застосування цієї архітектури дозволило значно знизити навантаження на систему при обробці великої кількості сутностей, що важливо для мобільних платформ з обмеженими ресурсами. Рішення, запропоновані в роботі, перевищують традиційні підходи до роботи з даними в грі та надають гнучкість у розробці складних ігрових систем. Практичний аналіз показав, що розроблений фреймворк на основі ECS значно покращує продуктивність. Отримані результати підтвердили ефективність запропонованих рішень у порівнянні з іншими методами організації даних, такими як використання спарсетів чи збереження компонентів в архетипі напряму. Розроблений фреймворк може бути успішно використаний у мобільних іграх та інших реальних додатках, що вимагають високої ефективності обробки даних. Для подальших досліджень та вдосконалення системи можна рекомендувати реалізацію додаткових механізмів для автоматичного масштабування та адаптації до різних платформ. Це дозволить забезпечити більш гнучке та ефективне використання ресурсів в залежності від специфічних вимог і можливостей пристроїв, а також удосконалення механізмів для обробки подій у реальному часі. 81 ЧДТУ 242323.010 ПЗ Отже, була сформована гіпотеза та підтверджена теоретично і експериментально, на основі дослідження спроектовано, розроблено та протестовано програмне забезпечення у виді ECS фреймворку, що оптимізує структуру мобільних ігор жанру Roguelike. 82 ЧДТУ 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-types/s truct 10 Sander Mertens, Building an ECS #2: Archetypes and Vectorization - [Електронний ресурс] - Режим доступу: https://ajmmertens.medium.com/ building-an-ecs-2-archetypes-and-vectorization-fe21690805f9 11 BenchmarkDotNet - [Електронний ресурс] - Режим доступу: https://benchmarkdotnet.org/ 12 Entity Component System - [Електронний ресурс] - Режим доступу: https://en.wikipedia.org/wiki/Entity_component_system 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. – 83 ЧДТУ 242323.010 ПЗ 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-databases-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 с. 24 SH42913, Всё что нужно знать про ECS - [Електронний ресурс] - Режим доступу: https://habr.com/ru/articles/665276/ 84 ЧДТУ 242323.010 ПЗ 25 PatientZero, Шаблон проектирования Entity-Component-System — реализация и пример игры - [Електронний ресурс] - Режим доступу: https://habr.com/ru/articles/343778/ 26 John Terra, Entity Component System: An Introductory Guide - [Електронний ресурс] - Режим доступу: https://www.simplilearn.com/ entity-component-system-introductory-guide-article 27 AUSTIN MORLAN, A Simple Entity Component System (ECS) [C++] - [Електронний ресурс] - Режим доступу: https://austinmorlan.com /posts/entity_component_system/ 28 Sander Mertens, ECS: From Tool to Paradigm - [Електронний ресурс] - Режим доступу: https://ajmmertens.medium.com/ ecs-from-tool-to-paradigm-350587cdf216 29 Entity-Component-System - [Електронний ресурс] - Режим доступу: https://docs.3dverse.com/docs/core-concepts/entity-component-system 30 ECS concepts - [Електронний ресурс] - Режим доступу: https://docs.unity3d.com/Packages/[email protected]/manual/ecs_core.html 31 What data structure do I use to store Archetypes in ECS - [Електронний ресурс] - Режим доступу: https://gamedev.stackexchange.com /questions/192502/what-data-structure-do-i-use-to-store-archetypes-in-ecs 32 Hamad Rana, Gang of Four (GOF) Design Patterns - [Електронний ресурс] - Режим доступу: https://medium.com/@hamadrana23/ gang-of-four-gof-design-patterns-b07a7770a6e8s 33 ECS with sparse array notes (EnTT style) - [Електронний ресурс] - Режим доступу: https://gist.github.com/dakom/82551fff5d2b843cbe1601bbaff2acbf 34 Entity Component System - [Електронний ресурс] - Режим доступу: https://www.roguebasin.com/index.php/Entity_Component_System 35 Chapter 2 - Entities and Components - [Електронний ресурс] - Режим доступу: https://bfnightly.bracketproductions.com/chapter_2.html 36 Roguelike - [Електронний ресурс] - Режим доступу: https://en.wikipedia.org/wiki/Roguelike 85 ЧДТУ 242323.010 ПЗ 37 il2cpp - [Електронний ресурс] - Режим доступу: https:// il2cppdumper.com/il2cpp/il2cpp-internals-generic-sharing-implementation 38 il2cpp overview - [Електронний ресурс] - Режим доступу: https://docs.unity3d.com/6000.0/Documentation/Manual/scripting-backends-il2cp p.html 39 The .NET Compiler Platform SDK - [Електронний ресурс] - Режим доступу: https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/ 40 JacksonDunstan - [Електронний ресурс] - Режим доступу: https://www.jacksondunstan.com/articles 86 ДОДАТОК А ЗАТВЕРДЖЕНО: Зав. кафедри ПЗАС проф. Голуб С. В. ___________________________ СТРУКТУРНА ОПТИМІЗАЦІЯ ПРОГРАМНОГО ЗАБЕЗПЕЧЕННЯ МОБІЛЬНИХ ІГОР ЖАНРУ ROGUELIKE НА ОСНОВІ РУШІЯ UNITY Специфікація 482.ЧДТУ.242323 ПЗ Листів 1 Розробник ________________ Чуплий М. А. Керівник ________________ Олексюк В.В. Черкаси 2024 Позначення Найменування Примітка Документація 482.ЧДТУ.242323 12-01 Лістинг програми 482.ЧДТУ.242323 34-01 Інструкція користувача 482.ЧДТУ.242323 90-01 Графічні матеріали 88 ДОДАТОК Б СТРУКТУРНА ОПТИМІЗАЦІЯ ПРОГРАМНОГО ЗАБЕЗПЕЧЕННЯ МОБІЛЬНИХ ІГОР ЖАНРУ ROGUELIKE НА ОСНОВІ РУШІЯ UNITY Лістинг програми 482.ЧДТУ.242323 12-01 Листів 61 Розробник ________________ Чуплий М. А. Черкаси 2024 482.ЧДТУ.242323 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 #region Components internal IntHashMap ComponentsIdsLookup; internal Row[] ComponentRows; 90 482.ЧДТУ.242323 12-01 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]; 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]; 91 482.ЧДТУ.242323 12-01 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(); } ComponentRows = null; ComponentsIdsLookup = null; Config = null; CachedFilters = null; EntitiesData = null; Entities = null; AliveEntitesMask = default; RecycledEntitiesIndex = null; NextNewEntityId = 0; 92 482.ЧДТУ.242323 12-01 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; 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() { 93 482.ЧДТУ.242323 12-01 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]; 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; 94 482.ЧДТУ.242323 12-01 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"); } 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]; 95 482.ЧДТУ.242323 12-01 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]; 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]; 96 482.ЧДТУ.242323 12-01 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 { 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)] 97 482.ЧДТУ.242323 12-01 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>(); 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); 98 482.ЧДТУ.242323 12-01 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) 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); 99 482.ЧДТУ.242323 12-01 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]; 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); 100 482.ЧДТУ.242323 12-01 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); } [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); 101 482.ЧДТУ.242323 12-01 componentRow5.RemoveComponentInternal(offset5, entityId, ref entityData, ref archetype.EntitiesKeys); } [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) where T : struct, IComponentData { 102 482.ЧДТУ.242323 12-01 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!"); } DestroyEntityInternal(entityRef.Id); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ResolveComponentRow<T>(out Row<T> row) where T : struct, IComponentData 103 482.ЧДТУ.242323 12-01 { 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); 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; 104 482.ЧДТУ.242323 12-01 } [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; } [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; } 105 482.ЧДТУ.242323 12-01 [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; 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; 106 482.ЧДТУ.242323 12-01 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; 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) 107 482.ЧДТУ.242323 12-01 { 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); } } } 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; 108 482.ЧДТУ.242323 12-01 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; 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); 109 482.ЧДТУ.242323 12-01 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() { 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"); } 110 482.ЧДТУ.242323 12-01 ref var child = ref Childs.GetRef(childId); if (child.ParentId == parentId) { var relation = GetCurrentRelation(ref child, childId); relation.RemoveChild(child.ParentId, childId); 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; 111 482.ЧДТУ.242323 12-01 var relation = Relations[relationGroup.RelationId]; relation.RemoveChildsGroup(ref relationGroup); if (relation.FirstRelationId == -1) { 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); } 112 482.ЧДТУ.242323 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); } relation.Id = index; Relations[index] = relation; 113 482.ЧДТУ.242323 12-01 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) { var relationKey = ((long)parentArchetypeId << 32) | (uint)childArchetypeId; if (RelationsIdsLookup.TryGet(relationKey, out var relationIdx)) { return Relations[relationIdx]; 114 482.ЧДТУ.242323 12-01 } 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 using Unity.IL2CPP.CompilerServices; [Il2CppSetOption(Option.NullChecks, false)] [Il2CppSetOption(Option.ArrayBoundsChecks, false)] [Il2CppSetOption(Option.DivideByZeroChecks, false)] [Il2CppEagerStaticClassConstruction] #endif public sealed class Archetype { 115 482.ЧДТУ.242323 12-01 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); ComponentIdToOffset.Fill(0); } public int ComponentsCount { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ComponentsIds.Length; } public int NextKeyStep { [MethodImpl(MethodImplOptions.AggressiveInlining)] 116 482.ЧДТУ.242323 12-01 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); } [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)); } 117 482.ЧДТУ.242323 12-01 [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; 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; 118 482.ЧДТУ.242323 12-01 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]; } 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; 119 482.ЧДТУ.242323 12-01 } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void SetEmpty(int id, ref EntityArchetypeData archetypeData) { if (EntitiesKeys.Length <= id) { EntitiesKeys.Resize(EntitiesKeys.Length << 1); } 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)] 120 482.ЧДТУ.242323 12-01 internal void Reset() { for (var i = FiltersCount - 1; i >= 0; i--) { Filters[i].RemoveArchetype(Id); } 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; 121 482.ЧДТУ.242323 12-01 using System.Collections; 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 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]; 122 482.ЧДТУ.242323 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); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void TryAddArchetype(Archetype archetype) 123 482.ЧДТУ.242323 12-01 { 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); Archetypes[slotIndex] = null; } public void Dispose() { } 124 482.ЧДТУ.242323 12-01 [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; internal Archetype[] Archetypes; internal IntHashMap.Enumerator MapEnumerator; public Archetype Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Archetypes[MapEnumerator.Current]; } 125 482.ЧДТУ.242323 12-01 [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 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; 126 482.ЧДТУ.242323 12-01 [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; 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; 127 482.ЧДТУ.242323 12-01 } 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); 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; 128 482.ЧДТУ.242323 12-01 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(); } 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) 129 482.ЧДТУ.242323 12-01 { 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); 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)] 130 482.ЧДТУ.242323 12-01 internal T[] GetData() { return Data; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal ref T AddComponentInternal(int offset, int entityId, 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) 131 482.ЧДТУ.242323 12-01 { if (Id != removedComponents[i]) continue; 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]; migration.AddedComponentsValues -= 2; 132 482.ЧДТУ.242323 12-01 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--; id = RecycledData[RecycledCount]; DataCount++; } else 133 482.ЧДТУ.242323 12-01 { 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)] private void ResizeRecycle() { Array.Resize(ref RecycledData, RecycledData.Length << 1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ResizeData() 134 482.ЧДТУ.242323 12-01 { 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) { 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"); 135 482.ЧДТУ.242323 12-01 } 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 [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) { 136 482.ЧДТУ.242323 12-01 _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 { #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); 137 482.ЧДТУ.242323 12-01 } 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); } 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; 138 482.ЧДТУ.242323 12-01 } } 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) { 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 139 482.ЧДТУ.242323 12-01 return Static.Worlds[entityRef.WorldId]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AddChild(this in EntityRef entityRef, in EntityRef child) { entityRef.GetWorld().AddChild(entityRef, child); } [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 140 482.ЧДТУ.242323 12-01 { #if ENABLE_IL2CPP using Unity.IL2CPP.CompilerServices; [Il2CppSetOption(Option.NullChecks, false)] [Il2CppSetOption(Option.ArrayBoundsChecks, false)] [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; 141 482.ЧДТУ.242323 12-01 _nullArchetype.Reset(); _nullArchetype.Dispose(); _nullArchetype.EntityKeySize = 1; _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]; } 142 482.ЧДТУ.242323 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; } var entityArchetypeData = _world.EntitiesData; 143 482.ЧДТУ.242323 12-01 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; } } for (var j = migrationData.RemovedComponentsCount - 1; j >= 0; j--) { if (_default.RemovedComponents[j] != removedComponents[j]) { 144 482.ЧДТУ.242323 12-01 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--; _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) 145 482.ЧДТУ.242323 12-01 { 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)] 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; 146 482.ЧДТУ.242323 12-01 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); } #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 147 482.ЧДТУ.242323 12-01 { [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; } [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) 148 482.ЧДТУ.242323 12-01 { ResizeRemovedComponents(); } RemovedComponents[RemovedComponentsCount] = componentId; RemovedComponentsCount++; Hash ^= hash; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void ResizeAddedComponents() { Array.Resize(ref AddedComponents, AddedComponents.Length << 1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void ResizeRemovedComponents() { Array.Resize(ref RemovedComponents, RemovedComponents.Length << 1); } } } } 149 ДОДАТОК В СТРУКТУРНА ОПТИМІЗАЦІЯ ПРОГРАМНОГО ЗАБЕЗПЕЧЕННЯ МОБІЛЬНИХ ІГОР ЖАНРУ ROGUELIKE НА ОСНОВІ РУШІЯ UNITY Інструкція користувача 482.ЧДТУ.242323 34-01 Листів 1 Розробник ________________ Чуплий М. А. Черкаси 2024 482.ЧДТУ.242323 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, що ввімкне її та додасть до виконання в ігровому циклі. 151 ДОДАТОК Г ПРОГРАМНЕ ЗАБЕЗПЕЧЕННЯ КООПЕРАТИВНОГО ІГРОВОГО МОБІЛЬНОГО ДОДАТКУ НА ОСНОВІ UNITY Графічні матеріали 482.ЧДТУ.242323 90-01 Листів 10 Розробник ________________ Чуплий М. А. Черкаси 2024 482.ЧДТУ.242323 90-01 Рис. Г.1 - Слайд вступний Рис. Г.2 - Слайд вступу 153 482.ЧДТУ.242323 90-01 Рис. Г.3 - Слайд вимог Рис. Г.4 - Слайд формування гіпотези 154 482.ЧДТУ.242323 90-01 Рис. Г.5 - Слайд гіпотези Рис. Г.6 - Слайд експериментальних досліджень 155 482.ЧДТУ.242323 90-01 Рис. Г.7 - Слайд експериментальні дослідження 2 Рис. Г.8 - Слайд діаграма прецедентів 156 482.ЧДТУ.242323 90-01 Рис. Г.9 - Слайд логічної структури Рис. Г.10 - Слайд діаграма пакетів 157 482.ЧДТУ.242323 90-01 Рис. Г.11 - Слайд діаграма прецедентів Рис. Г.12 - Слайд діаграма діяльності 158 482.ЧДТУ.242323 90-01 Рис. Г.13 - Слайд діаграма послідовності Рис. Г.14 - Слайд діаграма комунікації 159 482.ЧДТУ.242323 90-01 Рис. Г.15 - Слайд діаграма станів Рис. Г.16 - Слайд функціональна схема 160 482.ЧДТУ.242323 90-01 Рис. Г.17 - Слайд логічна схема Рис. Г.18 - Слайд програмний интерфейс 161 482.ЧДТУ.242323 90-01 Рис. Г.19 - Слайд тестування Рис. Г.20 - Слайд кінцевий 162