├── .gitignore ├── .haxerc ├── .travis.yml ├── .vscode └── tasks.json ├── README.md ├── haxe_libraries ├── ansi.hxml ├── exp-ecs.hxml ├── exp-fsm.hxml ├── hxcpp.hxml ├── hxnodejs.hxml ├── lime.hxml ├── openfl.hxml ├── tink_chunk.hxml ├── tink_cli.hxml ├── tink_core.hxml ├── tink_io.hxml ├── tink_macro.hxml ├── tink_priority.hxml ├── tink_state.hxml ├── tink_streams.hxml ├── tink_stringly.hxml ├── tink_syntaxhub.hxml ├── tink_testrunner.hxml ├── tink_unittest.hxml └── travix.hxml ├── haxelib.json ├── playgound.hxml ├── sample └── asteroid │ ├── .gitignore │ ├── .vscode │ ├── settings.json │ └── tasks.json │ ├── project.flow │ ├── project.xml │ └── src │ ├── GameState.hx │ ├── Main.hx │ ├── component │ ├── Animation.hx │ ├── Asteroid.hx │ ├── Bullet.hx │ ├── Collision.hx │ ├── Death.hx │ ├── Display.hx │ ├── Gun.hx │ ├── GunControls.hx │ ├── Lifespan.hx │ ├── Motion.hx │ ├── MotionControls.hx │ ├── Position.hx │ └── Spaceship.hx │ ├── entity │ ├── Asteroid.hx │ ├── Bullet.hx │ └── Spaceship.hx │ ├── graphic │ ├── AsteroidView.hx │ ├── BulletView.hx │ ├── IAnimatable.hx │ ├── SpaceshipDeathView.hx │ └── SpaceshipView.hx │ ├── system │ ├── AnimationSystem.hx │ ├── BulletAsteroidCollisionHandlerSystem.hx │ ├── CollisionSystem.hx │ ├── DeathSystem.hx │ ├── GameSystem.hx │ ├── GunControlSystem.hx │ ├── LifespanSystem.hx │ ├── MotionControlSystem.hx │ ├── MovementSystem.hx │ ├── RenderSystem.hx │ └── SpaceshipAsteroidCollisionHandlerSystem.hx │ └── util │ ├── Config.hx │ ├── Input.hx │ └── Point.hx ├── src └── exp │ └── ecs │ ├── Engine.hx │ ├── component │ ├── Component.hx │ ├── ComponentProvider.hx │ └── ComponentType.hx │ ├── entity │ ├── Entity.hx │ └── EntityCollection.hx │ ├── event │ ├── EventEmitter.hx │ ├── EventFactory.hx │ └── EventSelector.hx │ ├── node │ ├── Node.hx │ ├── NodeBase.hx │ ├── NodeList.hx │ ├── NodeType.hx │ ├── TrackingNode.hx │ └── TrackingNodeList.hx │ ├── state │ ├── EngineState.hx │ └── EntityState.hx │ ├── system │ ├── EventHandlerSystem.hx │ ├── FixedUpdateSystem.hx │ ├── System.hx │ ├── SystemBase.hx │ ├── SystemCollection.hx │ └── SystemId.hx │ └── util │ ├── Collection.hx │ ├── ConstArrayIterator.hx │ ├── Macro.hx │ └── ReadOnlyArray.hx ├── tests.hxml └── tests ├── Base.hx ├── EngineBenchmark.hx ├── EngineTest.hx ├── NodeListBenchmark.hx ├── NodeListTest.hx ├── NodeTest.hx ├── Playground.hx ├── RunTests.hx ├── StateMachineTest.hx ├── SystemTest.hx └── Types.hx /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | -------------------------------------------------------------------------------- /.haxerc: -------------------------------------------------------------------------------- 1 | { 2 | "version": "f2f4dc1", 3 | "resolveLibs": "scoped" 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: xenial 3 | 4 | language: node_js 5 | node_js: 8 6 | 7 | os: 8 | - linux 9 | - osx 10 | 11 | env: 12 | - HAXE_VERSION=latest 13 | - HAXE_VERSION=nightly 14 | 15 | install: 16 | - npm i -g lix@15.3.13 17 | - lix install haxe $HAXE_VERSION 18 | - lix download 19 | 20 | script: 21 | - lix run travix js 22 | - lix run travix node 23 | - lix run travix lua 24 | - lix run travix php 25 | - lix run travix cpp 26 | - lix run travix python 27 | - if [ "$HAXE_VERSION" == "latest" ]; then lix run travix hl; fi 28 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "command": "lix", 4 | "args": ["run", "travix", "node"], 5 | "problemMatcher": "$haxe", 6 | "group": { 7 | "kind": "build", 8 | "isDefault": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Macro-powered Entity-Component-System framework 2 | 3 | [![Build Status](https://travis-ci.org/kevinresol/exp-ecs.svg?branch=develop)](https://travis-ci.org/kevinresol/exp-ecs) 4 | 5 | This is a Entity-Component-System framework, inspired by the [Ash Framework](http://www.ashframework.org/) 6 | Thanks to the Haxe macro system we are able to reduce boilerplate code and allow some optimizations. 7 | 8 | This is platform-agnostic, you can use it on any targets supported by Haxe (e.g. cpp, js, lua, etc) 9 | This is also game-framework-agnostic, you can use it with any game frameworks such as Kha, OpenFL, Heaps, etc. 10 | 11 | ## Elements of the framework 12 | 13 | **Engine** 14 | The "core" that manages everything 15 | 16 | **Entity** 17 | A container holding various components 18 | 19 | **Component** 20 | Building blocks of an entity, contains attributes/properties but not any logics 21 | 22 | **System** 23 | Logics that operate on Components 24 | 25 | **Node** 26 | A container holding an Entity and the Components of interest. 27 | This is mostly for optimization, pre-fetching components from the entity so that we don't need to do so on every iteration. 28 | 29 | **NodeList** 30 | A list of Nodes. The specialized `TrackingNodeList` will keep track of entities together with their components and add/remove them from the list when appropriate. 31 | 32 | ## Example 33 | 34 | ```haxe 35 | import exp.ecs.*; 36 | import exp.ecs.node.*; 37 | import exp.ecs.entity.*; 38 | import exp.ecs.system.*; 39 | import component.*; 40 | import haxe.Timer; 41 | 42 | class Playground { 43 | static function main() { 44 | // create an engine 45 | var engine = new Engine(); 46 | 47 | // create an entity and adds 2 components to it 48 | var entity = new Entity(); 49 | entity.add(new Velocity(1, 0)); 50 | entity.add(new Position(0, 0)); 51 | 52 | // add entity to engine 53 | engine.entities.add(entity); 54 | 55 | // create and add 2 systems to engine 56 | engine.systems.add(new MovementSystem()); 57 | engine.systems.add(new RenderSystem()); 58 | engine.systems.add(new CustomSystem()); 59 | 60 | // run the engine 61 | new Timer(16).run = function() engine.update(16 / 1000); 62 | } 63 | } 64 | 65 | // Use metadata `@:nodes` to create a TrackingNodeList that contains nodes of entities that contains the specified components 66 | // NodeList created this way will be cached in the engine, i.e. multiple systems will share the same NodeList instance if their Node type is the same 67 | class MovementSystem extends System { 68 | // prepares a NodeList that contains entities having both the Position and Velocity components 69 | @:nodes var nodes:Node; 70 | 71 | override function update(dt:Float) { 72 | // on each update we iterate all the nodes and update their Position components 73 | for(node in nodes) { 74 | node.position.x += node.velocity.x * dt; 75 | node.position.y += node.velocity.y * dt; 76 | } 77 | } 78 | } 79 | 80 | class RenderSystem extends System { 81 | // prepares a NodeList that contains entities having the Position component 82 | @:nodes var nodes:Node; 83 | 84 | override function update(dt:Float) { 85 | // on each update we iterate all the nodes and print out their positions on screen 86 | for(node in nodes) { 87 | trace('${node.entity} @ ${node.position.x}, ${node.position.y}'); 88 | } 89 | } 90 | } 91 | 92 | // Besides using `@:nodes`, you can also create a NodeList manually 93 | class CustomSystem extends System { 94 | var nodes:NodeList; 95 | 96 | override function update(dt:Float) { 97 | for(node in nodes) { 98 | $type(node); // CustomNode 99 | } 100 | } 101 | 102 | override function onAdded(engine) { 103 | super.onAdded(engine); 104 | nodes = new TrackingNodeList(engine, CustomNode.new, entity -> entity.has(Position)); 105 | } 106 | 107 | override function onRemoved(engine) { 108 | super.onRemoved(engine); 109 | nodes = null; 110 | } 111 | } 112 | 113 | // Manually declare a Node type 114 | class CustomNode implements NodeBase { 115 | public var entity(default, null):Entity; 116 | public function new(entity) this.entity = entity; 117 | } 118 | ``` 119 | -------------------------------------------------------------------------------- /haxe_libraries/ansi.hxml: -------------------------------------------------------------------------------- 1 | -D ansi=1.0.0 2 | # @install: lix --silent download "haxelib:/ansi#1.0.0" into ansi/1.0.0/haxelib 3 | -cp ${HAXE_LIBCACHE}/ansi/1.0.0/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/exp-ecs.hxml: -------------------------------------------------------------------------------- 1 | -cp ${SCOPE_DIR}/src 2 | -D exp-ecs 3 | -lib tink_macro 4 | -lib tink_priority 5 | -lib tink_state 6 | -lib exp-fsm -------------------------------------------------------------------------------- /haxe_libraries/exp-fsm.hxml: -------------------------------------------------------------------------------- 1 | -D exp-fsm=0.0.0 2 | # @install: lix --silent download "gh://github.com/kevinresol/exp-fsm#d1addb208ae239121e86ed9ef8305796877bd702" into exp-fsm/0.0.0/github/d1addb208ae239121e86ed9ef8305796877bd702 3 | -lib tink_core 4 | -cp ${HAXE_LIBCACHE}/exp-fsm/0.0.0/github/d1addb208ae239121e86ed9ef8305796877bd702/src 5 | -------------------------------------------------------------------------------- /haxe_libraries/hxcpp.hxml: -------------------------------------------------------------------------------- 1 | -D hxcpp=4.0.8 2 | # @install: lix --silent download "haxelib:/hxcpp#4.0.8" into hxcpp/4.0.8/haxelib 3 | # @run: haxelib run-dir hxcpp ${HAXE_LIBCACHE}/hxcpp/4.0.8/haxelib 4 | -cp ${HAXE_LIBCACHE}/hxcpp/4.0.8/haxelib/ 5 | -------------------------------------------------------------------------------- /haxe_libraries/hxnodejs.hxml: -------------------------------------------------------------------------------- 1 | -D hxnodejs=6.9.0 2 | # @install: lix --silent download "gh://github.com/haxefoundation/hxnodejs#ed9676a923e1f8f76a8643a9144df3795b021b33" into hxnodejs/6.9.0/github/ed9676a923e1f8f76a8643a9144df3795b021b33 3 | -cp ${HAXE_LIBCACHE}/hxnodejs/6.9.0/github/ed9676a923e1f8f76a8643a9144df3795b021b33/src 4 | --macro allowPackage('sys') 5 | # should behave like other target defines and not be defined in macro context 6 | --macro define('nodejs') 7 | -------------------------------------------------------------------------------- /haxe_libraries/lime.hxml: -------------------------------------------------------------------------------- 1 | -D lime=7.3.0 2 | # @install: lix --silent download "haxelib:/lime#7.3.0" into lime/7.3.0/haxelib 3 | # @run: haxelib run-dir lime ${HAXE_LIBCACHE}/lime/7.3.0/haxelib 4 | -cp ${HAXE_LIBCACHE}/lime/7.3.0/haxelib/src 5 | --macro lime._internal.macros.DefineMacro.run() -------------------------------------------------------------------------------- /haxe_libraries/openfl.hxml: -------------------------------------------------------------------------------- 1 | -D openfl=8.9.0 2 | # @install: lix --silent download "gh://github.com/openfl/openfl#e74c0fa67e635d45e4c15176f8e20d62861d71c9" into openfl/8.9.0/github/e74c0fa67e635d45e4c15176f8e20d62861d71c9 3 | # @run: haxelib run-dir openfl ${HAXE_LIBCACHE}/openfl/8.9.0/github/e74c0fa67e635d45e4c15176f8e20d62861d71c9 4 | -cp ${HAXE_LIBCACHE}/openfl/8.9.0/github/e74c0fa67e635d45e4c15176f8e20d62861d71c9/src 5 | --macro openfl._internal.macros.ExtraParams.include() -------------------------------------------------------------------------------- /haxe_libraries/tink_chunk.hxml: -------------------------------------------------------------------------------- 1 | -D tink_chunk=0.2.0 2 | # @install: lix --silent download "haxelib:/tink_chunk#0.2.0" into tink_chunk/0.2.0/haxelib 3 | -cp ${HAXE_LIBCACHE}/tink_chunk/0.2.0/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/tink_cli.hxml: -------------------------------------------------------------------------------- 1 | -D tink_cli=0.3.1 2 | # @install: lix --silent download "haxelib:/tink_cli#0.3.1" into tink_cli/0.3.1/haxelib 3 | -lib tink_io 4 | -lib tink_stringly 5 | -lib tink_macro 6 | -cp ${HAXE_LIBCACHE}/tink_cli/0.3.1/haxelib/src 7 | # Make sure docs are generated 8 | -D use-rtti-doc -------------------------------------------------------------------------------- /haxe_libraries/tink_core.hxml: -------------------------------------------------------------------------------- 1 | -D tink_core=1.22.0 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_core#3fb8f3305bc61211b9e94258118d5d8a4cde89ed" into tink_core/1.22.0/github/3fb8f3305bc61211b9e94258118d5d8a4cde89ed 3 | -cp ${HAXE_LIBCACHE}/tink_core/1.22.0/github/3fb8f3305bc61211b9e94258118d5d8a4cde89ed/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/tink_io.hxml: -------------------------------------------------------------------------------- 1 | -D tink_io=0.6.0 2 | # @install: lix --silent download "haxelib:/tink_io#0.6.0" into tink_io/0.6.0/haxelib 3 | -lib tink_chunk 4 | -lib tink_streams 5 | -cp ${HAXE_LIBCACHE}/tink_io/0.6.0/haxelib/src 6 | -------------------------------------------------------------------------------- /haxe_libraries/tink_macro.hxml: -------------------------------------------------------------------------------- 1 | -D tink_macro=0.17.6 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_macro#888481094af44b29c2dd69f1dac826901602c8b7" into tink_macro/0.17.6/github/888481094af44b29c2dd69f1dac826901602c8b7 3 | -lib tink_core 4 | -cp ${HAXE_LIBCACHE}/tink_macro/0.17.6/github/888481094af44b29c2dd69f1dac826901602c8b7/src 5 | -------------------------------------------------------------------------------- /haxe_libraries/tink_priority.hxml: -------------------------------------------------------------------------------- 1 | -D tink_priority=0.1.3 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_priority#9c83be54d05e88960d10e3c6cb0dc6b5185f305f" into tink_priority/0.1.3/github/9c83be54d05e88960d10e3c6cb0dc6b5185f305f 3 | -cp ${HAXE_LIBCACHE}/tink_priority/0.1.3/github/9c83be54d05e88960d10e3c6cb0dc6b5185f305f/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/tink_state.hxml: -------------------------------------------------------------------------------- 1 | -D tink_state=0.9.0 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_state#a2f4e784d6eea91db0ed6dd10b7f015f4f68da70" into tink_state/0.9.0/github/a2f4e784d6eea91db0ed6dd10b7f015f4f68da70 3 | -lib tink_core 4 | -cp ${HAXE_LIBCACHE}/tink_state/0.9.0/github/a2f4e784d6eea91db0ed6dd10b7f015f4f68da70/src 5 | -------------------------------------------------------------------------------- /haxe_libraries/tink_streams.hxml: -------------------------------------------------------------------------------- 1 | -D tink_streams=0.3.2 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_streams#48c7793bf20fe1fe03cf284a2cb31115908a5753" into tink_streams/0.3.2/github/48c7793bf20fe1fe03cf284a2cb31115908a5753 3 | -lib tink_core 4 | -cp ${HAXE_LIBCACHE}/tink_streams/0.3.2/github/48c7793bf20fe1fe03cf284a2cb31115908a5753/src 5 | # temp for development, delete this file when pure branch merged 6 | -D pure -------------------------------------------------------------------------------- /haxe_libraries/tink_stringly.hxml: -------------------------------------------------------------------------------- 1 | -D tink_stringly=0.2.0 2 | # @install: lix --silent download "haxelib:/tink_stringly#0.2.0" into tink_stringly/0.2.0/haxelib 3 | -lib tink_core 4 | -cp ${HAXE_LIBCACHE}/tink_stringly/0.2.0/haxelib/src 5 | -------------------------------------------------------------------------------- /haxe_libraries/tink_syntaxhub.hxml: -------------------------------------------------------------------------------- 1 | -D tink_syntaxhub=0.4.2 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_syntaxhub#8e4aed73a6922bc3f9c299a7b1b9809a18559581" into tink_syntaxhub/0.4.2/github/8e4aed73a6922bc3f9c299a7b1b9809a18559581 3 | -lib tink_priority 4 | -lib tink_macro 5 | -cp ${HAXE_LIBCACHE}/tink_syntaxhub/0.4.2/github/8e4aed73a6922bc3f9c299a7b1b9809a18559581/src 6 | --macro tink.SyntaxHub.use() -------------------------------------------------------------------------------- /haxe_libraries/tink_testrunner.hxml: -------------------------------------------------------------------------------- 1 | -D tink_testrunner=0.7.2 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_testrunner#9a2e3cbb9ddff7269e08584f30fc425226f10aae" into tink_testrunner/0.7.2/github/9a2e3cbb9ddff7269e08584f30fc425226f10aae 3 | -lib ansi 4 | -lib tink_macro 5 | -lib tink_streams 6 | -cp ${HAXE_LIBCACHE}/tink_testrunner/0.7.2/github/9a2e3cbb9ddff7269e08584f30fc425226f10aae/src 7 | -------------------------------------------------------------------------------- /haxe_libraries/tink_unittest.hxml: -------------------------------------------------------------------------------- 1 | -D tink_unittest=0.6.2 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_unittest#0b0c7de647e522ca42662e2cdfc59e21ed8d4eb4" into tink_unittest/0.6.2/github/0b0c7de647e522ca42662e2cdfc59e21ed8d4eb4 3 | -lib tink_syntaxhub 4 | -lib tink_testrunner 5 | -cp ${HAXE_LIBCACHE}/tink_unittest/0.6.2/github/0b0c7de647e522ca42662e2cdfc59e21ed8d4eb4/src 6 | --macro tink.unit.AssertionBufferInjector.use() -------------------------------------------------------------------------------- /haxe_libraries/travix.hxml: -------------------------------------------------------------------------------- 1 | -D travix=0.12.2 2 | # @install: lix --silent download "gh://github.com/back2dos/travix#d9f15d14098deea42a89334b0473f088e670e7de" into travix/0.12.2/github/d9f15d14098deea42a89334b0473f088e670e7de 3 | # @post-install: cd ${HAXE_LIBCACHE}/travix/0.12.2/github/d9f15d14098deea42a89334b0473f088e670e7de && haxe -cp src --run travix.PostDownload 4 | # @run: haxelib run-dir travix ${HAXE_LIBCACHE}/travix/0.12.2/github/d9f15d14098deea42a89334b0473f088e670e7de 5 | -lib tink_cli 6 | -cp ${HAXE_LIBCACHE}/travix/0.12.2/github/d9f15d14098deea42a89334b0473f088e670e7de/src 7 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exp-ecs", 3 | "license": "MIT", 4 | "tags": [], 5 | "classPath": "src", 6 | "contributors": [ 7 | "kevinresol" 8 | ], 9 | "releasenote": "initial release", 10 | "version": "0.0.0", 11 | "dependencies": { 12 | "tink_macro": "", 13 | "tink_priority": "", 14 | "tink_state": "", 15 | "exp-fsm": "gh:kevinresol/exp-fsm" 16 | } 17 | } -------------------------------------------------------------------------------- /playgound.hxml: -------------------------------------------------------------------------------- 1 | -cp tests 2 | -lib exp-ecs 3 | -main Playground 4 | -js bin/playground/index.js -------------------------------------------------------------------------------- /sample/asteroid/.gitignore: -------------------------------------------------------------------------------- 1 | openfl.hxml 2 | bin -------------------------------------------------------------------------------- /sample/asteroid/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // These are configurations used for haxe completion. 3 | // 4 | // Each configuration is an array of arguments that will be passed to the Haxe completion server, 5 | // they should only contain arguments and/or hxml files that are needed for completion, 6 | // such as -cp, -lib, target output settings and defines. 7 | "haxe.displayConfigurations": [ 8 | ["openfl.hxml"] // if a hxml file is safe to use, we can just pass it as argument 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /sample/asteroid/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "openfl", 4 | "args": ["test", "flash"], 5 | "problemMatcher": { 6 | "owner": "haxe", 7 | "pattern": { 8 | "regexp": "^(.+):(\\d+): (?:lines \\d+-(\\d+)|character(?:s (\\d+)-| )(\\d+)) : (?:(Warning) : )?(.*)$", 9 | "file": 1, 10 | "line": 2, 11 | "endLine": 3, 12 | "column": 4, 13 | "endColumn": 5, 14 | "severity": 6, 15 | "message": 7 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sample/asteroid/project.flow: -------------------------------------------------------------------------------- 1 | { 2 | 3 | project : { 4 | 5 | //These first two are required 6 | name : 'empty', 7 | version : '1.0.0', 8 | author : 'luxeengine', 9 | 10 | //This configures your app. 11 | //The package is important, it's used 12 | //for save locations, initializing mobile project files etc 13 | app : { 14 | name : 'luxe_empty', 15 | package : 'com.luxeengine.empty' 16 | }, 17 | 18 | //This configures the build process 19 | build : { 20 | flags: ['-dce full'], 21 | dependencies : { 22 | luxe : '*', 23 | ecs : '*' 24 | } 25 | }, 26 | 27 | //Copies over all the assets to the output 28 | files : { 29 | assets : 'assets/' 30 | } 31 | 32 | } //project 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /sample/asteroid/project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /sample/asteroid/src/GameState.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | class GameState { 4 | public var lives:Int; 5 | public var level:Int; 6 | public var points:Int; 7 | public var over:Bool; 8 | 9 | public function new() 10 | reset(); 11 | 12 | public function reset() { 13 | lives = 1; 14 | level = 0; 15 | points = 0; 16 | over = false; 17 | } 18 | } -------------------------------------------------------------------------------- /sample/asteroid/src/Main.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import system.*; 4 | import entity.*; 5 | import exp.ecs.Engine; 6 | import exp.ecs.entity.*; 7 | import exp.ecs.state.*; 8 | import exp.ecs.system.*; 9 | import exp.fsm.*; 10 | import util.*; 11 | using tink.CoreApi; 12 | 13 | class Main extends #if openfl openfl.display.Sprite #else luxe.Game #end { 14 | 15 | var state = new GameState(); 16 | var engine = new Engine(); 17 | var input = new Input(); 18 | 19 | #if openfl 20 | 21 | public function new() { 22 | super(); 23 | stage.addEventListener(openfl.events.KeyboardEvent.KEY_DOWN, function(e) input.keyDown(e.keyCode)); 24 | stage.addEventListener(openfl.events.KeyboardEvent.KEY_UP, function(e) input.keyUp(e.keyCode)); 25 | start(input); 26 | 27 | var time = haxe.Timer.stamp(); 28 | this.addEventListener(openfl.events.Event.ENTER_FRAME, function(_) { 29 | var now = haxe.Timer.stamp(); 30 | engine.update(now - time); 31 | time = now; 32 | }); 33 | } 34 | 35 | #elseif luxe 36 | override function config(config:luxe.GameConfig) { 37 | config.render.antialiasing = 4; 38 | return config; 39 | } 40 | 41 | override function ready() { 42 | start(input); 43 | } 44 | 45 | override function onkeyup(event:luxe.Input.KeyEvent) { 46 | if(event.keycode == luxe.Input.Key.escape) { 47 | Luxe.shutdown(); 48 | } else input.keyUp(event.keycode); 49 | } 50 | 51 | override function onkeydown(event:luxe.Input.KeyEvent) { 52 | input.keyDown(event.keycode); 53 | } 54 | 55 | override function update(dt:Float) { 56 | engine.update(dt); 57 | } 58 | 59 | #end 60 | 61 | function start(input) { 62 | var config = { 63 | width: #if openfl stage.stageWidth #elseif luxe Luxe.screen.w #end, 64 | height: #if openfl stage.stageHeight #elseif luxe Luxe.screen.h #end, 65 | }; 66 | 67 | engine.systems.add(new GameSystem(config, state, function(_) return GameOver)); 68 | engine.systems.add(new GunControlSystem(input)); 69 | engine.systems.add(new MotionControlSystem(input)); 70 | // engine.systems.add(new MovementSystem(config)); 71 | engine.systems.add(new CollisionSystem(Collision)); 72 | engine.systems.add(new SpaceshipAsteroidCollisionHandlerSystem()); 73 | engine.systems.add(new BulletAsteroidCollisionHandlerSystem()); 74 | engine.systems.add(new LifespanSystem()); 75 | engine.systems.add(new AnimationSystem()); 76 | engine.systems.add(new DeathSystem()); 77 | 78 | var fsm = StateMachine.create([ 79 | new EngineState('playing', ['gameover'], engine, [ 80 | {system: new MovementSystem(config), before: CollisionSystem}, 81 | {system: new RenderSystem(#if openfl this #end)}, 82 | ]), 83 | new EngineState('gameover', ['playing'], engine, []), 84 | ]); 85 | 86 | engine.systems.add(EventHandlerSystem.simple( 87 | function(e) return e == GameOver ? Some(Noise) : None, 88 | function(_) return { 89 | fsm.transit('gameover'); 90 | stage.addEventListener(openfl.events.KeyboardEvent.KEY_DOWN, function listener(e) { 91 | if(e.keyCode == openfl.ui.Keyboard.SPACE) { 92 | stage.removeEventListener(openfl.events.KeyboardEvent.KEY_DOWN, listener); 93 | state.reset(); 94 | fsm.transit('playing'); 95 | } 96 | }); 97 | } 98 | )); 99 | } 100 | 101 | } 102 | 103 | enum Event { 104 | Collision(data:{entity1:Entity, entity2:Entity, group1:Int, group2:Int}); 105 | GameOver; 106 | } -------------------------------------------------------------------------------- /sample/asteroid/src/component/Animation.hx: -------------------------------------------------------------------------------- 1 | package component; 2 | 3 | import exp.ecs.component.*; 4 | 5 | class Animation extends Component { 6 | public var anime:graphic.IAnimatable; 7 | public function new(anime) 8 | this.anime = anime; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /sample/asteroid/src/component/Asteroid.hx: -------------------------------------------------------------------------------- 1 | package component; 2 | 3 | import exp.ecs.component.*; 4 | 5 | class Asteroid extends Component { 6 | public var radius:Int; 7 | public function new(radius) { 8 | this.radius = radius; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /sample/asteroid/src/component/Bullet.hx: -------------------------------------------------------------------------------- 1 | package component; 2 | 3 | import exp.ecs.component.*; 4 | 5 | class Bullet extends Component { 6 | public function new() {} 7 | } 8 | -------------------------------------------------------------------------------- /sample/asteroid/src/component/Collision.hx: -------------------------------------------------------------------------------- 1 | package component; 2 | 3 | import exp.ecs.component.*; 4 | 5 | class Collision extends Component { 6 | public var group:Int; 7 | public var with:Array; 8 | public var radius:Int; 9 | public function new(group, with, radius) { 10 | this.group = group; 11 | this.with = with; 12 | this.radius = radius; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /sample/asteroid/src/component/Death.hx: -------------------------------------------------------------------------------- 1 | package component; 2 | 3 | import exp.ecs.component.*; 4 | 5 | class Death extends Component { 6 | public var countdown:Float; 7 | public function new(countdown) 8 | this.countdown = countdown; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /sample/asteroid/src/component/Display.hx: -------------------------------------------------------------------------------- 1 | package component; 2 | 3 | import exp.ecs.component.*; 4 | 5 | class Display extends Component { 6 | public var object:#if openfl openfl.display.DisplayObject #elseif luxe luxe.Visual #end; 7 | public function new(object) { 8 | this.object = object; 9 | #if luxe object.visible = false; #end 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /sample/asteroid/src/component/Gun.hx: -------------------------------------------------------------------------------- 1 | package component; 2 | 3 | import exp.ecs.component.*; 4 | import util.*; 5 | 6 | class Gun extends Component { 7 | public var offset:Point; 8 | public var elapsed:Float = 0; 9 | public var interval:Float; 10 | public var bulletLifeTime:Float; 11 | 12 | public function new(x, y, i, l) { 13 | offset = new Point(x, y); 14 | interval = i; 15 | bulletLifeTime = l; 16 | } 17 | } -------------------------------------------------------------------------------- /sample/asteroid/src/component/GunControls.hx: -------------------------------------------------------------------------------- 1 | package component; 2 | 3 | import exp.ecs.component.*; 4 | 5 | class GunControls extends Component { 6 | public var trigger:Int; 7 | public function new(trigger:Int) { 8 | this.trigger = trigger; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /sample/asteroid/src/component/Lifespan.hx: -------------------------------------------------------------------------------- 1 | package component; 2 | 3 | import exp.ecs.component.*; 4 | 5 | class Lifespan extends Component { 6 | public var lifespan:Float; 7 | public function new(lifespan) 8 | this.lifespan = lifespan; 9 | } 10 | -------------------------------------------------------------------------------- /sample/asteroid/src/component/Motion.hx: -------------------------------------------------------------------------------- 1 | package component; 2 | 3 | import exp.ecs.component.*; 4 | import util.*; 5 | 6 | class Motion extends Component { 7 | public var velocity:Point; 8 | public var angularVelocity:Float; 9 | public var damping:Float; 10 | 11 | public function new(x, y, a, d) { 12 | velocity = new Point(x, y); 13 | angularVelocity = a; 14 | damping = d; 15 | } 16 | } -------------------------------------------------------------------------------- /sample/asteroid/src/component/MotionControls.hx: -------------------------------------------------------------------------------- 1 | package component; 2 | 3 | import exp.ecs.component.*; 4 | 5 | class MotionControls extends Component { 6 | public var left:Int; 7 | public var right:Int; 8 | public var accelerate:Int; 9 | 10 | public var accelerationRate:Float; 11 | public var rotationRate:Float; 12 | 13 | public function new(left:Int, right:Int, accelerate:Int, accelerationRate:Float, rotationRate:Float) { 14 | this.left = left; 15 | this.right = right; 16 | this.accelerate = accelerate; 17 | this.accelerationRate = accelerationRate; 18 | this.rotationRate = rotationRate; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample/asteroid/src/component/Position.hx: -------------------------------------------------------------------------------- 1 | package component; 2 | 3 | import exp.ecs.component.*; 4 | import util.*; 5 | 6 | class Position extends Component { 7 | public var position:Point; 8 | public var rotation:Float; 9 | 10 | public function new(x, y, r) { 11 | position = new Point(x, y); 12 | rotation = r; 13 | } 14 | } -------------------------------------------------------------------------------- /sample/asteroid/src/component/Spaceship.hx: -------------------------------------------------------------------------------- 1 | package component; 2 | 3 | import exp.ecs.component.*; 4 | import exp.ecs.state.*; 5 | import exp.fsm.*; 6 | 7 | class Spaceship extends Component { 8 | public var fsm:StateMachine>; 9 | public function new(fsm) 10 | this.fsm = fsm; 11 | } 12 | -------------------------------------------------------------------------------- /sample/asteroid/src/entity/Asteroid.hx: -------------------------------------------------------------------------------- 1 | package entity; 2 | 3 | import component.*; 4 | import exp.ecs.entity.*; 5 | 6 | abstract Asteroid(Entity) to Entity { 7 | public function new(radius, x, y) { 8 | this = new Entity(); 9 | 10 | this.add(new component.Asteroid(radius)); 11 | this.add(new Position(x, y, 0)); 12 | this.add(new Collision(0, [1, 2], radius)); 13 | this.add(new Motion((Math.random() - 0.5) * 4 * (50 - radius), (Math.random() - 0.5) * 4 * (50 - radius), Math.random() * 2 - 1, 0)); 14 | this.add(new Display(new graphic.AsteroidView(radius))); 15 | } 16 | } -------------------------------------------------------------------------------- /sample/asteroid/src/entity/Bullet.hx: -------------------------------------------------------------------------------- 1 | package entity; 2 | 3 | import component.*; 4 | import exp.ecs.entity.*; 5 | 6 | abstract Bullet(Entity) to Entity { 7 | public function new(gun:Gun, position:Position) { 8 | this = new Entity(); 9 | 10 | var cos = Math.cos(position.rotation); 11 | var sin = Math.sin(position.rotation); 12 | 13 | this.add(new component.Bullet()); 14 | this.add(new Lifespan(gun.bulletLifeTime)); 15 | this.add(new Position(cos * gun.offset.x - sin * gun.offset.y + position.position.x, sin * gun.offset.x + cos * gun.offset.y + position.position.y, 0)); 16 | this.add(new Motion(cos * 150, sin * 150, 0, 0)); 17 | this.add(new Collision(2, [0], 0)); 18 | this.add(new Display(new graphic.BulletView())); 19 | } 20 | } -------------------------------------------------------------------------------- /sample/asteroid/src/entity/Spaceship.hx: -------------------------------------------------------------------------------- 1 | package entity; 2 | 3 | import component.*; 4 | import exp.ecs.entity.*; 5 | import exp.ecs.component.*; 6 | import exp.ecs.state.*; 7 | import exp.fsm.*; 8 | 9 | abstract Spaceship(Entity) to Entity { 10 | public function new(x, y) { 11 | this = new Entity(); 12 | 13 | var left = #if openfl openfl.ui.Keyboard.LEFT #elseif luxe luxe.Input.Key.left #end; 14 | var right = #if openfl openfl.ui.Keyboard.RIGHT #elseif luxe luxe.Input.Key.right #end; 15 | var up = #if openfl openfl.ui.Keyboard.UP #elseif luxe luxe.Input.Key.up #end; 16 | var space = #if openfl openfl.ui.Keyboard.SPACE #elseif luxe luxe.Input.Key.space #end; 17 | 18 | var view = new graphic.SpaceshipDeathView(); 19 | 20 | var fsm = StateMachine.create([ 21 | new EntityState('playing', ['destroyed'], this, [ 22 | new Motion(0, 0, 0, 15), 23 | new MotionControls(left, right, up, 100, 3), 24 | new Display(new graphic.SpaceshipView()), 25 | new Gun(8, 0, 0.3, 2), 26 | new GunControls(space), 27 | new Collision(1, [0], 9), 28 | ]), 29 | new EntityState('destroyed', ['playing'], this, [ 30 | new Display(view), 31 | new Animation(view), 32 | new Death(2), 33 | ]), 34 | ]); 35 | 36 | this.add(new component.Spaceship(fsm)); 37 | this.add(new Position(x, y, 0)); 38 | 39 | } 40 | } -------------------------------------------------------------------------------- /sample/asteroid/src/graphic/AsteroidView.hx: -------------------------------------------------------------------------------- 1 | package graphic; 2 | 3 | #if openfl 4 | import openfl.display.*; 5 | 6 | class AsteroidView extends Shape { 7 | public function new(radius:Float) { 8 | super(); 9 | 10 | var angle = 0.; 11 | graphics.beginFill(0xFFFFFF); 12 | graphics.moveTo(radius, 0); 13 | while (angle < Math.PI * 2) { 14 | var length = ( 0.75 + Math.random() * 0.25 ) * radius; 15 | graphics.lineTo(Math.cos(angle) * length, Math.sin(angle) * length); 16 | angle += Math.random() * 0.5; 17 | } 18 | graphics.lineTo(radius, 0); 19 | graphics.endFill(); 20 | } 21 | } 22 | 23 | #elseif luxe 24 | import luxe.*; 25 | 26 | class AsteroidView extends Visual { 27 | public function new(radius:Float) { 28 | var angle = 0.; 29 | var points = []; 30 | while (angle < Math.PI * 2) { 31 | var length = ( 0.75 + Math.random() * 0.25 ) * radius; 32 | points.push(new Vector(Math.cos(angle) * length, Math.sin(angle) * length)); 33 | angle += Math.random() * 0.5; 34 | } 35 | super({ 36 | color: new Color(1, 1, 1, 1), 37 | geometry: Luxe.draw.poly({ 38 | points: points 39 | }), 40 | }); 41 | } 42 | } 43 | #end -------------------------------------------------------------------------------- /sample/asteroid/src/graphic/BulletView.hx: -------------------------------------------------------------------------------- 1 | package graphic; 2 | 3 | #if openfl 4 | import openfl.display.*; 5 | 6 | class BulletView extends Shape { 7 | public function new() { 8 | super(); 9 | 10 | graphics.beginFill(0xFFFFFF); 11 | graphics.drawCircle(0, 0, 2); 12 | graphics.endFill(); 13 | } 14 | } 15 | 16 | #elseif luxe 17 | import luxe.*; 18 | class BulletView extends Visual { 19 | public function new() { 20 | super({ 21 | color: new Color(1, 1, 1, 1), 22 | geometry: Luxe.draw.circle({ 23 | x: -1, 24 | y: -1, 25 | r: 2, 26 | }), 27 | }); 28 | } 29 | } 30 | #end -------------------------------------------------------------------------------- /sample/asteroid/src/graphic/IAnimatable.hx: -------------------------------------------------------------------------------- 1 | package graphic; 2 | 3 | interface IAnimatable { 4 | function animate(time:Float):Void; 5 | } -------------------------------------------------------------------------------- /sample/asteroid/src/graphic/SpaceshipDeathView.hx: -------------------------------------------------------------------------------- 1 | package graphic; 2 | 3 | import util.*; 4 | 5 | #if openfl 6 | import openfl.display.*; 7 | #elseif luxe 8 | import luxe.*; 9 | #end 10 | 11 | class SpaceshipDeathView extends #if openfl Sprite #elseif luxe Visual #end implements IAnimatable { 12 | var shape1:#if openfl Shape #elseif luxe Visual #end; 13 | var shape2:#if openfl Shape #elseif luxe Visual #end; 14 | var vel1:Point; 15 | var vel2:Point; 16 | var rot1:Float; 17 | var rot2:Float; 18 | 19 | public function new() { 20 | 21 | 22 | #if openfl 23 | super(); 24 | shape1 = new Shape(); 25 | shape1.graphics.beginFill(0xFFFFFF); 26 | shape1.graphics.moveTo(10, 0); 27 | shape1.graphics.lineTo(-7, 7); 28 | shape1.graphics.lineTo(-4, 0); 29 | shape1.graphics.lineTo(10, 0); 30 | shape1.graphics.endFill(); 31 | addChild(shape1); 32 | 33 | shape2 = new Shape(); 34 | shape2.graphics.beginFill(0xFFFFFF); 35 | shape2.graphics.moveTo(10, 0); 36 | shape2.graphics.lineTo(-7, -7); 37 | shape2.graphics.lineTo(-4, 0); 38 | shape2.graphics.lineTo(10, 0); 39 | shape2.graphics.endFill(); 40 | addChild(shape2); 41 | 42 | #elseif luxe 43 | super({ 44 | no_geometry: true, 45 | }); 46 | shape1 = new Visual({ 47 | parent: this, 48 | color: new Color(1, 1, 1, 1), 49 | geometry: Luxe.draw.poly({ 50 | points: [ 51 | new Vector (10, 0), 52 | new Vector (-7, 7), 53 | new Vector (-4, 0), 54 | new Vector (10, 0), 55 | ] 56 | }) 57 | }); 58 | shape2 = new Visual({ 59 | parent: this, 60 | color: new Color(1, 1, 1, 1), 61 | geometry: Luxe.draw.poly({ 62 | points: [ 63 | new Vector (10, 0), 64 | new Vector (-7, -7), 65 | new Vector (-4, 0), 66 | new Vector (10, 0), 67 | ] 68 | }) 69 | }); 70 | 71 | #end 72 | 73 | vel1 = new Point(Math.random() * 10 - 5, Math.random() * 10 + 10); 74 | vel2 = new Point(Math.random() * 10 - 5, -( Math.random() * 10 + 10 )); 75 | 76 | rot1 = Math.random() * 300 - 150; 77 | rot2 = Math.random() * 300 - 150; 78 | } 79 | 80 | public function animate(time:Float):Void { 81 | #if openfl shape1.x #elseif luxe shape1.pos.x #end += vel1.x * time; 82 | #if openfl shape1.y #elseif luxe shape1.pos.y #end += vel1.y * time; 83 | #if openfl shape1.rotation #elseif luxe shape1.rotation_z #end += rot1 * time; 84 | #if openfl shape2.x #elseif luxe shape2.pos.x #end += vel2.x * time; 85 | #if openfl shape2.y #elseif luxe shape2.pos.y #end += vel2.y * time; 86 | #if openfl shape2.rotation #elseif luxe shape2.rotation_z #end += rot2 * time; 87 | } 88 | 89 | #if luxe 90 | override function set_visible(v) { 91 | shape1.visible = shape2.visible = v; 92 | return super.set_visible(v); 93 | } 94 | #end 95 | } -------------------------------------------------------------------------------- /sample/asteroid/src/graphic/SpaceshipView.hx: -------------------------------------------------------------------------------- 1 | package graphic; 2 | 3 | #if openfl 4 | import openfl.display.*; 5 | 6 | class SpaceshipView extends Shape { 7 | public function new() { 8 | super(); 9 | 10 | graphics.beginFill(0xFFFFFF); 11 | graphics.moveTo(10, 0); 12 | graphics.lineTo(-7, 7); 13 | graphics.lineTo(-4, 0); 14 | graphics.lineTo(-7, -7); 15 | graphics.lineTo(10, 0); 16 | graphics.endFill(); 17 | } 18 | } 19 | #elseif luxe 20 | 21 | import luxe.*; 22 | class SpaceshipView extends Visual { 23 | public function new() { 24 | super({ 25 | color: new Color(1, 1, 1, 1), 26 | geometry: Luxe.draw.poly({ 27 | points: [ 28 | new Vector(10, 0), 29 | new Vector(-7, 7), 30 | new Vector(-4, 0), 31 | new Vector(-7, -7), 32 | new Vector(10, 0), 33 | ] 34 | }), 35 | }); 36 | } 37 | } 38 | #end -------------------------------------------------------------------------------- /sample/asteroid/src/system/AnimationSystem.hx: -------------------------------------------------------------------------------- 1 | package system; 2 | 3 | import component.*; 4 | import exp.ecs.node.*; 5 | import exp.ecs.system.*; 6 | 7 | using tink.CoreApi; 8 | 9 | class AnimationSystem extends System { 10 | @:nodes var nodes:Node; 11 | override function update(dt:Float) { 12 | for(node in nodes) 13 | node.animation.anime.animate(dt); 14 | } 15 | } -------------------------------------------------------------------------------- /sample/asteroid/src/system/BulletAsteroidCollisionHandlerSystem.hx: -------------------------------------------------------------------------------- 1 | package system; 2 | 3 | import exp.ecs.entity.*; 4 | import exp.ecs.system.*; 5 | import Main; 6 | using tink.CoreApi; 7 | 8 | class BulletAsteroidCollisionHandlerSystem extends EventHandlerSystem> { 9 | override function select(e:Event) { 10 | return switch e { 11 | case Collision({entity1: e2, entity2: e1, group2: g}) | Collision({entity1: e1, entity2: e2, group1: g}) if(g == 2): Some(new Pair(e1, e2)); 12 | case _: None; 13 | } 14 | } 15 | 16 | override function handle(pair:Pair) { 17 | var bullet = pair.a; 18 | var asteroid = pair.b; 19 | var radius = asteroid.get(component.Asteroid).radius; 20 | var position = asteroid.get(component.Position).position; 21 | 22 | if(radius > 10) { 23 | // break into 2 small asteroids 24 | engine.entities.add(new entity.Asteroid(radius - 10, position.x + Math.random() * 10 - 5, position.y + Math.random() * 10 - 5)); 25 | engine.entities.add(new entity.Asteroid(radius - 10, position.x + Math.random() * 10 - 5, position.y + Math.random() * 10 - 5)); 26 | } 27 | 28 | engine.entities.remove(bullet); 29 | engine.entities.remove(asteroid); 30 | } 31 | } -------------------------------------------------------------------------------- /sample/asteroid/src/system/CollisionSystem.hx: -------------------------------------------------------------------------------- 1 | package system; 2 | 3 | import component.*; 4 | import exp.ecs.system.*; 5 | import exp.ecs.node.*; 6 | import exp.ecs.event.*; 7 | import exp.ecs.entity.*; 8 | import util.*; 9 | using tink.CoreApi; 10 | 11 | class CollisionSystem extends System { 12 | @:nodes var nodes:Node; 13 | 14 | var factory:EventFactory; 15 | 16 | public function new(factory) { 17 | super(); 18 | this.factory = factory; 19 | } 20 | 21 | override function update(dt:Float) { 22 | var arr = nodes.nodes; 23 | 24 | for(i in 0...arr.length) for(j in i+1...arr.length) { 25 | var n1 = arr[i]; 26 | var n2 = arr[j]; 27 | if(match(n1.collision, n2.collision)) { 28 | if(Point.distance(n1.position.position, n2.position.position) < n1.collision.radius + n2.collision.radius) { 29 | engine.events.afterSystemUpdate(factory({ 30 | entity1: n1.entity, 31 | entity2: n2.entity, 32 | group1: n1.collision.group, 33 | group2: n2.collision.group, 34 | })); 35 | } 36 | } 37 | } 38 | } 39 | 40 | function match(c1:Collision, c2:Collision) { 41 | return c2.with.indexOf(c1.group) != -1 && c1.with.indexOf(c2.group) != -1; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sample/asteroid/src/system/DeathSystem.hx: -------------------------------------------------------------------------------- 1 | package system; 2 | 3 | import component.*; 4 | import exp.ecs.node.*; 5 | import exp.ecs.system.*; 6 | 7 | using tink.CoreApi; 8 | 9 | class DeathSystem extends System { 10 | @:nodes var nodes:Node; 11 | 12 | override function update(dt:Float) { 13 | for(node in nodes) { 14 | node.death.countdown -= dt; 15 | if(node.death.countdown < 0) engine.entities.remove(node.entity); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /sample/asteroid/src/system/GameSystem.hx: -------------------------------------------------------------------------------- 1 | package system; 2 | 3 | import component.*; 4 | import exp.ecs.event.*; 5 | import exp.ecs.system.*; 6 | import exp.ecs.node.*; 7 | import util.*; 8 | 9 | using tink.CoreApi; 10 | 11 | class GameSystem extends System { 12 | @:nodes var spaceships:Node; 13 | @:nodes var asteroids:Node; 14 | @:nodes var bullets:Node; 15 | 16 | var config:Config; 17 | var game:GameState; 18 | var gameover:EventFactory; 19 | 20 | public function new(config, game, gameover) { 21 | super(); 22 | this.config = config; 23 | this.game = game; 24 | this.gameover = gameover; 25 | } 26 | 27 | override function update(dt:Float) { 28 | if(!game.over) { 29 | if(spaceships.empty) { 30 | if(game.lives > 0) { 31 | var newPos = new Point(config.width * 0.5, config.height * 0.5); 32 | var clear = true; 33 | for(asteroid in asteroids) { 34 | if(Point.distance(asteroid.position.position, newPos) <= asteroid.collision.radius + 50) { 35 | clear = false; 36 | break; 37 | } 38 | } 39 | if(clear) { 40 | engine.entities.add(new entity.Spaceship(newPos.x, newPos.y)); 41 | game.lives--; 42 | } 43 | } else { 44 | // game over 45 | trace('Game Over'); 46 | game.over = true; 47 | engine.events.afterEngineUpdate(gameover(Noise)); 48 | } 49 | } 50 | 51 | if(asteroids.empty && bullets.empty && !spaceships.empty) { 52 | // next level 53 | var spaceship = spaceships.head; 54 | game.level++; 55 | for(i in 0... 2 + game.level) { 56 | var position = null; 57 | do position = new Point(Math.random() * config.width, Math.random() * config.height) 58 | while(Point.distance(position, spaceship.position.position) < 80); 59 | engine.entities.add(new entity.Asteroid(30, position.x, position.y)); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /sample/asteroid/src/system/GunControlSystem.hx: -------------------------------------------------------------------------------- 1 | package system; 2 | 3 | import component.*; 4 | import exp.ecs.node.*; 5 | import exp.ecs.system.*; 6 | import util.*; 7 | 8 | using tink.CoreApi; 9 | 10 | class GunControlSystem extends System { 11 | @:nodes var nodes:Node; 12 | var input:Input; 13 | 14 | public function new(input) { 15 | super(); 16 | this.input = input; 17 | } 18 | 19 | override function update(dt:Float) { 20 | for(node in nodes) { 21 | var control = node.gunControls; 22 | var position = node.position; 23 | var gun = node.gun; 24 | 25 | var triggered = input.isDown(control.trigger); 26 | gun.elapsed += dt; 27 | if(triggered && gun.elapsed > gun.interval) { 28 | engine.entities.add(new entity.Bullet(gun, position)); 29 | gun.elapsed = 0; 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /sample/asteroid/src/system/LifespanSystem.hx: -------------------------------------------------------------------------------- 1 | package system; 2 | 3 | import component.*; 4 | import exp.ecs.node.*; 5 | import exp.ecs.system.*; 6 | 7 | using tink.CoreApi; 8 | 9 | class LifespanSystem extends System { 10 | @:nodes var nodes:Node; 11 | 12 | override function update(dt:Float) { 13 | for(node in nodes) { 14 | node.lifespan.lifespan -= dt; 15 | if(node.lifespan.lifespan < 0) 16 | engine.entities.remove(node.entity); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /sample/asteroid/src/system/MotionControlSystem.hx: -------------------------------------------------------------------------------- 1 | package system; 2 | 3 | import component.*; 4 | import exp.ecs.node.*; 5 | import exp.ecs.system.*; 6 | import util.*; 7 | 8 | using tink.CoreApi; 9 | 10 | class MotionControlSystem extends System { 11 | @:nodes var nodes:Node; 12 | var input:Input; 13 | 14 | public function new(input) { 15 | super(); 16 | this.input = input; 17 | } 18 | 19 | override function update(dt:Float) { 20 | for(node in nodes) { 21 | var control = node.motionControls; 22 | var position = node.position; 23 | var motion = node.motion; 24 | 25 | if(input.isDown(control.left)) position.rotation -= control.rotationRate * dt; 26 | if(input.isDown(control.right)) position.rotation += control.rotationRate * dt; 27 | if(input.isDown(control.accelerate)) { 28 | motion.velocity.x += Math.cos(position.rotation) * control.accelerationRate * dt; 29 | motion.velocity.y += Math.sin(position.rotation) * control.accelerationRate * dt; 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /sample/asteroid/src/system/MovementSystem.hx: -------------------------------------------------------------------------------- 1 | package system; 2 | 3 | import component.*; 4 | import exp.ecs.node.*; 5 | import exp.ecs.system.*; 6 | import util.*; 7 | 8 | using tink.CoreApi; 9 | 10 | class MovementSystem extends System { 11 | @:nodes var nodes:Node; 12 | 13 | var config:Config; 14 | 15 | public function new(config) { 16 | super(); 17 | this.config = config; 18 | } 19 | 20 | override function update(dt:Float) { 21 | for(node in nodes) { 22 | var position = node.position; 23 | var motion = node.motion; 24 | 25 | position.position.x += motion.velocity.x * dt; 26 | position.position.y += motion.velocity.y * dt; 27 | if(position.position.x < 0) position.position.x += config.width; 28 | if(position.position.x > config.width) position.position.x -= config.width; 29 | if(position.position.y < 0) position.position.y += config.height; 30 | if(position.position.y > config.height) position.position.y -= config.height; 31 | position.rotation += motion.angularVelocity * dt; 32 | 33 | if(motion.damping > 0) { 34 | var x = Math.abs(Math.cos(position.rotation)) * motion.damping * dt; 35 | var y = Math.abs(Math.sin(position.rotation)) * motion.damping * dt; 36 | if(motion.velocity.x > x) motion.velocity.x -= x; 37 | else if(motion.velocity.x < -x) motion.velocity.x += x; 38 | else motion.velocity.x = 0; 39 | if(motion.velocity.y > y) motion.velocity.y -= y; 40 | else if(motion.velocity.y < -y) motion.velocity.y += y; 41 | else motion.velocity.y = 0; 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /sample/asteroid/src/system/RenderSystem.hx: -------------------------------------------------------------------------------- 1 | package system; 2 | 3 | import component.*; 4 | import exp.ecs.Engine; 5 | import exp.ecs.node.*; 6 | import exp.ecs.system.*; 7 | 8 | using tink.CoreApi; 9 | 10 | class RenderSystem extends System { 11 | 12 | @:nodes var nodes:Node; 13 | 14 | var listeners:CallbackLink; 15 | 16 | #if openfl 17 | var container:openfl.display.DisplayObjectContainer; 18 | 19 | public function new(container) { 20 | super(); 21 | this.container = container; 22 | } 23 | #end 24 | 25 | override function onAdded(engine:Engine) { 26 | super.onAdded(engine); 27 | for(node in nodes) addToDisplay(node); 28 | listeners = [ 29 | nodes.nodeAdded.handle(addToDisplay), 30 | nodes.nodeRemoved.handle(removeFromDisplay), 31 | ]; 32 | } 33 | 34 | override function onRemoved(engine:Engine) { 35 | for(node in nodes) removeFromDisplay(node); 36 | super.onRemoved(engine); 37 | listeners.dissolve(); 38 | listeners = null; 39 | } 40 | 41 | function addToDisplay(node:Node) { 42 | #if openfl 43 | container.addChild(node.display.object); 44 | #elseif luxe 45 | node.display.object.visible = true; 46 | #end 47 | } 48 | 49 | function removeFromDisplay(node:Node) { 50 | #if openfl 51 | container.removeChild(node.display.object); 52 | #elseif luxe 53 | node.display.object.destroy(); 54 | #end 55 | } 56 | 57 | override function update(dt:Float) { 58 | for(node in nodes) { 59 | var display = node.display.object; 60 | var position = node.position; 61 | #if openfl display.x #elseif luxe display.pos.x #end = position.position.x; 62 | #if openfl display.y #elseif luxe display.pos.y #end = position.position.y; 63 | #if openfl display.rotation #elseif luxe display.rotation_z #end = position.rotation * 180 / Math.PI; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /sample/asteroid/src/system/SpaceshipAsteroidCollisionHandlerSystem.hx: -------------------------------------------------------------------------------- 1 | package system; 2 | 3 | import exp.ecs.entity.*; 4 | import exp.ecs.system.*; 5 | import Main; 6 | using tink.CoreApi; 7 | 8 | class SpaceshipAsteroidCollisionHandlerSystem extends EventHandlerSystem { 9 | override function select(e:Event) { 10 | return switch e { 11 | case Collision({entity2: e, group2: g}) | Collision({entity1: e, group1: g}) if(g == 1): Some(e); 12 | case _: None; 13 | } 14 | } 15 | 16 | override function handle(entity:Entity) { 17 | entity.get(component.Spaceship).fsm.transit('destroyed'); 18 | } 19 | } -------------------------------------------------------------------------------- /sample/asteroid/src/util/Config.hx: -------------------------------------------------------------------------------- 1 | package util; 2 | 3 | typedef Config = { 4 | width:Int, 5 | height:Int, 6 | } -------------------------------------------------------------------------------- /sample/asteroid/src/util/Input.hx: -------------------------------------------------------------------------------- 1 | package util; 2 | 3 | class Input { 4 | var keys:Array; // TODO: what a naive but quick & dirty method 5 | 6 | public function new() { 7 | keys = []; 8 | } 9 | 10 | public function isDown(key:Int) { 11 | return keys[key] == true; 12 | } 13 | public function keyDown(code:Int) { 14 | keys[code] = true; 15 | } 16 | public function keyUp(code:Int) { 17 | keys[code] = false; 18 | } 19 | } -------------------------------------------------------------------------------- /sample/asteroid/src/util/Point.hx: -------------------------------------------------------------------------------- 1 | package util; 2 | 3 | class Point { 4 | public var x:Float; 5 | public var y:Float; 6 | 7 | public function new(x, y) { 8 | this.x = x; 9 | this.y = y; 10 | } 11 | 12 | public static function distance(p1:Point, p2:Point) { 13 | var dx = p1.x - p2.x; 14 | var dy = p1.y - p2.y; 15 | return Math.sqrt(dx * dx + dy * dy); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/exp/ecs/Engine.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs; 2 | 3 | import exp.ecs.entity.*; 4 | import exp.ecs.event.*; 5 | import exp.ecs.node.*; 6 | import exp.ecs.node.NodeList; 7 | import exp.ecs.system.*; 8 | import exp.ecs.state.*; 9 | import exp.ecs.util.*; 10 | import exp.fsm.*; 11 | import tink.state.State; 12 | 13 | using tink.CoreApi; 14 | 15 | class Engine { 16 | 17 | public var entities(default, null):EntityCollection; 18 | public var systems(default, null):SystemCollection; 19 | public var events(default, null):EventEmitter; 20 | public var delay(default, null):Delay; 21 | 22 | public var systemUpdated(default, null):Signal>; 23 | public var updated(default, null):Signal; 24 | 25 | var nodeLists:Map>; 26 | var systemUpdatedTrigger:SignalTrigger>; 27 | var updatedTrigger:SignalTrigger; 28 | 29 | public function new() { 30 | systemUpdated = systemUpdatedTrigger = Signal.trigger(); 31 | updated = updatedTrigger = Signal.trigger(); 32 | entities = new EntityCollection(); 33 | systems = new SystemCollection(this); 34 | events = new EventEmitter(this); 35 | delay = new Delay(this); 36 | nodeLists = new Map(); 37 | } 38 | 39 | public function update(dt:Float) { 40 | systems.lock(); 41 | for(system in systems) { 42 | entities.lock(); 43 | system.update(dt); 44 | entities.unlock(); 45 | systemUpdatedTrigger.trigger(system); 46 | } 47 | systems.unlock(); 48 | updatedTrigger.trigger(Noise); 49 | } 50 | 51 | public function getNodeList(type:NodeType, factory:Engine->NodeList):NodeList { 52 | if(!nodeLists.exists(type)) nodeLists.set(type, factory(this)); 53 | return cast nodeLists.get(type); 54 | } 55 | 56 | public function destroy() { 57 | for(list in nodeLists) list.destroy(); 58 | nodeLists = null; 59 | 60 | entities.destroy(); 61 | entities = null; 62 | 63 | systems.destroy(); 64 | systems = null; 65 | } 66 | 67 | public function toString() { 68 | var buf = new StringBuf(); 69 | // buf.add(entities); 70 | // buf.add([for(s in systems) s.toString()]); 71 | // buf.add(nodeLists); 72 | return buf.toString(); 73 | } 74 | } 75 | 76 | class Delay { 77 | var postSystemUpdate:ArrayVoid>; 78 | var postEngineUpdate:ArrayVoid>; 79 | var engine:Engine; 80 | 81 | public function new(engine) { 82 | this.engine = engine; 83 | postSystemUpdate = []; 84 | postEngineUpdate = []; 85 | engine.systemUpdated.handle(function(_) flushSystem()); 86 | engine.updated.handle(function(_) flushUpdate()); 87 | } 88 | 89 | public inline function afterSystemUpdate(v:Void->Void) { 90 | postSystemUpdate.push(v); 91 | } 92 | 93 | public inline function afterEngineUpdate(v:Void->Void) { 94 | postEngineUpdate.push(v); 95 | } 96 | 97 | public function flushSystem() { 98 | if(flush(postSystemUpdate)) 99 | postSystemUpdate = []; 100 | // TODO: use this in haxe 4: postSystemUpdate.resize(0); 101 | } 102 | 103 | public function flushUpdate() { 104 | if(flush(postEngineUpdate)) 105 | postEngineUpdate = []; 106 | // TODO: use this in haxe 4: postEngineUpdate.resize(0); 107 | } 108 | 109 | function flush(calls:ArrayVoid>) { 110 | for(call in new ConstArrayIterator(calls)) call(); 111 | return calls.length > 0; 112 | } 113 | } 114 | 115 | -------------------------------------------------------------------------------- /src/exp/ecs/component/Component.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.component; 2 | 3 | class Component { 4 | public var componentType(get, never):ComponentType; 5 | 6 | inline function get_componentType():ComponentType 7 | return this; 8 | 9 | public inline function asProvider(?id:String) 10 | return new ComponentProvider.ComponentInstanceProvider(this, id); 11 | } 12 | -------------------------------------------------------------------------------- /src/exp/ecs/component/ComponentProvider.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.component; 2 | 3 | @:forward 4 | abstract ComponentProvider(ComponentProviderObject) from ComponentProviderObject to ComponentProviderObject { 5 | @:from 6 | public static inline function ofComponent(v:Component):ComponentProvider { 7 | return new ComponentInstanceProvider(v); 8 | } 9 | } 10 | 11 | class ComponentInstanceProvider implements ComponentProviderObject { 12 | var component:Component; 13 | var id:String; 14 | 15 | public function new(component, ?id) { 16 | this.component = component; 17 | this.id = id == null ? (component:ComponentType) : id; 18 | } 19 | 20 | public function get() 21 | return component; 22 | 23 | public function identifier():String 24 | return id; 25 | } 26 | 27 | interface ComponentProviderObject { 28 | function get():Component; 29 | function identifier():String; 30 | } -------------------------------------------------------------------------------- /src/exp/ecs/component/ComponentType.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.component; 2 | 3 | @:forward(split) 4 | abstract ComponentType(String) { 5 | inline function new(v:String) 6 | this = v; 7 | 8 | @:from 9 | public static inline function ofClass(v:Class):ComponentType 10 | return new ComponentType(Type.getClassName(v)); 11 | 12 | @:from 13 | public static inline function ofInstance(v:Component):ComponentType 14 | return ofClass(Type.getClass(v)); 15 | 16 | @:to 17 | public inline function toClass():Class 18 | return cast Type.resolveClass(this); 19 | 20 | @:to 21 | public inline function toString():String 22 | return this; 23 | } 24 | -------------------------------------------------------------------------------- /src/exp/ecs/entity/Entity.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.entity; 2 | 3 | import exp.ecs.component.*; 4 | using tink.CoreApi; 5 | 6 | class Entity { 7 | 8 | public var id(default, null):Int; 9 | public var componentAdded:Signal; 10 | public var componentRemoved:Signal; 11 | 12 | var components:Map; 13 | var componentAddedTrigger:SignalTrigger; 14 | var componentRemovedTrigger:SignalTrigger; 15 | var name:String; 16 | 17 | static var ids:Int = 0; 18 | 19 | public function new(?name) { 20 | this.id = ++ids; 21 | this.name = name; 22 | components = new Map(); 23 | componentAdded = componentAddedTrigger = Signal.trigger(); 24 | componentRemoved = componentRemovedTrigger = Signal.trigger(); 25 | } 26 | 27 | public function add(component:Component, ?type:ComponentType) { 28 | if(type == null) type = component; 29 | if(components.exists(type)) remove(type); 30 | components.set(type, component); 31 | componentAddedTrigger.trigger(component); 32 | } 33 | 34 | public function remove(type:ComponentType) { 35 | var component = components.get(type); 36 | if(component != null) { 37 | components.remove(type); 38 | componentRemovedTrigger.trigger(component); 39 | } 40 | return component; 41 | } 42 | 43 | public inline function get(type:Class):T { 44 | return cast components.get((cast type:Class)); 45 | } 46 | 47 | public inline function has(type:ComponentType) { 48 | return components.exists(type); 49 | } 50 | 51 | public function hasAll(types:Array) { 52 | for(type in types) if(!has(type)) return false; 53 | return true; 54 | } 55 | 56 | public function destroy() { 57 | components = null; 58 | componentAddedTrigger.clear(); 59 | componentRemovedTrigger.clear(); 60 | componentAddedTrigger = null; 61 | componentRemovedTrigger = null; 62 | } 63 | 64 | public function toString():String { 65 | return name == null ? 'Entity#$id' : name; 66 | } 67 | } -------------------------------------------------------------------------------- /src/exp/ecs/entity/EntityCollection.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.entity; 2 | 3 | import exp.ecs.util.Collection; 4 | 5 | using tink.CoreApi; 6 | 7 | class EntityCollection extends Collection { 8 | public var added(default, null):Signal; 9 | public var removed(default, null):Signal; 10 | 11 | var array:Array; 12 | var addedTrigger:SignalTrigger; 13 | var removedTrigger:SignalTrigger; 14 | 15 | public function new() { 16 | super(); 17 | array = []; 18 | added = addedTrigger = Signal.trigger(); 19 | removed = removedTrigger = Signal.trigger(); 20 | } 21 | 22 | public inline function add(entity:Entity) 23 | schedule(entity, Add); 24 | 25 | public inline function remove(entity:Entity, destroy = false) 26 | schedule(entity, destroy ? RemoveAndDestroy : Remove); 27 | 28 | override function operate(entity:Entity, operation:Operation) { 29 | switch operation { 30 | case Add: 31 | remove(entity); // re-add to the end of the list 32 | array.push(entity); 33 | addedTrigger.trigger(entity); 34 | case Remove | RemoveAndDestroy: 35 | if(array.remove(entity)) 36 | removedTrigger.trigger(entity); 37 | if(operation == RemoveAndDestroy) 38 | entity.destroy(); 39 | } 40 | } 41 | 42 | override function destroy() { 43 | // for(entity in array) entity.destroy(); // rethink: should we do this? 44 | array = null; 45 | addedTrigger.clear(); 46 | removedTrigger.clear(); 47 | addedTrigger = null; 48 | removedTrigger = null; 49 | } 50 | 51 | public inline function iterator() 52 | return array.iterator(); 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/exp/ecs/event/EventEmitter.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.event; 2 | 3 | import exp.ecs.util.*; 4 | using tink.CoreApi; 5 | 6 | @:allow(exp.ecs) 7 | class EventEmitter { 8 | var trigger:SignalTrigger; 9 | var postSystemUpdate:Array; 10 | var postEngineUpdate:Array; 11 | var engine:Engine; 12 | 13 | function new(engine) { 14 | this.engine = engine; 15 | trigger = Signal.trigger(); 16 | postSystemUpdate = []; 17 | postEngineUpdate = []; 18 | engine.systemUpdated.handle(function(_) flushSystem()); 19 | engine.updated.handle(function(_) flushUpdate()); 20 | } 21 | 22 | /** 23 | * Register a callback that will be invoked for each event emitted 24 | * @param f 25 | */ 26 | public inline function handle(f) { 27 | return trigger.asSignal().handle(f); 28 | } 29 | 30 | /** 31 | * Create a filtered and transformed Signal. 32 | * @param f A filter+transform function. Return `None` to discard an event. Returning `Some(data)` will cause the resulting Signal to emit `data`. 33 | */ 34 | public inline function select(f) { 35 | return trigger.asSignal().select(f); 36 | } 37 | 38 | /** 39 | * Emit an event immediately 40 | * @param v 41 | */ 42 | public inline function immediate(v:Event) { 43 | trigger.trigger(v); 44 | } 45 | 46 | /** 47 | * Emit an event after the current running System finishes its current `update` 48 | * @param v 49 | */ 50 | public inline function afterSystemUpdate(v:Event) { 51 | postSystemUpdate.push(v); 52 | } 53 | 54 | /** 55 | * Emit an event after the Engine finishes its current `update` 56 | * @param v 57 | */ 58 | public inline function afterEngineUpdate(v:Event) { 59 | postEngineUpdate.push(v); 60 | } 61 | 62 | function flushSystem() { 63 | if(flush(postSystemUpdate)) 64 | postSystemUpdate = []; 65 | // TODO: use this in haxe 4: postSystemUpdate.resize(0); 66 | } 67 | 68 | function flushUpdate() { 69 | if(flush(postEngineUpdate)) 70 | postEngineUpdate = []; 71 | // TODO: use this in haxe 4: postEngineUpdate.resize(0); 72 | } 73 | 74 | inline function flush(events:Array) { 75 | for(e in new ConstArrayIterator(events)) trigger.trigger(e); 76 | return events.length > 0; 77 | } 78 | } -------------------------------------------------------------------------------- /src/exp/ecs/event/EventFactory.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.event; 2 | 3 | typedef EventFactory = Data->Event; -------------------------------------------------------------------------------- /src/exp/ecs/event/EventSelector.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.event; 2 | 3 | import haxe.ds.Option; 4 | 5 | typedef EventSelector = Event->Option; -------------------------------------------------------------------------------- /src/exp/ecs/node/Node.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.node; 2 | 3 | #if !macro 4 | @:genericBuild(exp.ecs.node.Node.build()) 5 | class Node {} 6 | #else 7 | 8 | import haxe.macro.Expr; 9 | import haxe.macro.Context; 10 | import haxe.macro.Type; 11 | import tink.macro.BuildCache; 12 | 13 | using tink.MacroApi; 14 | using haxe.macro.TypeTools; 15 | using StringTools; 16 | using Lambda; 17 | 18 | class Node { 19 | public static function build() { 20 | return switch Context.getLocalType() { 21 | case TInst(_, [type = _.reduce() => TAnonymous(_)]): 22 | buildAnon(type); 23 | case TInst(_, params): 24 | var pos = Context.currentPos(); 25 | var anon = ComplexType.TAnonymous([for(type in params) { 26 | name: { 27 | var cls = type.getClass(); 28 | cls.name.substr(0, 1).toLowerCase() + cls.name.substr(1); // auto named in the component's class name camel-cased 29 | }, 30 | kind: FVar(type.toComplex(), null), 31 | pos: pos, 32 | }]); 33 | buildAnon(anon.toType().sure()); 34 | // params.sort(function(t1, t2) return Reflect.compare(t1.getID(), t2.getID())); 35 | // buildRest(params); 36 | default: throw 'assert'; 37 | } 38 | } 39 | 40 | static function buildAnon(type:Type) { 41 | return BuildCache.getType('exp.ecs.node.Node', type, function(ctx:BuildContext) { 42 | return switch ctx.type { 43 | case TAnonymous(_.get() => {fields: f}): 44 | var fields = []; 45 | for(field in f) { 46 | if(isComponent(field.type, field.pos)) { 47 | fields.push({ 48 | name: field.name, 49 | type: field.type.reduce().toComplex(), 50 | optional: field.meta.has(':optional'), 51 | }); 52 | } 53 | } 54 | buildClass(ctx.name, fields, ctx.pos); 55 | case _: 56 | throw 'unreachable'; 57 | } 58 | }); 59 | } 60 | 61 | static function isComponent(type, pos:Position) { 62 | return if(exp.ecs.util.Macro.isComponent(type)) { 63 | true; 64 | } else { 65 | pos.error('Expected a class that extends Component, but ${type.getID()} doesn\'t.'); 66 | false; 67 | } 68 | } 69 | 70 | static function buildClass(name:String, fields:Array, pos) { 71 | var tp = 'exp.ecs.node.$name'.asTypePath(); 72 | var nodeListTp = 'exp.ecs.node.TrackingNodeList'.asTypePath([TPType(TPath(tp)), TPType(macro:Event)]); 73 | var ctExprs = [for(field in fields) if(!field.optional) macro $p{field.type.toString().split('.')}]; 74 | var description = [for(field in fields) (field.optional ? '?' : '') + field.type.toString().split('.').pop()].join(','); 75 | var ctorExprs = []; 76 | var onAddedExprs = []; 77 | var onRemovedExprs = []; 78 | var destroyExprs = []; 79 | 80 | var def = macro class $name extends exp.ecs.node.TrackingNode { 81 | static var componentTypes:Array = $a{ctExprs}; 82 | 83 | public function new(entity) { 84 | super(entity, 'TrackingNode#' + $v{description}); 85 | $b{ctorExprs} 86 | } 87 | 88 | public static function createNodeList(engine:exp.ecs.Engine) { 89 | return new $nodeListTp( 90 | engine, 91 | $p{['exp', 'ecs', 'node', name, 'new']}, 92 | function(entity) return entity.hasAll(componentTypes), 93 | 'TrackingNodeList#' + $v{description} 94 | ); 95 | } 96 | 97 | override function destroy() { 98 | super.destroy(); 99 | $b{destroyExprs} 100 | } 101 | 102 | override function onComponentAdded(__component:exp.ecs.component.Component) { 103 | var __type = __component.componentType; 104 | $b{onAddedExprs}; 105 | } 106 | override function onComponentRemoved(__component:exp.ecs.component.Component) { 107 | $b{onRemovedExprs}; 108 | } 109 | } 110 | 111 | for(field in fields) { 112 | var name = field.name; 113 | var ct = field.type; 114 | var ctExpr = macro $p{ct.toString().split('.')} 115 | 116 | ctorExprs.push(macro this.$name = entity.get($p{ct.toString().split('.')})); 117 | onAddedExprs.push(macro if(__type == $ctExpr) this.$name = cast __component); 118 | onRemovedExprs.push(macro if(this.$name == __component) this.$name = null); 119 | destroyExprs.push(macro this.$name = null); 120 | 121 | def.fields.push({ 122 | name: name, 123 | kind: FVar(ct), 124 | pos: pos, 125 | access: [APublic], 126 | meta: null, 127 | }); 128 | } 129 | 130 | def.pack = ['exp', 'ecs', 'node']; 131 | return def; 132 | } 133 | } 134 | 135 | typedef NodeField = { 136 | name:String, 137 | type:ComplexType, 138 | optional:Bool, 139 | } 140 | #end 141 | -------------------------------------------------------------------------------- /src/exp/ecs/node/NodeBase.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.node; 2 | 3 | import exp.ecs.entity.*; 4 | 5 | class NodeBase { 6 | public var entity(default, null):Entity; 7 | 8 | var name:String = 'NodeBase'; 9 | 10 | public function destroy() { 11 | entity = null; 12 | } 13 | 14 | public function toString() { 15 | return '$name($entity)'; 16 | } 17 | } -------------------------------------------------------------------------------- /src/exp/ecs/node/NodeList.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.node; 2 | 3 | import exp.ecs.entity.*; 4 | import exp.ecs.util.*; 5 | using tink.CoreApi; 6 | using Lambda; 7 | 8 | class NodeList { 9 | public var length(get, never):Int; 10 | public var empty(get, never):Bool; 11 | public var head(get, never):T; 12 | public var id(default, null):Int; 13 | public var nodeAdded(default, null):Signal; 14 | public var nodeRemoved(default, null):Signal; 15 | public var nodes(get, never):ReadOnlyArray; 16 | 17 | var nodeAddedTrigger:SignalTrigger; 18 | var nodeRemovedTrigger:SignalTrigger; 19 | var entities:Array; 20 | var _nodes:Array; 21 | var factory:Entity->T; 22 | var name:String; 23 | 24 | static var ids:Int = 0; 25 | 26 | public function new(factory, ?name) { 27 | entities = []; 28 | _nodes = []; 29 | id = ++ids; 30 | nodeAdded = nodeAddedTrigger = Signal.trigger(); 31 | nodeRemoved = nodeRemovedTrigger = Signal.trigger(); 32 | this.factory = factory; 33 | this.name = name; 34 | } 35 | 36 | public function add(entity:Entity) { 37 | return 38 | if(entities.indexOf(entity.id) == -1) { 39 | var node = factory(entity); 40 | entities.push(entity.id); 41 | _nodes.push(node); 42 | nodeAddedTrigger.trigger(node); 43 | true; 44 | } else { 45 | false; 46 | } 47 | } 48 | 49 | public function remove(entity:Entity) { 50 | return 51 | switch entities.indexOf(entity.id) { 52 | case -1: 53 | false; 54 | case i: 55 | var node = _nodes[i]; 56 | _nodes.splice(i, 1); 57 | entities.splice(i, 1); 58 | nodeRemovedTrigger.trigger(node); 59 | node.destroy(); 60 | true; 61 | } 62 | } 63 | 64 | public function destroy() { 65 | for(node in _nodes) node.destroy(); 66 | _nodes = null; 67 | entities = null; 68 | 69 | // TODO: destroy signals 70 | } 71 | 72 | public inline function iterator() return new ConstArrayIterator(_nodes); 73 | 74 | inline function get_nodes():ReadOnlyArray return _nodes; 75 | inline function get_length() return _nodes.length; 76 | inline function get_empty() return _nodes.length == 0; 77 | inline function get_head() return _nodes[0]; 78 | 79 | public function toString():String { 80 | return name == null ? 'NodeList#$id' : name; 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /src/exp/ecs/node/NodeType.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.node; 2 | 3 | abstract NodeType(String) to String { 4 | inline function new(v:String) 5 | this = v; 6 | 7 | @:from 8 | public static inline function ofClass(v:Class):NodeType 9 | return new NodeType(Type.getClassName(v)); 10 | 11 | @:from 12 | public static inline function ofInstance(v:NodeBase):NodeType 13 | return ofClass(Type.getClass(v)); 14 | 15 | @:to 16 | public inline function toClass():Class 17 | return cast Type.resolveClass(this); 18 | } -------------------------------------------------------------------------------- /src/exp/ecs/node/TrackingNode.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.node; 2 | 3 | import exp.ecs.component.*; 4 | using tink.CoreApi; 5 | 6 | class TrackingNode extends NodeBase { 7 | var binding:CallbackLink; 8 | 9 | public function new(entity, ?name) { 10 | this.entity = entity; 11 | this.name = name; 12 | 13 | binding = [ 14 | entity.componentAdded.handle(onComponentAdded), 15 | entity.componentRemoved.handle(onComponentRemoved), 16 | ]; 17 | } 18 | 19 | override function destroy() { 20 | super.destroy(); 21 | binding.dissolve(); 22 | binding = null; 23 | } 24 | 25 | function onComponentAdded(component:Component) {} 26 | function onComponentRemoved(component:Component) {} 27 | } -------------------------------------------------------------------------------- /src/exp/ecs/node/TrackingNodeList.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.node; 2 | 3 | import exp.ecs.*; 4 | import exp.ecs.entity.*; 5 | import exp.ecs.node.Node; 6 | import exp.ecs.component.*; 7 | 8 | using tink.CoreApi; 9 | 10 | /** 11 | * This is a specialized NodeList implementation that will watch the engine's entity list and their component list and: 12 | * 1. add to the list those entities fulfilling the condition 13 | * 2. remove from the list those entities not fulfilling the condition 14 | */ 15 | class TrackingNodeList extends NodeList { 16 | 17 | var condition:Entity->Bool; 18 | var engine:Engine; 19 | var listeners:Map = new Map(); 20 | var binding:CallbackLink; 21 | 22 | public function new(engine, factory, condition, ?name) { 23 | super(factory, name); 24 | 25 | this.engine = engine; 26 | this.condition = condition; 27 | 28 | for(entity in engine.entities) { 29 | track(entity); 30 | if(condition(entity)) add(entity); 31 | } 32 | 33 | binding = [ 34 | engine.entities.added.handle(function(entity) { 35 | track(entity); 36 | if(condition(entity)) add(entity); 37 | }), 38 | engine.entities.removed.handle(function(entity) { 39 | untrack(entity); 40 | remove(entity); 41 | }), 42 | ]; 43 | } 44 | 45 | override function destroy() { 46 | super.destroy(); 47 | for(l in listeners) l.dissolve(); 48 | listeners = null; 49 | binding.dissolve(); 50 | binding = null; 51 | } 52 | 53 | function track(entity:Entity) { 54 | if(listeners.exists(entity)) return; // already tracking 55 | listeners.set(entity, [ 56 | entity.componentAdded.handle(function(_) if(condition(entity)) add(entity)), 57 | entity.componentRemoved.handle(function(_) if(!condition(entity)) remove(entity)), 58 | ]); 59 | } 60 | 61 | function untrack(entity:Entity) { 62 | if(!listeners.exists(entity)) return; // not tracking 63 | listeners.get(entity).dissolve(); 64 | listeners.remove(entity); 65 | } 66 | 67 | override function toString():String { 68 | return name == null ? 'TrackingNodeList#$id' : name; 69 | } 70 | } -------------------------------------------------------------------------------- /src/exp/ecs/state/EngineState.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.state; 2 | 3 | import exp.fsm.State; 4 | import exp.ecs.system.*; 5 | 6 | class EngineState extends BasicState { 7 | var engine:Engine; 8 | var infos:Array>; 9 | 10 | public function new(key, next, engine, infos) { 11 | super(key, next); 12 | this.engine = engine; 13 | this.infos = infos; 14 | } 15 | 16 | override function onActivate() { 17 | for(info in infos) engine.systems.addBetween(info.before, info.after, info.system, info.id); 18 | } 19 | 20 | override function onDeactivate() { 21 | for(info in infos) engine.systems.remove(info.system); 22 | } 23 | } 24 | 25 | typedef SystemInfo = {system:System, ?before:SystemId, ?after:SystemId, ?id:SystemId} 26 | -------------------------------------------------------------------------------- /src/exp/ecs/state/EntityState.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.state; 2 | 3 | import exp.fsm.State; 4 | import exp.ecs.component.*; 5 | import exp.ecs.entity.*; 6 | 7 | class EntityState extends BasicState { 8 | var entity:Entity; 9 | var components:Array; 10 | public function new(key, next, entity, components) { 11 | super(key, next); 12 | this.entity = entity; 13 | this.components = components; 14 | } 15 | 16 | override function onActivate() { 17 | for(component in components) entity.add(component); 18 | } 19 | 20 | override function onDeactivate() { 21 | for(component in components) entity.remove(component); 22 | } 23 | } -------------------------------------------------------------------------------- /src/exp/ecs/system/EventHandlerSystem.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.system; 2 | 3 | import exp.ecs.event.*; 4 | using tink.CoreApi; 5 | 6 | /** 7 | * A System that monitors and handles events from the engine event bus. 8 | */ 9 | class EventHandlerSystem extends System { 10 | 11 | var binding:CallbackLink; 12 | 13 | /** 14 | * Override this function to filter and extract data from an event in the event bus. 15 | * If you are not interested in a event, simply return `None`. 16 | * Otherwise if `Some(data)` is returned from this function, the `handle` function will be invoked accordingly. 17 | * @param event 18 | * @return Option 19 | */ 20 | function select(event:Event):Option 21 | return None; 22 | 23 | /** 24 | * Override this function to handle event data selected by the `select` function. 25 | * You can produce the desired effect right in this function. 26 | * Or you can also store up the data and handle them later, e.g. in the `update` function 27 | * @param data 28 | */ 29 | function handle(data:Data) {} 30 | 31 | override function onAdded(engine) { 32 | super.onAdded(engine); 33 | binding = engine.events.select(select).handle(handle); 34 | } 35 | 36 | override function onRemoved(engine) { 37 | super.onRemoved(engine); 38 | binding.dissolve(); 39 | } 40 | 41 | public static inline function simple(selector, handler) 42 | return new SimpleEventHandlerSystem(selector, handler); 43 | } 44 | 45 | class SimpleEventHandlerSystem extends EventHandlerSystem { 46 | 47 | var selector:EventSelector; 48 | var handler:Callback; 49 | 50 | public function new(selector, handler) { 51 | super(); 52 | this.selector = selector; 53 | this.handler = handler; 54 | } 55 | 56 | override function select(event:Event):Option 57 | return selector(event); 58 | 59 | override function handle(data:Data) { 60 | handler.invoke(data); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/exp/ecs/system/FixedUpdateSystem.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.system; 2 | 3 | class FixedUpdateSystem extends System { 4 | 5 | var delta:Float; 6 | var residue:Float = 0; 7 | 8 | public function new(delta = 0.01) { 9 | super(); 10 | this.delta = delta; 11 | } 12 | 13 | final override function update(dt:Float) { 14 | residue += dt; 15 | while(residue > delta) { 16 | fixedUpdate(delta); 17 | residue -= delta; 18 | } 19 | } 20 | 21 | function fixedUpdate(dt:Float) {} 22 | } 23 | -------------------------------------------------------------------------------- /src/exp/ecs/system/System.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.system; 2 | 3 | 4 | #if !macro 5 | import exp.ecs.*; 6 | 7 | @:autoBuild(exp.ecs.system.System.build()) 8 | class System implements SystemBase { 9 | var engine:Engine; 10 | 11 | public function new() {} 12 | 13 | public function update(dt:Float) {} 14 | 15 | public function onAdded(engine:Engine) { 16 | this.engine = engine; 17 | setNodeLists(engine); 18 | } 19 | 20 | public function onRemoved(engine:Engine) { 21 | this.engine = null; 22 | unsetNodeLists(); 23 | } 24 | 25 | public function destroy() {} 26 | 27 | function setNodeLists(engine:Engine) {} 28 | 29 | function unsetNodeLists() {} 30 | 31 | public function toString():String 32 | return Type.getClassName(Type.getClass(this)); 33 | } 34 | #else 35 | 36 | import haxe.macro.Expr; 37 | import haxe.macro.Type; 38 | import haxe.macro.Context; 39 | import tink.macro.BuildCache; 40 | 41 | using tink.MacroApi; 42 | using StringTools; 43 | 44 | class System { 45 | public static function build():Array { 46 | var builder = new ClassBuilder(); 47 | var addedExprs = []; 48 | var removedExprs = []; 49 | for(field in builder) { 50 | switch field.extractMeta(':nodes') { 51 | case Success(_): 52 | var name = field.name; 53 | switch field.kind { 54 | case FVar(ct, e): 55 | var ct = ct.toType().sure().toComplex(); 56 | field.kind = FieldType.FVar(macro:exp.ecs.node.NodeList<$ct>, e); 57 | var parts = ct.toString().split('.'); 58 | addedExprs.push(macro $i{name} = engine.getNodeList($p{parts}, $p{parts.concat(['createNodeList'])})); 59 | removedExprs.push(macro $i{name} = null); 60 | case _: 61 | field.pos.error('Unsupported'); 62 | } 63 | case Failure(_): 64 | } 65 | } 66 | 67 | 68 | function getEventType(type:Type) { 69 | var ct = type.toComplex(); 70 | return (macro { 71 | function get(s:exp.ecs.system.System):A return null; 72 | get((null:$ct)); 73 | }).typeof().sure(); 74 | } 75 | 76 | var event = getEventType(Context.getLocalType()).toComplex(); 77 | if(addedExprs.length > 0) builder.addMembers(macro class {override function setNodeLists(engine:exp.ecs.Engine<$event>) $b{addedExprs}}); 78 | if(removedExprs.length > 0) builder.addMembers(macro class {override function unsetNodeLists() $b{removedExprs}}); 79 | 80 | return builder.export(); 81 | } 82 | } 83 | #end 84 | -------------------------------------------------------------------------------- /src/exp/ecs/system/SystemBase.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.system; 2 | 3 | import exp.ecs.*; 4 | import exp.ecs.node.*; 5 | using tink.CoreApi; 6 | 7 | interface SystemBase { 8 | function update(dt:Float):Void; 9 | function onAdded(engine:Engine):Void; 10 | function onRemoved(engine:Engine):Void; 11 | function destroy():Void; 12 | } -------------------------------------------------------------------------------- /src/exp/ecs/system/SystemCollection.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.system; 2 | 3 | import exp.ecs.*; 4 | import exp.ecs.util.Collection; 5 | import haxe.PosInfos; 6 | import tink.priority.*; 7 | 8 | class SystemCollection extends Collection>> { 9 | var queue:Queue>; 10 | var engine:Engine; 11 | 12 | public function new(engine) { 13 | super(); 14 | this.engine = engine; 15 | queue = new Queue(); 16 | } 17 | 18 | public function add(system:System, ?id:SystemId, ?pos:PosInfos) { 19 | if(id == null) id = system; 20 | var all = @:privateAccess queue.toArray(); 21 | var after = all[all.length - 1]; 22 | schedule({data: system, id: new ID(id), after: selector(after)}, Add); 23 | } 24 | 25 | public function addBefore(before:SystemId, system:System, ?id:SystemId, ?pos:PosInfos) { 26 | if(id == null) id = system; 27 | schedule({data: system, id: new ID(id), before: selector(before)}, Add); 28 | } 29 | 30 | public function addAfter(after:SystemId, system:System, ?id:SystemId, ?pos:PosInfos) { 31 | if(id == null) id = system; 32 | schedule({data: system, id: new ID(id), after: selector(after)}, Add); 33 | } 34 | 35 | public function addBetween(before:SystemId, after:SystemId, system:System, ?id:SystemId, ?pos:PosInfos) { 36 | if(id == null) id = system; 37 | schedule({data: system, id: new ID(id), before: selector(before), after: selector(after)}, Add); 38 | } 39 | 40 | public inline function remove(system:System, destroy = false) { 41 | schedule({data: system}, destroy ? RemoveAndDestroy : Remove); 42 | } 43 | 44 | override function operate(item:Item>, operation:Operation) { 45 | var system = item.data; 46 | switch operation { 47 | case Add: 48 | queue.add(item); 49 | system.onAdded(engine); 50 | case Remove | RemoveAndDestroy: 51 | queue.remove(system); 52 | system.onRemoved(engine); 53 | if(operation == RemoveAndDestroy) 54 | system.destroy(); 55 | } 56 | } 57 | 58 | override function destroy() { 59 | queue = null; 60 | engine = null; 61 | } 62 | 63 | public inline function iterator() 64 | return queue.iterator(); 65 | 66 | function selector(id:SystemId):Selector> 67 | return id == null ? null : function(i:Item>) return i.id == new ID(id); 68 | } 69 | -------------------------------------------------------------------------------- /src/exp/ecs/system/SystemId.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.system; 2 | 3 | abstract SystemId(String) from String to String { 4 | @:from 5 | public static function fromInstance(system:System):SystemId 6 | return system == null ? null : Type.getClassName(Type.getClass(system)); 7 | 8 | @:from 9 | public static function fromClass(cls:Class>):SystemId 10 | return cls == null ? null : Type.getClassName(cls); 11 | } -------------------------------------------------------------------------------- /src/exp/ecs/util/Collection.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.util; 2 | 3 | import tink.state.*; 4 | 5 | using tink.CoreApi; 6 | 7 | class Collection { 8 | var pending:Array>; 9 | var locked:State; 10 | 11 | public function new() { 12 | pending = []; 13 | locked = new State(false); 14 | } 15 | 16 | public inline function lock() { 17 | locked.set(true); 18 | } 19 | 20 | public inline function unlock() { 21 | locked.set(false); 22 | } 23 | 24 | public function destroy() {} 25 | 26 | function schedule(item:Item, operation:Operation) { 27 | if(locked.value) { 28 | if(pending.length == 0) 29 | locked.observe().nextTime(function(v) return !v).handle(update); 30 | pending.push(new Pair(item, operation)); 31 | } else { 32 | operate(item, operation); 33 | } 34 | } 35 | 36 | function update(_) { 37 | for(v in pending) operate(v.a, v.b); 38 | pending = []; 39 | } 40 | 41 | function operate(item:Item, operation:Operation) {} 42 | 43 | } 44 | 45 | enum abstract Operation(Int) { 46 | var Add; 47 | var Remove; 48 | var RemoveAndDestroy; 49 | } -------------------------------------------------------------------------------- /src/exp/ecs/util/ConstArrayIterator.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.util; 2 | 3 | class ConstArrayIterator { 4 | 5 | var cur:Int; 6 | var max:Int; 7 | var array:Array; 8 | 9 | public inline function new(arr:Array) { 10 | cur = 0; 11 | max = arr.length; 12 | array = arr; 13 | } 14 | 15 | public inline function hasNext():Bool { 16 | return cur != max; 17 | } 18 | 19 | public inline function next():T { 20 | return array[cur++]; 21 | } 22 | } -------------------------------------------------------------------------------- /src/exp/ecs/util/Macro.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.util; 2 | 3 | import haxe.macro.Expr; 4 | import haxe.macro.Context; 5 | import tink.macro.BuildCache; 6 | 7 | using tink.MacroApi; 8 | using StringTools; 9 | 10 | class Macro { 11 | static var COMPONENT_TYPE = Context.getType('exp.ecs.component.Component'); 12 | public static function isComponent(type:haxe.macro.Type) { 13 | return type.isSubTypeOf(COMPONENT_TYPE).isSuccess(); 14 | } 15 | } -------------------------------------------------------------------------------- /src/exp/ecs/util/ReadOnlyArray.hx: -------------------------------------------------------------------------------- 1 | package exp.ecs.util; 2 | 3 | @:forward(concat, copy, filter, indexOf, join, lastIndexOf, map, slice, toString) 4 | abstract ReadOnlyArray(Array) from Array { 5 | public var length(get,never):Int; 6 | inline function get_length() return this.length; 7 | @:arrayAccess inline function get(i:Int) return this[i]; 8 | public inline function iterator() return new ConstArrayIterator(this); 9 | } -------------------------------------------------------------------------------- /tests.hxml: -------------------------------------------------------------------------------- 1 | -cp tests 2 | -main RunTests 3 | -dce full 4 | -lib tink_unittest 5 | 6 | # -D analyzer-optimize -------------------------------------------------------------------------------- /tests/Base.hx: -------------------------------------------------------------------------------- 1 | class Base { 2 | public function new() {} 3 | inline function getClassName(v:Dynamic) { 4 | return Type.getClassName(Type.getClass(v)); 5 | } 6 | } -------------------------------------------------------------------------------- /tests/EngineBenchmark.hx: -------------------------------------------------------------------------------- 1 | import Types; 2 | 3 | import exp.ecs.Engine; 4 | import exp.ecs.entity.*; 5 | import exp.ecs.system.*; 6 | import tink.unit.*; 7 | 8 | using tink.CoreApi; 9 | 10 | class EngineBenchmark implements Benchmark { 11 | 12 | var engine:Engine = new Engine(); 13 | 14 | public function new() {} 15 | 16 | @:setup 17 | public function setup() { 18 | engine = new Engine(); 19 | return Noise; 20 | } 21 | 22 | @:benchmark(10000) 23 | public function addEntity() { 24 | var entity = new Entity(); 25 | engine.entities.add(entity); 26 | } 27 | 28 | @:benchmark(10000) 29 | public function addSystem() { 30 | var system = new System(); 31 | engine.systems.add(system); 32 | } 33 | 34 | @:teardown 35 | public function teardown() { 36 | engine.destroy(); 37 | engine = null; 38 | return Noise; 39 | } 40 | } -------------------------------------------------------------------------------- /tests/EngineTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import Types; 4 | import exp.ecs.*; 5 | import exp.ecs.entity.*; 6 | import exp.ecs.state.*; 7 | import exp.fsm.*; 8 | 9 | 10 | @:asserts 11 | class EngineTest extends Base { 12 | public function addEntity() { 13 | var engine = new Engine(); 14 | var entity = new Entity(); 15 | 16 | var added = 0; 17 | engine.entities.added.handle(function(entity) added++); 18 | engine.entities.add(entity); 19 | 20 | asserts.assert(added == 1); 21 | 22 | engine.destroy(); 23 | return asserts.done(); 24 | } 25 | 26 | public function reAddEntity() { 27 | var engine = new Engine(); 28 | var entity1 = new Entity(); 29 | var entity2 = new Entity(); 30 | 31 | var added = 0; 32 | engine.entities.added.handle(function(entity) added++); 33 | engine.entities.add(entity1); 34 | engine.entities.add(entity2); 35 | asserts.assert(added == 2); 36 | var entities = [for(e in engine.entities) e]; 37 | asserts.assert(entities[0] == entity1, 'engine.entities[0] == entity1'); 38 | asserts.assert(entities[1] == entity2, 'engine.entities[1] == entity2'); 39 | 40 | engine.entities.add(entity1); 41 | asserts.assert(added == 3); 42 | var entities = [for(e in engine.entities) e]; 43 | asserts.assert(entities[0] == entity2, 'engine.entities[0] == entity2'); 44 | asserts.assert(entities[1] == entity1, 'engine.entities[1] == entity1'); 45 | 46 | engine.destroy(); 47 | return asserts.done(); 48 | } 49 | 50 | public function addSystem() { 51 | 52 | var engine = new Engine(); 53 | var fsm = StateMachine.create([ 54 | new EngineState('foo', [], engine, [{system: new MovementSystem(), before: MovementSystem}]), 55 | ]); 56 | return asserts.done(); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /tests/NodeListBenchmark.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import Types; 4 | import exp.ecs.Engine; 5 | import exp.ecs.entity.*; 6 | import exp.ecs.node.*; 7 | import exp.ecs.*; 8 | import tink.unit.*; 9 | 10 | using tink.CoreApi; 11 | 12 | class NodeListBenchmark implements Benchmark { 13 | 14 | var list:NodeList; 15 | 16 | public function new() {} 17 | 18 | @:before 19 | public function before() { 20 | var engine = new Engine(); 21 | for(i in 0...1000) { 22 | var entity = new Entity(); 23 | entity.add(new Velocity(0, 0)); 24 | entity.add(new Position(0, 0)); 25 | engine.entities.add(entity); 26 | } 27 | list = engine.getNodeList(MovementNode, MovementNode.createNodeList); 28 | return Noise; 29 | } 30 | 31 | @:benchmark(10000) 32 | public function iterate() { 33 | for(node in list) { 34 | var pos = node.position; 35 | var vel = node.velocity; 36 | pos.x += vel.x; 37 | pos.y += vel.y; 38 | } 39 | } 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /tests/NodeListTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import Types; 4 | import exp.ecs.Engine; 5 | import exp.ecs.entity.Entity; 6 | 7 | using StringTools; 8 | 9 | @:asserts 10 | class NodeListTest extends Base { 11 | @:variant('Add entity to engine before creating the node list'(true)) 12 | @:variant('Add entity to engine after creating the node list'(false)) 13 | public function add(lateAdd) { 14 | return asserts.defer(function() { 15 | 16 | var engine = new Engine(); 17 | var entity = new Entity(); 18 | 19 | var added = 0, removed = 0; 20 | 21 | if(!lateAdd) engine.entities.add(entity); 22 | 23 | var list = engine.getNodeList(MovementNode, MovementNode.createNodeList); 24 | list.nodeAdded.handle(function(_) added++); 25 | list.nodeRemoved.handle(function(_) removed++); 26 | 27 | if(lateAdd) engine.entities.add(entity); 28 | 29 | asserts.assert(list.toString() == 'TrackingNodeList#Position,Velocity'); 30 | asserts.assert(added == 0); 31 | asserts.assert(removed == 0); 32 | var velocity = new Velocity(0, 0); 33 | var position = new Position(0, 0); 34 | 35 | entity.add(velocity); 36 | asserts.assert(list.length == 0); 37 | asserts.assert(added == 0); 38 | asserts.assert(removed == 0); 39 | 40 | entity.add(position); 41 | asserts.assert(list.length == 1); 42 | asserts.assert(added == 1); 43 | asserts.assert(removed == 0); 44 | 45 | entity.remove(velocity); 46 | asserts.assert(list.length == 0); 47 | asserts.assert(added == 1); 48 | asserts.assert(removed == 1); 49 | 50 | entity.add(velocity); 51 | asserts.assert(list.length == 1); 52 | asserts.assert(added == 2); 53 | asserts.assert(removed == 1); 54 | 55 | engine.destroy(); 56 | asserts.done(); 57 | }); 58 | } 59 | 60 | @:variant('Add entity to engine before creating the node list'(true)) 61 | @:variant('Add entity to engine after creating the node list'(false)) 62 | public function optional(lateAdd) { 63 | return asserts.defer(function() { 64 | var engine = new Engine(); 65 | var entity = new Entity(); 66 | 67 | var added = 0, removed = 0; 68 | 69 | if(!lateAdd) engine.entities.add(entity); 70 | 71 | var list = engine.getNodeList(OptionalNode, OptionalNode.createNodeList); 72 | list.nodeAdded.handle(function(_) added++); 73 | list.nodeRemoved.handle(function(_) removed++); 74 | 75 | if(lateAdd) engine.entities.add(entity); 76 | 77 | asserts.assert(list.toString() == 'TrackingNodeList#Position,?Velocity'); 78 | asserts.assert(added == 0); 79 | var velocity = new Velocity(0, 0); 80 | var position = new Position(0, 0); 81 | 82 | entity.add(position); 83 | asserts.assert(list.length == 1); 84 | asserts.assert(added == 1); 85 | asserts.assert(removed == 0); 86 | for(node in list) { 87 | asserts.assert(node.toString().startsWith('TrackingNode#Position,?Velocity')); 88 | asserts.assert(node.position != null, 'node.position != null'); 89 | asserts.assert(node.velocity == null, 'node.velocity == null'); 90 | } 91 | 92 | entity.add(velocity); 93 | asserts.assert(list.length == 1); 94 | asserts.assert(added == 1); 95 | asserts.assert(removed == 0); 96 | for(node in list) { 97 | asserts.assert(node.position != null, 'node.position != null'); 98 | asserts.assert(node.velocity != null, 'node.velocity != null'); 99 | } 100 | 101 | entity.remove(velocity); 102 | asserts.assert(list.length == 1); 103 | asserts.assert(added == 1); 104 | asserts.assert(removed == 0); 105 | for(node in list) { 106 | asserts.assert(node.position != null, 'node.position != null'); 107 | asserts.assert(node.velocity == null, 'node.velocity == null'); 108 | } 109 | 110 | entity.remove(position); 111 | asserts.assert(list.length == 0); 112 | asserts.assert(added == 1); 113 | asserts.assert(removed == 1); 114 | 115 | engine.destroy(); 116 | asserts.done(); 117 | }); 118 | } 119 | } -------------------------------------------------------------------------------- /tests/NodeTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import Types; 4 | import exp.ecs.node.*; 5 | import exp.ecs.entity.*; 6 | 7 | @:asserts 8 | class NodeTest extends Base { 9 | public function typeEquality() { 10 | var entity = new Entity(); 11 | 12 | var node1 = new Node(entity); 13 | var node2 = new Node(entity); // duplicated just to make sure they generate the same type 14 | asserts.assert(getClassName(node1) == getClassName(node2)); 15 | 16 | var node1 = new Node(entity); 17 | var node2 = new Node(entity); // duplicated just to make sure they generate the same type 18 | var node3 = new Node(entity); 19 | asserts.assert(getClassName(node1) == getClassName(node2)); 20 | asserts.assert(getClassName(node2) == getClassName(node3)); 21 | 22 | return asserts.done(); 23 | } 24 | 25 | public function componentChange() { 26 | var entity = new Entity(); 27 | var vel1 = new Velocity(1, 1); 28 | var vel2 = new Velocity(2, 2); 29 | var node = new Node(entity); 30 | 31 | asserts.assert(node.velocity == null, 'Initial reference'); 32 | 33 | // new component 34 | entity.add(vel1); 35 | asserts.assert(node.velocity == vel1, 'Add reference to component'); 36 | 37 | // change component 38 | entity.add(vel2); 39 | asserts.assert(node.velocity == vel2, 'Change reference to component'); 40 | 41 | // remove component 42 | entity.remove(Velocity); 43 | asserts.assert(node.velocity == null, 'Remove reference to component'); 44 | 45 | 46 | return asserts.done(); 47 | } 48 | } -------------------------------------------------------------------------------- /tests/Playground.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import exp.ecs.*; 4 | import exp.ecs.node.*; 5 | import exp.ecs.entity.*; 6 | import exp.ecs.system.*; 7 | import exp.ecs.event.*; 8 | import component.*; 9 | import haxe.Timer; 10 | 11 | using tink.CoreApi; 12 | 13 | class Playground { 14 | static function main() { 15 | var engine = new Engine(); 16 | var entity = new Entity(); 17 | entity.add(new Velocity(1, 0)); 18 | entity.add(new Position(0, 0)); 19 | 20 | engine.entities.add(entity); 21 | engine.systems.add(new MovementSystem()); 22 | engine.systems.add(new RenderSystem()); 23 | engine.systems.add(new CustomSystem()); 24 | engine.systems.add(new CollisionSystem({factory: Collsion})); 25 | 26 | new Timer(16).run = function() engine.update(16 / 1000); 27 | } 28 | } 29 | 30 | enum Event { 31 | Collsion(data:{entites:Pair}); 32 | } 33 | 34 | typedef MovementNode = Node; 35 | 36 | class MovementSystem extends System { 37 | @:nodes var nodes:Node; 38 | 39 | override function update(dt:Float) { 40 | for(node in nodes) { 41 | node.position.x += node.velocity.x * dt; 42 | node.position.y += node.velocity.y * dt; 43 | } 44 | } 45 | } 46 | 47 | class RenderSystem extends System { 48 | @:nodes var nodes:Node; 49 | 50 | override function update(dt:Float) { 51 | for(node in nodes) { 52 | trace('${node.entity} @ ${node.position.x}, ${node.position.y}'); 53 | } 54 | } 55 | } 56 | 57 | class CollisionSystem extends System { 58 | @:nodes var nodes:Node; 59 | 60 | var factory:EventFactory}>; 61 | 62 | public function new(options) { 63 | super(); 64 | factory = options.factory; 65 | } 66 | 67 | override function update(dt:Float) { 68 | // when two entities collide: 69 | engine.events.immediate(factory({entites: new Pair(null, null)})); 70 | } 71 | } 72 | 73 | class DamageSystem extends System { 74 | 75 | var selector:EventSelector}>; 76 | var binding:CallbackLink; 77 | 78 | public function new(options) { 79 | super(); 80 | selector = options.selector; 81 | } 82 | 83 | override function onAdded(engine:Engine) { 84 | super.onAdded(engine); 85 | binding = engine.events.select(selector).handle(data -> $type(data)); 86 | } 87 | 88 | override function onRemoved(engine:Engine) { 89 | super.onRemoved(engine); 90 | binding.dissolve(); 91 | } 92 | } 93 | 94 | class CustomSystem extends System { 95 | var nodes:NodeList; 96 | 97 | override function update(dt:Float) { 98 | for(node in nodes) { 99 | $type(node); // CustomNode 100 | trace(node.entity.get(Position)); 101 | } 102 | } 103 | 104 | override function onAdded(engine) { 105 | super.onAdded(engine); 106 | nodes = engine.getNodeList(CustomNode, engine -> new TrackingNodeList(engine, CustomNode.new, entity -> entity.has(Position))); 107 | } 108 | 109 | override function onRemoved(engine) { 110 | super.onRemoved(engine); 111 | nodes = null; 112 | } 113 | } 114 | 115 | class CustomNode extends NodeBase { 116 | public function new(entity) { 117 | this.entity = entity; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/RunTests.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | 3 | import exp.ecs.*; 4 | import exp.ecs.entity.*; 5 | import exp.ecs.component.*; 6 | import exp.ecs.system.*; 7 | import exp.ecs.state.*; 8 | import exp.ecs.node.*; 9 | import exp.fsm.*; 10 | import component.*; 11 | import node.*; 12 | import system.*; 13 | import haxe.*; 14 | 15 | import tink.unit.*; 16 | import tink.testrunner.*; 17 | 18 | using StringTools; 19 | using tink.CoreApi; 20 | 21 | class RunTests { 22 | static function main() { 23 | Runner.run(TestBatch.make([ 24 | new EngineTest(), 25 | new StateMachineTest(), 26 | new NodeListTest(), 27 | new NodeTest(), 28 | new SystemTest(), 29 | new EngineBenchmark(), 30 | new NodeListBenchmark(), 31 | ])).handle(Runner.exit); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/StateMachineTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import Types; 4 | import exp.ecs.entity.*; 5 | import exp.ecs.state.*; 6 | import exp.fsm.*; 7 | 8 | @:asserts 9 | class StateMachineTest extends Base { 10 | public function fsm() { 11 | var entity = new Entity(); 12 | 13 | var forwardVelocity = new Velocity(1, 0); 14 | var backwardVelocity = new Velocity(-1, 0); 15 | 16 | var fsm = StateMachine.create([ 17 | new EntityState('forward', ['backward'], entity, [forwardVelocity]), 18 | new EntityState('backward', ['forward'], entity, [backwardVelocity]), 19 | ]); 20 | 21 | asserts.assert(entity.get(Velocity) == forwardVelocity, 'entity.get(Velocity) == forwardVelocity'); 22 | 23 | fsm.transit('backward'); 24 | asserts.assert(entity.get(Velocity) == backwardVelocity, 'entity.get(Velocity) == backwardVelocity'); 25 | 26 | fsm.transit('forward'); 27 | asserts.assert(entity.get(Velocity) == forwardVelocity, 'entity.get(Velocity) == forwardVelocity'); 28 | 29 | 30 | return asserts.done(); 31 | } 32 | 33 | inline function equals(v1:T, v2:T) 34 | return v1 == v2; 35 | } -------------------------------------------------------------------------------- /tests/SystemTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import exp.ecs.Engine; 4 | import Types; 5 | import exp.ecs.node.*; 6 | import exp.ecs.entity.*; 7 | import exp.ecs.system.*; 8 | 9 | @:asserts 10 | class SystemTest extends Base { 11 | public function typeEquality() { 12 | var engine = new Engine(); 13 | 14 | var entity = new Entity(); 15 | entity.add(new Velocity(0, 0)); 16 | entity.add(new Position(0, 0)); 17 | engine.entities.add(entity); 18 | 19 | var system = new MySystem(); 20 | engine.systems.add(system); 21 | 22 | asserts.assert(getClassName(system.single1.nodes[0]) == getClassName(system.single2.nodes[0])); 23 | asserts.assert(getClassName(system.single2.nodes[0]) == getClassName(system.single3.nodes[0])); 24 | 25 | asserts.assert(getClassName(system.double1.nodes[0]) == getClassName(system.double2.nodes[0])); 26 | asserts.assert(getClassName(system.double2.nodes[0]) == getClassName(system.double3.nodes[0])); 27 | asserts.assert(getClassName(system.double3.nodes[0]) == getClassName(system.double4.nodes[0])); 28 | asserts.assert(getClassName(system.double4.nodes[0]) == getClassName(system.double5.nodes[0])); 29 | asserts.assert(getClassName(system.double5.nodes[0]) == getClassName(system.double6.nodes[0])); 30 | 31 | // asserts.assert(Type.getClass(system.single1.nodes[0]) == Type.getClass(system.filtered1.nodes[0])); 32 | 33 | return asserts.done(); 34 | } 35 | } 36 | 37 | enum MyEvents {} 38 | 39 | class MySystem extends System{ 40 | @:nodes public var single1:Node; 41 | @:nodes public var single2:Node; 42 | @:nodes public var single3:Node<{var velocity:Velocity;}>; 43 | 44 | @:nodes public var double1:Node; 45 | @:nodes public var double2:Node; 46 | @:nodes public var double3:Node; 47 | @:nodes public var double4:Node; 48 | @:nodes public var double5:Node<{var position:Position; var velocity:Velocity;}>; 49 | @:nodes public var double6:Node<{var velocity:Velocity; var position:Position;}>; 50 | 51 | @:nodes public var filtered1:Node<{@:filter(_.x == 0) var velocity:Velocity;}>; 52 | } -------------------------------------------------------------------------------- /tests/Types.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import exp.ecs.component.*; 4 | import exp.ecs.system.*; 5 | import exp.ecs.node.*; 6 | import tink.state.State; 7 | 8 | class Observable extends Component { 9 | public var state:State; 10 | 11 | public function new(value) { 12 | this.state = new State(value); 13 | } 14 | } 15 | class Velocity extends Component { 16 | public var x:Float; 17 | public var y:Float; 18 | 19 | public function new(x, y) { 20 | this.x = x; 21 | this.y = y; 22 | } 23 | } 24 | 25 | class Position extends Component { 26 | public var x:Float; 27 | public var y:Float; 28 | 29 | public function new(x, y) { 30 | this.x = x; 31 | this.y = y; 32 | } 33 | } 34 | 35 | typedef MovementNode = Node; 36 | typedef OptionalNode = Node<{ 37 | position:Position, 38 | ?velocity:Velocity, 39 | }>; 40 | 41 | class MovementSystem extends System { 42 | @:nodes var nodes:MovementNode; 43 | } 44 | class RenderSystem extends System { 45 | @:nodes var nodes:Node; 46 | } 47 | 48 | enum Events { 49 | 50 | } --------------------------------------------------------------------------------