├── .gitignore ├── LICENSE.md ├── LICENSE.md.meta ├── Leopotam.EcsLite.asmdef ├── Leopotam.EcsLite.asmdef.meta ├── README.md ├── README.md.meta ├── package.json ├── package.json.meta ├── src.meta └── src ├── components.cs ├── components.cs.meta ├── entities.cs ├── entities.cs.meta ├── filters.cs ├── filters.cs.meta ├── systems.cs ├── systems.cs.meta ├── worlds.cs └── worlds.cs.meta /.gitignore: -------------------------------------------------------------------------------- 1 | *.sln 2 | .vscode 3 | .idea 4 | .DS_Store 5 | bin 6 | obj 7 | Library 8 | Temp 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2025 leopotam@yandex.ru 2 | 3 | Данное программное обеспечение и сопутствующая документация (далее - Продукт) 4 | выпускается под MIT-Red лицензией. 5 | 6 | MIT-Red регулируется совокупностью следующих правил, если хотя бы 7 | одно из них невыполнимо - использование Продукта запрещено: 8 | 9 | Если вы за применение opensource программного обеспечения в 10 | военной сфере - вы не можете использовать этот Продукт. 11 | 12 | Если вы испытываете ненависть к русским или поддерживаете 13 | любые нападки на них - вы не можете использовать этот Продукт. 14 | 15 | Данная лицензия разрешает лицам, получившим копию данного Продукта, 16 | безвозмездно использовать Программное обеспечение без ограничений, включая 17 | неограниченное право на использование, копирование, изменение, слияние, 18 | публикацию и распространение копий Продукта. 19 | 20 | Указанное выше уведомление об авторском праве и данные условия должны быть 21 | включены во все копии или значимые части данного Продукта. 22 | 23 | ДАННЫЙ ПРОДУКТ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНО 24 | ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ ГАРАНТИИ ТОВАРНОЙ ПРИГОДНОСТИ, 25 | СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ, НО 26 | НЕ ОГРАНИЧИВАЯСЬ ИМИ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ 27 | ОТВЕТСТВЕННОСТИ ПО КАКИМ-ЛИБО ИСКАМ, ЗА УЩЕРБ ИЛИ ПО ИНЫМ ТРЕБОВАНИЯМ, 28 | В ТОМ ЧИСЛЕ, ПРИ ДЕЙСТВИИ КОНТРАКТА, ДЕЛИКТЕ ИЛИ ИНОЙ СИТУАЦИИ, ВОЗНИКШИМ 29 | ИЗ-ЗА ИСПОЛЬЗОВАНИЯ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫХ ДЕЙСТВИЙ С ПРОДУКТОМ. -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f123f63dd77c407d8383719e587f37f9 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Leopotam.EcsLite.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Leopotam.EcsLite", 3 | "rootNamespace": "Leopotam.EcsLite", 4 | "references": [], 5 | "includePlatforms": [], 6 | "excludePlatforms": [], 7 | "allowUnsafeCode": false, 8 | "overrideReferences": false, 9 | "precompiledReferences": [], 10 | "autoReferenced": true, 11 | "defineConstraints": [], 12 | "versionDefines": [], 13 | "noEngineReferences": false 14 | } -------------------------------------------------------------------------------- /Leopotam.EcsLite.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e59bf9fc7d5a44b18a819a92edd46163 3 | timeCreated: 1595884462 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LeoEcsLite - Легковесный C# Entity Component System фреймворк 2 | Производительность, нулевые или минимальные аллокации, минимизация использования памяти, отсутствие зависимостей от любого игрового движка - это основные цели данного фреймворка. 3 | 4 | > **ВАЖНО!** АКТИВНАЯ РАЗРАБОТКА ПРЕКРАЩЕНА, ВОЗМОЖНО ТОЛЬКО ИСПРАВЛЕНИЕ ОБНАРУЖЕННЫХ ОШИБОК. СОСТОЯНИЕ СТАБИЛЬНОЕ, ИЗВЕСТНЫХ ОШИБОК НЕ ОБНАРУЖЕНО. ЗА НОВЫМ ПОКОЛЕНИЕМ ФРЕЙМВОРКА СТОИТ СЛЕДИТЬ В БЛОГЕ https://leopotam.ru/ 5 | 6 | > **ВАЖНО!** ПОДДЕРЖКА БУДЕТ ПРЕКРАЩЕНА И РЕПОЗИТОРИЙ БУДЕТ ОТПРАВЛЕН В АРХИВ 22.04.2026. 7 | 8 | > **ВАЖНО!** Не забывайте использовать `DEBUG`-версии билдов для разработки и `RELEASE`-версии билдов для релизов: все внутренние проверки/исключения будут работать только в `DEBUG`-версиях и удалены для увеличения производительности в `RELEASE`-версиях. 9 | 10 | > **ВАЖНО!** LeoEcsLite-фрейморк **не потокобезопасен** и никогда не будет таким! Если вам нужна многопоточность - вы должны реализовать ее самостоятельно и интегрировать синхронизацию в виде ecs-системы. 11 | 12 | # Содержание 13 | * [Социальные ресурсы](#Социальные-ресурсы) 14 | * [Установка](#Установка) 15 | * [В виде unity модуля](#В-виде-unity-модуля) 16 | * [В виде исходников](#В-виде-исходников) 17 | * [Прочие источники](#Прочие-источники) 18 | * [Основные типы](#Основные-типы) 19 | * [Сущность](#Сущность) 20 | * [Компонент](#Компонент) 21 | * [Система](#Система) 22 | * [Совместное использование данных](#Совместное-использование-данных) 23 | * [Специальные типы](#Специальные-типы) 24 | * [EcsPool](#EcsPool) 25 | * [EcsFilter](#EcsFilter) 26 | * [EcsWorld](#EcsWorld) 27 | * [EcsSystems](#EcsSystems) 28 | * [Интеграция с движками](#Интеграция-с-движками) 29 | * [Unity](#Unity) 30 | * [Кастомный движок](#Кастомный-движок) 31 | * [Статьи](#Статьи) 32 | * [Проекты на LeoECS Lite](#Проекты-на-LeoECS-Lite) 33 | * [С исходниками](#С-исходниками) 34 | * [Без исходников](#Без-исходников) 35 | * [Расширения](#Расширения) 36 | * [Лицензия](#Лицензия) 37 | * [ЧаВо](#ЧаВо) 38 | 39 | # Социальные ресурсы 40 | [Блог разработчика](https://leopotam.ru/) 41 | 42 | # Установка 43 | 44 | ## В виде unity модуля 45 | Поддерживается установка в виде unity-модуля через git-ссылку в PackageManager или прямое редактирование `Packages/manifest.json`: 46 | ``` 47 | "com.leopotam.ecslite": "https://github.com/Leopotam/ecslite.git", 48 | ``` 49 | По умолчанию используется последняя релизная версия. Если требуется версия "в разработке" с актуальными изменениями - следует переключиться на ветку `develop`: 50 | ``` 51 | "com.leopotam.ecslite": "https://github.com/Leopotam/ecslite.git#develop", 52 | ``` 53 | 54 | ## В виде исходников 55 | Код так же может быть склонирован или получен в виде архива со страницы релизов. 56 | 57 | ## Прочие источники 58 | Официальная работоспособная версия размещена по адресу [https://github.com/Leopotam/ecslite](https://github.com/Leopotam/ecslite), все остальные версии (включая *nuget*, *npm* и прочие репозитории) являются неофициальными клонами или сторонним кодом с неизвестным содержимым. 59 | 60 | > **ВАЖНО!** Использование этих источников не рекомендуется, только на свой страх и риск. 61 | 62 | # Основные типы 63 | 64 | ## Сущность 65 | Сама по себе ничего не значит и не существует, является исключительно контейнером для компонентов. Реализована как `int`: 66 | ```c# 67 | // Создаем новую сущность в мире. 68 | int entity = _world.NewEntity (); 69 | 70 | // Любая сущность может быть удалена, при этом сначала все компоненты будут автоматически удалены и только потом энтити будет считаться уничтоженной. 71 | world.DelEntity (entity); 72 | 73 | // Компоненты с любой сущности могут быть скопированы на другую. Если исходная или целевая сущность не существует - будет брошено исключение в DEBUG-версии. 74 | world.CopyEntity (srcEntity, dstEntity); 75 | ``` 76 | 77 | > **ВАЖНО!** Сущности не могут существовать без компонентов и будут автоматически уничтожаться при удалении последнего компонента на них. 78 | 79 | ## Компонент 80 | Является контейнером для данных пользователя и не должен содержать логику (допускаются минимальные хелперы, но не куски основной логики): 81 | ```c# 82 | struct Component1 { 83 | public int Id; 84 | public string Name; 85 | } 86 | ``` 87 | Компоненты могут быть добавлены, запрошены или удалены через [компонентные пулы](#ecspool). 88 | 89 | ## Система 90 | Является контейнером для основной логики для обработки отфильтрованных сущностей. Существует в виде пользовательского класса, реализующего как минимум один из `IEcsInitSystem`, `IEcsDestroySystem`, `IEcsRunSystem` (и прочих поддерживаемых) интерфейсов: 91 | ```c# 92 | class UserSystem : IEcsPreInitSystem, IEcsInitSystem, IEcsRunSystem, IEcsPostRunSystem, IEcsDestroySystem, IEcsPostDestroySystem { 93 | public void PreInit (IEcsSystems systems) { 94 | // Будет вызван один раз в момент работы IEcsSystems.Init() и до срабатывания IEcsInitSystem.Init() у всех систем. 95 | } 96 | 97 | public void Init (IEcsSystems systems) { 98 | // Будет вызван один раз в момент работы IEcsSystems.Init() и после срабатывания IEcsPreInitSystem.PreInit() у всех систем. 99 | } 100 | 101 | public void Run (IEcsSystems systems) { 102 | // Будет вызван один раз в момент работы IEcsSystems.Run(). 103 | } 104 | 105 | public void PostRun (IEcsSystems systems) { 106 | // Будет вызван один раз в момент работы IEcsSystems.Run() после срабатывания IEcsRunSystem.Run() у всех систем. 107 | } 108 | 109 | public void Destroy (IEcsSystems systems) { 110 | // Будет вызван один раз в момент работы IEcsSystems.Destroy() и до срабатывания IEcsPostDestroySystem.PostDestroy() у всех систем. 111 | } 112 | 113 | public void PostDestroy (IEcsSystems systems) { 114 | // Будет вызван один раз в момент работы IEcsSystems.Destroy() и после срабатывания IEcsDestroySystem.Destroy() у всех систем. 115 | } 116 | } 117 | ``` 118 | 119 | # Совместное использование данных 120 | Экземпляр любого кастомного типа (класса) может быть одновременно подключен ко всем системам: 121 | ```c# 122 | class SharedData { 123 | public string PrefabsPath; 124 | } 125 | ... 126 | SharedData sharedData = new SharedData { PrefabsPath = "Items/{0}" }; 127 | IEcsSystems systems = new EcsSystems (world, sharedData); 128 | systems 129 | .Add (new TestSystem1 ()) 130 | .Init (); 131 | ... 132 | class TestSystem1 : IEcsInitSystem { 133 | public void Init(IEcsSystems systems) { 134 | SharedData shared = systems.GetShared (); 135 | string prefabPath = string.Format (shared.PrefabsPath, 123); 136 | // prefabPath = "Items/123" к этому моменту. 137 | } 138 | } 139 | ``` 140 | 141 | # Специальные типы 142 | 143 | ## EcsPool 144 | Является контейнером для компонентов, предоставляет апи для добавления / запроса / удаления компонентов на сущности: 145 | ```c# 146 | int entity = world.NewEntity (); 147 | EcsPool pool = world.GetPool (); 148 | 149 | // Add() добавляет компонент к сущности. Если компонент уже существует - будет брошено исключение в DEBUG-версии. 150 | ref Component1 c1 = ref pool.Add (entity); 151 | 152 | // Has() проверяет наличие компонента на сущности. 153 | bool c1Exists = pool.Has (entity); 154 | 155 | // Get() возвращает существующий на сущности компонент. Если компонент не существует - будет брошено исключение в DEBUG-версии. 156 | ref Component1 c1 = ref pool.Get (entity); 157 | 158 | // Del() удаляет компонент с сущности. Если компонента не было - никаких ошибок не будет. Если это был последний компонент - сущность будет удалена автоматически. 159 | pool.Del (entity); 160 | 161 | // Copy() выполняет копирование всех компонентов с одной сущности на другую. Если исходная или целевая сущность не существует - будет брошено исключение в DEBUG-версии. 162 | pool.Copy (srcEntity, dstEntity); 163 | 164 | ``` 165 | 166 | > **ВАЖНО!** После удаления, компонент будет помещен в пул для последующего переиспользования. Все поля компонента будут сброшены в значения по умолчанию автоматически. 167 | 168 | ## EcsFilter 169 | Является контейнером для хранения отфильтрованных сущностей по наличию или отсутствию определенных компонентов: 170 | ```c# 171 | class WeaponSystem : IEcsInitSystem, IEcsRunSystem { 172 | EcsFilter _filter; 173 | EcsPool _weapons; 174 | 175 | public void Init (IEcsSystems systems) { 176 | // Получаем экземпляр мира по умолчанию. 177 | EcsWorld world = systems.GetWorld (); 178 | 179 | // Мы хотим получить все сущности с компонентом "Weapon" и без компонента "Health". 180 | // Фильтр хранит только сущности, сами даные лежат в пуле компонентов "Weapon". 181 | // Фильтр может собираться динамически каждый раз, но рекомендуется кеширование. 182 | _filter = world.Filter ().Exc ().End (); 183 | 184 | // Запросим и закешируем пул компонентов "Weapon". 185 | _weapons = world.GetPool (); 186 | 187 | // Создаем новую сущность для теста. 188 | int entity = world.NewEntity (); 189 | 190 | // И добавляем к ней компонент "Weapon" - эта сущность должна попасть в фильтр. 191 | _weapons.Add (entity); 192 | } 193 | 194 | public void Run (IEcsSystems systems) { 195 | foreach (int entity in filter) { 196 | ref Weapon weapon = ref _weapons.Get (entity); 197 | weapon.Ammo = System.Math.Max (0, weapon.Ammo - 1); 198 | } 199 | } 200 | } 201 | ``` 202 | 203 | > **ВАЖНО!** Фильтр достаточно собрать один раз и закешировать, пересборка для обновления списка сущностей не нужна. 204 | 205 | Дополнительные требования к отфильтровываемым сущностям могут быть добавлены через методы `Inc<>()` / `Exc<>()`. 206 | 207 | > **ВАЖНО!** Фильтры поддерживают любое количество требований к компонентам, но один и тот же компонент не может быть в списках "include" и "exclude". 208 | 209 | ## EcsWorld 210 | Является контейнером для всех сущностей, компонентых пулов и фильтров, данные каждого экземпляра уникальны и изолированы от других миров. 211 | 212 | > **ВАЖНО!** Необходимо вызывать `EcsWorld.Destroy()` у экземпляра мира если он больше не нужен. 213 | 214 | ## EcsSystems 215 | Является контейнером для систем, которыми будет обрабатываться `EcsWorld`-экземпляр мира: 216 | ```c# 217 | class Startup : MonoBehaviour { 218 | EcsWorld _world; 219 | IEcsSystems _systems; 220 | 221 | void Start () { 222 | // Создаем окружение, подключаем системы. 223 | _world = new EcsWorld (); 224 | _systems = new EcsSystems (_world); 225 | _systems 226 | .Add (new WeaponSystem ()) 227 | .Init (); 228 | } 229 | 230 | void Update () { 231 | // Выполняем все подключенные системы. 232 | _systems?.Run (); 233 | } 234 | 235 | void OnDestroy () { 236 | // Уничтожаем подключенные системы. 237 | if (_systems != null) { 238 | _systems.Destroy (); 239 | _systems = null; 240 | } 241 | // Очищаем окружение. 242 | if (_world != null) { 243 | _world.Destroy (); 244 | _world = null; 245 | } 246 | } 247 | } 248 | ``` 249 | 250 | > **ВАЖНО!** Необходимо вызывать `IEcsSystems.Destroy()` у экземпляра группы систем если он больше не нужен. 251 | 252 | # Интеграция с движками 253 | 254 | ## Unity 255 | > Проверено на Unity 2020.3 (не зависит от нее) и содержит asmdef-описания для компиляции в виде отдельных сборок и уменьшения времени рекомпиляции основного проекта. 256 | 257 | [Интеграция в Unity editor](https://github.com/Leopotam/ecslite-unityeditor) содержит шаблоны кода, а так же предоставляет мониторинг состояния мира. 258 | 259 | ## Кастомный движок 260 | > Для использования фреймворка требуется C#7.3 или выше. 261 | 262 | Каждая часть примера ниже должна быть корректно интегрирована в правильное место выполнения кода движком: 263 | ```c# 264 | using Leopotam.EcsLite; 265 | 266 | class EcsStartup { 267 | EcsWorld _world; 268 | IEcsSystems _systems; 269 | 270 | // Инициализация окружения. 271 | void Init () { 272 | _world = new EcsWorld (); 273 | _systems = new EcsSystems (_world); 274 | _systems 275 | // Дополнительные экземпляры миров 276 | // должны быть зарегистрированы здесь. 277 | // .AddWorld (customWorldInstance, "events") 278 | 279 | // Системы с основной логикой должны 280 | // быть зарегистрированы здесь. 281 | // .Add (new TestSystem1 ()) 282 | // .Add (new TestSystem2 ()) 283 | 284 | .Init (); 285 | } 286 | 287 | // Метод должен быть вызван из 288 | // основного update-цикла движка. 289 | void UpdateLoop () { 290 | _systems?.Run (); 291 | } 292 | 293 | // Очистка окружения. 294 | void Destroy () { 295 | if (_systems != null) { 296 | _systems.Destroy (); 297 | _systems = null; 298 | } 299 | if (_world != null) { 300 | _world.Destroy (); 301 | _world = null; 302 | } 303 | } 304 | } 305 | ``` 306 | 307 | # Статьи 308 | 309 | * ["Создание dungeon crawler'а с LeoECS Lite. Часть 1"](https://habr.com/ru/post/661085/) 310 | 311 | [![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/372/b1c/ad3/372b1cad308788dac56f8db1ea16b9c9.png)](https://habr.com/ru/post/661085/) 312 | 313 | * ["Создание dungeon crawler'а с LeoECS Lite. Часть 2"](https://habr.com/ru/post/673926/) 314 | 315 | [![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/63f/3ef/c47/63f3efc473664fdaaf1a249f258e2486.png)](https://habr.com/ru/post/673926/) 316 | 317 | # Проекты на LeoECS Lite 318 | ## С исходниками 319 | 320 | * ["SlimeHunter"](https://github.com/JimboA/SlimeHunter-LeoEcsLite) 321 | 322 | [![](https://media.githubusercontent.com/media/JimboA/SlimeHunter-LeoEcsLite/main/Screenshot_1.png)](https://github.com/JimboA/SlimeHunter-LeoEcsLite) 323 | 324 | 325 | * ["3D Platformer"](https://github.com/supremestranger/3D-Platformer-Lite) 326 | 327 | [![](https://camo.githubusercontent.com/dcd2f525130d73f4688c1f1cfb12f6e37d166dae23a1c6fac70e5b7873c3ab21/68747470733a2f2f692e6962622e636f2f686d374c726d342f506c6174666f726d65722e706e67)](https://github.com/supremestranger/3D-Platformer-Lite) 328 | 329 | 330 | * ["SharpPhysics2D"](https://github.com/7Bpencil/sharpPhysics) 331 | 332 | [![](https://github.com/7Bpencil/sharpPhysics/raw/master/pictures/preview.png)](https://github.com/7Bpencil/sharpPhysics) 333 | 334 | ## Без исходников 335 | * ["Супер Био-Мужик" (STEAM)](https://store.steampowered.com/app/2144580/Super_BioMan/) 336 | 337 | [![](https://cdn.akamai.steamstatic.com/steam/apps/2144580/header.jpg)](https://cdn.akamai.steamstatic.com/steam/apps/2144580/header.jpg) 338 | 339 | 340 | * ["Microbiome" (Перенесен на LeoECS Proto)](https://vk.com/microbiomegame) 341 | 342 | [![](https://img.youtube.com/vi/WTciasBN2eQ/0.jpg)](https://www.youtube.com/watch?v=WTciasBN2eQ) 343 | 344 | # Расширения 345 | * [Инъекция зависимостей](https://github.com/Leopotam/ecslite-di) 346 | * [Расширенные системы](https://github.com/Leopotam/ecslite-extendedsystems) 347 | * [Поддержка многопоточности](https://github.com/Leopotam/ecslite-threads) 348 | * [Интеграция в редактор Unity](https://github.com/Leopotam/ecslite-unityeditor) 349 | * [Поддержка Unity uGui](https://github.com/Leopotam/ecslite-unity-ugui) 350 | * [Unity Physx events support](https://github.com/supremestranger/leoecs-lite-physics) 351 | * [Multiple Shared injection](https://github.com/GoodCatGames/ecslite-multiple-shared) 352 | * [EasyEvents](https://github.com/7Bpencil/ecslite-easyevents) 353 | * [Entity command buffer](https://github.com/JimboA/EcsLiteEntityCommandBuffer) 354 | * [Интеграция в редактор Unity на базе UIToolkit](https://github.com/Mitfart/LeoECSLite.UnityIntegration) 355 | * [Unity Entity Converter](https://github.com/AndreyBirchenko/LeoEcsLiteEntityConverter) 356 | * [Interval Systems](https://github.com/nenuacho/ecslite-interval-systems) 357 | * [Quadtree Systems](https://github.com/nenuacho/ecslite-quadtree) 358 | * [LeoECS Lite Unity Zoo](https://github.com/aleverdes/leoecslite-zoo) 359 | * [Adding/removing components debugger for LeoECS Lite](https://github.com/supremestranger/LiteEzDebuggerModule) 360 | 361 | # Лицензия 362 | Пакет выпускается под [MIT-Red лицензией](./LICENSE.md). 363 | 364 | В случаях лицензирования по условиям MIT-Red не стоит расчитывать на 365 | персональные консультации или какие-либо гарантии. 366 | 367 | # ЧаВо 368 | 369 | ### В чем отличие от старой версии LeoECS? 370 | 371 | Я предпочитаю называть их `лайт` (ecs-lite) и `классика` (leoecs). Основные отличия `лайта` следующие: 372 | * Кодовая база фреймворка уменьшилась в 2 раза, ее стало проще поддерживать и расширять. 373 | * `Лайт` не является порезанной версией `классики`, весь функционал сохранен в виде ядра и внешних модулей. 374 | * Отсутствие каких-либо статичных данных в ядре. 375 | * Отсутствие кешей компонентов в фильтрах, это уменьшает потребление памяти и увеличивает скорость перекладывания сущностей по фильтрам. 376 | * Быстрый доступ к любому компоненту на любой сущности (а не только отфильтрованной и через кеш фильтра). 377 | * Нет ограничений на количество требований/ограничений к компонентам для фильтров. 378 | * Общая линейная производительность близка к `классике`, но доступ к компонентам, перекладывание сущностей по фильтрам стал несоизмеримо быстрее. 379 | * Прицел на использование мультимиров - нескольких экземпляров миров одновременно с разделением по ним данных для оптимизации потребления памяти. 380 | * Отсутствие рефлексии в ядре, возможно использование агрессивного вырезания неиспользуемого кода компилятором (code stripping, dead code elimination). 381 | * Совместное использование общих данных между системами происходит без рефлексии (если она допускается, то рекомендуется использовать расширение `ecslite-di` из списка расширений). 382 | * Реализация сущностей вернулась к обычныму типу `int`, это сократило потребление памяти. Если сущности нужно сохранять где-то - их по-прежнему нужно упаковывать в специальную структуру. 383 | * Маленькое ядро, весь дополнительный функционал реализуется через подключение опциональных расширений. 384 | * Весь новый функционал будет выходить только к `лайт`-версии, `классика` переведена в режим поддержки на исправление ошибок. 385 | 386 | ### Я хочу одну систему вызвать в `MonoBehaviour.Update()`, а другую - в `MonoBehaviour.FixedUpdate()`. Как я могу это сделать? 387 | 388 | Для разделения систем на основе разных методов из `MonoBehaviour` необходимо создать под каждый метод отдельную `IEcsSystems`-группу: 389 | ```c# 390 | IEcsSystems _update; 391 | IEcsSystems _fixedUpdate; 392 | 393 | void Start () { 394 | EcsWorld world = new EcsWorld (); 395 | _update = new EcsSystems (world); 396 | _update 397 | .Add (new UpdateSystem ()) 398 | .Init (); 399 | _fixedUpdate = new EcsSystems (world); 400 | _fixedUpdate 401 | .Add (new FixedUpdateSystem ()) 402 | .Init (); 403 | } 404 | 405 | void Update () { 406 | _update?.Run (); 407 | } 408 | 409 | void FixedUpdate () { 410 | _fixedUpdate?.Run (); 411 | } 412 | ``` 413 | 414 | ### Меня не устраивают значения по умолчанию для полей компонентов. Как я могу это настроить? 415 | 416 | Компоненты поддерживают установку произвольных значений через реализацию интерфейса `IEcsAutoReset<>`: 417 | ```c# 418 | struct MyComponent : IEcsAutoReset { 419 | public int Id; 420 | public object SomeExternalData; 421 | 422 | public void AutoReset (ref MyComponent c) { 423 | c.Id = 2; 424 | c.SomeExternalData = null; 425 | } 426 | } 427 | ``` 428 | Этот метод будет автоматически вызываться для всех новых компонентов, а так же для всех только что удаленных, до помещения их в пул. 429 | > **ВАЖНО!** В случае применения `IEcsAutoReset` все дополнительные очистки/проверки полей компонента отключаются, что может привести к утечкам памяти. Ответственность лежит на пользователе! 430 | 431 | ### Меня не устраивают значения для полей компонентов при их копировании через EcsWorld.CopyEntity() или Pool<>.Copy(). Как я могу это настроить? 432 | 433 | Компоненты поддерживают установку произвольных значений при вызове `EcsWorld.CopyEntity()` или `EcsPool<>.Copy()` через реализацию интерфейса `IEcsAutoCopy<>`: 434 | ```c# 435 | struct MyComponent : IEcsAutoCopy { 436 | public int Id; 437 | 438 | public void AutoCopy (ref MyComponent src, ref MyComponent dst) { 439 | dst.Id = src.Id * 123; 440 | } 441 | } 442 | ``` 443 | > **ВАЖНО!** В случае применения `IEcsAutoCopy` никакого копирования по умолчанию не происходит. Ответственность за корректность заполнения данных и за целостность исходных лежит на пользователе! 444 | 445 | ### Я хочу сохранить ссылку на сущность в компоненте. Как я могу это сделать? 446 | 447 | Для сохранения ссылки на сущность ее необходимо упаковать в один из специальных контейнеров (`EcsPackedEntity` или `EcsPackedEntityWithWorld`): 448 | ```c# 449 | EcsWorld world = new EcsWorld (); 450 | int entity = world.NewEntity (); 451 | EcsPackedEntity packed = world.PackEntity (entity); 452 | EcsPackedEntityWithWorld packedWithWorld = world.PackEntityWithWorld (entity); 453 | ... 454 | // В момент распаковки мы проверяем - жива эта сущность или уже нет. 455 | if (packed.Unpack (world, out int unpacked)) { 456 | // "unpacked" является валидной сущностью и мы можем ее использовать. 457 | } 458 | 459 | // В момент распаковки мы проверяем - жива эта сущность или уже нет. 460 | if (packedWithWorld.Unpack (out EcsWorld unpackedWorld, out int unpackedWithWorld)) { 461 | // "unpackedWithWorld" является валидной сущностью и мы можем ее использовать. 462 | } 463 | ``` 464 | 465 | ### Я хочу добавить реактивности и обрабатывать события изменений в мире самостоятельно. Как я могу сделать это? 466 | 467 | > **ВАЖНО!** Так делать не рекомендуется из-за падения производительности. 468 | 469 | Для активации этого функционала следует добавить `LEOECSLITE_WORLD_EVENTS` в список директив комплятора, а затем - добавить слушатель событий: 470 | 471 | ```c# 472 | class TestWorldEventListener : IEcsWorldEventListener { 473 | public void OnEntityCreated (int entity) { 474 | // Сущность создана - метод будет вызван в момент вызова world.NewEntity(). 475 | } 476 | 477 | public void OnEntityChanged (int entity) { 478 | // Сущность изменена - метод будет вызван в момент вызова pool.Add() / pool.Del(). 479 | } 480 | 481 | public void OnEntityDestroyed (int entity) { 482 | // Сущность уничтожена - метод будет вызван в момент вызова world.DelEntity() или в момент удаления последнего компонента. 483 | } 484 | 485 | public void OnFilterCreated (EcsFilter filter) { 486 | // Фильтр создан - метод будет вызван в момент вызова world.Filter().End(), если фильтр не существовал ранее. 487 | } 488 | 489 | public void OnWorldResized (int newSize) { 490 | // Мир изменил размеры - метод будет вызван в случае изменения размеров кешей под сущности в момент вызова world.NewEntity(). 491 | } 492 | 493 | public void OnWorldDestroyed (EcsWorld world) { 494 | // Мир уничтожен - метод будет вызван в момент вызова world.Destroy(). 495 | } 496 | } 497 | ... 498 | var world = new EcsWorld (); 499 | var listener = new TestWorldEventListener (); 500 | world.AddEventListener (listener); 501 | ``` 502 | 503 | ### Я хочу добавить реактивщины и обрабатывать события изменения фильтров. Как я могу это сделать? 504 | 505 | > **ВАЖНО!** Так делать не рекомендуется из-за падения производительности. 506 | 507 | Для активации этого функционала следует добавить `LEOECSLITE_FILTER_EVENTS` в список директив комплятора, а затем - добавить слушатель событий: 508 | 509 | ```c# 510 | class TestFilterEventListener : IEcsFilterEventListener { 511 | public void OnEntityAdded (int entity) { 512 | // Сущность добавлена в фильтр. 513 | } 514 | 515 | public void OnEntityRemoved (int entity) { 516 | // Сущность удалена из фильтра. 517 | } 518 | } 519 | ... 520 | var world = new EcsWorld (); 521 | var filter = world.Filter ().End (); 522 | var listener = new TestFilterEventListener (); 523 | filter.AddEventListener (listener); 524 | ``` -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2bdb45a10caf4764bbfefcd766607194 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.leopotam.ecslite", 3 | "author": "Leopotam", 4 | "displayName": "LeoECS Lite", 5 | "description": "LeoECS Lite - легковесный ECS-фреймворк, основанный на структурах. Производительность, нулевые или минимальные аллокации, минимизация использования памяти, отсутствие зависимостей от любого игрового движка - это основные цели данного фреймворка.", 6 | "unity": "2020.3", 7 | "version": "2025.4.22", 8 | "keywords": [ 9 | "leoecslite", 10 | "leoecs", 11 | "ecs", 12 | "performance" 13 | ], 14 | "dependencies": {}, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Leopotam/ecslite.git" 18 | } 19 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 911105c007df4622bd82dc6c57e4b0b8 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /src.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8acc83891df8b4cdab9d3833a3a6e543 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /src/components.cs: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // The MIT-Red License 3 | // Copyright (c) 2012-2025 Leopotam 4 | // ---------------------------------------------------------------------------- 5 | 6 | using System; 7 | using System.Runtime.CompilerServices; 8 | 9 | #if ENABLE_IL2CPP 10 | using Unity.IL2CPP.CompilerServices; 11 | #endif 12 | 13 | namespace Leopotam.EcsLite { 14 | public interface IEcsPool { 15 | void Resize (int capacity); 16 | bool Has (int entity); 17 | void Del (int entity); 18 | void AddRaw (int entity, object dataRaw); 19 | object GetRaw (int entity); 20 | void SetRaw (int entity, object dataRaw); 21 | int GetId (); 22 | Type GetComponentType (); 23 | void Copy (int srcEntity, int dstEntity); 24 | } 25 | 26 | public interface IEcsAutoReset where T : struct { 27 | void AutoReset (ref T c); 28 | } 29 | 30 | public interface IEcsAutoCopy where T : struct { 31 | void AutoCopy (ref T src, ref T dst); 32 | } 33 | 34 | #if ENABLE_IL2CPP 35 | [Il2CppSetOption (Option.NullChecks, false)] 36 | [Il2CppSetOption (Option.ArrayBoundsChecks, false)] 37 | #endif 38 | public sealed class EcsPool : IEcsPool where T : struct { 39 | readonly Type _type; 40 | readonly EcsWorld _world; 41 | readonly short _id; 42 | readonly AutoResetHandler _autoResetHandler; 43 | readonly AutoCopyHandler _autoCopyHandler; 44 | // 1-based index. 45 | T[] _denseItems; 46 | int[] _sparseItems; 47 | int _denseItemsCount; 48 | int[] _recycledItems; 49 | int _recycledItemsCount; 50 | #if ENABLE_IL2CPP && !UNITY_EDITOR 51 | T _fakeInstance; 52 | #endif 53 | 54 | internal EcsPool (EcsWorld world, short id, int denseCapacity, int sparseCapacity, int recycledCapacity) { 55 | _type = typeof (T); 56 | _world = world; 57 | _id = id; 58 | _denseItems = new T[denseCapacity + 1]; 59 | _sparseItems = new int[sparseCapacity]; 60 | _denseItemsCount = 1; 61 | _recycledItems = new int[recycledCapacity]; 62 | _recycledItemsCount = 0; 63 | // autoreset init. 64 | var isAutoReset = typeof (IEcsAutoReset).IsAssignableFrom (_type); 65 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 66 | if (!isAutoReset && _type.GetInterface ("IEcsAutoReset`1") != null) { 67 | throw new Exception ($"IEcsAutoReset should have <{typeof (T).Name}> constraint for component \"{typeof (T).Name}\"."); 68 | } 69 | #endif 70 | if (isAutoReset) { 71 | var autoResetMethod = typeof (T).GetMethod (nameof (IEcsAutoReset.AutoReset)); 72 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 73 | if (autoResetMethod == null) { 74 | throw new Exception ( 75 | $"IEcsAutoReset<{typeof (T).Name}> explicit implementation not supported, use implicit instead."); 76 | } 77 | #endif 78 | _autoResetHandler = (AutoResetHandler) Delegate.CreateDelegate ( 79 | typeof (AutoResetHandler), 80 | #if ENABLE_IL2CPP && !UNITY_EDITOR 81 | _fakeInstance, 82 | #else 83 | null, 84 | #endif 85 | autoResetMethod); 86 | } 87 | // autocopy init. 88 | var isAutoCopy = typeof (IEcsAutoCopy).IsAssignableFrom (_type); 89 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 90 | if (!isAutoCopy && _type.GetInterface ("IEcsCopy`1") != null) { 91 | throw new Exception ($"IEcsCopy should have <{typeof (T).Name}> constraint for component \"{typeof (T).Name}\"."); 92 | } 93 | #endif 94 | if (isAutoCopy) { 95 | var copyMethod = typeof (T).GetMethod (nameof (IEcsAutoCopy.AutoCopy)); 96 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 97 | if (copyMethod == null) { 98 | throw new Exception ( 99 | $"IEcsCopy<{typeof (T).Name}> explicit implementation not supported, use implicit instead."); 100 | } 101 | #endif 102 | _autoCopyHandler = (AutoCopyHandler) Delegate.CreateDelegate ( 103 | typeof (AutoCopyHandler), 104 | #if ENABLE_IL2CPP && !UNITY_EDITOR 105 | _fakeInstance, 106 | #else 107 | null, 108 | #endif 109 | copyMethod); 110 | } 111 | } 112 | 113 | #if UNITY_2020_3_OR_NEWER 114 | [UnityEngine.Scripting.Preserve] 115 | #endif 116 | void ReflectionSupportHack () { 117 | _world.GetPool (); 118 | _world.Filter ().Exc ().End (); 119 | } 120 | 121 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 122 | public EcsWorld GetWorld () { 123 | return _world; 124 | } 125 | 126 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 127 | public int GetId () { 128 | return _id; 129 | } 130 | 131 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 132 | public Type GetComponentType () { 133 | return _type; 134 | } 135 | 136 | void IEcsPool.Resize (int capacity) { 137 | Array.Resize (ref _sparseItems, capacity); 138 | } 139 | 140 | object IEcsPool.GetRaw (int entity) { 141 | return Get (entity); 142 | } 143 | 144 | void IEcsPool.SetRaw (int entity, object dataRaw) { 145 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 146 | if (dataRaw == null || dataRaw.GetType () != _type) { throw new Exception ($"Invalid component data, valid \"{typeof (T).Name}\" instance required."); } 147 | if (_sparseItems[entity] <= 0) { throw new Exception ($"Component \"{typeof (T).Name}\" not attached to entity."); } 148 | #endif 149 | _denseItems[_sparseItems[entity]] = (T) dataRaw; 150 | } 151 | 152 | void IEcsPool.AddRaw (int entity, object dataRaw) { 153 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 154 | if (dataRaw == null || dataRaw.GetType () != _type) { throw new Exception ($"Invalid component data, valid \"{typeof (T).Name}\" instance required."); } 155 | #endif 156 | ref var data = ref Add (entity); 157 | data = (T) dataRaw; 158 | } 159 | 160 | public T[] GetRawDenseItems () { 161 | return _denseItems; 162 | } 163 | 164 | public ref int GetRawDenseItemsCount () { 165 | return ref _denseItemsCount; 166 | } 167 | 168 | public int[] GetRawSparseItems () { 169 | return _sparseItems; 170 | } 171 | 172 | public int[] GetRawRecycledItems () { 173 | return _recycledItems; 174 | } 175 | 176 | public ref int GetRawRecycledItemsCount () { 177 | return ref _recycledItemsCount; 178 | } 179 | 180 | public ref T Add (int entity) { 181 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 182 | if (!_world.IsEntityAliveInternal (entity)) { throw new Exception ("Cant touch destroyed entity."); } 183 | if (_sparseItems[entity] > 0) { throw new Exception ($"Component \"{typeof (T).Name}\" already attached to entity."); } 184 | #endif 185 | int idx; 186 | if (_recycledItemsCount > 0) { 187 | idx = _recycledItems[--_recycledItemsCount]; 188 | } else { 189 | idx = _denseItemsCount; 190 | if (_denseItemsCount == _denseItems.Length) { 191 | Array.Resize (ref _denseItems, _denseItemsCount << 1); 192 | } 193 | _denseItemsCount++; 194 | _autoResetHandler?.Invoke (ref _denseItems[idx]); 195 | } 196 | _sparseItems[entity] = idx; 197 | _world.OnEntityChangeInternal (entity, _id, true); 198 | _world.AddComponentToRawEntityInternal (entity, _id); 199 | #if DEBUG || LEOECSLITE_WORLD_EVENTS 200 | _world.RaiseEntityChangeEvent (entity, _id, true); 201 | #endif 202 | return ref _denseItems[idx]; 203 | } 204 | 205 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 206 | public ref T Get (int entity) { 207 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 208 | if (!_world.IsEntityAliveInternal (entity)) { throw new Exception ("Cant touch destroyed entity."); } 209 | if (_sparseItems[entity] == 0) { throw new Exception ($"Cant get \"{typeof (T).Name}\" component - not attached."); } 210 | #endif 211 | return ref _denseItems[_sparseItems[entity]]; 212 | } 213 | 214 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 215 | public bool Has (int entity) { 216 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 217 | if (!_world.IsEntityAliveInternal (entity)) { throw new Exception ("Cant touch destroyed entity."); } 218 | #endif 219 | return _sparseItems[entity] > 0; 220 | } 221 | 222 | public void Del (int entity) { 223 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 224 | if (!_world.IsEntityAliveInternal (entity)) { throw new Exception ("Cant touch destroyed entity."); } 225 | #endif 226 | ref var sparseData = ref _sparseItems[entity]; 227 | if (sparseData > 0) { 228 | _world.OnEntityChangeInternal (entity, _id, false); 229 | if (_recycledItemsCount == _recycledItems.Length) { 230 | Array.Resize (ref _recycledItems, _recycledItemsCount << 1); 231 | } 232 | _recycledItems[_recycledItemsCount++] = sparseData; 233 | if (_autoResetHandler != null) { 234 | _autoResetHandler.Invoke (ref _denseItems[sparseData]); 235 | } else { 236 | _denseItems[sparseData] = default; 237 | } 238 | sparseData = 0; 239 | var componentsCount = _world.RemoveComponentFromRawEntityInternal (entity, _id); 240 | #if DEBUG || LEOECSLITE_WORLD_EVENTS 241 | _world.RaiseEntityChangeEvent (entity, _id, false); 242 | #endif 243 | if (componentsCount == 0) { 244 | _world.DelEntity (entity); 245 | } 246 | } 247 | } 248 | 249 | public void Copy (int srcEntity, int dstEntity) { 250 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 251 | if (!_world.IsEntityAliveInternal (srcEntity)) { throw new Exception ("Cant touch destroyed src-entity."); } 252 | if (!_world.IsEntityAliveInternal (dstEntity)) { throw new Exception ("Cant touch destroyed dest-entity."); } 253 | #endif 254 | if (Has (srcEntity)) { 255 | ref var srcData = ref Get (srcEntity); 256 | if (!Has (dstEntity)) { 257 | Add (dstEntity); 258 | } 259 | ref var dstData = ref Get (dstEntity); 260 | if (_autoCopyHandler != null) { 261 | _autoCopyHandler.Invoke (ref srcData, ref dstData); 262 | } else { 263 | dstData = srcData; 264 | } 265 | } 266 | } 267 | 268 | delegate void AutoResetHandler (ref T component); 269 | 270 | delegate void AutoCopyHandler (ref T srcComponent, ref T dstComponent); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/components.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: de072e90f7ae4e0e970b30c5c9b5aa71 3 | timeCreated: 1618064138 -------------------------------------------------------------------------------- /src/entities.cs: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // The MIT-Red License 3 | // Copyright (c) 2012-2025 Leopotam 4 | // ---------------------------------------------------------------------------- 5 | 6 | using System.Runtime.CompilerServices; 7 | 8 | #if ENABLE_IL2CPP 9 | using Unity.IL2CPP.CompilerServices; 10 | #endif 11 | 12 | namespace Leopotam.EcsLite { 13 | public struct EcsPackedEntity { 14 | public int Id; 15 | public int Gen; 16 | 17 | public override int GetHashCode () { 18 | unchecked { 19 | return (23 * 31 + Id) * 31 + Gen; 20 | } 21 | } 22 | } 23 | 24 | public struct EcsPackedEntityWithWorld { 25 | public int Id; 26 | public int Gen; 27 | public EcsWorld World; 28 | 29 | public override int GetHashCode () { 30 | unchecked { 31 | return ((23 * 31 + Id) * 31 + Gen) * 31 + (World?.GetHashCode () ?? 0); 32 | } 33 | } 34 | #if DEBUG 35 | // For using in IDE debugger. 36 | internal object[] DebugComponentsView { 37 | get { 38 | object[] list = null; 39 | if (World != null && World.IsAlive () && World.IsEntityAliveInternal (Id) && World.GetEntityGen (Id) == Gen) { 40 | World.GetComponents (Id, ref list); 41 | } 42 | return list; 43 | } 44 | } 45 | // For using in IDE debugger. 46 | internal int DebugComponentsCount { 47 | get { 48 | if (World != null && World.IsAlive () && World.IsEntityAliveInternal (Id) && World.GetEntityGen (Id) == Gen) { 49 | return World.GetComponentsCount (Id); 50 | } 51 | return 0; 52 | } 53 | } 54 | 55 | // For using in IDE debugger. 56 | public override string ToString () { 57 | if (Id == 0 && Gen == 0) { return "Entity-Null"; } 58 | if (World == null || !World.IsAlive () || !World.IsEntityAliveInternal (Id) || World.GetEntityGen (Id) != Gen) { return "Entity-NonAlive"; } 59 | System.Type[] types = null; 60 | var count = World.GetComponentTypes (Id, ref types); 61 | System.Text.StringBuilder sb = null; 62 | if (count > 0) { 63 | sb = new System.Text.StringBuilder (512); 64 | for (var i = 0; i < count; i++) { 65 | if (sb.Length > 0) { sb.Append (","); } 66 | sb.Append (types[i].Name); 67 | } 68 | } 69 | return $"Entity-{Id}:{Gen} [{sb}]"; 70 | } 71 | #endif 72 | } 73 | 74 | #if ENABLE_IL2CPP 75 | [Il2CppSetOption (Option.NullChecks, false)] 76 | [Il2CppSetOption (Option.ArrayBoundsChecks, false)] 77 | #endif 78 | public static class EcsEntityExtensions { 79 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 80 | public static EcsPackedEntity PackEntity (this EcsWorld world, int entity) { 81 | EcsPackedEntity packed; 82 | packed.Id = entity; 83 | packed.Gen = world.GetEntityGen (entity); 84 | return packed; 85 | } 86 | 87 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 88 | public static bool Unpack (this in EcsPackedEntity packed, EcsWorld world, out int entity) { 89 | entity = packed.Id; 90 | return 91 | world != null 92 | && world.IsAlive () 93 | && world.IsEntityAliveInternal (packed.Id) 94 | && world.GetEntityGen (packed.Id) == packed.Gen; 95 | } 96 | 97 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 98 | public static bool EqualsTo (this in EcsPackedEntity a, in EcsPackedEntity b) { 99 | return a.Id == b.Id && a.Gen == b.Gen; 100 | } 101 | 102 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 103 | public static EcsPackedEntityWithWorld PackEntityWithWorld (this EcsWorld world, int entity) { 104 | EcsPackedEntityWithWorld packedEntity; 105 | packedEntity.World = world; 106 | packedEntity.Id = entity; 107 | packedEntity.Gen = world.GetEntityGen (entity); 108 | return packedEntity; 109 | } 110 | 111 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 112 | public static bool Unpack (this in EcsPackedEntityWithWorld packedEntity, out EcsWorld world, out int entity) { 113 | world = packedEntity.World; 114 | entity = packedEntity.Id; 115 | return 116 | world != null 117 | && world.IsAlive () 118 | && world.IsEntityAliveInternal (packedEntity.Id) 119 | && world.GetEntityGen (packedEntity.Id) == packedEntity.Gen; 120 | } 121 | 122 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 123 | public static bool EqualsTo (this in EcsPackedEntityWithWorld a, in EcsPackedEntityWithWorld b) { 124 | return a.Id == b.Id && a.Gen == b.Gen && a.World == b.World; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/entities.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c74142197318412c9a7f80aa16c5abac 3 | timeCreated: 1619359720 -------------------------------------------------------------------------------- /src/filters.cs: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // The MIT-Red License 3 | // Copyright (c) 2012-2025 Leopotam 4 | // ---------------------------------------------------------------------------- 5 | 6 | using System; 7 | using System.Runtime.CompilerServices; 8 | 9 | #if ENABLE_IL2CPP 10 | using Unity.IL2CPP.CompilerServices; 11 | #endif 12 | 13 | namespace Leopotam.EcsLite { 14 | #if LEOECSLITE_FILTER_EVENTS 15 | public interface IEcsFilterEventListener { 16 | void OnEntityAdded (int entity); 17 | void OnEntityRemoved (int entity); 18 | } 19 | #endif 20 | #if ENABLE_IL2CPP 21 | [Il2CppSetOption (Option.NullChecks, false)] 22 | [Il2CppSetOption (Option.ArrayBoundsChecks, false)] 23 | #endif 24 | public sealed class EcsFilter { 25 | readonly EcsWorld _world; 26 | readonly EcsWorld.Mask _mask; 27 | int[] _denseEntities; 28 | int _entitiesCount; 29 | internal int[] SparseEntities; 30 | int _lockCount; 31 | DelayedOp[] _delayedOps; 32 | int _delayedOpsCount; 33 | #if LEOECSLITE_FILTER_EVENTS 34 | IEcsFilterEventListener[] _eventListeners = new IEcsFilterEventListener[4]; 35 | int _eventListenersCount; 36 | #endif 37 | 38 | internal EcsFilter (EcsWorld world, EcsWorld.Mask mask, int denseCapacity, int sparseCapacity) { 39 | _world = world; 40 | _mask = mask; 41 | _denseEntities = new int[denseCapacity]; 42 | SparseEntities = new int[sparseCapacity]; 43 | _entitiesCount = 0; 44 | _delayedOps = new DelayedOp[512]; 45 | _delayedOpsCount = 0; 46 | _lockCount = 0; 47 | } 48 | 49 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 50 | public EcsWorld GetWorld () { 51 | return _world; 52 | } 53 | 54 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 55 | public int GetEntitiesCount () { 56 | return _entitiesCount; 57 | } 58 | 59 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 60 | public int[] GetRawEntities () { 61 | return _denseEntities; 62 | } 63 | 64 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 65 | public int[] GetSparseIndex () { 66 | return SparseEntities; 67 | } 68 | 69 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 70 | public Enumerator GetEnumerator () { 71 | _lockCount++; 72 | return new Enumerator (this); 73 | } 74 | 75 | #if LEOECSLITE_FILTER_EVENTS 76 | public void AddEventListener (IEcsFilterEventListener eventListener) { 77 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 78 | if (eventListener == null) { throw new Exception ("Listener is null."); } 79 | #endif 80 | if (_eventListeners.Length == _eventListenersCount) { 81 | Array.Resize (ref _eventListeners, _eventListenersCount << 1); 82 | } 83 | _eventListeners[_eventListenersCount++] = eventListener; 84 | } 85 | 86 | public void RemoveEventListener (IEcsFilterEventListener eventListener) { 87 | for (var i = 0; i < _eventListenersCount; i++) { 88 | if (_eventListeners[i] == eventListener) { 89 | _eventListenersCount--; 90 | // cant fill gap with last element due listeners order is important. 91 | Array.Copy (_eventListeners, i + 1, _eventListeners, i, _eventListenersCount - i); 92 | break; 93 | } 94 | } 95 | } 96 | #endif 97 | 98 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 99 | internal void ResizeSparseIndex (int capacity) { 100 | Array.Resize (ref SparseEntities, capacity); 101 | } 102 | 103 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 104 | internal EcsWorld.Mask GetMask () { 105 | return _mask; 106 | } 107 | 108 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 109 | internal void AddEntity (int entity) { 110 | if (AddDelayedOp (true, entity)) { return; } 111 | if (_entitiesCount == _denseEntities.Length) { 112 | Array.Resize (ref _denseEntities, _entitiesCount << 1); 113 | } 114 | _denseEntities[_entitiesCount++] = entity; 115 | SparseEntities[entity] = _entitiesCount; 116 | #if LEOECSLITE_FILTER_EVENTS 117 | ProcessEventListeners (true, entity); 118 | #endif 119 | } 120 | 121 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 122 | internal void RemoveEntity (int entity) { 123 | if (AddDelayedOp (false, entity)) { return; } 124 | var idx = SparseEntities[entity] - 1; 125 | SparseEntities[entity] = 0; 126 | _entitiesCount--; 127 | if (idx < _entitiesCount) { 128 | _denseEntities[idx] = _denseEntities[_entitiesCount]; 129 | SparseEntities[_denseEntities[idx]] = idx + 1; 130 | } 131 | #if LEOECSLITE_FILTER_EVENTS 132 | ProcessEventListeners (false, entity); 133 | #endif 134 | } 135 | 136 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 137 | bool AddDelayedOp (bool added, int entity) { 138 | if (_lockCount <= 0) { return false; } 139 | if (_delayedOpsCount == _delayedOps.Length) { 140 | Array.Resize (ref _delayedOps, _delayedOpsCount << 1); 141 | } 142 | ref var op = ref _delayedOps[_delayedOpsCount++]; 143 | op.Added = added; 144 | op.Entity = entity; 145 | return true; 146 | } 147 | 148 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 149 | void Unlock () { 150 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 151 | if (_lockCount <= 0) { throw new Exception ($"Invalid lock-unlock balance for \"{GetType ().Name}\"."); } 152 | #endif 153 | _lockCount--; 154 | if (_lockCount == 0 && _delayedOpsCount > 0) { 155 | for (int i = 0, iMax = _delayedOpsCount; i < iMax; i++) { 156 | ref var op = ref _delayedOps[i]; 157 | if (op.Added) { 158 | AddEntity (op.Entity); 159 | } else { 160 | RemoveEntity (op.Entity); 161 | } 162 | } 163 | _delayedOpsCount = 0; 164 | } 165 | } 166 | 167 | #if LEOECSLITE_FILTER_EVENTS 168 | void ProcessEventListeners (bool isAdd, int entity) { 169 | if (isAdd) { 170 | for (var i = 0; i < _eventListenersCount; i++) { 171 | _eventListeners[i].OnEntityAdded (entity); 172 | } 173 | } else { 174 | for (var i = 0; i < _eventListenersCount; i++) { 175 | _eventListeners[i].OnEntityRemoved (entity); 176 | } 177 | } 178 | } 179 | #endif 180 | 181 | public struct Enumerator : IDisposable { 182 | readonly EcsFilter _filter; 183 | readonly int[] _entities; 184 | readonly int _count; 185 | int _idx; 186 | 187 | public Enumerator (EcsFilter filter) { 188 | _filter = filter; 189 | _entities = filter._denseEntities; 190 | _count = filter._entitiesCount; 191 | _idx = -1; 192 | } 193 | 194 | public int Current { 195 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 196 | get => _entities[_idx]; 197 | } 198 | 199 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 200 | public bool MoveNext () { 201 | return ++_idx < _count; 202 | } 203 | 204 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 205 | public void Dispose () { 206 | _filter.Unlock (); 207 | } 208 | } 209 | 210 | struct DelayedOp { 211 | public bool Added; 212 | public int Entity; 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/filters.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6460a91dd76b4fa2a8ac079767e094fb 3 | timeCreated: 1619130516 -------------------------------------------------------------------------------- /src/systems.cs: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // The MIT-Red License 3 | // Copyright (c) 2012-2025 Leopotam 4 | // ---------------------------------------------------------------------------- 5 | 6 | using System.Collections.Generic; 7 | 8 | #if ENABLE_IL2CPP 9 | using Unity.IL2CPP.CompilerServices; 10 | #endif 11 | 12 | namespace Leopotam.EcsLite { 13 | public interface IEcsSystem { } 14 | 15 | public interface IEcsPreInitSystem : IEcsSystem { 16 | void PreInit (IEcsSystems systems); 17 | } 18 | 19 | public interface IEcsInitSystem : IEcsSystem { 20 | void Init (IEcsSystems systems); 21 | } 22 | 23 | public interface IEcsRunSystem : IEcsSystem { 24 | void Run (IEcsSystems systems); 25 | } 26 | 27 | public interface IEcsPostRunSystem : IEcsSystem { 28 | void PostRun (IEcsSystems systems); 29 | } 30 | 31 | public interface IEcsDestroySystem : IEcsSystem { 32 | void Destroy (IEcsSystems systems); 33 | } 34 | 35 | public interface IEcsPostDestroySystem : IEcsSystem { 36 | void PostDestroy (IEcsSystems systems); 37 | } 38 | 39 | public interface IEcsSystems { 40 | T GetShared () where T : class; 41 | IEcsSystems AddWorld (EcsWorld world, string name); 42 | EcsWorld GetWorld (string name = null); 43 | Dictionary GetAllNamedWorlds (); 44 | IEcsSystems Add (IEcsSystem system); 45 | List GetAllSystems (); 46 | void Init (); 47 | void Run (); 48 | void Destroy (); 49 | } 50 | 51 | #if ENABLE_IL2CPP 52 | [Il2CppSetOption (Option.NullChecks, false)] 53 | [Il2CppSetOption (Option.ArrayBoundsChecks, false)] 54 | #endif 55 | public class EcsSystems : IEcsSystems { 56 | readonly EcsWorld _defaultWorld; 57 | readonly Dictionary _worlds; 58 | readonly List _allSystems; 59 | readonly List _runSystems; 60 | readonly List _postRunSystems; 61 | readonly object _shared; 62 | #if DEBUG 63 | protected bool _inited; 64 | #endif 65 | 66 | public EcsSystems (EcsWorld defaultWorld, object shared = null) { 67 | _defaultWorld = defaultWorld; 68 | _shared = shared; 69 | _worlds = new Dictionary (8); 70 | _allSystems = new List (128); 71 | _runSystems = new List (128); 72 | _postRunSystems = new List (128); 73 | } 74 | 75 | public virtual T GetShared () where T : class { 76 | return _shared as T; 77 | } 78 | 79 | public virtual IEcsSystems AddWorld (EcsWorld world, string name) { 80 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 81 | if (_inited) { throw new System.Exception ("Cant add world after initialization."); } 82 | if (world == null) { throw new System.Exception ("World cant be null."); } 83 | if (string.IsNullOrEmpty (name)) { throw new System.Exception ("World name cant be null or empty."); } 84 | if (_worlds.ContainsKey (name)) { throw new System.Exception ($"World with name \"{name}\" already added."); } 85 | #endif 86 | _worlds[name] = world; 87 | return this; 88 | } 89 | 90 | public virtual EcsWorld GetWorld (string name = null) { 91 | if (name == null) { 92 | return _defaultWorld; 93 | } 94 | _worlds.TryGetValue (name, out var world); 95 | return world; 96 | } 97 | 98 | public virtual Dictionary GetAllNamedWorlds () { 99 | return _worlds; 100 | } 101 | 102 | public virtual IEcsSystems Add (IEcsSystem system) { 103 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 104 | if (_inited) { throw new System.Exception ("Cant add system after initialization."); } 105 | #endif 106 | _allSystems.Add (system); 107 | if (system is IEcsRunSystem runSystem) { 108 | _runSystems.Add (runSystem); 109 | } 110 | if (system is IEcsPostRunSystem postRunSystem) { 111 | _postRunSystems.Add (postRunSystem); 112 | } 113 | return this; 114 | } 115 | 116 | public virtual List GetAllSystems () { 117 | return _allSystems; 118 | } 119 | 120 | public virtual void Init () { 121 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 122 | if (_inited) { throw new System.Exception ("Already initialized."); } 123 | #endif 124 | foreach (var system in _allSystems) { 125 | if (system is IEcsPreInitSystem initSystem) { 126 | initSystem.PreInit (this); 127 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 128 | var worldName = CheckForLeakedEntities (this); 129 | if (worldName != null) { throw new System.Exception ($"Empty entity detected in world \"{worldName}\" after {initSystem.GetType ().Name}.PreInit()."); } 130 | #endif 131 | } 132 | } 133 | foreach (var system in _allSystems) { 134 | if (system is IEcsInitSystem initSystem) { 135 | initSystem.Init (this); 136 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 137 | var worldName = CheckForLeakedEntities (this); 138 | if (worldName != null) { throw new System.Exception ($"Empty entity detected in world \"{worldName}\" after {initSystem.GetType ().Name}.Init()."); } 139 | #endif 140 | } 141 | } 142 | #if DEBUG 143 | _inited = true; 144 | #endif 145 | } 146 | 147 | public virtual void Run () { 148 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 149 | if (!_inited) { throw new System.Exception ("Cant run without initialization."); } 150 | #endif 151 | for (int i = 0, iMax = _runSystems.Count; i < iMax; i++) { 152 | _runSystems[i].Run (this); 153 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 154 | var worldName = CheckForLeakedEntities (this); 155 | if (worldName != null) { throw new System.Exception ($"Empty entity detected in world \"{worldName}\" after {_runSystems[i].GetType ().Name}.Run()."); } 156 | #endif 157 | } 158 | for (int i = 0, iMax = _postRunSystems.Count; i < iMax; i++) { 159 | _postRunSystems[i].PostRun (this); 160 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 161 | var worldName = CheckForLeakedEntities (this); 162 | if (worldName != null) { throw new System.Exception ($"Empty entity detected in world \"{worldName}\" after {_postRunSystems[i].GetType ().Name}.PostRun()."); } 163 | #endif 164 | } 165 | } 166 | 167 | public virtual void Destroy () { 168 | for (var i = _allSystems.Count - 1; i >= 0; i--) { 169 | if (_allSystems[i] is IEcsDestroySystem destroySystem) { 170 | destroySystem.Destroy (this); 171 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 172 | var worldName = CheckForLeakedEntities (this); 173 | if (worldName != null) { throw new System.Exception ($"Empty entity detected in world \"{worldName}\" after {destroySystem.GetType ().Name}.Destroy()."); } 174 | #endif 175 | } 176 | } 177 | for (var i = _allSystems.Count - 1; i >= 0; i--) { 178 | if (_allSystems[i] is IEcsPostDestroySystem postDestroySystem) { 179 | postDestroySystem.PostDestroy (this); 180 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 181 | var worldName = CheckForLeakedEntities (this); 182 | if (worldName != null) { throw new System.Exception ($"Empty entity detected in world \"{worldName}\" after {postDestroySystem.GetType ().Name}.PostDestroy()."); } 183 | #endif 184 | } 185 | } 186 | _worlds.Clear (); 187 | _allSystems.Clear (); 188 | _runSystems.Clear (); 189 | _postRunSystems.Clear (); 190 | #if DEBUG 191 | _inited = false; 192 | #endif 193 | } 194 | 195 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 196 | public static string CheckForLeakedEntities (IEcsSystems systems) { 197 | if (systems.GetWorld ().CheckForLeakedEntities ()) { return "default"; } 198 | foreach (var pair in systems.GetAllNamedWorlds ()) { 199 | if (pair.Value.CheckForLeakedEntities ()) { 200 | return pair.Key; 201 | } 202 | } 203 | return null; 204 | } 205 | #endif 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/systems.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7ce0c346e4024f069419496fd77aadcb 3 | timeCreated: 1618698420 -------------------------------------------------------------------------------- /src/worlds.cs: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // The MIT-Red License 3 | // Copyright (c) 2012-2025 Leopotam 4 | // ---------------------------------------------------------------------------- 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Runtime.CompilerServices; 9 | 10 | #if ENABLE_IL2CPP 11 | using Unity.IL2CPP.CompilerServices; 12 | #endif 13 | 14 | namespace Leopotam.EcsLite { 15 | public static class RawEntityOffsets { 16 | public const int ComponentsCount = 0; 17 | public const int Gen = 1; 18 | public const int Components = 2; 19 | } 20 | 21 | #if ENABLE_IL2CPP 22 | [Il2CppSetOption (Option.NullChecks, false)] 23 | [Il2CppSetOption (Option.ArrayBoundsChecks, false)] 24 | #endif 25 | public class EcsWorld { 26 | // componentsCount, gen, c1, c2, ..., [next] 27 | short[] _entities; 28 | int _entitiesItemSize; 29 | int _entitiesCount; 30 | int[] _recycledEntities; 31 | int _recycledEntitiesCount; 32 | IEcsPool[] _pools; 33 | short _poolsCount; 34 | readonly int _poolDenseSize; 35 | readonly int _poolRecycledSize; 36 | readonly Dictionary _poolHashes; 37 | readonly Dictionary _hashedFilters; 38 | readonly List _allFilters; 39 | List[] _filtersByIncludedComponents; 40 | List[] _filtersByExcludedComponents; 41 | Mask[] _masks; 42 | int _masksCount; 43 | 44 | bool _destroyed; 45 | #if DEBUG || LEOECSLITE_WORLD_EVENTS 46 | List _eventListeners; 47 | 48 | public void AddEventListener (IEcsWorldEventListener listener) { 49 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 50 | if (listener == null) { throw new Exception ("Listener is null."); } 51 | #endif 52 | _eventListeners.Add (listener); 53 | } 54 | 55 | public void RemoveEventListener (IEcsWorldEventListener listener) { 56 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 57 | if (listener == null) { throw new Exception ("Listener is null."); } 58 | #endif 59 | _eventListeners.Remove (listener); 60 | } 61 | 62 | public void RaiseEntityChangeEvent (int entity, short poolId, bool added) { 63 | for (int ii = 0, iMax = _eventListeners.Count; ii < iMax; ii++) { 64 | _eventListeners[ii].OnEntityChanged (entity, poolId, added); 65 | } 66 | } 67 | #endif 68 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 69 | readonly List _leakedEntities = new List (512); 70 | 71 | internal bool CheckForLeakedEntities () { 72 | if (_leakedEntities.Count > 0) { 73 | for (int i = 0, iMax = _leakedEntities.Count; i < iMax; i++) { 74 | var entityData = GetRawEntityOffset (_leakedEntities[i]); 75 | if (_entities[entityData + RawEntityOffsets.Gen] > 0 && _entities[entityData + RawEntityOffsets.ComponentsCount] == 0) { 76 | return true; 77 | } 78 | } 79 | _leakedEntities.Clear (); 80 | } 81 | return false; 82 | } 83 | #endif 84 | public EcsWorld (in Config cfg = default) { 85 | // entities. 86 | var capacity = cfg.Entities > 0 ? cfg.Entities : Config.EntitiesDefault; 87 | _entitiesItemSize = RawEntityOffsets.Components + (cfg.EntityComponentsSize > 0 ? cfg.EntityComponentsSize : Config.EntityComponentsSizeDefault); 88 | _entities = new short[capacity * _entitiesItemSize]; 89 | capacity = cfg.RecycledEntities > 0 ? cfg.RecycledEntities : Config.RecycledEntitiesDefault; 90 | _recycledEntities = new int[capacity]; 91 | _entitiesCount = 0; 92 | _recycledEntitiesCount = 0; 93 | // pools. 94 | capacity = cfg.Pools > 0 ? cfg.Pools : Config.PoolsDefault; 95 | _pools = new IEcsPool[capacity]; 96 | _poolHashes = new Dictionary (capacity); 97 | _filtersByIncludedComponents = new List[capacity]; 98 | _filtersByExcludedComponents = new List[capacity]; 99 | _poolDenseSize = cfg.PoolDenseSize > 0 ? cfg.PoolDenseSize : Config.PoolDenseSizeDefault; 100 | _poolRecycledSize = cfg.PoolRecycledSize > 0 ? cfg.PoolRecycledSize : Config.PoolRecycledSizeDefault; 101 | _poolsCount = 0; 102 | // filters. 103 | capacity = cfg.Filters > 0 ? cfg.Filters : Config.FiltersDefault; 104 | _hashedFilters = new Dictionary (capacity); 105 | _allFilters = new List (capacity); 106 | // masks. 107 | _masks = new Mask[64]; 108 | _masksCount = 0; 109 | #if DEBUG || LEOECSLITE_WORLD_EVENTS 110 | _eventListeners = new List (4); 111 | #endif 112 | _destroyed = false; 113 | } 114 | 115 | public void Destroy () { 116 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 117 | if (CheckForLeakedEntities ()) { throw new Exception ($"Empty entity detected before EcsWorld.Destroy()."); } 118 | #endif 119 | _destroyed = true; 120 | for (var i = _entitiesCount - 1; i >= 0; i--) { 121 | if (_entities[GetRawEntityOffset (i) + RawEntityOffsets.ComponentsCount] > 0) { 122 | DelEntity (i); 123 | } 124 | } 125 | _pools = Array.Empty (); 126 | _poolHashes.Clear (); 127 | _hashedFilters.Clear (); 128 | _allFilters.Clear (); 129 | _filtersByIncludedComponents = Array.Empty> (); 130 | _filtersByExcludedComponents = Array.Empty> (); 131 | #if DEBUG || LEOECSLITE_WORLD_EVENTS 132 | for (var ii = _eventListeners.Count - 1; ii >= 0; ii--) { 133 | _eventListeners[ii].OnWorldDestroyed (this); 134 | } 135 | #endif 136 | } 137 | 138 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 139 | public int GetRawEntityOffset (int entity) { 140 | return entity * _entitiesItemSize; 141 | } 142 | 143 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 144 | public bool IsAlive () { 145 | return !_destroyed; 146 | } 147 | 148 | public int NewEntity () { 149 | int entity; 150 | if (_recycledEntitiesCount > 0) { 151 | entity = _recycledEntities[--_recycledEntitiesCount]; 152 | _entities[GetRawEntityOffset (entity) + RawEntityOffsets.Gen] *= -1; 153 | } else { 154 | // new entity. 155 | if (_entitiesCount * _entitiesItemSize == _entities.Length) { 156 | // resize entities and component pools. 157 | var newSize = _entitiesCount << 1; 158 | Array.Resize (ref _entities, newSize * _entitiesItemSize); 159 | for (int i = 0, iMax = _poolsCount; i < iMax; i++) { 160 | _pools[i].Resize (newSize); 161 | } 162 | for (int i = 0, iMax = _allFilters.Count; i < iMax; i++) { 163 | _allFilters[i].ResizeSparseIndex (newSize); 164 | } 165 | #if DEBUG || LEOECSLITE_WORLD_EVENTS 166 | for (int ii = 0, iMax = _eventListeners.Count; ii < iMax; ii++) { 167 | _eventListeners[ii].OnWorldResized (newSize); 168 | } 169 | #endif 170 | } 171 | entity = _entitiesCount++; 172 | _entities[GetRawEntityOffset (entity) + RawEntityOffsets.Gen] = 1; 173 | } 174 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 175 | _leakedEntities.Add (entity); 176 | #endif 177 | #if DEBUG || LEOECSLITE_WORLD_EVENTS 178 | for (int ii = 0, iMax = _eventListeners.Count; ii < iMax; ii++) { 179 | _eventListeners[ii].OnEntityCreated (entity); 180 | } 181 | #endif 182 | return entity; 183 | } 184 | 185 | public void DelEntity (int entity) { 186 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 187 | if (entity < 0 || entity >= _entitiesCount) { throw new Exception ("Cant touch destroyed entity."); } 188 | #endif 189 | var entityOffset = GetRawEntityOffset (entity); 190 | var componentsCount = _entities[entityOffset + RawEntityOffsets.ComponentsCount]; 191 | ref var entityGen = ref _entities[entityOffset + RawEntityOffsets.Gen]; 192 | if (entityGen < 0) { 193 | return; 194 | } 195 | if (componentsCount > 0) { 196 | for (var i = entityOffset + RawEntityOffsets.Components + componentsCount - 1; i >= entityOffset + RawEntityOffsets.Components; i--) { 197 | _pools[_entities[i]].Del (entity); 198 | } 199 | } else { 200 | entityGen = (short) (entityGen == short.MaxValue ? -1 : -(entityGen + 1)); 201 | if (_recycledEntitiesCount == _recycledEntities.Length) { 202 | Array.Resize (ref _recycledEntities, _recycledEntitiesCount << 1); 203 | } 204 | _recycledEntities[_recycledEntitiesCount++] = entity; 205 | #if DEBUG || LEOECSLITE_WORLD_EVENTS 206 | for (int ii = 0, iMax = _eventListeners.Count; ii < iMax; ii++) { 207 | _eventListeners[ii].OnEntityDestroyed (entity); 208 | } 209 | #endif 210 | } 211 | } 212 | 213 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 214 | public int GetComponentsCount (int entity) { 215 | return _entities[GetRawEntityOffset (entity) + RawEntityOffsets.ComponentsCount]; 216 | } 217 | 218 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 219 | public short GetEntityGen (int entity) { 220 | return _entities[GetRawEntityOffset (entity) + RawEntityOffsets.Gen]; 221 | } 222 | 223 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 224 | public int GetRawEntityItemSize () { 225 | return _entitiesItemSize; 226 | } 227 | 228 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 229 | public int GetUsedEntitiesCount () { 230 | return _entitiesCount; 231 | } 232 | 233 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 234 | public int GetWorldSize () { 235 | return _entities.Length / _entitiesItemSize; 236 | } 237 | 238 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 239 | public int GetPoolsCount () { 240 | return _poolsCount; 241 | } 242 | 243 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 244 | public int GetEntitiesCount () { 245 | return _entitiesCount - _recycledEntitiesCount; 246 | } 247 | 248 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 249 | public short[] GetRawEntities () { 250 | return _entities; 251 | } 252 | 253 | public EcsPool GetPool () where T : struct { 254 | var poolType = typeof (T); 255 | if (_poolHashes.TryGetValue (poolType, out var rawPool)) { 256 | return (EcsPool) rawPool; 257 | } 258 | #if DEBUG 259 | if (_poolsCount == short.MaxValue) { throw new Exception ("No more room for new component into this world."); } 260 | #endif 261 | var pool = new EcsPool (this, _poolsCount, _poolDenseSize, GetWorldSize (), _poolRecycledSize); 262 | _poolHashes[poolType] = pool; 263 | if (_poolsCount == _pools.Length) { 264 | var newSize = _poolsCount << 1; 265 | Array.Resize (ref _pools, newSize); 266 | Array.Resize (ref _filtersByIncludedComponents, newSize); 267 | Array.Resize (ref _filtersByExcludedComponents, newSize); 268 | } 269 | _pools[_poolsCount++] = pool; 270 | return pool; 271 | } 272 | 273 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 274 | public IEcsPool GetPoolById (int typeId) { 275 | return typeId >= 0 && typeId < _poolsCount ? _pools[typeId] : null; 276 | } 277 | 278 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 279 | public IEcsPool GetPoolByType (Type type) { 280 | return _poolHashes.TryGetValue (type, out var pool) ? pool : null; 281 | } 282 | 283 | public int GetAllEntities (ref int[] entities) { 284 | var count = _entitiesCount - _recycledEntitiesCount; 285 | if (entities == null || entities.Length < count) { 286 | entities = new int[count]; 287 | } 288 | var id = 0; 289 | var offset = 0; 290 | for (int i = 0, iMax = _entitiesCount; i < iMax; i++, offset += _entitiesItemSize) { 291 | if (_entities[offset + RawEntityOffsets.Gen] > 0 && _entities[offset + RawEntityOffsets.ComponentsCount] >= 0) { 292 | entities[id++] = i; 293 | } 294 | } 295 | return count; 296 | } 297 | 298 | public int GetAllPools (ref IEcsPool[] pools) { 299 | var count = _poolsCount; 300 | if (pools == null || pools.Length < count) { 301 | pools = new IEcsPool[count]; 302 | } 303 | Array.Copy (_pools, 0, pools, 0, _poolsCount); 304 | return _poolsCount; 305 | } 306 | 307 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 308 | public Mask Filter () where T : struct { 309 | var mask = _masksCount > 0 ? _masks[--_masksCount] : new Mask (this); 310 | return mask.Inc (); 311 | } 312 | 313 | public int GetComponents (int entity, ref object[] list) { 314 | var entityOffset = GetRawEntityOffset (entity); 315 | var itemsCount = _entities[entityOffset + RawEntityOffsets.ComponentsCount]; 316 | if (itemsCount == 0) { return 0; } 317 | if (list == null || list.Length < itemsCount) { 318 | list = new object[_pools.Length]; 319 | } 320 | var dataOffset = entityOffset + RawEntityOffsets.Components; 321 | for (var i = 0; i < itemsCount; i++) { 322 | list[i] = _pools[_entities[dataOffset + i]].GetRaw (entity); 323 | } 324 | return itemsCount; 325 | } 326 | 327 | public int GetComponentTypes (int entity, ref Type[] list) { 328 | var entityOffset = GetRawEntityOffset (entity); 329 | var itemsCount = _entities[entityOffset + RawEntityOffsets.ComponentsCount]; 330 | if (itemsCount == 0) { return 0; } 331 | if (list == null || list.Length < itemsCount) { 332 | list = new Type[_pools.Length]; 333 | } 334 | var dataOffset = entityOffset + RawEntityOffsets.Components; 335 | for (var i = 0; i < itemsCount; i++) { 336 | list[i] = _pools[_entities[dataOffset + i]].GetComponentType (); 337 | } 338 | return itemsCount; 339 | } 340 | 341 | public void CopyEntity (int srcEntity, int dstEntity) { 342 | var entityOffset = GetRawEntityOffset (srcEntity); 343 | var itemsCount = _entities[entityOffset + RawEntityOffsets.ComponentsCount]; 344 | if (itemsCount > 0) { 345 | var dataOffset = entityOffset + RawEntityOffsets.Components; 346 | for (var i = 0; i < itemsCount; i++) { 347 | _pools[_entities[dataOffset + i]].Copy (srcEntity, dstEntity); 348 | } 349 | } 350 | } 351 | 352 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 353 | internal bool IsEntityAliveInternal (int entity) { 354 | return entity >= 0 && entity < _entitiesCount && _entities[GetRawEntityOffset (entity) + RawEntityOffsets.Gen] > 0; 355 | } 356 | 357 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 358 | internal void AddComponentToRawEntityInternal (int entity, short poolId) { 359 | var offset = GetRawEntityOffset (entity); 360 | var dataCount = _entities[offset + RawEntityOffsets.ComponentsCount]; 361 | if (dataCount + RawEntityOffsets.Components == _entitiesItemSize) { 362 | // resize entities. 363 | ExtendEntitiesCache (); 364 | offset = GetRawEntityOffset (entity); 365 | } 366 | _entities[offset + RawEntityOffsets.ComponentsCount]++; 367 | _entities[offset + RawEntityOffsets.Components + dataCount] = poolId; 368 | } 369 | 370 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 371 | internal short RemoveComponentFromRawEntityInternal (int entity, short poolId) { 372 | var offset = GetRawEntityOffset (entity); 373 | var dataCount = _entities[offset + RawEntityOffsets.ComponentsCount]; 374 | dataCount--; 375 | _entities[offset + RawEntityOffsets.ComponentsCount] = dataCount; 376 | var dataOffset = offset + RawEntityOffsets.Components; 377 | for (var i = 0; i <= dataCount; i++) { 378 | if (_entities[dataOffset + i] == poolId) { 379 | if (i < dataCount) { 380 | // fill gap with last item. 381 | _entities[dataOffset + i] = _entities[dataOffset + dataCount]; 382 | } 383 | return dataCount; 384 | } 385 | } 386 | #if DEBUG 387 | throw new Exception ("Component not found in entity data"); 388 | #else 389 | return 0; 390 | #endif 391 | } 392 | 393 | void ExtendEntitiesCache () { 394 | var newItemSize = RawEntityOffsets.Components + ((_entitiesItemSize - RawEntityOffsets.Components) << 1); 395 | var newEntities = new short[GetWorldSize () * newItemSize]; 396 | var oldOffset = 0; 397 | var newOffset = 0; 398 | for (int i = 0, iMax = _entitiesCount; i < iMax; i++) { 399 | // amount of entity data (components + header). 400 | var entityDataLen = _entities[oldOffset + RawEntityOffsets.ComponentsCount] + RawEntityOffsets.Components; 401 | for (var j = 0; j < entityDataLen; j++) { 402 | newEntities[newOffset + j] = _entities[oldOffset + j]; 403 | } 404 | oldOffset += _entitiesItemSize; 405 | newOffset += newItemSize; 406 | } 407 | _entitiesItemSize = newItemSize; 408 | _entities = newEntities; 409 | } 410 | 411 | (EcsFilter, bool) GetFilterInternal (Mask mask, int capacity = 512) { 412 | var hash = mask.Hash; 413 | var exists = _hashedFilters.TryGetValue (hash, out var filter); 414 | if (exists) { return (filter, false); } 415 | filter = new EcsFilter (this, mask, capacity, GetWorldSize ()); 416 | _hashedFilters[hash] = filter; 417 | _allFilters.Add (filter); 418 | // add to component dictionaries for fast compatibility scan. 419 | for (int i = 0, iMax = mask.IncludeCount; i < iMax; i++) { 420 | var list = _filtersByIncludedComponents[mask.Include[i]]; 421 | if (list == null) { 422 | list = new List (8); 423 | _filtersByIncludedComponents[mask.Include[i]] = list; 424 | } 425 | list.Add (filter); 426 | } 427 | for (int i = 0, iMax = mask.ExcludeCount; i < iMax; i++) { 428 | var list = _filtersByExcludedComponents[mask.Exclude[i]]; 429 | if (list == null) { 430 | list = new List (8); 431 | _filtersByExcludedComponents[mask.Exclude[i]] = list; 432 | } 433 | list.Add (filter); 434 | } 435 | // scan exist entities for compatibility with new filter. 436 | for (int i = 0, iMax = _entitiesCount; i < iMax; i++) { 437 | if (_entities[GetRawEntityOffset (i) + RawEntityOffsets.ComponentsCount] > 0 && IsMaskCompatible (mask, i)) { 438 | filter.AddEntity (i); 439 | } 440 | } 441 | #if DEBUG || LEOECSLITE_WORLD_EVENTS 442 | for (int ii = 0, iMax = _eventListeners.Count; ii < iMax; ii++) { 443 | _eventListeners[ii].OnFilterCreated (filter); 444 | } 445 | #endif 446 | return (filter, true); 447 | } 448 | 449 | public void OnEntityChangeInternal (int entity, short componentType, bool added) { 450 | var includeList = _filtersByIncludedComponents[componentType]; 451 | var excludeList = _filtersByExcludedComponents[componentType]; 452 | if (added) { 453 | // add component. 454 | if (includeList != null) { 455 | foreach (var filter in includeList) { 456 | if (IsMaskCompatible (filter.GetMask (), entity)) { 457 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 458 | if (filter.SparseEntities[entity] > 0) { throw new Exception ("Entity already in filter."); } 459 | #endif 460 | filter.AddEntity (entity); 461 | } 462 | } 463 | } 464 | if (excludeList != null) { 465 | foreach (var filter in excludeList) { 466 | if (IsMaskCompatibleWithout (filter.GetMask (), entity, componentType)) { 467 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 468 | if (filter.SparseEntities[entity] == 0) { throw new Exception ("Entity not in filter."); } 469 | #endif 470 | filter.RemoveEntity (entity); 471 | } 472 | } 473 | } 474 | } else { 475 | // remove component. 476 | if (includeList != null) { 477 | foreach (var filter in includeList) { 478 | if (IsMaskCompatible (filter.GetMask (), entity)) { 479 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 480 | if (filter.SparseEntities[entity] == 0) { throw new Exception ("Entity not in filter."); } 481 | #endif 482 | filter.RemoveEntity (entity); 483 | } 484 | } 485 | } 486 | if (excludeList != null) { 487 | foreach (var filter in excludeList) { 488 | if (IsMaskCompatibleWithout (filter.GetMask (), entity, componentType)) { 489 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 490 | if (filter.SparseEntities[entity] > 0) { throw new Exception ("Entity already in filter."); } 491 | #endif 492 | filter.AddEntity (entity); 493 | } 494 | } 495 | } 496 | } 497 | } 498 | 499 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 500 | bool IsMaskCompatible (Mask filterMask, int entity) { 501 | for (int i = 0, iMax = filterMask.IncludeCount; i < iMax; i++) { 502 | if (!_pools[filterMask.Include[i]].Has (entity)) { 503 | return false; 504 | } 505 | } 506 | for (int i = 0, iMax = filterMask.ExcludeCount; i < iMax; i++) { 507 | if (_pools[filterMask.Exclude[i]].Has (entity)) { 508 | return false; 509 | } 510 | } 511 | return true; 512 | } 513 | 514 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 515 | bool IsMaskCompatibleWithout (Mask filterMask, int entity, int componentId) { 516 | for (int i = 0, iMax = filterMask.IncludeCount; i < iMax; i++) { 517 | var typeId = filterMask.Include[i]; 518 | if (typeId == componentId || !_pools[typeId].Has (entity)) { 519 | return false; 520 | } 521 | } 522 | for (int i = 0, iMax = filterMask.ExcludeCount; i < iMax; i++) { 523 | var typeId = filterMask.Exclude[i]; 524 | if (typeId != componentId && _pools[typeId].Has (entity)) { 525 | return false; 526 | } 527 | } 528 | return true; 529 | } 530 | 531 | public struct Config { 532 | public int Entities; 533 | public int RecycledEntities; 534 | public int Pools; 535 | public int Filters; 536 | public int PoolDenseSize; 537 | public int PoolRecycledSize; 538 | public int EntityComponentsSize; 539 | 540 | internal const int EntitiesDefault = 512; 541 | internal const int RecycledEntitiesDefault = 512; 542 | internal const int PoolsDefault = 512; 543 | internal const int FiltersDefault = 512; 544 | internal const int PoolDenseSizeDefault = 512; 545 | internal const int PoolRecycledSizeDefault = 512; 546 | internal const int EntityComponentsSizeDefault = 8; 547 | } 548 | 549 | #if ENABLE_IL2CPP 550 | [Il2CppSetOption (Option.NullChecks, false)] 551 | [Il2CppSetOption (Option.ArrayBoundsChecks, false)] 552 | #endif 553 | public sealed class Mask { 554 | readonly EcsWorld _world; 555 | internal int[] Include; 556 | internal int[] Exclude; 557 | internal int IncludeCount; 558 | internal int ExcludeCount; 559 | internal int Hash; 560 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 561 | bool _built; 562 | #endif 563 | 564 | internal Mask (EcsWorld world) { 565 | _world = world; 566 | Include = new int[8]; 567 | Exclude = new int[2]; 568 | Reset (); 569 | } 570 | 571 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 572 | void Reset () { 573 | IncludeCount = 0; 574 | ExcludeCount = 0; 575 | Hash = 0; 576 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 577 | _built = false; 578 | #endif 579 | } 580 | 581 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 582 | public Mask Inc () where T : struct { 583 | var poolId = _world.GetPool ().GetId (); 584 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 585 | if (_built) { throw new Exception ("Cant change built mask."); } 586 | if (Array.IndexOf (Include, poolId, 0, IncludeCount) != -1) { throw new Exception ($"{typeof (T).Name} already in constraints list."); } 587 | if (Array.IndexOf (Exclude, poolId, 0, ExcludeCount) != -1) { throw new Exception ($"{typeof (T).Name} already in constraints list."); } 588 | #endif 589 | if (IncludeCount == Include.Length) { Array.Resize (ref Include, IncludeCount << 1); } 590 | Include[IncludeCount++] = poolId; 591 | return this; 592 | } 593 | 594 | #if UNITY_2020_3_OR_NEWER 595 | [UnityEngine.Scripting.Preserve] 596 | #endif 597 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 598 | public Mask Exc () where T : struct { 599 | var poolId = _world.GetPool ().GetId (); 600 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 601 | if (_built) { throw new Exception ("Cant change built mask."); } 602 | if (Array.IndexOf (Include, poolId, 0, IncludeCount) != -1) { throw new Exception ($"{typeof (T).Name} already in constraints list."); } 603 | if (Array.IndexOf (Exclude, poolId, 0, ExcludeCount) != -1) { throw new Exception ($"{typeof (T).Name} already in constraints list."); } 604 | #endif 605 | if (ExcludeCount == Exclude.Length) { Array.Resize (ref Exclude, ExcludeCount << 1); } 606 | Exclude[ExcludeCount++] = poolId; 607 | return this; 608 | } 609 | 610 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 611 | public EcsFilter End (int capacity = 512) { 612 | #if DEBUG && !LEOECSLITE_NO_SANITIZE_CHECKS 613 | if (_built) { throw new Exception ("Cant change built mask."); } 614 | _built = true; 615 | #endif 616 | Array.Sort (Include, 0, IncludeCount); 617 | Array.Sort (Exclude, 0, ExcludeCount); 618 | // calculate hash. 619 | unchecked { 620 | Hash = IncludeCount + ExcludeCount; 621 | for (int i = 0, iMax = IncludeCount; i < iMax; i++) { 622 | Hash = Hash * 314159 + Include[i]; 623 | } 624 | for (int i = 0, iMax = ExcludeCount; i < iMax; i++) { 625 | Hash = Hash * 314159 - Exclude[i]; 626 | } 627 | } 628 | var (filter, isNew) = _world.GetFilterInternal (this, capacity); 629 | if (!isNew) { Recycle (); } 630 | return filter; 631 | } 632 | 633 | [MethodImpl (MethodImplOptions.AggressiveInlining)] 634 | void Recycle () { 635 | Reset (); 636 | if (_world._masksCount == _world._masks.Length) { 637 | Array.Resize (ref _world._masks, _world._masksCount << 1); 638 | } 639 | _world._masks[_world._masksCount++] = this; 640 | } 641 | } 642 | } 643 | 644 | #if DEBUG || LEOECSLITE_WORLD_EVENTS 645 | public interface IEcsWorldEventListener { 646 | void OnEntityCreated (int entity); 647 | void OnEntityChanged (int entity, short poolId, bool added); 648 | void OnEntityDestroyed (int entity); 649 | void OnFilterCreated (EcsFilter filter); 650 | void OnWorldResized (int newSize); 651 | void OnWorldDestroyed (EcsWorld world); 652 | } 653 | #endif 654 | } 655 | 656 | #if ENABLE_IL2CPP 657 | // Unity IL2CPP performance optimization attribute. 658 | namespace Unity.IL2CPP.CompilerServices { 659 | enum Option { 660 | NullChecks = 1, 661 | ArrayBoundsChecks = 2 662 | } 663 | 664 | [AttributeUsage (AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] 665 | class Il2CppSetOptionAttribute : Attribute { 666 | public Option Option { get; private set; } 667 | public object Value { get; private set; } 668 | 669 | public Il2CppSetOptionAttribute (Option option, object value) { Option = option; Value = value; } 670 | } 671 | } 672 | #endif 673 | -------------------------------------------------------------------------------- /src/worlds.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e4f8e32372cdd49a899e9ffe82841a20 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | --------------------------------------------------------------------------------