├── .gitignore ├── test.node.js.hxml ├── src └── ecx │ ├── types │ ├── ComponentTable.hx │ ├── EntityData.hx │ ├── TypeInfo.hx │ ├── ServiceType.hx │ ├── ServiceSpec.hx │ ├── ComponentType.hx │ ├── SystemFlags.hx │ ├── TypeManager.hx │ ├── EntityVector.hx │ └── FamilyData.hx │ ├── Wire.hx │ ├── macro │ ├── FamilyRestGeneric.hx │ ├── MacroComponentData.hx │ ├── MacroComponentCache.hx │ ├── MacroServiceCache.hx │ ├── MacroServiceData.hx │ ├── MacroBuildDebug.hx │ ├── ClassMacroTools.hx │ ├── MacroBuildGenerate.hx │ ├── ComponentBuilder.hx │ ├── MacroUtil.hx │ ├── FieldsBuilder.hx │ ├── SystemBuilder.hx │ ├── ServiceBuilder.hx │ └── AutoCompBuilder.hx │ ├── Family.hx │ ├── IComponent.hx │ ├── ds │ ├── CArrayIterator.hx │ ├── Cast.hx │ ├── CInt32Array.hx │ ├── PowerOfTwo.hx │ ├── CInt32RingBuffer.hx │ ├── CBitArray.hx │ └── CArray.hx │ ├── Service.hx │ ├── AutoComp.hx │ ├── Entity.hx │ ├── managers │ ├── WorldDebug.hx │ └── WorldConstructor.hx │ ├── WorldConfig.hx │ ├── Engine.hx │ ├── System.hx │ ├── reporting │ └── EcxBuildReport.hx │ └── World.hx ├── test ├── ecx │ ├── systems │ │ ├── EmptySystem.hx │ │ ├── BaseSystem.hx │ │ ├── CoreSystem.hx │ │ ├── DerivedOneSystem.hx │ │ ├── DerivedTwoSystem.hx │ │ └── MotionSystem.hx │ ├── EcxTest.hx │ ├── WorldTest.hx │ ├── components │ │ ├── Motion.hx │ │ ├── Position.hx │ │ └── Value.hx │ ├── MapToTest.hx │ ├── ServiceHierarchyTest.hx │ ├── FamilyTest.hx │ ├── Environment.hx │ ├── ComponentTest.hx │ ├── PowerOfTwoTest.hx │ ├── EntityVectorTest.hx │ ├── EntityTest.hx │ └── IssuesTest.hx └── TestAll.hx ├── dox.hxml ├── CHANGELOG.md ├── haxelib.json ├── .travis.yml ├── appveyor.yml ├── make └── EcxMake.hx └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /bin/ 3 | 4 | *.iml 5 | .idea/ 6 | /out/ 7 | make.n 8 | -------------------------------------------------------------------------------- /test.node.js.hxml: -------------------------------------------------------------------------------- 1 | -cp src 2 | -lib utest 3 | -js bin/test.node.js 4 | -cmd node bin/test.node.js -------------------------------------------------------------------------------- /src/ecx/types/ComponentTable.hx: -------------------------------------------------------------------------------- 1 | package ecx.types; 2 | 3 | typedef ComponentTable = ecx.ds.CArray; -------------------------------------------------------------------------------- /test/ecx/systems/EmptySystem.hx: -------------------------------------------------------------------------------- 1 | package ecx.systems; 2 | 3 | class EmptySystem extends System { 4 | public function new() {} 5 | } -------------------------------------------------------------------------------- /test/ecx/systems/BaseSystem.hx: -------------------------------------------------------------------------------- 1 | package ecx.systems; 2 | 3 | class BaseSystem extends CoreSystem { 4 | public function new() { 5 | super(); 6 | ok = "BASE_OK"; 7 | } 8 | } -------------------------------------------------------------------------------- /test/ecx/systems/CoreSystem.hx: -------------------------------------------------------------------------------- 1 | package ecx.systems; 2 | 3 | @:core 4 | class CoreSystem extends System { 5 | 6 | public var ok(default, null):String = "OK"; 7 | 8 | public function new() {} 9 | } -------------------------------------------------------------------------------- /test/ecx/systems/DerivedOneSystem.hx: -------------------------------------------------------------------------------- 1 | package ecx.systems; 2 | 3 | @:final 4 | class DerivedOneSystem extends BaseSystem { 5 | 6 | public function new() { 7 | super(); 8 | ok = "OK_1"; 9 | } 10 | } -------------------------------------------------------------------------------- /test/ecx/systems/DerivedTwoSystem.hx: -------------------------------------------------------------------------------- 1 | package ecx.systems; 2 | 3 | @:final 4 | class DerivedTwoSystem extends BaseSystem { 5 | 6 | public function new() { 7 | super(); 8 | ok = "OK_2"; 9 | } 10 | } -------------------------------------------------------------------------------- /src/ecx/Wire.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | /** 4 | Declare fields in Service classes with Wire to inject other Services. 5 | **/ 6 | #if idea 7 | typedef Wire = T; 8 | #else 9 | typedef Wire = T; 10 | #end -------------------------------------------------------------------------------- /test/ecx/EcxTest.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | class EcxTest { 4 | 5 | public var env(default, null):Environment; 6 | public var world(default, null):World; 7 | 8 | public function new() { 9 | env = Environment.get(); 10 | world = env.world; 11 | } 12 | } -------------------------------------------------------------------------------- /src/ecx/macro/FamilyRestGeneric.hx: -------------------------------------------------------------------------------- 1 | package ecx.macro; 2 | 3 | #if macro 4 | 5 | import haxe.macro.Expr; 6 | 7 | @:final 8 | class FamilyRestGeneric { 9 | public static function apply():ComplexType { 10 | return macro : ecx.types.EntityVector; 11 | } 12 | } 13 | 14 | #end -------------------------------------------------------------------------------- /dox.hxml: -------------------------------------------------------------------------------- 1 | -cp src 2 | -debug 3 | -D dox 4 | -dce no 5 | --macro haxe.macro.Compiler.include("", true, [], ["src"]) 6 | -xml bin/ecx.xml 7 | -js bin/none.js 8 | --no-output 9 | --next 10 | -cmd haxelib run dox -i bin/ecx.xml -o bin/api-minimal --toplevel-package ecx 11 | -cmd nekotools server -d bin/api-minimal 12 | -------------------------------------------------------------------------------- /src/ecx/Family.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | #if idea 4 | 5 | typedef Family = Array; 6 | 7 | #else 8 | 9 | /** 10 | Family is a set of Entities with required or optional Component types 11 | **/ 12 | 13 | @:genericBuild(ecx.macro.FamilyRestGeneric.apply()) 14 | class Family {} 15 | 16 | #end 17 | -------------------------------------------------------------------------------- /test/ecx/WorldTest.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import utest.Assert; 4 | 5 | class WorldTest extends EcxTest { 6 | 7 | public function new() { 8 | super(); 9 | } 10 | 11 | public function setup() { 12 | world.invalidate(); 13 | } 14 | 15 | public function testInitialize() { 16 | Assert.notNull(world); 17 | } 18 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.1 2 | 3 | - Basic Haxe 4.0 support 4 | - Remove `power-of-two` dependency 5 | - C++ unsafe cast for Haxe 4.0 (issue #10) 6 | - CI scripts updated 7 | - HashLink target compiles 8 | - EnumTools utility class removed 9 | - Simple `test.node.js.hxml` build file has been added for compilation smoke checks 10 | - `hl` tests on CI -------------------------------------------------------------------------------- /test/ecx/components/Motion.hx: -------------------------------------------------------------------------------- 1 | package ecx.components; 2 | 3 | class Motion extends AutoComp {} 4 | 5 | class MotionData { 6 | 7 | public var vx:Float = 0; 8 | public var vy:Float = 0; 9 | 10 | public function new() {} 11 | 12 | public function copyFrom(data:MotionData) { 13 | vx = data.vx; 14 | vy = data.vy; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/ecx/components/Position.hx: -------------------------------------------------------------------------------- 1 | package ecx.components; 2 | 3 | class Position extends AutoComp {} 4 | 5 | class PositionData { 6 | 7 | public var x:Float = 0; 8 | public var y:Float = 0; 9 | 10 | public function new() {} 11 | 12 | public function copyFrom(data:PositionData) { 13 | x = data.x; 14 | y = data.y; 15 | } 16 | } -------------------------------------------------------------------------------- /src/ecx/types/EntityData.hx: -------------------------------------------------------------------------------- 1 | package ecx.types; 2 | 3 | @:final 4 | @:unreflective 5 | class EntityData { 6 | 7 | public var entity(default, null):Entity; 8 | public var world(default, null):World; 9 | 10 | inline function new(entity:Entity, world:World) { 11 | this.entity = entity; 12 | this.world = world; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecx", 3 | "license": "MIT", 4 | "tags": [ 5 | "ecs", 6 | "entity", 7 | "component", 8 | "system", 9 | "cross" 10 | ], 11 | "classPath": "src", 12 | "description": "Haxe Entity System library", 13 | "contributors": [ 14 | "eliasku" 15 | ], 16 | "releasenote": "Fix compatibility", 17 | "version": "0.1.1", 18 | "url": "https://github.com/eliasku/ecx" 19 | } -------------------------------------------------------------------------------- /test/ecx/systems/MotionSystem.hx: -------------------------------------------------------------------------------- 1 | package ecx.systems; 2 | 3 | import ecx.components.Position; 4 | import ecx.components.Motion; 5 | 6 | class MotionSystem extends System { 7 | 8 | public var entities(default, null):Family; 9 | 10 | public var motion(default, null):Wire; 11 | public var position(default, null):Wire; 12 | 13 | public function new() {} 14 | } 15 | -------------------------------------------------------------------------------- /src/ecx/macro/MacroComponentData.hx: -------------------------------------------------------------------------------- 1 | package ecx.macro; 2 | 3 | #if macro 4 | 5 | @:final 6 | class MacroComponentData { 7 | 8 | static var NEXT_TYPE_ID:Int = 0; 9 | 10 | // Full class path (some.foo.Bar) 11 | public var path(default, null):String; 12 | 13 | // common base type id 14 | public var typeId(default, null):Int; 15 | 16 | public function new(path:String) { 17 | this.path = path; 18 | typeId = NEXT_TYPE_ID++; 19 | } 20 | } 21 | 22 | #end -------------------------------------------------------------------------------- /src/ecx/IComponent.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import ecx.types.ComponentType; 4 | 5 | /** 6 | Base interface for implementing Component type. 7 | **/ 8 | #if !macro 9 | @:autoBuild(ecx.macro.ComponentBuilder.build()) 10 | #end 11 | interface IComponent { 12 | 13 | function destroy(entity:Entity):Void; 14 | function has(entity:Entity):Bool; 15 | function copy(source:Entity, destination:Entity):Void; 16 | 17 | function getObjectSize():Int; 18 | function __componentType():ComponentType; 19 | } 20 | -------------------------------------------------------------------------------- /test/ecx/components/Value.hx: -------------------------------------------------------------------------------- 1 | package ecx.components; 2 | 3 | class Value extends AutoComp {} 4 | 5 | class ValueData { 6 | 7 | var _value:Int = 0; 8 | 9 | public var value(get, set):Int; 10 | 11 | public function new() {} 12 | 13 | function get_value():Int { 14 | return _value; 15 | } 16 | 17 | function set_value(value:Int):Int { 18 | return _value = value; 19 | } 20 | 21 | public function copyFrom(data:ValueData) { 22 | _value = data._value; 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/ecx/ds/CArrayIterator.hx: -------------------------------------------------------------------------------- 1 | package ecx.ds; 2 | 3 | @:final @:unreflective @:dce @:generic 4 | class CArrayIterator { 5 | 6 | public var index:Int; 7 | public var end:Int; 8 | public var array:CArray; 9 | 10 | inline public function new(array:CArray) { 11 | index = 0; 12 | end = array.length; 13 | this.array = array; 14 | } 15 | 16 | inline public function hasNext():Bool { 17 | return index != end; 18 | } 19 | 20 | inline public function next():T { 21 | return array[index++]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ecx/types/TypeInfo.hx: -------------------------------------------------------------------------------- 1 | package ecx.types; 2 | 3 | @:final 4 | @:unreflective 5 | class TypeInfo { 6 | 7 | public var path(default, null):String; 8 | public var basePath(default, null):String; 9 | public var typeId(default, null):Int; 10 | public var specId(default, null):Int; 11 | 12 | function new(path:String, basePath:String, typeId:Int, specId:Int) { 13 | this.path = path; 14 | this.basePath = basePath; 15 | this.typeId = typeId; 16 | this.specId = specId; 17 | } 18 | } -------------------------------------------------------------------------------- /src/ecx/types/ServiceType.hx: -------------------------------------------------------------------------------- 1 | package ecx.types; 2 | 3 | /** [INTERNAL] Service's base type-identifier **/ 4 | 5 | @:dce @:final @:unreflective 6 | abstract ServiceType(Int) { 7 | 8 | public inline static var INVALID:ServiceType = new ServiceType(-1); 9 | 10 | public var id(get, never):Int; 11 | 12 | inline public function new(typeId:Int) { 13 | this = typeId; 14 | } 15 | 16 | inline function get_id():Int { 17 | return this; 18 | } 19 | 20 | inline public function toString() { 21 | return 'SystemType: #$this'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ecx/types/ServiceSpec.hx: -------------------------------------------------------------------------------- 1 | package ecx.types; 2 | 3 | /** [INTERNAL] Service's specialization type-identifier **/ 4 | 5 | @:dce @:final @:unreflective 6 | abstract ServiceSpec(Int) { 7 | 8 | public inline static var INVALID:ServiceSpec = new ServiceSpec(-1); 9 | 10 | public var id(get, never):Int; 11 | 12 | inline public function new(specId:Int) { 13 | this = specId; 14 | } 15 | 16 | inline function get_id():Int { 17 | return this; 18 | } 19 | 20 | inline public function toString() { 21 | return 'Spec: #$this'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ecx/types/ComponentType.hx: -------------------------------------------------------------------------------- 1 | package ecx.types; 2 | 3 | /** [INTERNAL] Component Base Type identifier**/ 4 | 5 | @:dce @:final @:unreflective 6 | abstract ComponentType(Int) { 7 | 8 | public inline static var INVALID:ComponentType = new ComponentType(-1); 9 | 10 | public var id(get, never):Int; 11 | inline function get_id():Int { 12 | return this; 13 | } 14 | 15 | inline public function new(typeId:Int) { 16 | this = typeId; 17 | } 18 | 19 | inline public function toString() { 20 | return 'ComponentType: #$this'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | language: haxe 5 | 6 | os: 7 | - linux 8 | # - osx 9 | 10 | haxe: 11 | - "3.4.7" 12 | - development 13 | 14 | branches: 15 | only: 16 | - develop 17 | 18 | env: 19 | matrix: 20 | - TARGET=js,node,neko 21 | # - TARGET=hl 22 | - TARGET=cpp 23 | - TARGET=cs,java 24 | - TARGET=flash 25 | 26 | matrix: 27 | allow_failures: 28 | - haxe: development 29 | 30 | install: 31 | - haxelib dev ecx . 32 | - haxelib git hxmake https://github.com/eliasku/hxmake.git 33 | 34 | script: 35 | - haxelib run hxmake test --override-test-target=${TARGET} -------------------------------------------------------------------------------- /test/ecx/MapToTest.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import ecx.components.Value; 4 | import utest.Assert; 5 | 6 | @:keep 7 | class MapToTest extends EcxTest { 8 | 9 | public function new() { 10 | super(); 11 | } 12 | 13 | public function testMapTo() { 14 | var values:Value = world.resolve(Value); 15 | 16 | var entity = world.create(); 17 | var emptyEntity = world.create(); 18 | 19 | //var data = world.edit(entity); 20 | var value = values.create(entity); 21 | 22 | //Assert.isTrue(value == values[entity]); 23 | Assert.isTrue(value == values.get(entity)); 24 | Assert.isNull(values.get(emptyEntity)); 25 | } 26 | } -------------------------------------------------------------------------------- /src/ecx/macro/MacroComponentCache.hx: -------------------------------------------------------------------------------- 1 | package ecx.macro; 2 | #if macro 3 | 4 | @:final 5 | class MacroComponentCache { 6 | 7 | public static var cache(default, null):Map = new Map(); 8 | 9 | public static function get(path:String):Null { 10 | return cache.get(path); 11 | } 12 | 13 | public static function set(data:MacroComponentData) { 14 | cache.set(data.path, data); 15 | } 16 | 17 | public static function getBaseTypeId(path:String, basePath:String):Int { 18 | if(path == basePath) { 19 | return -1; 20 | } 21 | var baseTypeData = get(basePath); 22 | if(baseTypeData == null) { 23 | return -1; 24 | } 25 | return baseTypeData.typeId; 26 | } 27 | } 28 | 29 | #end -------------------------------------------------------------------------------- /test/ecx/ServiceHierarchyTest.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import ecx.systems.DerivedTwoSystem; 4 | import ecx.systems.BaseSystem; 5 | import utest.Assert; 6 | 7 | class ServiceHierarchyTest extends EcxTest { 8 | 9 | public function new() { 10 | super(); 11 | } 12 | 13 | public function setup() { 14 | world.invalidate(); 15 | } 16 | 17 | public function testResolve() { 18 | var base:BaseSystem = world.resolve(BaseSystem); 19 | var derivedTwo:DerivedTwoSystem = world.resolve(DerivedTwoSystem); 20 | Assert.notNull(base); 21 | Assert.notNull(derivedTwo); 22 | Assert.isTrue(base == derivedTwo); 23 | Assert.equals(base.ok, "OK_2"); 24 | 25 | // TODO: 26 | // var derivedOne = world.resolve(DerivedOneSystem); 27 | // Assert.isNull(derivedOne); 28 | } 29 | } -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | 3 | environment: 4 | global: 5 | HAXELIB_ROOT: C:\projects\haxelib 6 | matrix: 7 | - TARGET: js,node,neko 8 | # - TARGET: hl 9 | - TARGET: cpp 10 | - TARGET: cs,java 11 | - TARGET: flash 12 | 13 | matrix: 14 | fast_finish: true 15 | 16 | branches: 17 | only: 18 | - develop 19 | 20 | install: 21 | - ps: Set-Service wuauserv -StartupType Manual 22 | - cinst neko --version 2.2.0 -y 23 | - cinst haxe --version 3.4.7 --ignore-dependencies -y 24 | - RefreshEnv 25 | - mkdir "%HAXELIB_ROOT%" 26 | - haxelib setup "%HAXELIB_ROOT%" 27 | - haxelib git hxmake https://github.com/eliasku/hxmake.git 28 | - haxelib dev ecx . 29 | 30 | build: off 31 | 32 | test_script: 33 | - haxelib run hxmake test --override-test-target="%TARGET%" 34 | -------------------------------------------------------------------------------- /src/ecx/ds/Cast.hx: -------------------------------------------------------------------------------- 1 | package ecx.ds; 2 | 3 | @:final 4 | @:unreflective 5 | @:dce 6 | class Cast { 7 | 8 | @:extern inline public static function unsafe(value:TIn, clazz:Class):TClass { 9 | #if (cpp && haxe_ver >= 3.3) 10 | return (cpp.Pointer.fromRaw(cpp.Pointer.addressOf(value).rawCast()):cpp.Pointer).value; 11 | #else 12 | return cast value; 13 | #end 14 | } 15 | 16 | @:unreflective // no using / generic ? 17 | @:extern inline public static function unsafe_T(value:TIn):TOut { 18 | #if (cpp && haxe_ver >= 3.3) 19 | return (cpp.Pointer.fromRaw(cpp.Pointer.addressOf(value).rawCast()):cpp.Pointer).value; 20 | #else 21 | return cast value; 22 | #end 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ecx/macro/MacroServiceCache.hx: -------------------------------------------------------------------------------- 1 | package ecx.macro; 2 | 3 | #if macro 4 | 5 | @:final 6 | class MacroServiceCache { 7 | 8 | public static var cache(default, null):Map = new Map(); 9 | 10 | public static function get(path:String):Null { 11 | return cache.get(path); 12 | } 13 | 14 | public static function getBaseTypeId(path:String, basePath:String):Int { 15 | if(path == basePath) { 16 | return -1; 17 | } 18 | var baseTypeData = get(basePath); 19 | if(baseTypeData == null) { 20 | return -1; 21 | } 22 | return baseTypeData.typeId; 23 | } 24 | 25 | public static function set(data:MacroServiceData) { 26 | cache.set(data.path, data); 27 | } 28 | } 29 | 30 | #end -------------------------------------------------------------------------------- /src/ecx/types/SystemFlags.hx: -------------------------------------------------------------------------------- 1 | package ecx.types; 2 | 3 | @:dce @:final @:unreflective 4 | @:enum abstract SystemFlags(Int) { 5 | 6 | // system is not a part of game loop, update method is not called every frame 7 | var IDLE = 2; 8 | 9 | // system only initialize something, so it will be removed after initialization 10 | var CONFIG = 4; 11 | 12 | inline public function new(bits:Int = 0) { 13 | this = bits; 14 | } 15 | 16 | inline public function has(flags:SystemFlags):Bool { 17 | return (this & flags.value) != 0; 18 | } 19 | 20 | inline public function add(flags:SystemFlags):SystemFlags { 21 | return new SystemFlags(this | flags.value); 22 | } 23 | 24 | public var value(get, never):Int; 25 | inline function get_value():Int { 26 | return this; 27 | } 28 | } -------------------------------------------------------------------------------- /src/ecx/ds/CInt32Array.hx: -------------------------------------------------------------------------------- 1 | package ecx.ds; 2 | 3 | #if js 4 | private typedef CInt32ArrayData = js.html.Int32Array; 5 | #else 6 | private typedef CInt32ArrayData = CArray; 7 | #end 8 | 9 | @:generic 10 | @:final 11 | @:unreflective 12 | @:dce 13 | abstract CInt32Array(CInt32ArrayData) { 14 | 15 | public var length(get, never):Int; 16 | 17 | inline public function new(length:Int) { 18 | this = new CInt32ArrayData(length); 19 | } 20 | 21 | @:arrayAccess 22 | inline public function get(index:Int):Int { 23 | return this[index]; 24 | } 25 | 26 | @:arrayAccess 27 | inline public function set(index:Int, element:Int):Void { 28 | this[index] = element; 29 | } 30 | 31 | inline public function getObjectSize():Int { 32 | return this.length << 2; 33 | } 34 | 35 | inline function get_length() { 36 | return this.length; 37 | } 38 | } -------------------------------------------------------------------------------- /test/ecx/FamilyTest.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import utest.Assert; 4 | import ecx.systems.MotionSystem; 5 | 6 | class FamilyTest extends EcxTest { 7 | 8 | public function new() { 9 | super(); 10 | } 11 | 12 | public function testCommit() { 13 | var ms:MotionSystem = world.resolve(MotionSystem); 14 | 15 | Assert.notNull(ms); 16 | Assert.notNull(ms.entities); 17 | Assert.notNull(ms.position); 18 | Assert.notNull(ms.motion); 19 | 20 | var entity = world.create(); 21 | ms.motion.create(entity); 22 | ms.position.create(entity); 23 | world.commit(entity); 24 | world.invalidate(); 25 | 26 | Assert.equals(1, ms.entities.length); 27 | Assert.equals(entity.id, ms.entities.get(0)); 28 | 29 | ms.motion.destroy(entity); 30 | world.commit(entity); 31 | world.invalidate(); 32 | Assert.equals(0, ms.entities.length); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ecx/Service.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import ecx.types.ServiceSpec; 4 | import ecx.types.ServiceType; 5 | 6 | /** 7 | Service is injectable world-scope type. 8 | 9 | Initialization steps: 10 | - services are instantiated by passing to world-configuration 11 | - services are wired with each other 12 | - services are initialized 13 | - systems are able to be updated (if not IDLE) 14 | 15 | @see ecx.Wire 16 | **/ 17 | 18 | #if !macro 19 | @:autoBuild(ecx.macro.ServiceBuilder.build()) 20 | #end 21 | @:core 22 | class Service { 23 | 24 | var world(default, null):World; 25 | 26 | function initialize() {} 27 | 28 | function __serviceType():ServiceType { 29 | return ServiceType.INVALID; 30 | } 31 | 32 | function __serviceSpec():ServiceSpec { 33 | return ServiceSpec.INVALID; 34 | } 35 | 36 | function __allocate() {} 37 | function __inject() {} 38 | } 39 | -------------------------------------------------------------------------------- /src/ecx/AutoComp.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import ecx.types.ComponentType; 4 | 5 | /** 6 | Shortcut to define Components, see examples on how to use it. 7 | **/ 8 | #if !macro 9 | @:autoBuild(ecx.macro.AutoCompBuilder.build()) 10 | #end 11 | @:core 12 | class AutoComp extends Service implements IComponent { 13 | 14 | #if idea 15 | public function get(entity:Entity):T return null; 16 | public function set(entity:Entity, component:T) {} 17 | public function create(entity:Entity):T return null; 18 | #end 19 | 20 | public function has(entity:Entity):Bool { 21 | return false; 22 | } 23 | 24 | public function destroy(entity:Entity) {} 25 | public function copy(source:Entity, destination:Entity) {} 26 | 27 | public function __componentType() { 28 | return ComponentType.INVALID; 29 | } 30 | 31 | public function getObjectSize():Int { 32 | return 0; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/ecx/Environment.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import ecx.systems.DerivedTwoSystem; 4 | import ecx.systems.EmptySystem; 5 | import ecx.systems.MotionSystem; 6 | import ecx.components.Motion; 7 | import ecx.components.Value; 8 | import ecx.components.Position; 9 | 10 | class Environment { 11 | static var _current:Environment; 12 | 13 | public static function get() { 14 | if(_current == null) { 15 | _current = new Environment(); 16 | } 17 | return _current; 18 | } 19 | 20 | public var world(default, null):World; 21 | 22 | public function new() { 23 | var config = new WorldConfig(); 24 | 25 | config.add(new EmptySystem()); 26 | config.add(new DerivedTwoSystem()); 27 | config.add(new MotionSystem()); 28 | 29 | config.add(new Value()); 30 | config.add(new Motion()); 31 | config.add(new Position()); 32 | 33 | world = Engine.createWorld(config, 1000); 34 | } 35 | } -------------------------------------------------------------------------------- /src/ecx/ds/PowerOfTwo.hx: -------------------------------------------------------------------------------- 1 | package ecx.ds; 2 | 3 | /** 4 | Power Of Two integers utility 5 | **/ 6 | @:final 7 | @:unreflective 8 | @:dce 9 | class PowerOfTwo { 10 | 11 | /** Returns the next power of two. */ 12 | public static function next(x:Int):Int { 13 | x |= x >> 1; 14 | x |= x >> 2; 15 | x |= x >> 4; 16 | x |= x >> 8; 17 | x |= x >> 16; 18 | return x + 1; 19 | } 20 | 21 | /** Checks if value is power of two **/ 22 | public static function check(x:Int):Bool { 23 | return x != 0 && (x & (x - 1)) == 0; 24 | } 25 | 26 | /** 27 | Returns the specified value if the value is already a power of two. 28 | Returns next power of two else. 29 | **/ 30 | public static function require(x:Int):Int { 31 | if (x == 0) { 32 | return 1; 33 | } 34 | --x; 35 | x |= x >> 1; 36 | x |= x >> 2; 37 | x |= x >> 4; 38 | x |= x >> 8; 39 | x |= x >> 16; 40 | return x + 1; 41 | } 42 | } -------------------------------------------------------------------------------- /src/ecx/Entity.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | /** 4 | Entity handle (just integer id) 5 | - `0` entity id is reserved as INVALID 6 | - Components slot `0` could be used for storing internal information 7 | **/ 8 | @:dce @:final @:unreflective 9 | abstract Entity(Int) { 10 | 11 | /** Index reserved for null-entity **/ 12 | public static inline var ID_NULL:Int = 0; 13 | 14 | /** Null-entity constant **/ 15 | public static inline var NULL:Entity = new Entity(ID_NULL); 16 | 17 | /** Entity handle ID **/ 18 | public var id(get, never):Int; 19 | 20 | inline function new(id:Int) { 21 | this = id; 22 | } 23 | 24 | inline public function notNull():Bool { 25 | return id != 0; 26 | } 27 | 28 | inline public function isNull():Bool { 29 | return id == 0; 30 | } 31 | 32 | inline function get_id():Int { 33 | #if js 34 | return this | 0; 35 | #else 36 | return this; 37 | #end 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ecx/macro/MacroServiceData.hx: -------------------------------------------------------------------------------- 1 | package ecx.macro; 2 | 3 | #if macro 4 | 5 | @:final 6 | class MacroServiceData { 7 | 8 | static var NEXT_TYPE_ID:Int = 0; 9 | static var NEXT_SPEC_ID:Int = 0; 10 | 11 | // Base Family full name (some.foo.Bar) 12 | public var basePath(default, null):String; 13 | 14 | // Full class path (some.foo.Bar) 15 | public var path(default, null):String; 16 | 17 | // common base type id 18 | public var typeId(default, null):Int; 19 | 20 | // specific unique type index for implementations 21 | public var specId(default, null):Int; 22 | 23 | public var isBase(default, null):Bool; 24 | 25 | public function new(basePath:String, path:String, baseTypeId:Int = -1) { 26 | this.basePath = basePath; 27 | this.path = path; 28 | typeId = baseTypeId >= 0 ? baseTypeId : (NEXT_TYPE_ID++); 29 | specId = NEXT_SPEC_ID++; 30 | isBase = basePath == path; 31 | } 32 | } 33 | 34 | #end -------------------------------------------------------------------------------- /test/ecx/ComponentTest.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import ecx.components.Value; 4 | import utest.Assert; 5 | 6 | class ComponentTest extends EcxTest { 7 | 8 | public function new() { 9 | super(); 10 | } 11 | 12 | public function setup() { 13 | world.invalidate(); 14 | } 15 | 16 | public function testComponentCreate() { 17 | var e = world.create(); 18 | var value:Value = world.resolve(Value); 19 | 20 | var v:ValueData = value.create(e); 21 | Assert.notNull(v); 22 | // Assert.equals(e.id, v.entity.id); 23 | // Assert.equals(world, v.world); 24 | 25 | v.value = 10; 26 | Assert.equals(10, v.value); 27 | 28 | world.destroy(e); 29 | 30 | world.invalidate(); 31 | 32 | // Assert.isNull(v.world); 33 | // Assert.isTrue(!v.entity); 34 | } 35 | 36 | public function testComponentDelete() { 37 | var e = world.create(); 38 | var value:Value = world.resolve(Value); 39 | var v:ValueData = value.create(e); 40 | Assert.notNull(v); 41 | 42 | value.destroy(e); 43 | 44 | var noValue:ValueData = value.get(e); 45 | Assert.isNull(noValue); 46 | 47 | world.destroy(e); 48 | } 49 | } -------------------------------------------------------------------------------- /src/ecx/macro/MacroBuildDebug.hx: -------------------------------------------------------------------------------- 1 | package ecx.macro; 2 | 3 | #if macro 4 | 5 | @:final 6 | class MacroBuildDebug { 7 | 8 | static var _depth:Int = 0; 9 | 10 | public static function begin() { 11 | ++_depth; 12 | } 13 | 14 | public static function end() { 15 | --_depth; 16 | } 17 | 18 | public static function printSystem(data:MacroServiceData) { 19 | #if ecx_debug 20 | var prefix = indent("-", _depth - 1) + ">"; 21 | var base = data.isBase ? "" : ' : ${data.basePath}'; 22 | var kind = "(S)"; 23 | trace('$prefix $kind type-${data.typeId} spec-${data.specId} ${data.path}$base'); 24 | #end 25 | } 26 | 27 | public static function printComponent(data:MacroComponentData) { 28 | #if ecx_debug 29 | var prefix = indent("-", _depth - 1) + ">"; 30 | var kind = "[C]"; 31 | trace('$prefix $kind #${data.typeId} ${data.path}'); 32 | #end 33 | } 34 | 35 | static function indent(symbol:String, amount:Int) { 36 | return StringTools.rpad("", symbol, amount); 37 | } 38 | } 39 | 40 | #end -------------------------------------------------------------------------------- /src/ecx/macro/ClassMacroTools.hx: -------------------------------------------------------------------------------- 1 | package ecx.macro; 2 | 3 | #if macro 4 | 5 | import ecx.types.ServiceType; 6 | import ecx.types.ServiceSpec; 7 | import ecx.types.ComponentType; 8 | import haxe.macro.Expr; 9 | 10 | @:final 11 | class ClassMacroTools { 12 | 13 | public static function componentType(componentClass:ExprOf>):ExprOf { 14 | return macro #if !ecx_macro_debug @:pos($v{componentClass.pos}) #end $componentClass.__COMPONENT; 15 | } 16 | 17 | public static function serviceType(serviceClass:ExprOf>):ExprOf { 18 | return macro #if !ecx_macro_debug @:pos($v{serviceClass.pos}) #end $serviceClass.__TYPE; 19 | } 20 | 21 | public static function serviceSpec(serviceClass:ExprOf>):ExprOf { 22 | return macro #if !ecx_macro_debug @:pos($v{serviceClass.pos}) #end $serviceClass.__SPEC; 23 | } 24 | 25 | public static function componentTypeList(componentClasses:Array>>):ExprOf> { 26 | var types = [ for(cls in componentClasses) componentType(cls) ]; 27 | return macro $a{types}; 28 | } 29 | } 30 | 31 | #end 32 | -------------------------------------------------------------------------------- /src/ecx/managers/WorldDebug.hx: -------------------------------------------------------------------------------- 1 | package ecx.managers; 2 | 3 | #if ecx_debug 4 | 5 | class WorldDebug { 6 | 7 | public static function guardEntity(world:World, entity:Entity) { 8 | if(entity.isNull()) throw "Invalid entity"; 9 | if(!world.checkAlive(entity)) throw "Dead entity"; 10 | } 11 | 12 | @:access(ecx.World) 13 | public static function guardFamilies(world:World) { 14 | for(i in 0...world._families.length) { 15 | var family = world._families.get(i); 16 | for(entity in family.entities) { 17 | if(entity.isNull()) throw 'FAMILY GUARD: Invalid entity id: ${entity.id}'; 18 | if(!world.checkAlive(entity)) throw 'FAMILY GUARD: ${entity.id} is dead, but in family ${family.entities.length}}'; 19 | } 20 | } 21 | } 22 | 23 | @:access(ecx.World) 24 | public static function makeFamiliesMutable(world:World) { 25 | for(i in 0...world._families.length) { 26 | world._families.get(i).debugMakeMutable(); 27 | } 28 | } 29 | 30 | @:access(ecx.World) 31 | public static function makeFamiliesImmutable(world:World) { 32 | for(i in 0...world._families.length) { 33 | world._families.get(i).debugMakeImmutable(); 34 | } 35 | } 36 | } 37 | 38 | #end -------------------------------------------------------------------------------- /test/ecx/PowerOfTwoTest.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import ecx.ds.PowerOfTwo; 4 | import utest.Assert; 5 | 6 | class PowerOfTwoTest { 7 | 8 | public function new() {} 9 | 10 | public function testCheck() { 11 | // smoke tests 12 | Assert.isTrue(PowerOfTwo.check(0x1)); 13 | Assert.isTrue(PowerOfTwo.check(0x1)); 14 | Assert.isTrue(PowerOfTwo.check(0x2)); 15 | Assert.isTrue(PowerOfTwo.check(0x4)); 16 | Assert.isTrue(PowerOfTwo.check(0x8)); 17 | Assert.isTrue(PowerOfTwo.check(0x10)); 18 | Assert.isTrue(PowerOfTwo.check(0x100)); 19 | 20 | Assert.isFalse(PowerOfTwo.check(0)); 21 | Assert.isFalse(PowerOfTwo.check(0xFF)); 22 | Assert.isFalse(PowerOfTwo.check(0xF)); 23 | Assert.isFalse(PowerOfTwo.check(0x3)); 24 | } 25 | 26 | public function testRequire() { 27 | Assert.equals(1, PowerOfTwo.require(0)); 28 | Assert.equals(0x100, PowerOfTwo.require(0xFF)); 29 | Assert.equals(0x100, PowerOfTwo.require(0x100)); 30 | } 31 | 32 | public function testNext() { 33 | Assert.equals(1, PowerOfTwo.next(0)); 34 | Assert.equals(16, PowerOfTwo.next(10)); 35 | Assert.equals(512, PowerOfTwo.next(256)); 36 | Assert.equals(256, PowerOfTwo.next(255)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /make/EcxMake.hx: -------------------------------------------------------------------------------- 1 | import hxmake.test.SetupTask; 2 | import hxmake.test.TestTask; 3 | import hxmake.haxelib.HaxelibExt; 4 | import hxmake.idea.IdeaPlugin; 5 | import hxmake.haxelib.HaxelibPlugin; 6 | 7 | using hxmake.haxelib.HaxelibPlugin; 8 | 9 | class EcxMake extends hxmake.Module { 10 | 11 | function new() { 12 | config.classPath = ["src"]; 13 | config.testPath = ["test"]; 14 | config.devDependencies = [ 15 | "utest" => "haxelib" 16 | ]; 17 | 18 | apply(HaxelibPlugin); 19 | apply(IdeaPlugin); 20 | 21 | this.library(function(ext:HaxelibExt) { 22 | ext.config.version = "0.1.1"; 23 | ext.config.description = "Haxe Entity System library"; 24 | ext.config.url = "https://github.com/eliasku/ecx"; 25 | ext.config.tags = ["entity", "component", "system", "cross"]; 26 | ext.config.contributors = ["eliasku"]; 27 | ext.config.license = "MIT"; 28 | ext.config.releasenote = "Fix compatibility"; 29 | 30 | ext.pack.includes = ["src", "haxelib.json", "README.md", "CHANGELOG.md"]; 31 | }); 32 | 33 | var testTask = new TestTask(); 34 | testTask.debug = true; 35 | testTask.targets = ["neko", "swf", "node", "js", "cpp", "java", "cs"]; 36 | testTask.libraries = ["ecx"]; 37 | testTask.defines.push("eval-stack"); 38 | testTask.defines.push("ecx_debug"); 39 | task("test", testTask); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/ecx/EntityVectorTest.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import utest.Assert; 4 | import ecx.types.EntityVector; 5 | 6 | class EntityVectorTest { 7 | 8 | public function new() {} 9 | 10 | public function testEnsure() { 11 | var vec = new EntityVector(2); 12 | vec.ensure(2); 13 | Assert.isTrue(vec.buffer.length > 2); 14 | } 15 | 16 | @:access(ecx.Entity) 17 | public function testPush() { 18 | var vec = new EntityVector(2); 19 | vec.push(new Entity(0)); 20 | vec.push(new Entity(1)); 21 | vec.push(new Entity(2)); 22 | vec.push(new Entity(3)); 23 | 24 | Assert.equals(4, vec.length); 25 | Assert.equals(3, vec.get(3).id); 26 | } 27 | 28 | @:access(ecx.Entity) 29 | public function testReset() { 30 | var vec = new EntityVector(2); 31 | vec.push(new Entity(0)); 32 | vec.push(new Entity(1)); 33 | vec.push(new Entity(2)); 34 | vec.push(new Entity(3)); 35 | 36 | Assert.equals(4, vec.length); 37 | vec.reset(); 38 | Assert.equals(0, vec.length); 39 | } 40 | 41 | @:access(ecx.Entity) 42 | public function testIterator() { 43 | var vec = new EntityVector(100); 44 | var elements = []; 45 | vec.push(new Entity(0)); 46 | vec.push(new Entity(1)); 47 | vec.push(new Entity(2)); 48 | vec.push(new Entity(3)); 49 | 50 | var str = ""; 51 | for(el in vec) { 52 | str += el.id + ","; 53 | } 54 | 55 | Assert.equals("0,1,2,3,", str); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ecx/ds/CInt32RingBuffer.hx: -------------------------------------------------------------------------------- 1 | package ecx.ds; 2 | 3 | /** https://github.com/zeliard/Dispatcher/blob/master/JobDispatcher/ObjectPool.h **/ 4 | 5 | @:unreflective 6 | @:final 7 | class CInt32RingBuffer { 8 | 9 | public var length(get, never):Int; 10 | 11 | var _buffer:CInt32Array; 12 | var _mask:Int; 13 | var _head:Int = 0; 14 | var _tail:Int = 0; 15 | 16 | public function new(capacity:Int) { 17 | _mask = capacity - 1; 18 | #if ecx_debug 19 | if(capacity == 0) throw 'non-zero capacity is required'; 20 | if((_mask & capacity) != 0) throw 'capacity $capacity must be power of two'; 21 | #end 22 | _buffer = new CInt32Array(capacity); 23 | } 24 | 25 | inline public function set(index:Int, value:Int) { 26 | _buffer[index] = value; 27 | } 28 | 29 | public function pop():Int { 30 | var popAt = _head; 31 | _head = popAt + 1; 32 | _head &= _mask; 33 | return _buffer[popAt]; 34 | } 35 | 36 | public function push(value:Int) { 37 | var placeAt = _tail; 38 | _buffer[placeAt] = value; 39 | ++placeAt; 40 | _tail = placeAt & _mask; 41 | } 42 | 43 | public function getObjectSize():Int { 44 | return _buffer.getObjectSize() + 16; 45 | } 46 | 47 | inline function get_length() { 48 | return _buffer.length; 49 | } 50 | } -------------------------------------------------------------------------------- /src/ecx/WorldConfig.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | /** 4 | Config for `World` instantiation. 5 | Extend this class to define your own `Plugins` 6 | **/ 7 | @:unreflective 8 | class WorldConfig { 9 | 10 | var _services:Array = []; 11 | var _priorities:Array = []; 12 | 13 | /** 14 | Create empty `WorldConfig` 15 | **/ 16 | public function new() {} 17 | 18 | /** 19 | Adds `service` instance to config. It could be `Component`/`System`/`Service`. 20 | `priority` is optional value for `System` instances. 21 | Systems with the lowest priority running first. 22 | **/ 23 | public function add(service:Service, priority:Int = 0):WorldConfig { 24 | if(service == null) throw "WorldConfig: service should be not null"; 25 | if(_services.lastIndexOf(service) >= 0) throw "WorldConfig: service duplicated"; 26 | 27 | _services.push(service); 28 | _priorities.push(priority); 29 | 30 | return this; 31 | } 32 | 33 | /** 34 | Include plugin `WorldConfig` instance. 35 | **/ 36 | public function include(config:WorldConfig):WorldConfig { 37 | if(config == null) throw "WorldConfig: config should be not null"; 38 | if(config == this) throw "WorldConfig: should not include self"; 39 | 40 | var services = config._services; 41 | var priorities = config._priorities; 42 | for(i in 0...services.length) { 43 | add(services[i], priorities[i]); 44 | } 45 | 46 | return this; 47 | } 48 | } -------------------------------------------------------------------------------- /src/ecx/Engine.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import ecx.types.TypeManager; 4 | 5 | /** 6 | Engine store global data about types and manage world's allocation 7 | **/ 8 | @:unreflective 9 | @:final 10 | @:access(ecx.World) 11 | class Engine { 12 | 13 | /** Types information **/ 14 | public static var types(get, never):TypeManager; 15 | 16 | /** Total worlds allocated **/ 17 | public static var worldsTotal(get, never):Int; 18 | 19 | /** 20 | Create new world. 21 | `config` is required. 22 | `capacity` is max count of entities. 23 | **/ 24 | public static function createWorld(config:WorldConfig, capacity:Int = 0x10000):World { 25 | var world = new World(_worlds.length, config, capacity); 26 | _worlds[world.id] = world; 27 | return world; 28 | } 29 | 30 | /** Get world by `index` **/ 31 | inline public static function getWorld(index:Int):World { 32 | return _worlds[index]; 33 | } 34 | 35 | /** 36 | Theoretic memory consuming in bytes 37 | **/ 38 | public function getObjectSize():Int { 39 | var total = 0; 40 | for(world in _worlds) { 41 | total += world.getObjectSize(); 42 | } 43 | return total; 44 | } 45 | 46 | static var _worlds:Array = []; 47 | static var _types:TypeManager; 48 | 49 | inline static function get_worldsTotal() { 50 | return _worlds.length; 51 | } 52 | 53 | inline static function get_types() { 54 | if(_types == null) { 55 | _types = new TypeManager(); 56 | } 57 | return _types; 58 | } 59 | } -------------------------------------------------------------------------------- /test/ecx/EntityTest.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import utest.Assert; 4 | 5 | class EntityTest extends EcxTest { 6 | 7 | public function new() { 8 | super(); 9 | } 10 | 11 | public function setup() { 12 | world.invalidate(); 13 | } 14 | 15 | public function testInitialize() { 16 | Assert.notNull(world); 17 | } 18 | 19 | public function testEntityCreateDelete() { 20 | var e = world.create(); 21 | var inactiveEntity = world.createPassive(); 22 | 23 | Assert.isTrue(e.notNull()); 24 | Assert.isTrue(inactiveEntity.notNull()); 25 | 26 | Assert.isTrue(world.checkAlive(e)); 27 | Assert.isTrue(world.checkAlive(inactiveEntity)); 28 | 29 | Assert.isTrue(world.isActive(e)); 30 | Assert.isFalse(world.isActive(inactiveEntity)); 31 | 32 | world.destroy(e); 33 | world.destroy(inactiveEntity); 34 | 35 | world.invalidate(); 36 | 37 | Assert.isFalse(world.checkAlive(e)); 38 | Assert.isFalse(world.checkAlive(inactiveEntity)); 39 | 40 | Assert.isFalse(world.isActive(e)); 41 | Assert.isFalse(world.isActive(inactiveEntity)); 42 | 43 | // Assert.equals(world, e.world); 44 | // Assert.equals(world, inactiveEntity.world); 45 | 46 | // Assert.isTrue(e.alive); 47 | // Assert.isTrue(inactiveEntity.alive); 48 | 49 | // Assert.isTrue(e.active); 50 | // Assert.isFalse(inactiveEntity.active); 51 | 52 | // e.delete(); 53 | // inactiveEntity.delete(); 54 | 55 | // world.invalidate(); 56 | // 57 | // Assert.isFalse(e.active); 58 | // Assert.isFalse(e.alive); 59 | // 60 | // Assert.isFalse(inactiveEntity.active); 61 | // Assert.isFalse(inactiveEntity.alive); 62 | } 63 | } -------------------------------------------------------------------------------- /src/ecx/macro/MacroBuildGenerate.hx: -------------------------------------------------------------------------------- 1 | package ecx.macro; 2 | 3 | #if macro 4 | 5 | import haxe.macro.Context; 6 | import haxe.macro.Type; 7 | 8 | @:final 9 | class MacroBuildGenerate { 10 | 11 | static var _callbackAdded:Bool = false; 12 | 13 | public static function invoke() { 14 | if(_callbackAdded == false) { 15 | Context.onGenerate(process); 16 | _callbackAdded = true; 17 | } 18 | } 19 | 20 | static function process(types:Array) { 21 | var metaAccess:MetaAccess = switch(Context.getType("ecx.types.TypeManager")) { 22 | case Type.TInst(cl, _): cl.get().meta; 23 | default: throw "type not found"; 24 | }; 25 | 26 | var exprs = []; 27 | for(systemData in MacroServiceCache.cache) { 28 | exprs.push(macro $v{systemData.path}); 29 | exprs.push(macro $v{systemData.basePath}); 30 | exprs.push(macro $v{systemData.typeId}); 31 | exprs.push(macro $v{systemData.specId}); 32 | } 33 | metaAccess.add("systems", exprs, Context.currentPos()); 34 | 35 | #if ecx_report 36 | ecx.reporting.EcxBuildReport.save(); 37 | #end 38 | // 39 | // var exprs = []; 40 | // for(componentData in MacroComponentCache.cache) { 41 | // exprs.push(macro $v{componentData.path}); 42 | // exprs.push(macro $v{componentData.basePath}); 43 | // exprs.push(macro $v{componentData.typeId}); 44 | // exprs.push(macro $v{componentData.specId}); 45 | // } 46 | // metaAccess.add("components", exprs, Context.currentPos()); 47 | } 48 | } 49 | 50 | #end -------------------------------------------------------------------------------- /test/TestAll.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | 3 | import ecx.PowerOfTwoTest; 4 | import ecx.EntityVectorTest; 5 | import ecx.ComponentTest; 6 | import ecx.EntityTest; 7 | import ecx.FamilyTest; 8 | import ecx.IssuesTest; 9 | import ecx.MapToTest; 10 | import ecx.ServiceHierarchyTest; 11 | import ecx.WorldTest; 12 | import utest.Runner; 13 | import utest.TestResult; 14 | import utest.ui.Report; 15 | 16 | class TestAll { 17 | 18 | public static function main() { 19 | var runner = new Runner(); 20 | addTests(runner); 21 | run(runner); 22 | } 23 | 24 | static function addTests(runner:Runner) { 25 | runner.addCase(new PowerOfTwoTest()); 26 | runner.addCase(new WorldTest()); 27 | runner.addCase(new EntityTest()); 28 | runner.addCase(new ComponentTest()); 29 | runner.addCase(new MapToTest()); 30 | runner.addCase(new IssuesTest()); 31 | runner.addCase(new ServiceHierarchyTest()); 32 | runner.addCase(new FamilyTest()); 33 | runner.addCase(new EntityVectorTest()); 34 | } 35 | 36 | static function run(runner:Runner) { 37 | Report.create(runner); 38 | 39 | // get test result to determine exit status 40 | var isOk:Bool = true; 41 | runner.onProgress.add(function(o) { 42 | isOk = isAllOk(o.result) && isOk; 43 | }); 44 | runner.onComplete.add(function(r) { 45 | var exitCode = isOk ? 0 : -1; 46 | 47 | #if flash 48 | flash.system.System.exit(exitCode); 49 | #end 50 | 51 | #if js 52 | trace("" + exitCode); 53 | #end 54 | }); 55 | 56 | runner.run(); 57 | } 58 | 59 | static function isAllOk(result:TestResult):Bool { 60 | for (l in result.assertations) { 61 | switch (l){ 62 | case Success(_): 63 | default: return false; 64 | } 65 | } 66 | return true; 67 | } 68 | } -------------------------------------------------------------------------------- /src/ecx/types/TypeManager.hx: -------------------------------------------------------------------------------- 1 | package ecx.types; 2 | 3 | @:final 4 | @:unreflective 5 | @:access(ecx.types.TypeInfo) 6 | class TypeManager { 7 | 8 | public var components(default, null):Array = []; 9 | public var componentsTotal(default, null):Int; 10 | // public var systems(default, null):Array = []; 11 | // public var lookup(default, null):Map = new Map(); 12 | 13 | public function new() { 14 | var meta = haxe.rtti.Meta.getType(TypeManager); 15 | var systemsData:Array = Reflect.field(meta, "systems"); 16 | var componentsData:Array = Reflect.field(meta, "components"); 17 | if(systemsData == null) { 18 | systemsData = []; 19 | } 20 | if(componentsData == null) { 21 | componentsData = []; 22 | } 23 | 24 | var i = 0; 25 | var maxComponentTypeId = -1; 26 | while(i < componentsData.length) { 27 | var path = componentsData[i]; 28 | var basePath = componentsData[i + 1]; 29 | var typeId = componentsData[i + 2]; 30 | var specId = componentsData[i + 3]; 31 | 32 | if(typeId > maxComponentTypeId) { 33 | maxComponentTypeId = typeId; 34 | } 35 | 36 | i += 4; 37 | } 38 | 39 | componentsTotal = maxComponentTypeId + 1; 40 | } 41 | 42 | // public function getTypeInfoByComponentType(componentType:ComponentType):TypeInfo { 43 | // for(typeInfo in components) { 44 | // if(typeInfo.typeId == componentType.id) { 45 | // return typeInfo; 46 | // } 47 | // } 48 | // throw 'Component type-info with type: ${componentType.id} not found'; 49 | // } 50 | } 51 | -------------------------------------------------------------------------------- /src/ecx/macro/ComponentBuilder.hx: -------------------------------------------------------------------------------- 1 | package ecx.macro; 2 | 3 | #if macro 4 | 5 | import haxe.macro.Context; 6 | import haxe.macro.Expr; 7 | import haxe.macro.Type; 8 | 9 | class ComponentBuilder { 10 | 11 | public static function build():Array { 12 | var cls:ClassType = Context.getLocalClass().get(); 13 | var pos = Context.currentPos(); 14 | var fields:Array = Context.getBuildFields(); 15 | var implementation:Bool = MacroUtil.hasInterface(cls, "ecx.IComponent"); 16 | 17 | var compData = getComponentData(cls); 18 | if(compData == null) { 19 | return null; 20 | } 21 | 22 | MacroBuildDebug.printComponent(compData); 23 | var typeId = Context.makeExpr(compData.typeId, pos); 24 | var exprs = null; 25 | if(implementation) { 26 | exprs = macro function public_X__componentType() { return new ecx.types.ComponentType($typeId); }; 27 | } 28 | else { 29 | exprs = macro function public_Xoverride_X__componentType() { return new ecx.types.ComponentType($typeId); }; 30 | } 31 | FieldsBuilder.buildAndPush(fields, exprs); 32 | 33 | var componentExpr = macro var public_Xstatic_Xinline_X__COMPONENT = new ecx.types.ComponentType($typeId); 34 | FieldsBuilder.buildAndPush(fields, componentExpr); 35 | return fields; 36 | } 37 | 38 | static function getComponentData(classType:ClassType):MacroComponentData { 39 | if(classType.meta.has(ServiceBuilder.META_CORE)) { 40 | return null; 41 | } 42 | 43 | // Look up the ID, otherwise generate one 44 | var fullName = MacroUtil.getFullNameFromBaseType(classType); 45 | 46 | var componentData = MacroComponentCache.get(fullName); 47 | if(componentData != null) { 48 | return componentData; 49 | } 50 | 51 | componentData = new MacroComponentData(fullName); 52 | MacroComponentCache.set(componentData); 53 | return componentData; 54 | } 55 | 56 | 57 | 58 | } 59 | 60 | #end -------------------------------------------------------------------------------- /src/ecx/System.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import ecx.types.EntityVector; 4 | import ecx.types.FamilyData; 5 | import ecx.types.SystemFlags; 6 | import haxe.macro.Expr; 7 | 8 | using ecx.macro.ClassMacroTools; 9 | 10 | /** 11 | System is Service which need to update. 12 | System's priority will be defined in `WorldConfig`. 13 | Has ability to define aspects `Family<...>` for oredered entity sets 14 | 15 | @see ecx.Family 16 | **/ 17 | #if !macro 18 | @:autoBuild(ecx.macro.SystemBuilder.build()) 19 | #end 20 | @:core 21 | class System extends Service { 22 | 23 | @:unreflective 24 | var _flags:SystemFlags = new SystemFlags(); 25 | 26 | @:unreflective 27 | var _families:Array; 28 | 29 | @:unreflective 30 | function update() {} 31 | 32 | //@:unreflective 33 | function onEntityAdded(entity:Entity, family:FamilyData) {} 34 | 35 | //@:unreflective 36 | function onEntityRemoved(entity:Entity, family:FamilyData) {} 37 | 38 | macro function _family(self:ExprOf, requiredComponents:Array>>):ExprOf { 39 | var componentTypes = requiredComponents.componentTypeList(); 40 | return macro $self._addFamily(@:privateAccess new ecx.types.FamilyData(world, $self).require($componentTypes)); 41 | } 42 | 43 | function __configure() {} 44 | 45 | @:nonVirtual @:unreflective 46 | function _addFamily(family:FamilyData):EntityVector { 47 | if (_families == null) { 48 | _families = []; 49 | } 50 | _families.push(family); 51 | return family.entities; 52 | } 53 | 54 | @:nonVirtual @:unreflective @:extern 55 | inline function _isIdle():Bool { 56 | return _flags.has(SystemFlags.IDLE); 57 | } 58 | 59 | inline function toString():String { 60 | return 'System(Type: #${__serviceType().id}, Spec: #${__serviceSpec().id})'; 61 | } 62 | } -------------------------------------------------------------------------------- /src/ecx/ds/CBitArray.hx: -------------------------------------------------------------------------------- 1 | package ecx.ds; 2 | 3 | @:final 4 | @:unreflective 5 | @:dce 6 | abstract CBitArray(CInt32Array) { 7 | 8 | inline public static var BITS_PER_ELEMENT:Int = 32; 9 | inline public static var BIT_SHIFT:Int = 5; 10 | inline public static var BIT_MASK:Int = 0x1F; 11 | 12 | inline public function new(count:Int) { 13 | this = new CInt32Array(Math.ceil(count / BITS_PER_ELEMENT)); 14 | #if neko 15 | for(i in 0...this.length) { 16 | this[i] = 0; 17 | } 18 | #end 19 | } 20 | 21 | inline public function enable(index:Int) { 22 | this[address(index)] |= mask(index); 23 | } 24 | 25 | inline public function disable(index:Int) { 26 | this[address(index)] &= ~(mask(index)); 27 | } 28 | 29 | @:arrayAccess 30 | inline public function get(index:Int):Bool { 31 | return (this[address(index)] & mask(index)) != 0; 32 | } 33 | 34 | @:arrayAccess 35 | inline public function set(index:Int, value:Bool):Void { 36 | value ? enable(index) : disable(index); 37 | } 38 | 39 | inline public function isFalse(index:Int):Bool { 40 | return (this[address(index)] & mask(index)) == 0; 41 | } 42 | 43 | inline public function enableIfNot(index:Int):Bool { 44 | var a = address(index); 45 | var m = mask(index); 46 | var v = this[a]; 47 | if((v & m) == 0) { 48 | this[a] = v | m; 49 | return true; 50 | } 51 | return false; 52 | } 53 | 54 | inline public function getObjectSize():Int { 55 | return this.length << 2; 56 | } 57 | 58 | // TODO: @:pure after haxe 3.3.0 release 59 | inline public static function address(index:Int):Int { 60 | return index >>> BIT_SHIFT; 61 | } 62 | 63 | // TODO: @:pure after haxe 3.3.0 release 64 | inline public static function mask(index:Int):Int { 65 | return 0x1 << (index & BIT_MASK); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/ecx/IssuesTest.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import ecx.components.Position; 4 | import ecx.components.Value; 5 | import utest.Assert; 6 | 7 | @:keep 8 | class IssuesTest extends EcxTest { 9 | 10 | public function new() { 11 | super(); 12 | } 13 | 14 | public function testAccess() { 15 | var value:Value = world.resolve(Value); 16 | var entity = world.create(); 17 | value.create(entity); 18 | Assert.notNull(value.get(entity)); 19 | Assert.equals(0, value.get(entity).value); 20 | value.get(entity).value = 1; 21 | Assert.equals(1, value.get(entity).value); 22 | 23 | var testPosition:Position = world.resolve(Position); 24 | Assert.isNull(testPosition.get(entity)); 25 | } 26 | 27 | public function testClone() { 28 | var value:Value = world.resolve(Value); 29 | var e1 = world.create(); 30 | var e2 = world.create(); 31 | value.create(e1); 32 | 33 | var e3 = world.clone(e1); 34 | var e4 = world.clone(e2); 35 | 36 | Assert.isTrue(value.has(e3)); 37 | Assert.isFalse(value.has(e4)); 38 | } 39 | 40 | public function testComponentsTraversal() { 41 | var e = world.create(); 42 | var value:Value = world.resolve(Value); 43 | var position:Position = world.resolve(Position); 44 | 45 | value.create(e); 46 | position.create(e); 47 | 48 | var values = []; 49 | for(component in world.components()) { 50 | var value = component.has(e); 51 | if(value) { 52 | values.push(value); 53 | } 54 | } 55 | Assert.equals(2, values.length); 56 | } 57 | 58 | public function testGetMacro() { 59 | var value:Value = world.resolve(Value); 60 | var testPosition:Position = world.resolve(Position); 61 | var expectedEntitiesCount:Int = world.used; 62 | 63 | // entity need to be created once! 64 | Assert.isNull(testPosition.get(world.create())); 65 | expectedEntitiesCount++; 66 | 67 | // entity need to be created once! 68 | // Assert.isNull((e != world.edit(world.create()) ? e : null).tryGet(TestPosition)); 69 | // expectedEntitiesCount++; 70 | 71 | Assert.equals(expectedEntitiesCount, world.used); 72 | } 73 | } -------------------------------------------------------------------------------- /src/ecx/macro/MacroUtil.hx: -------------------------------------------------------------------------------- 1 | package ecx.macro; 2 | 3 | #if macro 4 | 5 | import haxe.macro.Type; 6 | import haxe.macro.Expr; 7 | 8 | @:final 9 | class MacroUtil { 10 | 11 | public static function getFullNameFromBaseType(baseType:BaseType):String { 12 | return baseType.pack.concat([baseType.name]).join("."); 13 | } 14 | 15 | public static function getFullNameFromTypePath(typePath:TypePath):String { 16 | return typePath.pack.concat([typePath.name]).join("."); 17 | } 18 | 19 | public static function getConstTypePath(cls:ExprOf>):TypePath { 20 | var path = getIdentPath(cls, []); 21 | var name = path.pop(); 22 | return { name: name, pack: path }; 23 | } 24 | 25 | static function getIdentPath(expr:Expr, path:Array):Array { 26 | switch (expr.expr) { 27 | case EConst(CIdent(name)): 28 | path.push(name); 29 | case EField(childExpr, name): 30 | getIdentPath(childExpr, path); 31 | path.push(name); 32 | default: 33 | throw "unexcepted expr"; 34 | } 35 | return path; 36 | } 37 | 38 | public static function extendsMeta(classType:ClassType, meta:String):Bool { 39 | var superClass = classType.superClass.t.get(); 40 | return superClass.meta.has(meta); 41 | } 42 | 43 | public static function hasMethod(fields:Array, name:String):Bool { 44 | for(field in fields) { 45 | if(field.name == name) { 46 | return switch(field.kind) { 47 | case FFun(_): true; 48 | default: false; 49 | } 50 | } 51 | } 52 | return false; 53 | } 54 | 55 | public static function hasMethodInClassFields(fields:Array, name:String):Bool { 56 | for(field in fields) { 57 | if(field.name == name) { 58 | return switch(field.kind) { 59 | case FMethod(_): true; 60 | default: false; 61 | } 62 | } 63 | } 64 | return false; 65 | } 66 | 67 | public static function hasInterface(classType:ClassType, fullName:String) { 68 | for(iref in classType.interfaces) { 69 | if(MacroUtil.getFullNameFromBaseType(iref.t.get()) == fullName) { 70 | return true; 71 | } 72 | } 73 | return false; 74 | } 75 | } 76 | 77 | #end 78 | -------------------------------------------------------------------------------- /src/ecx/types/EntityVector.hx: -------------------------------------------------------------------------------- 1 | package ecx.types; 2 | 3 | import ecx.ds.CBitArray; 4 | import ecx.ds.CInt32Array; 5 | 6 | @:final @:unreflective @:dce 7 | class EntityVector { 8 | 9 | public var length(default, null):Int = 0; 10 | public var buffer(default, null):CInt32Array; 11 | 12 | inline public function new(initialCapacity:Int = 16) { 13 | buffer = new CInt32Array(initialCapacity); 14 | } 15 | 16 | inline public function ensure(maxLength:Int) { 17 | if (maxLength >= buffer.length) { 18 | grow(maxLength + 1); 19 | } 20 | } 21 | 22 | inline public function push(entity:Entity) { 23 | ensure(length); 24 | place(entity); 25 | } 26 | 27 | inline public function place(entity:Entity) { 28 | buffer[length] = entity.id; 29 | ++length; 30 | } 31 | 32 | @:access(ecx.Entity) 33 | inline public function get(index:Int):Entity { 34 | return new Entity(buffer[index]); 35 | } 36 | 37 | public function restoreOrder(mask:CBitArray, startIndex:Int = 0, endIndex:Int = 0) { 38 | var array:CInt32Array = cast mask; 39 | var begin = Std.int(startIndex / CBitArray.BITS_PER_ELEMENT); 40 | var end = endIndex == 0 ? array.length : Math.ceil(endIndex / CBitArray.BITS_PER_ELEMENT); 41 | var at = 0; 42 | for(i in begin...end) { 43 | var value = array[i]; 44 | if(value != 0) { 45 | var index = i << CBitArray.BIT_SHIFT; 46 | for(j in 0...CBitArray.BITS_PER_ELEMENT) { 47 | if((value & (1 << j)) != 0) { 48 | buffer[at] = index + j; 49 | ++at; 50 | } 51 | } 52 | } 53 | } 54 | length = at; 55 | } 56 | 57 | inline public function reset() { 58 | length = 0; 59 | } 60 | 61 | inline public function iterator():EntityVectorIterator { 62 | return new EntityVectorIterator(this); 63 | } 64 | 65 | public function getObjectSize():Int { 66 | return buffer.getObjectSize() + 4; 67 | } 68 | 69 | function grow(requiredLength:Int) { 70 | var prevBuffer = buffer; 71 | var newLength = prevBuffer.length; 72 | while(newLength < requiredLength) { 73 | newLength = Std.int(1.0 + newLength * 1.5); 74 | } 75 | if(newLength > prevBuffer.length) { 76 | buffer = new CInt32Array(newLength); 77 | // copy only used entities 78 | for (i in 0...length) { 79 | buffer[i] = prevBuffer[i]; 80 | } 81 | } 82 | } 83 | } 84 | 85 | @:final @:unreflective @:dce 86 | class EntityVectorIterator { 87 | 88 | public var index:Int; 89 | public var end:Int; 90 | public var data:CInt32Array; 91 | 92 | inline public function new(vector:EntityVector) { 93 | index = 0; 94 | end = vector.length; 95 | data = vector.buffer; 96 | } 97 | 98 | inline public function hasNext():Bool { 99 | return index != end; 100 | } 101 | 102 | @:access(ecx.Entity) 103 | inline public function next():Entity { 104 | return new Entity(data[index++]); 105 | } 106 | } 107 | 108 | -------------------------------------------------------------------------------- /src/ecx/ds/CArray.hx: -------------------------------------------------------------------------------- 1 | package ecx.ds; 2 | 3 | #if neko 4 | private typedef CArrayData = neko.NativeArray; 5 | #elseif flash 6 | private typedef CArrayData = flash.Vector; 7 | #elseif java 8 | private typedef CArrayData = java.NativeArray; 9 | #elseif js 10 | private typedef CArrayData = Array; 11 | #elseif cs 12 | private typedef CArrayData = cs.NativeArray; 13 | #elseif hl 14 | private typedef CArrayData = hl.NativeArray; 15 | #else 16 | private typedef CArrayData = Array; 17 | #end 18 | 19 | /*** 20 | Dense Fixed-size array (CArray is const-size array) 21 | **/ 22 | @:generic 23 | @:final 24 | @:unreflective 25 | @:dce 26 | abstract CArray(CArrayData) from CArrayData { 27 | 28 | public var length(get, never):Int; 29 | 30 | inline public function new(length:Int) { 31 | #if flash 32 | this = new flash.Vector(length, true); 33 | #elseif js 34 | #if (haxe_ver >= 4) 35 | this = js.Syntax.construct(Array, length); 36 | #else 37 | this = untyped __new__(Array, length); 38 | #end 39 | for(i in 0...length) this[i] = null; 40 | // this = untyped Array.apply(null, __new__(Array, length)); 41 | // this = untyped __js__("Array.apply(null, new Array({0}))", length); 42 | #elseif cpp 43 | this = new Array(); 44 | cpp.NativeArray.setSize(this, length); 45 | #elseif java 46 | this = new java.NativeArray(length); 47 | #elseif cs 48 | this = new cs.NativeArray(length); 49 | #elseif neko 50 | this = neko.NativeArray.alloc(length); 51 | #elseif hl 52 | this = new hl.NativeArray(length); 53 | #else 54 | this = [for (i in 0...length) null]; 55 | #end 56 | } 57 | 58 | inline function get_length() { 59 | #if neko 60 | return neko.NativeArray.length(this); 61 | #else 62 | return this.length; 63 | #end 64 | } 65 | 66 | @:arrayAccess 67 | inline public function get(index:Int):T { 68 | return this[index]; 69 | } 70 | 71 | @:arrayAccess 72 | inline public function set(index:Int, element:T):Void { 73 | this[index] = element; 74 | } 75 | 76 | inline public function iterator():CArrayIterator { 77 | return new CArrayIterator(this); 78 | } 79 | 80 | /** 81 | Theoretic memory size consumed by array, references are not included 82 | **/ 83 | inline public function getObjectSize():Int { 84 | return length << 2; 85 | } 86 | 87 | inline public static function fromArray(array:Array):CArray { 88 | #if (cpp||python) 89 | return array.copy(); 90 | #elseif flash 91 | return flash.Vector.ofArray(array); 92 | #elseif java 93 | return java.Lib.nativeArray(array, false); 94 | #elseif cs 95 | return cs.Lib.nativeArray(array, false); 96 | #elseif neko 97 | return neko.NativeArray.ofArrayCopy(array); 98 | #else 99 | var result = new CArray(array.length); 100 | for(i in 0...array.length) { 101 | result[i] = array[i]; 102 | } 103 | return result; 104 | #end 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/ecx/macro/FieldsBuilder.hx: -------------------------------------------------------------------------------- 1 | package ecx.macro; 2 | 3 | #if macro 4 | 5 | import haxe.macro.Expr; 6 | 7 | @:final 8 | class FieldsBuilder { 9 | 10 | public static function buildAndPush(fields:Array, block:Expr) { 11 | pushFields(fields, build(block)); 12 | } 13 | 14 | public static function appendMacroClass(fields:Array, typeDefinition:TypeDefinition) { 15 | pushFields(fields, typeDefinition.fields); 16 | } 17 | 18 | public static function pushFields(fields:Array, newFields:Array) { 19 | for(newField in newFields) { 20 | fields.push(newField); 21 | } 22 | } 23 | 24 | public static function build(block:Expr):Array { 25 | var fields:Array = []; 26 | var exprs = switch(block.expr) { 27 | case ExprDef.EBlock(x): x; 28 | case ExprDef.EFunction(_, _): [block]; 29 | case ExprDef.EVars(_): [block]; 30 | default: throw "Bad expression for building"; 31 | } 32 | var metas = []; 33 | for (expr in exprs) { 34 | switch (expr.expr) { 35 | case ExprDef.EMeta(meta, e): 36 | metas.push(meta); 37 | case ExprDef.EVars(vars): 38 | for (v in vars) { 39 | fields.push({ 40 | name: getFieldName(v.name), 41 | doc: null, 42 | access: getAccess(v.name), 43 | kind: FieldType.FVar(v.type, v.expr), 44 | pos: v.expr.pos, 45 | meta: metas 46 | }); 47 | } 48 | metas = []; 49 | case ExprDef.EFunction(name, f): 50 | fields.push({ 51 | name: getFieldName(name), 52 | doc: null, 53 | access: getAccess(name), 54 | kind: FieldType.FFun(f), 55 | pos: f.expr.pos, 56 | meta: metas 57 | }); 58 | metas = []; 59 | default: 60 | } 61 | } 62 | return fields; 63 | } 64 | 65 | static function getAccess(name:String):Array { 66 | var result = []; 67 | for (token in name.split("_X")) { 68 | var access = switch (token) { 69 | case "public": Access.APublic; 70 | case "private": Access.APrivate; 71 | case "static": Access.AStatic; 72 | case "override": Access.AOverride; 73 | case "dynamic": Access.ADynamic; 74 | case "inline": Access.AInline; 75 | default: null; 76 | } 77 | if (access != null) { 78 | result.push(access); 79 | } 80 | } 81 | return result; 82 | } 83 | 84 | static function getFieldName(name:String):String { 85 | var parts = name.split("_X"); 86 | return parts[parts.length - 1]; 87 | } 88 | } 89 | 90 | #end -------------------------------------------------------------------------------- /src/ecx/reporting/EcxBuildReport.hx: -------------------------------------------------------------------------------- 1 | package ecx.reporting; 2 | 3 | #if ecx_report 4 | 5 | import haxe.Json; 6 | import sys.io.File; 7 | 8 | @:final 9 | class EcxBuildReport { 10 | 11 | static var _components:Array = []; 12 | static var _wires:Array = []; 13 | static var _families:Array = []; 14 | 15 | public static function addComponent(path:String) { 16 | _components.push(path); 17 | } 18 | 19 | public static function addFamily(name:String, system:String, components:Array, optional:Array) { 20 | _families.push({ 21 | name: name, 22 | system: system, 23 | components: components, 24 | optional: optional 25 | }); 26 | } 27 | 28 | public static function trackWire(service:String, dependency:String) { 29 | for (wire in _wires) { 30 | if (wire.service == service) { 31 | wire.dependencies.push(dependency); 32 | return; 33 | } 34 | } 35 | _wires.push({ 36 | service: service, 37 | dependencies: [dependency] 38 | }); 39 | } 40 | 41 | public static function save() { 42 | var content = Json.stringify({ 43 | families: _families 44 | }); 45 | File.saveContent("report.json", content); 46 | 47 | for (f in _families) { 48 | for (c in f.components) { 49 | if (_components.indexOf(c) < 0) { 50 | _components.push(c); 51 | } 52 | } 53 | } 54 | 55 | var htmlTableContent:String = ''; 56 | var htmlComponentsHeader = ''; 57 | for (c in _components) { 58 | htmlComponentsHeader += '$c'; 59 | } 60 | var htmlTableHeader = '$htmlComponentsHeader'; 61 | var htmlTableLines = ''; 62 | for (f in _families) { 63 | var line = '${f.system + ":" + f.name}'; 64 | for (c in _components) { 65 | var has = f.components.indexOf(c) >= 0; 66 | line += '${has ? "X" : ""}'; 67 | } 68 | htmlTableLines += '$line'; 69 | } 70 | 71 | var html = "" + 72 | '$htmlTableHeader $htmlTableLines
' + 73 | ""; 74 | 75 | File.saveContent("ecs_matrix.html", html); 76 | 77 | var services = []; 78 | for (w in _wires) { 79 | for (wd in w.dependencies) { 80 | if (services.indexOf(wd) < 0) { 81 | services.push(wd); 82 | } 83 | } 84 | } 85 | 86 | var htmlTableContent:String = ''; 87 | var htmlServicesHeader = ''; 88 | for (s in services) { 89 | htmlServicesHeader += '$s'; 90 | } 91 | var htmlTableHeader = '$htmlServicesHeader'; 92 | var htmlTableLines = ''; 93 | for (w in _wires) { 94 | var line = '${w.service}'; 95 | for (s in services) { 96 | var has = w.dependencies.indexOf(s) >= 0; 97 | line += '${has ? "X" : ""}'; 98 | } 99 | htmlTableLines += '$line'; 100 | } 101 | 102 | var html = "" + 103 | '$htmlTableHeader $htmlTableLines
' + 104 | ""; 105 | 106 | File.saveContent("ecs_wires.html", html); 107 | } 108 | } 109 | 110 | typedef EcxFamilyMeta = { 111 | var name:String; 112 | var system:String; 113 | var components:Array; 114 | var optional:Array; 115 | } 116 | 117 | typedef EcxWiresMeta = { 118 | var service:String; 119 | var dependencies:Array; 120 | } 121 | 122 | #end -------------------------------------------------------------------------------- /src/ecx/types/FamilyData.hx: -------------------------------------------------------------------------------- 1 | package ecx.types; 2 | 3 | import ecx.ds.CArray; 4 | import ecx.ds.CBitArray; 5 | 6 | @:final 7 | @:keep 8 | @:unreflective 9 | class FamilyData { 10 | 11 | public var entities(default, null):EntityVector; 12 | public var changed(default, null):Bool = false; 13 | public var total(default, null):Int = 0; 14 | 15 | var _containedMask:CBitArray; 16 | var _requiredComponents:ComponentTable; 17 | // TODO: should be systems array to notify 18 | var _system:System; 19 | var _world:World; 20 | 21 | function new(world:World, system:System) { 22 | var capacity = world.capacity; 23 | entities = new EntityVector(); 24 | _containedMask = new CBitArray(capacity); 25 | _world = world; 26 | _system = system; 27 | } 28 | 29 | inline function require(requiredComponentTypes:Array):FamilyData { 30 | _requiredComponents = new CArray(requiredComponentTypes != null ? requiredComponentTypes.length : 0); 31 | for(i in 0..._requiredComponents.length) { 32 | _requiredComponents[i] = _world.getComponentService(requiredComponentTypes[i]); 33 | } 34 | return this; 35 | } 36 | 37 | @:nonVirtual @:unreflective 38 | function check(entity:Entity) { 39 | var rc = _requiredComponents; 40 | for(i in 0...rc.length) { 41 | if(!rc[i].has(entity)) { 42 | return false; 43 | } 44 | } 45 | return true; 46 | } 47 | 48 | @:nonVirtual @:unreflective 49 | inline function __invalidate() { 50 | entities.ensure(total); 51 | entities.restoreOrder(_containedMask); 52 | changed = false; 53 | } 54 | 55 | // TODO: check array of entities 56 | 57 | @:nonVirtual @:unreflective 58 | function __change(entity:Entity) { 59 | var e = _containedMask.get(entity.id); 60 | var c = check(entity); 61 | if(c != e) { 62 | if(c) __enableEntity(entity); 63 | else __disableEntity(entity); 64 | } 65 | } 66 | 67 | @:nonVirtual @:unreflective 68 | @:access(ecx.System) 69 | function __enableEntity(entity:Entity) { 70 | #if ecx_debug 71 | if(_mutable == false) throw "IMMUTABLE"; 72 | #end 73 | if(!_containedMask.get(entity.id) && check(entity)) { 74 | _containedMask.enable(entity.id); 75 | _system.onEntityAdded(entity, this); 76 | changed = true; 77 | ++total; 78 | } 79 | } 80 | 81 | @:nonVirtual @:unreflective 82 | @:access(ecx.System) 83 | function __disableEntity(entity:Entity) { 84 | #if ecx_debug 85 | if(_mutable == false) throw "IMMUTABLE"; 86 | #end 87 | if(_containedMask.get(entity.id)) { 88 | _containedMask.disable(entity.id); 89 | _system.onEntityRemoved(entity, this); 90 | changed = true; 91 | --total; 92 | } 93 | } 94 | 95 | #if ecx_debug 96 | var _debugEntitiesCopy:Array; 97 | var _mutable:Bool = false; 98 | 99 | public function debugMakeMutable() { 100 | if(_mutable == true) throw 'imm1'; 101 | _mutable = true; 102 | if(_debugEntitiesCopy == null) return; 103 | if(_debugEntitiesCopy.length != entities.length) throw 'Family assert: entity list access violation'; 104 | for(i in 0..._debugEntitiesCopy.length) { 105 | if(_debugEntitiesCopy[i] != entities.get(i)) throw 'Family assert: entity list access violation (bad element at $i'; 106 | } 107 | 108 | } 109 | 110 | public function debugMakeImmutable() { 111 | if(_mutable == false) throw 'imm2'; 112 | _mutable = false; 113 | // create immutable copy for checking 114 | _debugEntitiesCopy = []; 115 | for(e in entities) { 116 | _debugEntitiesCopy.push(e); 117 | } 118 | } 119 | #end 120 | 121 | public static function updateVector(families:CArray) { 122 | for(i in 0...families.length) { 123 | var family = families.get(i); 124 | if(family.changed) { 125 | family.__invalidate(); 126 | } 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /src/ecx/macro/SystemBuilder.hx: -------------------------------------------------------------------------------- 1 | package ecx.macro; 2 | 3 | #if macro 4 | 5 | import haxe.macro.Type; 6 | import haxe.macro.Context; 7 | import haxe.macro.Expr; 8 | 9 | class SystemBuilder { 10 | 11 | public static function build():Array { 12 | var cls:ClassType = Context.getLocalClass().get(); 13 | 14 | //MacroBuildDebug.begin(); 15 | //MacroBuildGenerate.invoke(); 16 | 17 | var pos = Context.currentPos(); 18 | var fields:Array = Context.getBuildFields(); 19 | 20 | // TODO: cl & tp should be resolved from Type with Context 21 | var tp:TypePath = { pack: cls.pack, name: cls.name, params: [], sub: null }; 22 | var ct:ComplexType = ComplexType.TPath(tp); 23 | 24 | var injExprs:Array = []; 25 | addConfigurator(cls, fields, injExprs); 26 | makeConfigurate(fields, injExprs); 27 | patchUnreflective(fields); 28 | 29 | //MacroBuildDebug.end(); 30 | 31 | return fields; 32 | return null; 33 | } 34 | 35 | static function addConfigurator(cls:ClassType, fields:Array, exprs:Array) { 36 | var updateField = searchUpdate(fields); 37 | if(updateField == null) { 38 | exprs.push(macro { 39 | _flags = _flags.add(ecx.types.SystemFlags.IDLE); 40 | }); 41 | } 42 | if(cls.meta.has(":config")) { 43 | exprs.push(macro { 44 | _flags = _flags.add(ecx.types.SystemFlags.CONFIG); 45 | }); 46 | } 47 | 48 | for(field in fields) { 49 | switch(field.kind) { 50 | case FieldType.FVar(t, e) | FieldType.FProp(_, _, t, e): 51 | switch(t) { 52 | case ComplexType.TPath(tp): 53 | if(tp.name == "Family") { 54 | if(e != null) { 55 | Context.error("Remove initializer, family binding will be created at compile-time", field.pos); 56 | } 57 | var params:Array = tp.params; 58 | if(params == null || params.length == 0) { 59 | Context.error("Family required at least one component Class", field.pos); 60 | } 61 | else { 62 | var familyTypeParams = []; 63 | 64 | #if ecx_report 65 | var reportFamilyComponents:Array = []; 66 | var reportFamilyOptionalComponents:Array = []; 67 | #end 68 | 69 | for(param in params) { 70 | switch(param) { 71 | case TypeParam.TPType(TPath(componentTypePath)): 72 | var fullname = MacroUtil.getFullNameFromTypePath(componentTypePath); 73 | familyTypeParams.push(macro #if !ecx_macro_debug @:pos($v{field.pos}) #end $i{fullname}); 74 | #if ecx_report 75 | reportFamilyComponents.push(fullname); 76 | #end 77 | case TypeParam.TPType(TParent(TPath(tpOptionalComponent))): 78 | #if ecx_report 79 | reportFamilyOptionalComponents.push(MacroUtil.getFullNameFromTypePath(tpOptionalComponent)); 80 | #end 81 | // Ignore optional types 82 | default: 83 | Context.error("Bad family type: " + param, field.pos); 84 | } 85 | } 86 | 87 | #if ecx_report 88 | ecx.reporting.EcxBuildReport.addFamily( 89 | field.name, 90 | MacroUtil.getFullNameFromBaseType(cls), 91 | reportFamilyComponents, 92 | reportFamilyOptionalComponents 93 | ); 94 | #end 95 | 96 | exprs.push(macro { 97 | #if !ecx_macro_debug @:pos(${field.pos}) #end $i{field.name} = _family($a{familyTypeParams}); 98 | }); 99 | } 100 | } 101 | default: 102 | } 103 | default: 104 | } 105 | } 106 | } 107 | 108 | static function searchUpdate(fields:Array):Null { 109 | for(field in fields) { 110 | if(field.name == "update") { 111 | switch(field.kind) { 112 | case FFun(_): return field; 113 | default: 114 | } 115 | } 116 | } 117 | return null; 118 | } 119 | 120 | static function patchUnreflective(fields:Array) { 121 | for(field in fields) { 122 | switch(field.name) { 123 | case "update":// | "initialize" | "onEntityAdded" | "onEntityRemoved": 124 | if(field.meta == null) { 125 | field.meta = []; 126 | } 127 | field.meta.push({ 128 | name: ":unreflective", 129 | pos: Context.currentPos() 130 | }); 131 | } 132 | } 133 | } 134 | 135 | static function makeConfigurate(fields:Array, exprs:Array) { 136 | if(exprs.length == 0) { 137 | return; 138 | } 139 | var injExpr = macro { 140 | function override_X__configure() { 141 | var __world:ecx.World = this.world; 142 | $b{exprs} 143 | } 144 | } 145 | FieldsBuilder.buildAndPush(fields, injExpr); 146 | } 147 | } 148 | 149 | #end 150 | -------------------------------------------------------------------------------- /src/ecx/macro/ServiceBuilder.hx: -------------------------------------------------------------------------------- 1 | package ecx.macro; 2 | 3 | #if macro 4 | 5 | import ecx.macro.MacroUtil; 6 | import haxe.macro.Context; 7 | import haxe.macro.Expr; 8 | import haxe.macro.Type; 9 | 10 | @:final 11 | class ServiceBuilder { 12 | 13 | public inline static var META_CORE:String = ":core"; 14 | 15 | public static function build():Array { 16 | var cls:ClassType = Context.getLocalClass().get(); 17 | if(cls.isExtern) { 18 | cls.exclude(); 19 | return null; 20 | } 21 | 22 | MacroBuildDebug.begin(); 23 | MacroBuildGenerate.invoke(); 24 | 25 | var pos = Context.currentPos(); 26 | var fields:Array = Context.getBuildFields(); 27 | 28 | var typeInfo = getTypeInfo(cls); 29 | if(typeInfo != null) { 30 | var typeBasePath = Context.makeExpr(typeInfo.basePath, pos); 31 | var typePath = Context.makeExpr(typeInfo.path, pos); 32 | var typeId = Context.makeExpr(typeInfo.typeId, pos); 33 | var specId = Context.makeExpr(typeInfo.specId, pos); 34 | 35 | // TODO: cl & tp should be resolved from Type with Context 36 | var tp:TypePath = { pack: cls.pack, name: cls.name, params: [], sub: null }; 37 | var ct:ComplexType = ComplexType.TPath(tp); 38 | 39 | var tpType = getTypePathForType(false); 40 | var tpSpec = getTypePathForType(true); 41 | 42 | MacroBuildDebug.printSystem(typeInfo); 43 | 44 | var fieldsExpr = macro { 45 | function override_X__serviceType() { return new $tpType($typeId); } 46 | function override_X__serviceSpec() { return new $tpSpec($specId); } 47 | var public_Xstatic_Xinline_X__TYPE = new $tpType($typeId); 48 | var public_Xstatic_Xinline_X__SPEC = new $tpSpec($specId); 49 | } 50 | FieldsBuilder.buildAndPush(fields, fieldsExpr); 51 | } 52 | 53 | var injExprs:Array = []; 54 | addInjectors(fields, injExprs); 55 | makeInjectMethod(fields, injExprs); 56 | //patchUnreflective(fields); 57 | 58 | MacroBuildDebug.end(); 59 | 60 | return fields; 61 | } 62 | 63 | static function getTypePathForType(spec:Bool) { 64 | var name = "Service" + (spec ? "Spec" : "Type"); 65 | return { pack: ["ecx", "types"], name: name, params: [], sub: null }; 66 | } 67 | 68 | static function addInjectors(fields:Array, exprs:Array) { 69 | var injectFields:Array = []; 70 | var injectType:Array = []; 71 | for(field in fields) { 72 | switch(field.kind) { 73 | case FieldType.FVar(t, _) | FieldType.FProp(_, _, t, _): 74 | if(t != null) { 75 | switch(t) { 76 | case ComplexType.TPath(tp): 77 | switch(tp.name) { 78 | case "Wire": 79 | if(tp.params != null && tp.params.length == 1) { 80 | injectFields.push(field); 81 | switch(tp.params[0]) { 82 | case TPType(TPath(tp)): 83 | injectType.push(MacroUtil.getFullNameFromTypePath(tp)); 84 | default: 85 | Context.error("wrong", field.pos); 86 | } 87 | } 88 | else { 89 | Context.error("Wire require one Service Type", field.pos); 90 | } 91 | } 92 | default: 93 | } 94 | } 95 | default: 96 | } 97 | } 98 | 99 | #if ecx_report 100 | var rsn = Context.getLocalClass().get().name; 101 | for(it in injectType) { 102 | ecx.reporting.EcxBuildReport.trackWire(rsn, it); 103 | } 104 | #end 105 | if(injectFields.length > 0) { 106 | for(i in 0...injectFields.length) { 107 | var injectField = injectFields[i]; 108 | 109 | var expr = macro $i{injectField.name} = 110 | #if !ecx_macro_debug @:pos($v{injectField.pos}) #end 111 | __world.resolve($i{injectType[i]}); 112 | 113 | exprs.push(expr); 114 | } 115 | } 116 | } 117 | 118 | static function makeInjectMethod(fields:Array, exprs:Array) { 119 | if(exprs.length == 0) { 120 | return; 121 | } 122 | var injExpr = macro { 123 | function override_X__inject() { 124 | var __world:ecx.World = this.world; 125 | $b{exprs} 126 | } 127 | } 128 | FieldsBuilder.buildAndPush(fields, injExpr); 129 | } 130 | 131 | static function getTypeInfo(classType:ClassType):MacroServiceData { 132 | if(classType.meta.has(META_CORE)) { 133 | return null; 134 | } 135 | 136 | var baseClass = classType; 137 | // Traverse up to the last non-component base 138 | while(!MacroUtil.extendsMeta(baseClass, META_CORE)) { 139 | baseClass = baseClass.superClass.t.get(); 140 | } 141 | 142 | // Look up the ID, otherwise generate one 143 | var fullName = MacroUtil.getFullNameFromBaseType(classType); 144 | var baseFullName = MacroUtil.getFullNameFromBaseType(baseClass); 145 | 146 | var typeData = MacroServiceCache.get(fullName); 147 | if(typeData != null) { 148 | return typeData; 149 | } 150 | 151 | var baseTypeId = MacroServiceCache.getBaseTypeId(fullName, baseFullName); 152 | typeData = new MacroServiceData(baseFullName, fullName, baseTypeId); 153 | MacroServiceCache.set(typeData); 154 | return typeData; 155 | } 156 | } 157 | 158 | #end -------------------------------------------------------------------------------- /src/ecx/managers/WorldConstructor.hx: -------------------------------------------------------------------------------- 1 | package ecx.managers; 2 | 3 | import ecx.ds.PowerOfTwo; 4 | import ecx.types.EntityVector; 5 | import ecx.types.ComponentTable; 6 | import ecx.ds.CBitArray; 7 | import ecx.ds.CInt32RingBuffer; 8 | import ecx.ds.CArray; 9 | import ecx.types.FamilyData; 10 | 11 | @:dce @:final @:unreflective 12 | @:access(ecx.World, ecx.System, ecx.Engine, ecx.Entity) 13 | @:access(ecx.WorldConfig) 14 | @:allow(ecx.World) 15 | class WorldConstructor { 16 | 17 | static function construct(world:World, capacity:Int, config:WorldConfig) { 18 | #if ecx_debug 19 | if(config == null) throw "world config required"; 20 | if(capacity <= 1) throw "capacity is so low: " + capacity; 21 | if(capacity >= 0x3FFFFFFF - 1) throw "too much entities: " + capacity; 22 | #end 23 | 24 | // capacity alignment 25 | capacity = PowerOfTwo.require(capacity) + 1; 26 | world.capacity = capacity; 27 | 28 | // components table 29 | world._components = createComponentsData(config); 30 | 31 | // entities support 32 | world._pool = createEntityPool(capacity); 33 | //world._mapToData = createEntityWrappers(world); 34 | world._aliveMask = new CBitArray(capacity); 35 | world._activeMask = new CBitArray(capacity); 36 | 37 | world._changedVector = new EntityVector(capacity - 1); 38 | world._removedVector = new EntityVector(capacity - 1); 39 | world._changedMask = new CBitArray(capacity); 40 | world._removedMask = new CBitArray(capacity); 41 | 42 | // services 43 | world._services = createServicesLookup(config); 44 | world._orderedServices = createServicesOrder(config); 45 | routeServices(world); 46 | createFamilyList(world); 47 | initializeServices(world); 48 | deleteConfigurators(world); 49 | } 50 | 51 | @:access(ecx.IComponent) 52 | static function createComponentsData(config:WorldConfig):ComponentTable { 53 | var components:Array = []; 54 | var maxTypeId = 0; 55 | for(service in config._services) { 56 | if(Std.is(service, IComponent)) { 57 | var component:IComponent = cast service; 58 | components.push(component); 59 | var typeId = component.__componentType().id; 60 | if(typeId > maxTypeId) { 61 | maxTypeId = typeId; 62 | } 63 | } 64 | } 65 | 66 | var table = new ComponentTable(maxTypeId + 1); 67 | for(component in components) { 68 | table[component.__componentType().id] = component; 69 | } 70 | return table; 71 | } 72 | 73 | static function createEntityPool(capacity:Int):CInt32RingBuffer { 74 | // capacity is POT; 0 is invalid; 75 | // generate valid entities: {1, 2, 3, 4} for cap = 5 76 | var pool = new CInt32RingBuffer(capacity - 1); 77 | for(i in 0...pool.length) { 78 | pool.set(i, i + 1); 79 | } 80 | return pool; 81 | } 82 | 83 | // unused 84 | // static function createEntityWrappers(world:World) { 85 | // var wrappers = new CArray(world.capacity); 86 | // for(id in 1...wrappers.length) { 87 | // wrappers[id] = new EntityData(new Entity(id), world); 88 | // } 89 | // return wrappers; 90 | // } 91 | 92 | @:access(ecx.Service) 93 | static function createServicesLookup(config:WorldConfig):CArray { 94 | var services = new Array(); 95 | for(system in config._services) { 96 | services[system.__serviceType().id] = system; 97 | } 98 | return CArray.fromArray(services); 99 | } 100 | 101 | static function createServicesOrder(config:WorldConfig):CArray { 102 | var services = config._services; 103 | var priorities = config._priorities; 104 | 105 | var sortIndices:Array = []; 106 | for(i in 0...services.length) { 107 | sortIndices.push(i); 108 | } 109 | 110 | sortIndices.sort(function (x, y) { 111 | return priorities[x] - priorities[y]; 112 | }); 113 | 114 | var orderedServices = new CArray(services.length); 115 | for(i in 0...services.length) { 116 | orderedServices[i] = services[sortIndices[i]]; 117 | } 118 | return orderedServices; 119 | } 120 | 121 | @:access(ecx.Service) 122 | static function routeServices(world:World) { 123 | var processors = []; 124 | var active = []; 125 | 126 | for(service in world._orderedServices) { 127 | service.world = world; 128 | 129 | var system:System = Std.instance(service, System); 130 | if(system != null) { 131 | system.__configure(); 132 | if(system._families != null && system._families.length > 0) { 133 | processors.push(system); 134 | } 135 | if(!system._isIdle()) { 136 | active.push(system); 137 | } 138 | } 139 | 140 | service.__allocate(); 141 | service.__inject(); 142 | } 143 | world._systems = CArray.fromArray(active); 144 | world._processors = CArray.fromArray(processors); 145 | } 146 | 147 | static function createFamilyList(world:World) { 148 | var families:Array = []; 149 | 150 | for(processor in world._processors) { 151 | for(family in processor._families) { 152 | families.push(family); 153 | } 154 | } 155 | 156 | world._families = CArray.fromArray(families); 157 | } 158 | 159 | @:access(ecx.Service) 160 | static function initializeServices(world:World) { 161 | for(service in world._orderedServices) { 162 | service.initialize(); 163 | } 164 | } 165 | 166 | static function deleteConfigurators(world:World) { 167 | // TODO: somehow 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ecx 2 | 3 | [![Lang](https://img.shields.io/badge/language-haxe-orange.svg)](http://haxe.org) 4 | [![Version](https://img.shields.io/badge/version-v0.1.1-green.svg)](https://github.com/eliasku/ecx) 5 | [![Dependencies](https://img.shields.io/badge/dependencies-none-green.svg)](https://github.com/eliasku/ecx/blob/master/haxelib.json) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT) 7 | 8 | [![Build Status](https://travis-ci.org/eliasku/ecx.svg?branch=develop)](https://travis-ci.org/eliasku/ecx) 9 | [![Build Status](https://ci.appveyor.com/api/projects/status/t0ql3d9hjp5f72jt?svg=true)](https://ci.appveyor.com/project/eliasku/ecx) 10 | 11 | ECX is Entity Component System framework for Haxe 12 | 13 | - [Asteroids Example](https://github.com/eliasku/ecx-richardlord-asteroids) 14 | - [Documentation](https://eliasku.github.io/ecx/api-minimal) 15 | - [Benchmarks](https://github.com/eliasku/ecx-benchmarks) 16 | 17 | Libraries (work in progress): 18 | - [ecx-common](https://github.com/eliasku/ecx-common): Common utilities 19 | - [ecx-scene2d](https://github.com/eliasku/ecx-scene2d): Scene graph library example 20 | 21 | ## World 22 | 23 | ### Initialization 24 | 25 | ```haxe 26 | var config = new WorldConfig([...]); 27 | var world = Engine.createWorld(config, ?capacity); 28 | ``` 29 | 30 | ## Entity 31 | 32 | Entity is just integer id value. `0` is reserved as invalid id. 33 | 34 | ## Service 35 | 36 | All services are known at world creation. World provides possibility to resolve services. `World::resolve` use constant `Class` for resolving. At compile-time these expressions will be translated to lookup array access by constant index with unsafe cast (pseudo example: `cast _services[8]`). For `hxcpp` poiter trick is used to avoid generating `dynamic_cast`. 37 | 38 | ### Injection 39 | 40 | Each service could have dependencies on different services. With `Wire` you could inject your dependencies to instance fields. 41 | 42 | For example we need to inject TimeSystem system to our MovementSystem 43 | 44 | ```haxe 45 | class MovementSystem extends System { 46 | var _time:Wire; 47 | ... 48 | override function update() { 49 | var dt = _time.dt; 50 | ... 51 | } 52 | } 53 | ``` 54 | 55 | ### Family 56 | 57 | For all `System` types. 58 | For example we need to track all active(live) entities with components: Transform, Node and Renderable. 59 | 60 | ```haxe 61 | class MovementSystem extends System { 62 | var _entities:Family; 63 | ... 64 | override function update() { 65 | // Note: typeof _entities is Array 66 | for(entity in _entities) { 67 | // only entities with required component will be displayed 68 | trace(entity.id); 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | Sometimes it could be useful to mark some optional component in Family declaration just for readability. 75 | You can wrap each optional Component type in parentheses `()` and it will be ignored by Family, but 76 | will be notated. 77 | 78 | ```haxe 79 | var _entities:Family; 80 | ``` 81 | 82 | ### System Flags 83 | 84 | * `IDLE`: System doesn't override `update` method. Should not be updated. 85 | * `CONFIG`: System is defined with `@:config` meta. This system is just configurator. It will be deleted after World initialization phase. 86 | 87 | ## Component 88 | 89 | Component is a way to associate [data] per `Entity`. You could just use component-builders to define your own components. 90 | 91 | ```haxe 92 | class Position extends AutoComp {} 93 | 94 | /// later just use it like Point class per entity 95 | _position.get(entity).x = 10; 96 | ``` 97 | 98 | Or you could create any custom crazy ComponentStorage / ComponentManager. 99 | 100 | ```haxe 101 | class Color extends Service implements Component { 102 | // BitmapData is used just to demonstrate that you are not limited to anything to store per 103 | // Each pixel is color for entity 104 | var _colors:BitmapData; 105 | 106 | ... 107 | 108 | inline public function get(entity:Entity):Int { 109 | _colors.getPixel32(entity.id % _stride, Std.int(entity.id / _stride)); 110 | } 111 | 112 | .... 113 | } 114 | ``` 115 | 116 | **Injection:** World `Component` is `Service`, so you are able to invoke all messages directly to other services. 117 | **Implementation:** `Component` is just interface, you could iterate all registered components and access their base API per entity. It's handy for automatically cloning or serialization. 118 | 119 | ## CTTI 120 | `ServiceType`, `ServiceSpec`, `ComponentType`, `ClassMacroTools` 121 | 122 | ## RTTI 123 | `TypeManager` (WIP) 124 | 125 | ## Debug 126 | 127 | `-D ecx_debug` for debugging 128 | 129 | `-D ecx_macro_debug` for macro debugging 130 | 131 | `-D ecx_report` to get and analyse `ecx_wires.html` and `ecx_matrix.html` report files generated during compilation 132 | 133 | ## TODO: 134 | 135 | - Rethink world initialization: 136 | - - Are we are ok that instance of service could be created outside by default? 137 | - Rethink system-flags 138 | - Delete configurator services 139 | - Add more information on specific cases of AutoComp 140 | - Pack for dense storage 141 | - Entity Generations 142 | -------------------------------------------------------------------------------- /src/ecx/macro/AutoCompBuilder.hx: -------------------------------------------------------------------------------- 1 | package ecx.macro; 2 | 3 | #if macro 4 | import ecx.macro.FieldsBuilder; 5 | import ecx.macro.MacroUtil; 6 | import haxe.macro.Context; 7 | import haxe.macro.Expr; 8 | import haxe.macro.Type; 9 | 10 | typedef Nameble = { 11 | var name: String; 12 | } 13 | 14 | typedef ComponentMacroValues = { 15 | var none:Expr; 16 | var def:Expr; 17 | var primitive:Bool; 18 | @:optional var storage:TypePath; 19 | } 20 | 21 | @:final 22 | class AutoCompBuilder { 23 | 24 | public static function build():Array { 25 | var pos = Context.currentPos(); 26 | var fields = Context.getBuildFields(); 27 | var localClass:ClassType = Context.getLocalClass().get(); 28 | localClass.meta.add(":final", [], pos); 29 | 30 | buildDefaultConstructor(fields); 31 | 32 | var dataType:Type = localClass.superClass.params[0]; 33 | var ctData = Context.toComplexType(dataType); 34 | var tpData = switch(ctData) { 35 | case ComplexType.TPath(x): x; 36 | default: throw "bad generic param type"; 37 | } 38 | 39 | var values = getValues(dataType); 40 | buildCopy(fields, dataType, values); 41 | buildCreate(fields, dataType, values); 42 | buildStorageAndAllocator(fields, dataType, values); 43 | buildOther(fields, values, ctData); 44 | buildObjectSize(fields, values, ctData); 45 | 46 | return fields; 47 | } 48 | 49 | static function getValues(type:Type):ComponentMacroValues { 50 | type = Context.follow(type); 51 | switch(type) { 52 | case TInst(x, _): 53 | var ctData = Context.toComplexType(type); 54 | var tpData = switch(ctData) { 55 | case ComplexType.TPath(tp): tp; 56 | default: throw "bad class " + type; 57 | } 58 | if(tpData.name == "String") { 59 | return { none: macro null, def: macro "", primitive: true }; 60 | } 61 | if(x.get().isInterface) { 62 | return { none: macro null, def: macro null, primitive: false }; 63 | } 64 | return {none: macro null, def: macro new $tpData(), primitive: false}; 65 | case TAbstract(x, _): 66 | switch(x.get().name) { 67 | case "Bool": 68 | return { 69 | none: macro false, 70 | def: macro true, 71 | primitive: true, 72 | storage: { pack: ["ecx", "ds"], name: "CBitArray" } 73 | }; 74 | case "Float": 75 | return { 76 | none: macro 0.0, 77 | def: macro 0.0, 78 | primitive: true 79 | }; 80 | case "Int": 81 | return { 82 | none: macro 0, 83 | def: macro 0, 84 | primitive: true, 85 | storage: { pack: ["ecx", "ds"], name: "CInt32Array" } 86 | }; 87 | case "UInt": 88 | return { 89 | none: macro 0, 90 | def: macro 0, 91 | primitive: true, 92 | storage: { pack: ["ecx", "ds"], name: "CInt32Array" } 93 | }; 94 | } 95 | throw ("Unhandled Abstract: " + x); 96 | default: 97 | throw ("Unhandled Default value: " + type); 98 | } 99 | return { none: macro null, def: macro null, primitive: true }; 100 | } 101 | 102 | static function isConstructibleType(type:Type):Bool { 103 | switch(type) { 104 | case TInst(_.get() => cls, _): 105 | return cls.constructor != null && !cls.isInterface; 106 | default: 107 | } 108 | #if ecx_debug 109 | trace("NOT CONTRUCTABLE: " + type); 110 | #end 111 | return false; 112 | } 113 | 114 | static function buildDefaultConstructor(fields:Array) { 115 | if (!MacroUtil.hasMethod(fields, "new")) { 116 | FieldsBuilder.appendMacroClass(fields, macro class New { 117 | inline public function new() {} 118 | }); 119 | } 120 | } 121 | 122 | static function buildCreate(fields:Array, type:Type, values:ComponentMacroValues) { 123 | var ctData = Context.toComplexType(type); 124 | var tpData = switch(ctData) { 125 | case ComplexType.TPath(x): x; 126 | default: throw "bad generic param type"; 127 | } 128 | 129 | FieldsBuilder.appendMacroClass(fields, macro class Create { 130 | inline public function create(entity:ecx.Entity):$ctData { 131 | var component = ${values.def}; 132 | set(entity, component); 133 | return component; 134 | } 135 | }); 136 | } 137 | 138 | static function buildStorageAndAllocator(fields:Array, type:Type, values:ComponentMacroValues) { 139 | var sparse = false; 140 | if(values.storage == null) { 141 | sparse = true; 142 | values.storage = { 143 | // pack: ["ecx", "ds"], 144 | // name: "CArray", 145 | pack: [], 146 | name: "Array", 147 | params: [ TPType(Context.toComplexType(type)) ] 148 | }; 149 | } 150 | var ct = ComplexType.TPath(values.storage); 151 | var tp = values.storage; 152 | 153 | if(sparse) { 154 | FieldsBuilder.appendMacroClass(fields, macro class StoreAndAllocate { 155 | public var data(default, null):$ct; 156 | override function __allocate() { 157 | data = new $tp(); 158 | } 159 | }); 160 | } 161 | else { 162 | FieldsBuilder.appendMacroClass(fields, macro class StoreAndAllocate { 163 | public var data(default, null):$ct; 164 | override function __allocate() { 165 | data = new $tp(world.capacity); 166 | } 167 | }); 168 | } 169 | } 170 | 171 | static function buildCopy(fields:Array, type:Type, values:ComponentMacroValues) { 172 | // TODO: iterface clone? 173 | // TODO: value-type assignments 174 | var hasCopyFrom:Bool = false; 175 | var hasCreateInstance:Bool = false; 176 | switch(type) { 177 | case TInst(_.get() => x, _): 178 | var dataClass:ClassType = x; 179 | var dataFields = dataClass.fields.get(); 180 | if (dataClass.isInterface) { 181 | hasCreateInstance = MacroUtil.hasMethodInClassFields(dataFields, "instantiate"); 182 | } 183 | hasCopyFrom = MacroUtil.hasMethodInClassFields(dataFields, "copyFrom"); 184 | default: 185 | } 186 | 187 | if(values.primitive) { 188 | FieldsBuilder.appendMacroClass(fields, macro class CopyValue { 189 | inline override public function copy(source:ecx.Entity, destination:ecx.Entity) { 190 | set(destination, get(source)); 191 | } 192 | }); 193 | } 194 | else if (hasCreateInstance && hasCopyFrom) { 195 | FieldsBuilder.appendMacroClass(fields, macro class CopyInstance { 196 | inline override public function copy(source:ecx.Entity, destination:ecx.Entity) { 197 | var data = get(source); 198 | if(data != null) { 199 | var newInstance = data.instantiate(); 200 | newInstance.copyFrom(data); 201 | set(destination, newInstance); 202 | } 203 | } 204 | }); 205 | } 206 | else if (hasCopyFrom) { 207 | FieldsBuilder.appendMacroClass(fields, macro class Clone { 208 | inline override public function copy(source:ecx.Entity, destination:ecx.Entity) { 209 | var data = get(source); 210 | if(data != null) { 211 | create(destination).copyFrom(data); 212 | } 213 | } 214 | }); 215 | } 216 | else { 217 | #if ecx_debug 218 | trace("No copy for: " + type); 219 | #end 220 | } 221 | } 222 | 223 | static function buildOther(fields:Array, values:ComponentMacroValues, ctData:ComplexType) { 224 | FieldsBuilder.appendMacroClass(fields, macro class TempClass { 225 | inline public function get(entity:ecx.Entity):$ctData { 226 | return (data[entity.id]:$ctData); 227 | } 228 | 229 | inline public function set(entity:ecx.Entity, component:$ctData) { 230 | data[entity.id] = component; 231 | } 232 | 233 | inline override public function destroy(entity:ecx.Entity) { 234 | data[entity.id] = ${values.none}; 235 | } 236 | 237 | inline override public function has(entity:ecx.Entity):Bool { 238 | return data[entity.id] != ${values.none}; 239 | } 240 | }); 241 | } 242 | 243 | static function buildObjectSize(fields:Array, values:ComponentMacroValues, ctData:ComplexType) { 244 | FieldsBuilder.appendMacroClass(fields, macro class ObjectSize { 245 | override public function getObjectSize():Int { 246 | //return data.getObjectSize(); 247 | return 0; 248 | } 249 | }); 250 | } 251 | } 252 | 253 | #end 254 | -------------------------------------------------------------------------------- /src/ecx/World.hx: -------------------------------------------------------------------------------- 1 | package ecx; 2 | 3 | import ecx.types.ComponentType; 4 | import ecx.types.ComponentTable; 5 | import ecx.ds.CArray; 6 | import ecx.ds.CArrayIterator; 7 | import ecx.ds.CBitArray; 8 | import ecx.ds.CInt32RingBuffer; 9 | import ecx.managers.WorldConstructor; 10 | import ecx.types.EntityVector; 11 | import ecx.types.FamilyData; 12 | 13 | #if ecx_debug 14 | import ecx.managers.WorldDebug.*; 15 | #end 16 | 17 | /** 18 | World manages entities, components and services 19 | **/ 20 | @:final @:dce @:unreflective 21 | @:access(ecx.System, ecx.Family, ecx.Entity) 22 | class World { 23 | 24 | /** 25 | Identifier of this world 26 | **/ 27 | public var id(default, null):Int; 28 | 29 | /** 30 | Maximum amount of entities including invalid/reserved. 31 | 32 | Capacity will be rounded to power-of-two value plus one. 33 | For requested capacity 3 will be allocated: 34 | capacity = nearestPOT(3 - 1) + 1 = 3. 35 | For capacity = 5 we have set of valid entities {1, 2, 3, 4} and one invalid is 0 36 | **/ 37 | public var capacity(default, null):Int; 38 | 39 | /** Count of alive entities **/ 40 | public var used(default, null):Int = 0; 41 | 42 | /** Count of available entities in pool **/ 43 | public var available(get, never):Int; 44 | 45 | // components 46 | var _components:ComponentTable; 47 | 48 | // services 49 | var _services:CArray; 50 | 51 | // services (sorted by priority) 52 | var _orderedServices:CArray; 53 | 54 | // systems (not idle, sorted by priority) 55 | var _systems:CArray; 56 | 57 | // systems with families 58 | var _processors:CArray; 59 | 60 | var _families:CArray; 61 | 62 | var _changedVector:EntityVector; 63 | var _removedVector:EntityVector; 64 | 65 | var _pool:CInt32RingBuffer; 66 | 67 | // flags 68 | var _aliveMask:CBitArray; 69 | var _activeMask:CBitArray; 70 | var _changedMask:CBitArray; 71 | var _removedMask:CBitArray; 72 | 73 | function new(id:Int, config:WorldConfig, capacity:Int) { 74 | this.id = id; 75 | WorldConstructor.construct(this, capacity, config); 76 | } 77 | 78 | /** 79 | Get registered `Service` by compile-time `Class` constant. 80 | Note: macro generates unsafe-cast to `T:Service`. 81 | **/ 82 | macro public function resolve(self:ExprOf, serviceClass:ExprOf>):ExprOf { 83 | var serviceType = ecx.macro.ClassMacroTools.serviceType(serviceClass); 84 | return macro { 85 | var tmp = @:privateAccess $self._services[$serviceType.id]; 86 | ecx.ds.Cast.unsafe(tmp, $serviceClass); 87 | }; 88 | } 89 | 90 | /** 91 | Resolve `IComponent` service by run-time `ComponentType` 92 | **/ 93 | inline public function getComponentService(componentType:ComponentType):IComponent { 94 | return _components[componentType.id]; 95 | } 96 | 97 | /** 98 | Return new active entity (which will be marked as changed). 99 | **/ 100 | public function create():Entity { 101 | var entity = allocNextEntity(); 102 | _aliveMask.enable(entity.id); 103 | _activeMask.enable(entity.id); 104 | markEntityAsChanged(entity); 105 | return entity; 106 | } 107 | 108 | /** 109 | Returns new passive entity. 110 | **/ 111 | public function createPassive():Entity { 112 | var entity = allocNextEntity(); 113 | _aliveMask.enable(entity.id); 114 | return entity; 115 | } 116 | 117 | /** 118 | Creates active entity and clone data from `source` entity. 119 | **/ 120 | public function clone(source:Entity):Entity { 121 | var entity = create(); 122 | var componentsByType = _components; 123 | for(typeId in 0...componentsByType.length) { 124 | componentsByType[typeId].copy(source, entity); 125 | } 126 | commit(entity); 127 | return entity; 128 | } 129 | 130 | /** 131 | Entity will be destroyed on next world invalidation 132 | **/ 133 | public function destroy(entity:Entity) { 134 | #if ecx_debug 135 | guardEntity(this, entity); 136 | #end 137 | if(_removedMask.enableIfNot(entity.id)) { 138 | _removedVector.place(entity); 139 | } 140 | } 141 | 142 | /** 143 | Performs entities destroying, commits and update families 144 | **/ 145 | public function invalidate() { 146 | #if ecx_debug 147 | makeFamiliesMutable(this); 148 | #end 149 | 150 | if(_removedVector.length > 0 || _changedVector.length > 0) { 151 | destroyEntities(); 152 | changeEntities(); 153 | FamilyData.updateVector(_families); 154 | } 155 | 156 | #if ecx_debug 157 | guardFamilies(this); 158 | makeFamiliesImmutable(this); 159 | #end 160 | } 161 | 162 | /** 163 | Make `entity` active 164 | **/ 165 | public function activate(entity:Entity) { 166 | #if ecx_debug 167 | guardEntity(this, entity); 168 | if(_activeMask.get(entity.id)) throw 'This entity is already active'; 169 | #end 170 | _activeMask.enable(entity.id); 171 | markEntityAsChanged(entity); 172 | } 173 | 174 | /** 175 | Make `entity` passive 176 | **/ 177 | public function deactivate(entity:Entity) { 178 | #if ecx_debug 179 | guardEntity(this, entity); 180 | if(!_activeMask.get(entity.id)) throw "This entity is already inactive"; 181 | #end 182 | _activeMask.disable(entity.id); 183 | markEntityAsChanged(entity); 184 | } 185 | 186 | /** 187 | Convert integer `id` to abstract `Entity` handle. 188 | **/ 189 | inline public function getEntity(id:Int):Entity { 190 | return @:privateAccess new Entity(id); 191 | } 192 | 193 | /** 194 | Check if `entity` is active 195 | **/ 196 | inline public function isActive(entity:Entity):Bool { 197 | return _activeMask.get(entity.id); 198 | } 199 | 200 | /** 201 | Check if `entity` is alive 202 | **/ 203 | inline public function checkAlive(entity:Entity):Bool { 204 | return _aliveMask.get(entity.id); 205 | } 206 | 207 | public function toString():String { 208 | return 'World #$id'; 209 | } 210 | 211 | /** 212 | Destroy all components attached to `entity` 213 | **/ 214 | public function destroyComponents(entity:Entity) { 215 | var componentsData = _components; 216 | for(typeId in 0...componentsData.length) { 217 | var component = componentsData[typeId]; 218 | if(component.has(entity)) { 219 | componentsData[typeId].destroy(entity); 220 | } 221 | } 222 | if(isActive(entity)) { 223 | markEntityAsChanged(entity); 224 | } 225 | } 226 | 227 | /** 228 | Mark entity as changed 229 | **/ 230 | inline public function commit(entity:Entity) { 231 | if(isActive(entity)) { 232 | markEntityAsChanged(entity); 233 | } 234 | } 235 | 236 | /** Iterator for *active* systems ordered by priority **/ 237 | inline public function systems():CArrayIterator { 238 | return new CArrayIterator(_systems); 239 | } 240 | 241 | /** 242 | Iterator for components table 243 | Component could be null (should be fixed later) 244 | **/ 245 | inline public function components():CArrayIterator { 246 | return new CArrayIterator(_components); 247 | } 248 | 249 | /** 250 | Theoretic memory consuming in bytes 251 | **/ 252 | public function getObjectSize():Int { 253 | var total = _services.getObjectSize(); 254 | total += _orderedServices.getObjectSize(); 255 | total += _systems.getObjectSize(); 256 | total += _processors.getObjectSize(); 257 | total += _families.getObjectSize(); 258 | total += _changedVector.getObjectSize(); 259 | total += _removedVector.getObjectSize(); 260 | total += _pool.getObjectSize(); 261 | total += _aliveMask.getObjectSize(); 262 | total += _activeMask.getObjectSize(); 263 | total += _changedMask.getObjectSize(); 264 | total += _removedMask.getObjectSize(); 265 | 266 | for(i in 0..._components.length) { 267 | var component = _components.get(i); 268 | if(component != null) { 269 | total += component.getObjectSize(); 270 | } 271 | } 272 | 273 | return total; 274 | } 275 | 276 | function allocNextEntity():Entity { 277 | #if ecx_debug 278 | if(used >= capacity) throw 'Out of entities, max allowed $capacity'; 279 | #end 280 | 281 | ++used; 282 | return new Entity(_pool.pop()); 283 | } 284 | 285 | inline function get_available():Int { 286 | return capacity - used; 287 | } 288 | 289 | @:access(ecx.types.FamilyData) 290 | function destroyEntities() { 291 | var entities = _removedVector; 292 | var locPool = _pool; 293 | var locRemovedFlags = _removedMask; 294 | var locActiveFlags = _activeMask; 295 | var locAliveMask = _aliveMask; 296 | var families = _families; 297 | var i = 0; 298 | while(i < entities.length) { 299 | var tail = entities.length; 300 | while(i < tail) { 301 | var entity = entities.get(i); 302 | 303 | // Need to remove entities from families before deletion and notify systems 304 | for(j in 0...families.length) { 305 | families.get(j).__disableEntity(entity); 306 | } 307 | 308 | destroyComponents(entity); 309 | locActiveFlags.disable(entity.id); 310 | locAliveMask.disable(entity.id); 311 | locRemovedFlags.disable(entity.id); 312 | locPool.push(entity.id); 313 | ++i; 314 | } 315 | } 316 | 317 | var count = entities.length; 318 | used -= count; 319 | #if ecx_debug 320 | if(used < 0) throw "No way!"; 321 | #end 322 | entities.reset(); 323 | } 324 | 325 | @:access(ecx.types.FamilyData) 326 | function changeEntities() { 327 | var entities = _changedVector; 328 | var changedFlags = _changedMask; 329 | var activeFlags = _activeMask; 330 | var aliveMask = _aliveMask; 331 | var families = _families; 332 | var familiesTotal = families.length; 333 | var i = 0; 334 | while(i < entities.length) { 335 | var tail = entities.length; 336 | while (i < tail) { 337 | var entity = entities.get(i); 338 | var alive = aliveMask.get(entity.id); 339 | if(alive) { 340 | var active = activeFlags.get(entity.id); 341 | if(active) { 342 | for(j in 0...familiesTotal) { 343 | families.get(j).__change(entity); 344 | } 345 | } 346 | else { 347 | for(j in 0...familiesTotal) { 348 | families.get(j).__disableEntity(entity); 349 | } 350 | } 351 | } 352 | changedFlags.disable(entity.id); 353 | ++i; 354 | } 355 | } 356 | entities.reset(); 357 | } 358 | 359 | function markEntityAsChanged(entity:Entity) { 360 | #if ecx_debug 361 | guardEntity(this, entity); 362 | #end 363 | if(_changedMask.enableIfNot(entity.id)) { 364 | _changedVector.place(entity); 365 | } 366 | } 367 | } 368 | --------------------------------------------------------------------------------