├── .github
├── composites
│ └── build
│ │ └── action.yml
└── workflows
│ ├── build-and-release.yml
│ └── build.yml
├── .gitignore
├── .gitmodules
├── .travis.yml
├── .vscode
├── launch.json
├── settings.json
└── tasks.json
├── LICENSE
├── README.md
├── assets
├── icon.ico
├── index.html
└── logo.png
├── dox.hxml
├── echo
├── Body.hx
├── Collisions.hx
├── Echo.hx
├── Line.hx
├── Listener.hx
├── Macros.hx
├── Material.hx
├── Physics.hx
├── Shape.hx
├── World.hx
├── data
│ ├── Data.hx
│ ├── Options.hx
│ └── Types.hx
├── math
│ ├── Matrix2.hx
│ ├── Matrix3.hx
│ ├── Types.hx
│ ├── Vector2.hx
│ └── Vector3.hx
├── shape
│ ├── Circle.hx
│ ├── Polygon.hx
│ └── Rect.hx
└── util
│ ├── AABB.hx
│ ├── Bezier.hx
│ ├── BitMask.hx
│ ├── BodyOrBodies.hx
│ ├── Debug.hx
│ ├── Disposable.hx
│ ├── History.hx
│ ├── JSON.hx
│ ├── Poolable.hx
│ ├── Proxy.hx
│ ├── QuadTree.hx
│ ├── SAT.hx
│ ├── TileMap.hx
│ ├── Transform.hx
│ ├── ext
│ ├── ArrayExt.hx
│ ├── FloatExt.hx
│ └── IntExt.hx
│ └── verlet
│ ├── Composite.hx
│ ├── Constraints.hx
│ ├── Dot.hx
│ └── Verlet.hx
├── haxelib.json
├── hxformat.json
├── release_haxelib.sh
├── sample-hl.hxml
├── sample.hxml
├── sample
├── BaseApp.hx
├── Main.hx
├── build.hxml
├── ogmo
│ ├── project.ogmo
│ ├── slopes.json
│ └── terrain-tiles.png
├── profile.hxml
├── res
│ └── res.txt
├── state
│ ├── BaseState.hx
│ ├── BezierState.hx
│ ├── GroupsState.hx
│ ├── Linecast2State.hx
│ ├── LinecastState.hx
│ ├── MultiShapeState.hx
│ ├── PolygonState.hx
│ ├── ShapesState.hx
│ ├── StackingState.hx
│ ├── StaticState.hx
│ ├── TileMapState.hx
│ ├── TileMapState2.hx
│ └── VerletState.hx
├── test.hxml
└── util
│ ├── Assets.hx
│ ├── FSM.hx
│ └── Random.hx
├── test.hxml
└── test
└── Main.hx
/.github/composites/build/action.yml:
--------------------------------------------------------------------------------
1 | name: 'Checkout & Build'
2 | description: 'This action checks out the repo and builds all .hxmls'
3 | author: 'austineast'
4 | runs:
5 | using: 'composite'
6 | steps:
7 | - uses: krdlab/setup-haxe@v1
8 | with:
9 | haxe-version: 4.3.2
10 |
11 | - uses: actions/checkout@v3
12 | with:
13 | submodules: true
14 |
15 | - name: Install Haxelib Dependencies
16 | shell: bash
17 | run: |
18 | haxelib git dox https://github.com/HaxeFoundation/dox.git
19 | haxelib install heaps 2.0.0
20 | haxelib dev echo .
21 |
22 | - name: Run Builds
23 | shell: bash
24 | run: |
25 | haxe test.hxml
26 | haxe sample.hxml
27 | haxe dox.hxml
--------------------------------------------------------------------------------
/.github/workflows/build-and-release.yml:
--------------------------------------------------------------------------------
1 | name: Build & Release
2 |
3 | # Controls when the workflow will run
4 | on:
5 | # Triggers the workflow on push, but only for the master branch
6 | push:
7 | branches: [ master ]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
13 | jobs:
14 |
15 | build:
16 | name: Build & Upload Artifacts
17 | runs-on: ubuntu-latest
18 | outputs:
19 | released: ${{ steps.check.outputs.changed }}
20 | version: ${{ steps.check.outputs.version }}
21 | steps:
22 | - name: Build Library
23 | uses: AustinEast/echo/.github/composites/build@master
24 |
25 | - name: Check version
26 | uses: EndBug/version-check@v1
27 | id: check
28 | with:
29 | file-name: haxelib.json
30 | diff-search: true
31 |
32 | - name: Prepare Artifacts
33 | run: |
34 | zip -r echo-haxelib.zip echo LICENSE README.md haxelib.json
35 | zip -r echo-web.zip bin
36 |
37 | - name: Upload Echo Haxelib Artifact
38 | uses: actions/upload-artifact@v2.2.4
39 | with:
40 | name: echo-haxelib
41 | path: echo-haxelib.zip
42 |
43 | - name: Upload Echo Website Artifact
44 | uses: actions/upload-artifact@v2.2.4
45 | with:
46 | name: echo-web
47 | path: echo-web.zip
48 |
49 | release:
50 | name: Create Release
51 | needs: build
52 | if: needs.build.outputs.released
53 | runs-on: ubuntu-latest
54 | steps:
55 | - name: Create Release
56 | uses: actions/create-release@v1
57 | env:
58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 | with:
60 | tag_name: ${{ needs.build.outputs.version }}
61 | # tag_name: ${{ github.ref }}
62 | release_name: ${{ needs.build.outputs.version }}
63 | # release_name: ${{ github.ref }}
64 | draft: false
65 | prerelease: false
66 |
67 | deploy-website:
68 | name: Deploy to Website
69 | needs: build
70 | if: needs.build.outputs.released
71 | runs-on: ubuntu-latest
72 | steps:
73 | - name: Download Website Artifact
74 | uses: actions/download-artifact@v2
75 | with:
76 | name: echo-web
77 |
78 | - name: Unzip Website Artifacts
79 | run: unzip echo-web.zip
80 |
81 | - name: Deploy to Github Pages Branch
82 | uses: crazy-max/ghaction-github-pages@v2
83 | with:
84 | target_branch: gh-pages
85 | build_dir: bin
86 | env:
87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
88 |
89 | deploy-haxelib:
90 | name: Publish to Haxelib
91 | needs: build
92 | if: needs.build.outputs.released
93 | runs-on: ubuntu-latest
94 | steps:
95 | - uses: krdlab/setup-haxe@v1
96 |
97 | - name: Download Haxelib Artifact
98 | uses: actions/download-artifact@v2
99 | with:
100 | name: echo-haxelib
101 |
102 | - name: Submit to Haxelib
103 | run: haxelib submit echo-haxelib.zip ${{ secrets.HAXELIB_PWD }} --always
104 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: CI Build
2 |
3 | # Controls when the workflow will run
4 | on:
5 | # Triggers the workflow on push or pull request events
6 | push:
7 | branches-ignore: [ master ]
8 |
9 | pull_request:
10 |
11 | # Allows you to run this workflow manually from the Actions tab
12 | workflow_dispatch:
13 |
14 | jobs:
15 | build:
16 | name: Build
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: AustinEast/echo/.github/composites/build@master
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .tmp
3 | *.zip
4 | bin/
5 | dump/
6 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "assets/austineast.github.io.base"]
2 | path = assets/austineast.github.io.base
3 | url = git@github.com:AustinEast/austineast.github.io.base.git
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | dist: trusty
3 |
4 | language: haxe
5 |
6 | haxe:
7 | - "4.0.2"
8 |
9 | install:
10 | - haxelib git dox https://github.com/HaxeFoundation/dox.git
11 | - haxelib git hxmath https://github.com/tbrosman/hxmath.git
12 | - haxelib git heaps https://github.com/HeapsIO/heaps.git
13 | - haxelib dev echo .
14 |
15 | script:
16 | - haxe test.hxml
17 | - haxe sample.hxml
18 | - haxe dox.hxml
19 |
20 | deploy:
21 | - provider: pages
22 | local-dir: bin
23 | skip-cleanup: true
24 | github-token: $GITHUB_TOKEN
25 | on:
26 | tags: true
27 | - provider: script
28 | haxe: "4.0.2"
29 | script: bash ./release_haxelib.sh $HAXELIB_PWD
30 | on:
31 | tags: true
32 |
33 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "chrome",
6 | "request": "launch",
7 | "name": "Chrome Debugger",
8 | "url": "file://${workspaceFolder}/bin/index.html",
9 | "webRoot": "${workspaceFolder}",
10 | "preLaunchTask": {
11 | "type": "haxe",
12 | "args": "active configuration"
13 | }
14 | },
15 | {
16 | "name": "HashLink",
17 | "request": "launch",
18 | "type": "hl",
19 | "cwd": "${workspaceRoot}",
20 | "preLaunchTask": {
21 | "type": "haxe",
22 | "args": "active configuration"
23 | }
24 | },
25 | {
26 | "name": "Haxe Interpreter",
27 | "type": "haxe-eval",
28 | "request": "launch"
29 | }
30 | ]
31 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[haxe]": {
3 | "editor.formatOnSave": true,
4 | "files.trimTrailingWhitespace": false,
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "haxe",
6 | "args": "active configuration",
7 | "problemMatcher": [
8 | "$haxe-absolute",
9 | "$haxe",
10 | "$haxe-error",
11 | "$haxe-trace"
12 | ],
13 | "group": {
14 | "kind": "build",
15 | "isDefault": true
16 | }
17 | },
18 | {
19 | "type": "hxml",
20 | "file": "sample.hxml",
21 | "problemMatcher": [
22 | "$haxe-absolute",
23 | "$haxe",
24 | "$haxe-error",
25 | "$haxe-trace"
26 | ]
27 | }
28 | ]
29 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Austin East
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # echo
6 | A 2D Physics library written in Haxe.
7 |
8 | 
9 |
10 | Echo focuses on maintaining a simple API that is easy to integrate into any engine/framework (Heaps, OpenFL, Kha, etc). All Echo needs is an update loop and its ready to go!
11 |
12 | Try the [Samples 🎮](https://austineast.dev/echo)!
13 |
14 | Check out the [API 📖](https://austineast.dev/echo/api/echo/Echo)!
15 |
16 | # Features
17 | * Semi-implicit euler integration physics
18 | * SAT-powered collision detection
19 | * Quadtree for broadphase collision querying
20 | * Collision listeners to provide collision callbacks
21 | * Physics State History Management with Built-in Undo/Redo functionality
22 | * Extendable debug drawing
23 |
24 | # Getting Started
25 |
26 | Echo requires [Haxe 4.2+](https://haxe.org/download/) to run.
27 |
28 | Install the library from haxelib:
29 | ```
30 | haxelib install echo
31 | ```
32 | Alternatively the dev version of the library can be installed from github:
33 | ```
34 | haxelib git echo https://github.com/AustinEast/echo.git
35 | ```
36 |
37 | Then for standard Haxe applications, include the library in your project's `.hxml`:
38 | ```hxml
39 | -lib echo
40 | ```
41 |
42 | For OpenFL users, add the library into your `Project.xml`:
43 |
44 | ```xml
45 |
46 | ```
47 |
48 | For Kha users (who don't use haxelib), clone echo to thee `Libraries` folder in your project root, and then add the following to your `khafile.js`:
49 |
50 | ```js
51 | project.addLibrary('echo');
52 | ```
53 |
54 | # Usage
55 |
56 | ## Concepts
57 |
58 | ### Echo
59 |
60 | The `Echo` Class holds helpful utility methods to help streamline the creation and management of Physics Simulations.
61 |
62 | ### World
63 |
64 | A `World` is an Object representing the state of a Physics simulation and it configurations.
65 |
66 | ### Bodies
67 |
68 | A `Body` is an Object representing a Physical Body in a `World`. A `Body` has a position, velocity, mass, optional collider shapes, and many other properties that are used in a `World` simulation.
69 |
70 | ### Shapes
71 |
72 | A Body's collider is represented by different Shapes. Without a `Shape` to define it's form, a `Body` can be thought of a just a point in the `World` that cant collide with anything.
73 |
74 | Available Shapes:
75 | * Rectangle
76 | * Circle
77 | * Polygon (Convex Only)
78 |
79 | When a Shape is added to a Body, it's transform (x, y, rotation) becomes relative to its parent Body. In this case, a Shape's local transform can still be accessed through `shape.local_x`, `shape.local_y`, and `shape.local_rotation`.
80 |
81 | It's important to note that all Shapes (including Rectangles) have their origins centered.
82 |
83 | ### Lines
84 |
85 | Use Lines to perform Linecasts against other Lines, Bodies, and Shapes. Check out the `Echo` class for various methods to preform Linecasts.
86 |
87 | ### Listeners
88 |
89 | Listeners keep track of collisions between Bodies - enacting callbacks and physics responses depending on their configurations. Once you add a `Listener` to a `World`, it will automatically update itself as the `World` is stepped forward.
90 |
91 | ## Integration
92 |
93 | ### Codebase Integration
94 | Echo has a couple of ways to help integrate itself into codebases through the `Body` class.
95 |
96 | First, the `Body` class has two public fields named `on_move` and `on_rotate`. If these are set on a body, they'll be called any time the body moves or rotates. This is useful for things such as syncing the Body's transform with external objects:
97 | ```haxe
98 | var body = new echo.Body();
99 | body.on_move = (x,y) -> entity.position.set(x,y);
100 | body.on_rotate = (rotation) -> entity.rotation = rotation;
101 | ```
102 |
103 | Second, a build macro is available to add custom fields to the `Body` class, such as a reference to an `Entity` class:
104 |
105 | in build.hxml:
106 | ```hxml
107 | --macro echo.Macros.add_data("entity", "some.package.Entity")
108 | ```
109 |
110 | in Main.hx
111 | ```haxe
112 | var body = new echo.Body();
113 | body.entity = new some.package.Entity();
114 | ```
115 |
116 | ### Other Math Library Integration
117 |
118 | Echo comes with basic implementations of common math structures (Vector2, Vector3, Matrix3), but also allows these structures to be extended and used seamlessly with other popular Haxe math libraries.
119 |
120 | Support is currently available for the following libraries (activated by adding the listed compiler flag to your project's build parameters):
121 |
122 | | Library | Compiler Flag |
123 | | --- | --- |
124 | | [hxmath](https://github.com/tbrosman/hxmath) | ECHO_USE_HXMATH |
125 | | [vector-math](https://github.com/haxiomic/vector-math) | ECHO_USE_VECTORMATH |
126 | | [zerolib](https://github.com/01010111/zerolib) | ECHO_USE_ZEROLIB |
127 | | [heaps](https://heaps.io) | ECHO_USE_HEAPS |
128 |
129 | (pull requests for other libraries happily accepted!)
130 |
131 | If you compile your project with a standard `.hxml`:
132 | ```hxml
133 | # hxmath support
134 | -lib hxmath
135 | -D ECHO_USE_HXMATH
136 | ```
137 |
138 | For OpenFL users, add one of the following into your `Project.xml`:
139 | ```xml
140 |
141 |
142 |
143 | ```
144 |
145 | For Kha users, add one of the following into your `khafile.js`:
146 | ```js
147 | // hxmath support
148 | project.addLibrary('hxmath');
149 | project.addDefine('ECHO_USE_HXMATH');
150 | ```
151 |
152 | # Examples
153 |
154 | ## Basic
155 | ```haxe
156 | import echo.Echo;
157 |
158 | class Main {
159 | static function main() {
160 | // Create a World to hold all the Physics Bodies
161 | // Worlds, Bodies, and Listeners are all created with optional configuration objects.
162 | // This makes it easy to construct object configurations, reuse them, and even easily load them from JSON!
163 | var world = Echo.start({
164 | width: 64, // Affects the bounds for collision checks.
165 | height: 64, // Affects the bounds for collision checks.
166 | gravity_y: 20, // Force of Gravity on the Y axis. Also available for the X axis.
167 | iterations: 2 // Sets the number of Physics iterations that will occur each time the World steps.
168 | });
169 |
170 | // Create a Body with a Circle Collider and add it to the World
171 | var a = world.make({
172 | material: {elasticity: 0.2},
173 | shape: {
174 | type: CIRCLE,
175 | radius: 16,
176 | }
177 | });
178 |
179 | // Create a Body with a Rectangle collider and add it to the World
180 | // This Body will be static (ie have a Mass of `0`), rendering it as unmovable
181 | // This is useful for things like platforms or walls.
182 | var b = world.make({
183 | mass: STATIC, // Setting this to Static/`0` makes the body unmovable by forces and collisions
184 | y: 48, // Set the object's Y position below the Circle, so that gravity makes them collide
185 | material: {elasticity: 0.2},
186 | shape: {
187 | type: RECT,
188 | width: 10,
189 | height: 10
190 | }
191 | });
192 |
193 | // Create a listener and attach it to the World.
194 | // This listener will react to collisions between Body "a" and Body "b", based on the configuration options passed in
195 | world.listen(a, b, {
196 | separate: true, // Setting this to true will cause the Bodies to separate on Collision. This defaults to true
197 | enter: (a, b, c) -> trace("Collision Entered"), // This callback is called on the first frame that a collision starts
198 | stay: (a, b, c) -> trace("Collision Stayed"), // This callback is called on frames when the two Bodies are continuing to collide
199 | exit: (a, b) -> trace("Collision Exited"), // This callback is called when a collision between the two Bodies ends
200 | });
201 |
202 | // Set up a Timer to act as an update loop (at 60fps)
203 | new haxe.Timer(16).run = () -> {
204 | // Step the World's Physics Simulation forward (at 60fps)
205 | world.step(16 / 1000);
206 | // Log the World State in the Console
207 | echo.util.Debug.log(world);
208 | }
209 | }
210 | }
211 | ```
212 |
213 | ## Samples
214 | Check out the source code for the [Echo Samples](https://austineast.dev/echo/) here: https://github.com/AustinEast/echo/tree/master/sample/state
215 |
216 | ## Engine/Framework Specific
217 |
218 | * [HaxeFlixel](https://haxeflixel.com): https://github.com/AustinEast/echo-flixel
219 | * [Heaps](https://heaps.io): https://github.com/AustinEast/echo-heaps
220 | * [Peyote View](https://github.com/maitag/peote-view): https://github.com/maitag/peote-views-samples/tree/master/echo
221 | * [HaxePunk](https://haxepunk.com): https://github.com/XANOZOID/EchoHaxePunk
222 |
223 |
224 | # Roadmap
225 | ## Sooner
226 | * Endless length Line support
227 | * Update Readme with info on the various utilities (Tilemap, Bezier, etc)
228 | ## Later
229 | * Allow Concave Polygons (through Convex Decomposition)
230 | * Sleeping Body optimations
231 | * Constraints
232 | * Compiler Flag to turn off a majority of inlined functions (worse performance, but MUCH smaller filesize)
233 |
--------------------------------------------------------------------------------
/assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AustinEast/echo/d889af58823d11067fd8d62ca0cc059cf12a5efb/assets/icon.ico
--------------------------------------------------------------------------------
/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 |
14 |
15 |
16 | Echo Physics Library
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
79 |
80 |
81 |
82 |
83 |
84 | Echo
85 | Simple Arcade Physics Library for Haxe
86 |
87 | Known issue: the sample buttons do not work correctly! as a quick fix, resize your browser window and they'll work just fine ;)
88 |
89 |
93 |
94 |
95 |
96 |
97 |
103 |
104 |
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AustinEast/echo/d889af58823d11067fd8d62ca0cc059cf12a5efb/assets/logo.png
--------------------------------------------------------------------------------
/dox.hxml:
--------------------------------------------------------------------------------
1 | -lib echo
2 | -cp echo
3 | -D doc-gen
4 | --macro include('echo')
5 | --no-output
6 | -js bin/docs.js
7 | -xml bin/echo.xml
8 |
9 | --next
10 | -cmd haxelib run dox -i bin -o bin/api --title "Echo API" -D website "http://austineast.dev/echo" -D source-path "https://github.com/AustinEast/echo/blob/master/"
11 |
--------------------------------------------------------------------------------
/echo/Collisions.hx:
--------------------------------------------------------------------------------
1 | package echo;
2 |
3 | import echo.Body;
4 | import echo.Listener;
5 | import echo.data.Data;
6 | import echo.util.QuadTree;
7 | /**
8 | * Class containing methods for performing Collisions on a World
9 | */
10 | class Collisions {
11 | /**
12 | * Updates the World's dynamic QuadTree with any Bodies that have moved.
13 | */
14 | public static function update_quadtree(world:World) {
15 | inline function update_body(b:Body) {
16 | if (!b.disposed) {
17 | b.collided = false;
18 | for (shape in b.shapes) {
19 | shape.collided = false;
20 | }
21 | if (b.active && b.is_dynamic() && b.dirty && b.shapes.length > 0) {
22 | if (b.quadtree_data.bounds == null) b.quadtree_data.bounds = b.bounds();
23 | else b.bounds(b.quadtree_data.bounds);
24 | world.quadtree.update(b.quadtree_data, false);
25 | b.dirty = false;
26 | }
27 | }
28 | }
29 |
30 | world.for_each(b -> update_body(b));
31 | world.quadtree.shake();
32 | }
33 | /**
34 | * Queries a World's Listeners for Collisions.
35 | * @param world The World to query.
36 | * @param listeners Optional collection of listeners to query. If this is set, the World's listeners will not be queried.
37 | */
38 | public static function query(world:World, ?listeners:Listeners) {
39 | update_quadtree(world);
40 | // Process the Listeners
41 | var members = listeners == null ? world.listeners.members : listeners.members;
42 | for (listener in members) {
43 | // BroadPhase
44 | listener.quadtree_results.resize(0);
45 | switch (listener.a) {
46 | case Left(ba):
47 | switch (listener.b) {
48 | case Left(bb):
49 | var col = overlap_body_and_body_bounds(ba, bb);
50 | if (col != null) listener.quadtree_results.push(col);
51 | case Right(ab):
52 | overlap_body_and_bodies_bounds(ba, ab, world, listener.quadtree_results);
53 | }
54 | case Right(aa):
55 | switch (listener.b) {
56 | case Left(bb):
57 | overlap_body_and_bodies_bounds(bb, aa, world, listener.quadtree_results);
58 | case Right(ab):
59 | overlap_bodies_and_bodies_bounds(aa, ab, world, listener.quadtree_results);
60 | if (aa != ab) overlap_bodies_and_bodies_bounds(ab, aa, world, listener.quadtree_results);
61 | }
62 | }
63 | // Narrow Phase
64 | for (collision in listener.last_collisions) collision.put();
65 | listener.last_collisions.resize(listener.collisions.length);
66 | for (i in 0...listener.collisions.length) listener.last_collisions[i] = listener.collisions[i];
67 | listener.collisions.resize(0);
68 | for (result in listener.quadtree_results) {
69 | // Filter out disposed bodies
70 | if (result.a.disposed || result.b.disposed) {
71 | result.put();
72 | continue;
73 | }
74 | // Filter out self collisions
75 | if (result.a.id == result.b.id) {
76 | result.put();
77 | continue;
78 | }
79 | // Filter out duplicate pairs
80 | var flag = false;
81 | for (collision in listener.collisions) {
82 | if ((collision.a.id == result.a.id && collision.b.id == result.b.id)
83 | || (collision.b.id == result.a.id && collision.a.id == result.b.id)) {
84 | flag = true;
85 | break;
86 | }
87 | }
88 | if (flag) {
89 | result.put();
90 | continue;
91 | }
92 |
93 | // Preform the full collision check
94 | if (result.a.shapes.length == 1 && result.b.shapes.length == 1) {
95 | var col = result.a.shape.collides(result.b.shape);
96 | if (col != null) result.data.push(col);
97 | }
98 | // If either body has more than one shape, iterate over each shape and perform bounds checks before checking for actual collision
99 | else {
100 | var sa = result.a.shapes;
101 | for (i in 0...sa.length) {
102 | var sb = result.b.shapes;
103 | var b1 = sa[i].bounds();
104 | for (j in 0...sb.length) {
105 | var b2 = sb[j].bounds();
106 | if (b1.overlaps(b2)) {
107 | var col = sa[i].collides(sb[j]);
108 | if (col != null) result.data.push(col);
109 | }
110 | b2.put();
111 | }
112 | b1.put();
113 | }
114 | }
115 |
116 | // If there was no collision, continue
117 | if (result.data.length == 0) {
118 | result.put();
119 | continue;
120 | }
121 | // Check if the collision passes the listener's condition if it has one
122 | if (listener.condition != null) {
123 | if (!listener.condition(result.a, result.b, result.data) || result.a.disposed || result.b.disposed) {
124 | result.put();
125 | continue;
126 | }
127 | }
128 | for (data in result.data) data.sa.collided = data.sb.collided = true;
129 | result.a.collided = result.b.collided = true;
130 | listener.collisions.push(result);
131 | }
132 | }
133 | }
134 | /**
135 | * Enacts the Callbacks defined in a World's Listeners
136 | */
137 | public static function notify(world:World, ?listeners:Listeners) {
138 | var members = listeners == null ? world.listeners.members : listeners.members;
139 | for (listener in members) {
140 | if (listener.enter != null || listener.stay != null) {
141 | for (c in listener.collisions) {
142 | if (!c.a.disposed && !c.b.disposed) {
143 | inline function find_match() {
144 | var found = false;
145 | for (l in listener.last_collisions) {
146 | if (l.a == c.a && l.b == c.b || l.a == c.b && l.b == c.a) {
147 | found = true;
148 | break;
149 | }
150 | }
151 | return found;
152 | }
153 | if (listener.enter != null && !find_match()) {
154 | listener.enter(c.a, c.b, c.data);
155 | }
156 | else if (listener.stay != null) {
157 | listener.stay(c.a, c.b, c.data);
158 | }
159 | }
160 | }
161 | }
162 | if (listener.exit != null) {
163 | for (lc in listener.last_collisions) {
164 | inline function find_match() {
165 | var found = false;
166 | for (c in listener.collisions) {
167 | if (c.a == lc.a && c.b == lc.b || c.a == lc.b && c.b == lc.a) {
168 | found = true;
169 | break;
170 | }
171 | }
172 | return found;
173 | }
174 | if (!lc.a.disposed && !lc.b.disposed && !find_match()) {
175 | listener.exit(lc.a, lc.b);
176 | }
177 | }
178 | }
179 | }
180 | }
181 |
182 | public static function overlap_bodies_and_bodies_bounds(a:Array, b:Array, world:World, results:Array) {
183 | if (a.length == 0 || b.length == 0) return;
184 | for (body in a) overlap_body_and_bodies_bounds(body, b, world, results);
185 | }
186 |
187 | static var qr:Array = [];
188 | static var sqr:Array = [];
189 |
190 | public static function overlap_body_and_bodies_bounds(body:Body, bodies:Array, world:World, results:Array) {
191 | if (body.disposed || body.shapes.length == 0 || !body.active || body.is_static()) return;
192 | var bounds = body.bounds();
193 | qr.resize(0);
194 | sqr.resize(0);
195 | world.quadtree.query(bounds, qr);
196 | world.static_quadtree.query(bounds, sqr);
197 | for (member in bodies) {
198 | if (member.disposed || member.shapes.length == 0 || !member.active || !layer_match(body, member)) continue;
199 | for (result in (member.is_dynamic() ? qr : sqr)) {
200 | if (result.id == member.id) results.push(Collision.get(body, member));
201 | }
202 | }
203 | bounds.put();
204 | }
205 |
206 | public static function overlap_body_and_body_bounds(a:Body, b:Body):Null {
207 | if (a.disposed || b.disposed || a.shapes.length == 0 || b.shapes.length == 0 || !a.active || !b.active || a == b || !layer_match(a, b) || a.is_static()
208 | && b.is_static()) return null;
209 | var ab = a.bounds();
210 | var bb = b.bounds();
211 | var col = ab.overlaps(bb);
212 | ab.put();
213 | bb.put();
214 | return col ? Collision.get(a, b) : null;
215 | }
216 |
217 | static inline function layer_match(a:Body, b:Body) {
218 | return a.layer_mask.is_empty() || b.layer_mask.is_empty() || (a.layer_mask.contains(b.layers) && b.layer_mask.contains(a.layers));
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/echo/Line.hx:
--------------------------------------------------------------------------------
1 | package echo;
2 |
3 | import echo.data.Data.IntersectionData;
4 | import echo.util.AABB;
5 | import echo.util.Poolable;
6 | import echo.util.Proxy;
7 | import echo.math.Vector2;
8 |
9 | using echo.util.ext.FloatExt;
10 |
11 | @:using(echo.Echo)
12 | class Line implements Proxy implements Poolable {
13 | @:alias(start.x)
14 | public var x:Float;
15 | @:alias(start.y)
16 | public var y:Float;
17 | public var start:Vector2;
18 | @:alias(end.x)
19 | public var dx:Float;
20 | @:alias(end.y)
21 | public var dy:Float;
22 | public var end:Vector2;
23 | @:alias(start.distanceTo(end))
24 | public var length(get, never):Float;
25 | public var radians(get, never):Float;
26 |
27 | public static inline function get(x:Float = 0, y:Float = 0, dx:Float = 1, dy:Float = 1):Line {
28 | var line = pool.get();
29 | line.set(x, y, dx, dy);
30 | line.pooled = false;
31 | return line;
32 | }
33 | /**
34 | * Gets a Line with the defined start point, angle (in degrees), and length.
35 | * @param start A Vector2 describing the starting position of the Line.
36 | * @param degrees The angle of the Line (in degrees).
37 | * @param length The length of the Line.
38 | */
39 | public static inline function get_from_vector(start:Vector2, degrees:Float, length:Float) {
40 | var line = pool.get();
41 | line.set_from_vector(start, degrees, length);
42 | line.pooled = false;
43 | return line;
44 | }
45 |
46 | public static inline function get_from_vectors(start:Vector2, end:Vector2) {
47 | return get(start.x, start.y, end.x, end.y);
48 | }
49 |
50 | inline function new(x:Float = 0, y:Float = 0, dx:Float = 1, dy:Float = 1) {
51 | start = new Vector2(x, y);
52 | end = new Vector2(dx, dy);
53 | }
54 |
55 | public inline function set(x:Float = 0, y:Float = 0, dx:Float = 1, dy:Float = 1):Line {
56 | start.set(x, y);
57 | end.set(dx, dy);
58 | return this;
59 | }
60 | /**
61 | * Sets the Line with the defined start point, angle (in degrees), and length.
62 | * @param start A Vector2 describing the starting position of the Line.
63 | * @param degrees The angle of the Line (in degrees).
64 | * @param length The length of the Line.
65 | */
66 | public function set_from_vector(start:Vector2, degrees:Float, length:Float) {
67 | var rad = degrees.deg_to_rad();
68 | var end = new Vector2(start.x + (length * Math.cos(rad)), start.y + (length * Math.sin(rad)));
69 | return set(start.x, start.y, end.x, end.y);
70 | }
71 |
72 | public inline function set_from_vectors(start:Vector2, end:Vector2) {
73 | return set(start.x, start.y, end.x, end.y);
74 | }
75 |
76 | public inline function put() {
77 | if (!pooled) {
78 | pooled = true;
79 | pool.put_unsafe(this);
80 | }
81 | }
82 |
83 | public function contains(v:Vector2):Bool {
84 | // Find the slope
85 | var m = (dy - y) / (dx - y);
86 | var b = y - m * x;
87 | return v.y == m * v.x + b;
88 | }
89 |
90 | public inline function intersect(shape:Shape):Null {
91 | return shape.intersect(this);
92 | }
93 | /**
94 | * Gets a position on the `Line` at the specified ratio.
95 | * @param ratio The ratio from the Line's `start` and `end` points (expects a value between 0.0 and 1.0).
96 | * @return Vector2
97 | */
98 | public inline function point_along_ratio(ratio:Float):Vector2 {
99 | return start + ratio * (end - start);
100 | }
101 |
102 | public inline function ratio_of_point(point:Vector2, clamp:Bool = true):Float {
103 | var ab = end - start;
104 | var ap = point - start;
105 | var t = (ab.dot(ap) / ab.length_sq);
106 | if (clamp) t = t.clamp(0, 1);
107 | return t;
108 | }
109 |
110 | public inline function project_point(point:Vector2, clamp:Bool = true):Vector2 {
111 | return point_along_ratio(ratio_of_point(point, clamp));
112 | }
113 | /**
114 | * Gets the Line's normal based on the relative position of the point.
115 | */
116 | public inline function side(point:Vector2, ?set:Vector2) {
117 | var rad = (dx - x) * (point.y - y) - (dy - y) * (point.x - x);
118 | var dir = start - end;
119 | var n = set == null ? new Vector2(0, 0) : set;
120 |
121 | if (rad > 0) n.set(dir.y, -dir.x);
122 | else n.set(-dir.y, dir.x);
123 | return n.normal;
124 | }
125 |
126 | public inline function to_aabb(put_self:Bool = false) {
127 | if (put_self) {
128 | var aabb = bounds();
129 | put();
130 | return aabb;
131 | }
132 | return bounds();
133 | }
134 |
135 | public inline function bounds(?aabb:AABB) {
136 | var min_x = 0.;
137 | var min_y = 0.;
138 | var max_x = 0.;
139 | var max_y = 0.;
140 | if (x < dx) {
141 | min_x = x;
142 | max_x = dx;
143 | }
144 | else {
145 | min_x = dx;
146 | max_x = x;
147 | }
148 | if (y < dy) {
149 | min_y = y;
150 | max_y = dy;
151 | }
152 | else {
153 | min_y = dy;
154 | max_y = y;
155 | }
156 |
157 | if (min_x - max_x == 0) max_x += 1;
158 | if (min_y + max_y == 0) max_y += 1;
159 |
160 | return (aabb == null) ? AABB.get_from_min_max(min_x, min_y, max_x, max_y) : aabb.set_from_min_max(min_x, min_y, max_x, max_y);
161 | }
162 |
163 | inline function get_length() return start.distance(end);
164 |
165 | inline function get_radians() return Math.atan2(dy - y, dx - x);
166 |
167 | public function set_length(l:Float):Float {
168 | var old = length;
169 | if (old > 0) l /= old;
170 | dx = x + (dx - x) * l;
171 | dy = y + (dy - y) * l;
172 | return l;
173 | }
174 |
175 | public function set_radians(r:Float):Float {
176 | var len = length;
177 | dx = x + Math.cos(r) * len;
178 | dy = y + Math.sin(r) * len;
179 | return r;
180 | }
181 |
182 | function toString() return 'Line: {start: $start, end: $end}';
183 | }
184 |
--------------------------------------------------------------------------------
/echo/Listener.hx:
--------------------------------------------------------------------------------
1 | package echo;
2 |
3 | import haxe.ds.Either;
4 | import echo.util.Disposable;
5 | import echo.Body;
6 | import echo.data.Data;
7 | import echo.data.Options;
8 | import echo.util.BodyOrBodies;
9 | /**
10 | * Data Structure used to listen for Collisions between Bodies.
11 | */
12 | @:structInit()
13 | class Listener {
14 | public static var defaults(get, null):ListenerOptions;
15 | /**
16 | * The first Body or Array of Bodies the listener checks each step.
17 | */
18 | public var a:Either>;
19 | /**
20 | * The second Body or Array of Bodies the listener checks each step.
21 | */
22 | public var b:Either>;
23 | /**
24 | * Flag that determines if Collisions found by this listener should separate the Bodies. Defaults to `true`.
25 | */
26 | public var separate:Bool;
27 | /**
28 | * Store of the latest Collisions.
29 | */
30 | public var collisions:Array;
31 | /**
32 | * Store of the Collisions from the Prior Frame.
33 | */
34 | public var last_collisions:Array;
35 | /**
36 | * A callback function that is called on the first frame that a collision starts.
37 | */
38 | @:optional public var enter:Body->Body->Array->Void;
39 | /**
40 | * A callback function that is called on frames when two Bodies are continuing to collide.
41 | */
42 | @:optional public var stay:Body->Body->Array->Void;
43 | /**
44 | * A callback function that is called when a collision between two Bodies ends.
45 | */
46 | @:optional public var exit:Body->Body->Void;
47 | /**
48 | * A callback function that allows extra logic to be run on a potential collision.
49 | *
50 | * If it returns true, the collision is valid. Otherwise the collision is discarded and no physics resolution/collision callbacks occur
51 | */
52 | @:optional public var condition:Body->Body->Array->Bool;
53 | /**
54 | * Store of the latest quadtree query results
55 | */
56 | @:optional public var quadtree_results:Array;
57 | /**
58 | * Percentage of correction along the collision normal to be applied to seperating bodies. Helps prevent objects sinking into each other.
59 | */
60 | public var percent_correction:Float;
61 | /**
62 | * Threshold determining how close two separating bodies must be before position correction occurs. Helps reduce jitter.
63 | */
64 | public var correction_threshold:Float;
65 |
66 | static function get_defaults():ListenerOptions return {
67 | separate: true,
68 | percent_correction: 0.9,
69 | correction_threshold: 0.013
70 | }
71 | }
72 | /**
73 | * Container used to store Listeners
74 | */
75 | class Listeners implements Disposable {
76 | public var members:Array;
77 |
78 | public function new(?members:Array) {
79 | this.members = members == null ? [] : members;
80 | }
81 | /**
82 | * Add a new Listener to the collection.
83 | * @param a The first `Body` or Array of Bodies to collide against.
84 | * @param b The second `Body` or Array of Bodies to collide against.
85 | * @param options Options to define the Listener's behavior.
86 | * @return The new Listener.
87 | */
88 | public function add(a:BodyOrBodies, b:BodyOrBodies, ?options:ListenerOptions):Listener {
89 | options = echo.util.JSON.copy_fields(options, Listener.defaults);
90 | var listener:Listener = {
91 | a: a,
92 | b: b,
93 | separate: options.separate,
94 | collisions: [],
95 | last_collisions: [],
96 | quadtree_results: [],
97 | correction_threshold: options.correction_threshold,
98 | percent_correction: options.percent_correction
99 | };
100 | if (options.enter != null) listener.enter = options.enter;
101 | if (options.stay != null) listener.stay = options.stay;
102 | if (options.exit != null) listener.exit = options.exit;
103 | if (options.condition != null) listener.condition = options.condition;
104 | members.push(listener);
105 | return listener;
106 | }
107 | /**
108 | * Removes a Listener from the Container.
109 | * @param listener Listener to remove.
110 | * @return The removed Listener.
111 | */
112 | public function remove(listener:Listener):Listener {
113 | members.remove(listener);
114 | return listener;
115 | }
116 | /**
117 | * Clears the collection of all Listeners.
118 | */
119 | public function clear() {
120 | members.resize(0);
121 | }
122 | /**
123 | * Disposes of the collection. Do not use once disposed.
124 | */
125 | public function dispose() {
126 | members = null;
127 | }
128 |
129 | public inline function iterator():Iterator return members.iterator();
130 | }
131 |
--------------------------------------------------------------------------------
/echo/Macros.hx:
--------------------------------------------------------------------------------
1 | package echo;
2 |
3 | #if macro
4 | import haxe.macro.Expr;
5 | import haxe.macro.Context;
6 |
7 | class Macros {
8 | static var dataFields:Map = [];
9 |
10 | static function build_body() {
11 | if (Lambda.count(dataFields) == 0) return null;
12 | var fields = Context.getBuildFields();
13 | for (kv in dataFields.keyValueIterator()) {
14 | fields.push({
15 | name: kv.key,
16 | access: [Access.APublic],
17 | kind: FieldType.FVar(Context.toComplexType(Context.getType(kv.value))),
18 | pos: Context.currentPos()
19 | });
20 | }
21 | return fields;
22 | }
23 | /**
24 | * Build Macro to add extra fields to the Body class. Inspired by [@Yanrishatum](https://github.com/Yanrishatum).
25 | *
26 | * Example: in build.hxml - `--macro echo.Macros.add_data("entity", "some.package.Entity")
27 | * @param name
28 | * @param type
29 | */
30 | public static function add_data(name:String, type:String) {
31 | dataFields[name] = type;
32 | }
33 | }
34 | #end
35 |
--------------------------------------------------------------------------------
/echo/Material.hx:
--------------------------------------------------------------------------------
1 | package echo;
2 |
3 | import echo.math.Vector2;
4 | import echo.util.BitMask;
5 | /**
6 | * A Structure that describes the physical properties of a `Body`.
7 | */
8 | @:structInit
9 | class Material {
10 | public static var global:Material = {};
11 | /**
12 | * Value to determine how much of a Body's `velocity` should be retained during collisions (or how much should the `Body` "bounce" in other words).
13 | */
14 | public var elasticity:Float = 0;
15 | /**
16 | *
17 | */
18 | public var density:Float = 1;
19 | /**
20 | * TODO
21 | */
22 | @:dox(hide)
23 | @:noCompletion
24 | public var friction:Float = 0;
25 | /**
26 | * TODO
27 | */
28 | @:dox(hide)
29 | @:noCompletion
30 | public var static_friction:Float = 0;
31 | /**
32 | * Percentage value that represents how much a World's gravity affects the Body.
33 | */
34 | public var gravity_scale:Float = 1;
35 | }
36 |
--------------------------------------------------------------------------------
/echo/Physics.hx:
--------------------------------------------------------------------------------
1 | package echo;
2 |
3 | import echo.math.Vector2;
4 | import echo.Listener;
5 | import echo.data.Data;
6 |
7 | using echo.util.ext.FloatExt;
8 | /**
9 | * Class containing methods for performing Physics simulations on a World
10 | */
11 | class Physics {
12 | /**
13 | * Applies movement forces to a World's Bodies
14 | * @param world World to step forward
15 | * @param dt elapsed time since the last step
16 | */
17 | public static function step(world:World, dt:Float) {
18 | world.for_each_dynamic(member -> step_body(member, dt, world.gravity.x, world.gravity.y));
19 | }
20 |
21 | public static inline function step_body(body:Body, dt:Float, gravity_x:Float, gravity_y:Float) {
22 | if (!body.disposed && body.active) {
23 | body.last_x = body.x;
24 | body.last_y = body.y;
25 | body.last_rotation = body.rotation;
26 | var accel_x = body.acceleration.x * body.inverse_mass;
27 | var accel_y = body.acceleration.y * body.inverse_mass;
28 |
29 | // Apply Gravity (after applying body's inverse mass to acceleration)
30 | if (!body.kinematic) {
31 | accel_x += gravity_x * body.material.gravity_scale;
32 | accel_y += gravity_y * body.material.gravity_scale;
33 | }
34 |
35 | // Apply Acceleration, Drag, and Max Velocity
36 | body.velocity.x = compute_velocity(body.velocity.x, accel_x, body.drag.x, body.max_velocity.x, dt);
37 | body.velocity.y = compute_velocity(body.velocity.y, accel_y, body.drag.y, body.max_velocity.y, dt);
38 |
39 | // Apply Linear Drag
40 | if (body.drag_length > 0 && body.acceleration == Vector2.zero && body.velocity != Vector2.zero) {
41 | body.velocity.length = body.velocity.length - body.drag_length * dt;
42 | }
43 |
44 | // Apply Linear Max Velocity
45 | if (body.max_velocity_length > 0 && body.velocity.length > body.max_velocity_length) {
46 | body.velocity.length = body.max_velocity_length;
47 | }
48 |
49 | // Apply Velocity
50 | body.x += body.velocity.x * dt;
51 | body.y += body.velocity.y * dt;
52 |
53 | // Apply Rotational Acceleration, Drag, and Max Velocity
54 | var accel_rot = body.torque * body.inverse_mass;
55 | body.rotational_velocity = compute_velocity(body.rotational_velocity, accel_rot, body.rotational_drag, body.max_rotational_velocity, dt);
56 |
57 | // Apply Rotational Velocity
58 | body.rotation += body.rotational_velocity * dt;
59 | }
60 | }
61 | /**
62 | * Loops through all of a World's Listeners, separating all collided Bodies in the World. Use `Collisions.query()` before calling this to query the World's Listeners for collisions.
63 | * @param world
64 | * @param dt
65 | */
66 | public static function separate(world:World, ?listeners:Listeners) {
67 | var members = listeners == null ? world.listeners.members : listeners.members;
68 | for (listener in members) {
69 | if (listener.separate) for (collision in listener.collisions) {
70 | for (i in 0...collision.data.length) resolve(collision.a, collision.b, collision.data[i], listener.correction_threshold, listener.percent_correction);
71 | }
72 | }
73 | }
74 | /**
75 | * Resolves a Collision between two Bodies, separating them if the conditions are correct.
76 | * @param a the first `Body` in the Collision
77 | * @param b the second `Body` in the Collision
78 | * @param cd Data related to the Collision
79 | */
80 | public static inline function resolve(a:Body, b:Body, cd:CollisionData, correction_threshold:Float = 0.013, percent_correction:Float = 0.9,
81 | advanced:Bool = false) {
82 | // Do not resolve if either objects arent solid
83 | if (!cd.sa.solid || !cd.sb.solid || !a.active || !b.active || a.disposed || b.disposed || a.is_static() && b.is_static()) return;
84 |
85 | // Calculate relative velocity
86 | var rvx = a.velocity.x - b.velocity.x;
87 | var rvy = a.velocity.y - b.velocity.y;
88 |
89 | // Calculate relative velocity in terms of the normal direction
90 | var vel_to_normal = rvx * cd.normal.x + rvy * cd.normal.y;
91 | var inv_mass_sum = a.inverse_mass + b.inverse_mass;
92 |
93 | // Do not resolve if velocities are separating
94 | if (vel_to_normal > 0) {
95 | // Calculate elasticity
96 | var e = (a.material.elasticity + b.material.elasticity) * 0.5;
97 |
98 | // Calculate impulse scalar
99 | var j = (-(1 + e) * vel_to_normal) / inv_mass_sum;
100 | var impulse_x = -j * cd.normal.x;
101 | var impulse_y = -j * cd.normal.y;
102 |
103 | // Apply impulse
104 | var mass_sum = a.mass + b.mass;
105 | var ratio = a.mass / mass_sum;
106 | if (!a.kinematic) {
107 | a.velocity.x -= impulse_x * a.inverse_mass;
108 | a.velocity.y -= impulse_y * a.inverse_mass;
109 | }
110 | ratio = b.mass / mass_sum;
111 | if (!b.kinematic) {
112 | b.velocity.x += impulse_x * b.inverse_mass;
113 | b.velocity.y += impulse_y * b.inverse_mass;
114 | }
115 |
116 | if (advanced) {
117 | // Calculate static and dynamic friction
118 | var sf = Math.sqrt(a.material.static_friction * a.material.static_friction + b.material.static_friction * b.material.static_friction);
119 | var df = Math.sqrt(a.material.friction * a.material.friction + b.material.friction * b.material.friction);
120 |
121 | // TODO - FRICTION / TORQUE / CONTACT POINT RESOLUTION
122 | }
123 | }
124 |
125 | // Provide some positional correction to the objects to help prevent jitter
126 | var correction = (Math.max(cd.overlap - correction_threshold, 0) / inv_mass_sum) * percent_correction;
127 | var cx = correction * cd.normal.x;
128 | var cy = correction * cd.normal.y;
129 | if (!a.kinematic) {
130 | a.x -= a.inverse_mass * cx;
131 | a.y -= a.inverse_mass * cy;
132 | }
133 | if (!b.kinematic) {
134 | b.x += b.inverse_mass * cx;
135 | b.y += b.inverse_mass * cy;
136 | }
137 | }
138 |
139 | // TODO
140 | // public static function resolve_intersection(id:Intersection, correction_threshold:Float = 0.013, percent_correction:Float = 0.9) {}
141 |
142 | public static inline function compute_velocity(v:Float, a:Float, d:Float, m:Float, dt:Float) {
143 | // Apply Acceleration to Velocity
144 | if (!a.equals(0)) {
145 | v += a * dt;
146 | }
147 | else if (!d.equals(0)) {
148 | d = d * dt;
149 | if (v - d > 0) v -= d;
150 | else if (v + d < 0) v += d;
151 | else v = 0;
152 | }
153 | // Clamp Velocity if it has a Max
154 | if (!m.equals(0)) v = v.clamp(-m, m);
155 | return v;
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/echo/Shape.hx:
--------------------------------------------------------------------------------
1 | package echo;
2 |
3 | import echo.data.Data;
4 | import echo.data.Options;
5 | import echo.data.Types;
6 | import echo.shape.*;
7 | import echo.util.AABB;
8 | import echo.util.Transform;
9 | import echo.math.Vector2;
10 | /**
11 | * Base Shape Class. Acts as a Body's collider. Check out `echo.shapes` for all available shapes.
12 | */
13 | class Shape #if cog implements cog.IComponent #end {
14 | /**
15 | * Default Shape Options
16 | */
17 | public static var defaults(get, null):ShapeOptions;
18 | /**
19 | * Gets a Shape. If one is available, it will be grabbed from the Object Pool. Otherwise a new Shape will be created.
20 | * @param options
21 | * @return Shape
22 | */
23 | public static function get(options:ShapeOptions):Shape {
24 | options = echo.util.JSON.copy_fields(options, defaults);
25 | var s:Shape;
26 | switch (options.type) {
27 | case RECT:
28 | s = Rect.get(options.offset_x, options.offset_y, options.width, options.height, options.rotation, options.scale_x, options.scale_y);
29 | case CIRCLE:
30 | s = Circle.get(options.offset_x, options.offset_y, options.radius, options.rotation, options.scale_x, options.scale_y);
31 | case POLYGON:
32 | if (options.vertices != null) s = Polygon.get_from_vertices(options.offset_x, options.offset_y, options.rotation, options.vertices, options.scale_x,
33 | options.scale_y);
34 | else s = Polygon.get(options.offset_x, options.offset_y, options.sides, options.radius, options.rotation, options.scale_x, options.scale_y);
35 | }
36 | s.solid = options.solid;
37 | return s;
38 | }
39 | /**
40 | * Gets a `Rect` from the Rect Classes' Object Pool. Shortcut for `Rect.get()`.
41 | * @param x The X position of the Rect
42 | * @param y The Y position of the Rect
43 | * @param width The width of the Rect
44 | * @param height The height of the Rect
45 | * @return Rect
46 | */
47 | public static inline function rect(?x:Float, ?y:Float, ?width:Float, ?height:Float, ?scale_x:Float,
48 | ?scale_y:Float) return Rect.get(x, y, width, height, 0, scale_x, scale_y);
49 | /**
50 | * Gets a `Rect` with uniform width/height from the Rect Classes' Object Pool. Shortcut for `Rect.get()`.
51 | * @param x The X position of the Rect
52 | * @param y The Y position of the Rect
53 | * @param width The width of the Rect
54 | * @return Rect
55 | */
56 | public static inline function square(?x:Float, ?y:Float, ?width:Float) return Rect.get(x, y, width, width);
57 | /**
58 | * Gets a `Circle` from the Circle Classes' Object Pool. Shortcut for `Circle.get()`.
59 | * @param x The X position of the Circle
60 | * @param y The Y position of the Circle
61 | * @param radius The radius of the Circle
62 | * @return Rect
63 | */
64 | public static inline function circle(?x:Float, ?y:Float, ?radius:Float, ?scale_x:Float, ?scale_y:Float) return Circle.get(x, y, radius, scale_x, scale_y);
65 | /**
66 | * Enum value determining what shape this Object is (Rect, Circle, Polygon).
67 | */
68 | public var type:ShapeType;
69 | /**
70 | * The Shape's position on the X axis. For Rects, Circles, and simple Polygons, this position is based on the center of the Shape.
71 | *
72 | * If added to a `Body`, this value is relative to the Body's X position. To get the Shape's local X position in this case, use `local_x`.
73 | */
74 | public var x(get, set):Float;
75 | /**
76 | * The Shape's position on the Y axis. For Rects, Circles, and simple Polygons, this position is based on the center of the Shape.
77 | *
78 | * If added to a `Body`, this value is relative to the Body's Y position. To get the Shape's local Y position in this case, use `local_y`.
79 | */
80 | public var y(get, set):Float;
81 | /**
82 | * The Shape's angular rotation.
83 | *
84 | * If added to a `Body`, this value is relative to the Body's rotation. To get the Shape's local rotation in this case, use `local_rotation`.
85 | */
86 | public var rotation(get, set):Float;
87 |
88 | public var scale_x(get, set):Float;
89 |
90 | public var scale_y(get, set):Float;
91 | /**
92 | * The Shape's position on the X axis. For Rects, Circles, and simple Polygons, this position is based on the center of the Shape.
93 | *
94 | * If added to a `Body`, this value is treated as an offset to the Body's X position.
95 | */
96 | public var local_x(get, set):Float;
97 | /**
98 | * The Shape's position on the Y axis. For Rects, Circles, and simple Polygons, this position is based on the center of the Shape.
99 | *
100 | * If added to a `Body`, this value is treated as an offset to the Body's Y position.
101 | */
102 | public var local_y(get, set):Float;
103 |
104 | public var local_rotation(get, set):Float;
105 |
106 | public var local_scale_x(get, set):Float;
107 |
108 | public var local_scale_y(get, set):Float;
109 |
110 | public var transform:Transform = new Transform();
111 | /**
112 | * Flag to set whether the Shape collides with other Shapes.
113 | *
114 | * If false, this Shape's Body will not have its position or velocity affected by other Bodies, but it will still call collision callbacks
115 | */
116 | public var solid:Bool = true;
117 | /**
118 | * The Upper Bounds of the Shape.
119 | */
120 | public var top(get, never):Float;
121 | /**
122 | * The Lower Bounds of the Shape.
123 | */
124 | public var bottom(get, never):Float;
125 | /**
126 | * The Left Bounds of the Shape.
127 | */
128 | public var left(get, never):Float;
129 | /**
130 | * The Right Bounds of the Shape.
131 | */
132 | public var right(get, never):Float;
133 | /**
134 | * Flag to determine if the Shape has collided in the last `World` step. Used Internally for Debugging.
135 | */
136 | public var collided:Bool;
137 |
138 | var parent(default, null):Body;
139 | /**
140 | * Creates a new Shape
141 | * @param x
142 | * @param y
143 | */
144 | inline function new(x:Float = 0, y:Float = 0, rotation:Float = 0) {
145 | local_x = x;
146 | local_y = y;
147 | local_rotation = rotation;
148 | }
149 |
150 | public function put() {
151 | transform.set_parent(null);
152 | parent = null;
153 | collided = false;
154 | }
155 | /**
156 | * Gets the Shape's position on the X and Y axis as a `Vector2`.
157 | */
158 | public inline function get_position():Vector2 return transform.get_position();
159 |
160 | public inline function get_local_position():Vector2 return transform.get_local_position();
161 |
162 | public inline function set_position(position:Vector2):Void {
163 | transform.set_position(position);
164 | }
165 |
166 | public inline function set_local_position(position:Vector2):Void {
167 | transform.set_local_position(position);
168 | }
169 |
170 | public function set_parent(?body:Body):Void {
171 | if (parent == body) return;
172 | parent = body;
173 | transform.set_parent(body == null ? null : body.transform);
174 | }
175 | /**
176 | * Returns an `AABB` representing the bounds of the `Shape`.
177 | * @param aabb Optional `AABB` to set the values to.
178 | * @return AABB
179 | */
180 | public function bounds(?aabb:AABB):AABB return aabb == null ? AABB.get(x, y, 0, 0) : aabb.set(x, y, 0, 0);
181 |
182 | public function volume():Float return 0;
183 | /**
184 | * Clones the Shape into a new Shape
185 | * @return Shape return new Shape(x, y)
186 | */
187 | public function clone():Shape return new Shape(x, y, rotation);
188 | /**
189 | * TODO
190 | */
191 | @:dox(hide)
192 | @:noCompletion
193 | public function scale(v:Float) {}
194 |
195 | public function contains(v:Vector2):Bool return get_position() == v;
196 | /**
197 | * TODO
198 | */
199 | @:dox(hide)
200 | @:noCompletion
201 | public function closest_point_on_edge(v:Vector2):Vector2 return get_position();
202 |
203 | public function intersect(l:Line):Null return null;
204 |
205 | public function overlaps(s:Shape):Bool return contains(s.get_position());
206 |
207 | public function collides(s:Shape):Null return null;
208 |
209 | function collide_rect(r:Rect):Null return null;
210 |
211 | function collide_circle(c:Circle):Null return null;
212 |
213 | function collide_polygon(p:Polygon):Null return null;
214 |
215 | function toString() {
216 | var s = switch (type) {
217 | case RECT: 'rect';
218 | case CIRCLE: 'circle';
219 | case POLYGON: 'polygon';
220 | }
221 | return 'Shape: {type: $s, x: $x, y: $y, rotation: $rotation}';
222 | }
223 |
224 | // getters
225 | inline function get_x():Float return transform.x;
226 |
227 | inline function get_y():Float return transform.y;
228 |
229 | inline function get_rotation():Float return transform.rotation;
230 |
231 | inline function get_scale_x():Float return transform.scale_x;
232 |
233 | inline function get_scale_y():Float return transform.scale_y;
234 |
235 | inline function get_local_x():Float return transform.local_x;
236 |
237 | inline function get_local_y():Float return transform.local_y;
238 |
239 | inline function get_local_rotation():Float return transform.local_rotation;
240 |
241 | inline function get_local_scale_x():Float return transform.local_scale_x;
242 |
243 | inline function get_local_scale_y():Float return transform.local_scale_y;
244 |
245 | function get_top():Float return y;
246 |
247 | function get_bottom():Float return y;
248 |
249 | function get_left():Float return x;
250 |
251 | function get_right():Float return x;
252 |
253 | // setters
254 | inline function set_x(v:Float):Float {
255 | return transform.x = v;
256 | }
257 |
258 | inline function set_y(v:Float):Float {
259 | return transform.y = v;
260 | }
261 |
262 | inline function set_rotation(v:Float):Float {
263 | return transform.rotation = v;
264 | }
265 |
266 | inline function set_scale_x(v:Float):Float {
267 | return transform.scale_x = v;
268 | }
269 |
270 | inline function set_scale_y(v:Float):Float {
271 | return transform.scale_y = v;
272 | }
273 |
274 | inline function set_local_x(v:Float):Float {
275 | return transform.local_x = v;
276 | }
277 |
278 | inline function set_local_y(v:Float):Float {
279 | return transform.local_y = v;
280 | }
281 |
282 | inline function set_local_rotation(v:Float):Float {
283 | return transform.local_rotation = v;
284 | }
285 |
286 | inline function set_local_scale_x(v:Float):Float {
287 | return transform.local_scale_x = v;
288 | }
289 |
290 | inline function set_local_scale_y(v:Float):Float {
291 | return transform.local_scale_y = v;
292 | }
293 |
294 | static function get_defaults():ShapeOptions return {
295 | type: RECT,
296 | radius: 1,
297 | width: 1,
298 | height: 0,
299 | sides: 3,
300 | rotation: 0,
301 | scale_x: 1,
302 | scale_y: 1,
303 | offset_x: 0,
304 | offset_y: 0,
305 | solid: true
306 | }
307 | }
308 |
--------------------------------------------------------------------------------
/echo/World.hx:
--------------------------------------------------------------------------------
1 | package echo;
2 |
3 | import echo.Listener;
4 | import echo.data.Data;
5 | import echo.data.Options;
6 | import echo.shape.Rect;
7 | import echo.util.Disposable;
8 | import echo.util.History;
9 | import echo.util.QuadTree;
10 | import echo.math.Vector2;
11 | /**
12 | * A `World` is an Object representing the state of a Physics simulation and it configurations.
13 | */
14 | @:using(echo.Echo)
15 | class World implements Disposable {
16 | /**
17 | * Width of the World, extending right from the World's X position.
18 | */
19 | public var width(default, set):Float;
20 | /**
21 | * Height of the World, extending down from the World's Y position.
22 | */
23 | public var height(default, set):Float;
24 | /**
25 | * The World's position on the X axis.
26 | */
27 | public var x(default, set):Float;
28 | /**
29 | * The World's position on the Y axis.
30 | */
31 | public var y(default, set):Float;
32 | /**
33 | * The amount of acceleration applied to each `Body` member every Step.
34 | */
35 | public var gravity(default, null):Vector2;
36 | /**
37 | * The World's QuadTree for dynamic Bodies. Generally doesn't need to be touched.
38 | */
39 | public var quadtree:QuadTree;
40 | /**
41 | * The World's QuadTree for static Bodies. Generally doesn't need to be touched.
42 | */
43 | public var static_quadtree:QuadTree;
44 |
45 | public var listeners:Listeners;
46 | public var members:Array;
47 | public var count(get, never):Int;
48 | /**
49 | * The amount of iterations that occur each time the World is stepped. The higher the number, the more stable the Physics Simulation will be, at the cost of performance.
50 | */
51 | public var iterations:Int;
52 |
53 | public var history:Null>>;
54 |
55 | public var accumulatedTime:Float = 0;
56 |
57 | var init:Bool;
58 |
59 | public function new(options:WorldOptions) {
60 | members = options.members == null ? [] : options.members;
61 | init = false;
62 | width = options.width < 1?throw("World must have a width of at least 1") : options.width;
63 | height = options.height < 1?throw("World must have a width of at least 1") : options.height;
64 | x = options.x == null ? 0 : options.x;
65 | y = options.y == null ? 0 : options.y;
66 | gravity = new Vector2(options.gravity_x == null ? 0 : options.gravity_x, options.gravity_y == null ? 0 : options.gravity_y);
67 | reset_quadtrees();
68 |
69 | listeners = new Listeners(options.listeners);
70 | iterations = options.iterations == null ? 5 : options.iterations;
71 | if (options.history != null) history = new History(options.history);
72 | }
73 | /**
74 | * Sets the size of the World. Only Bodies within the world bound will be collided
75 | * @param x The x position of the world bounds
76 | * @param y The y position of the world bounds
77 | * @param width The width of the world bounds
78 | * @param height The height of the world bounds
79 | */
80 | public inline function set(x:Float, y:Float, width:Float, height:Float) {
81 | init = false;
82 | this.x = x;
83 | this.y = y;
84 | this.width = width;
85 | this.height = height;
86 | init = true;
87 | reset_quadtrees();
88 | }
89 | /**
90 | * Sets the size of the World based on a given shape.
91 | * @param s The shape to use as the boundaries of the World
92 | */
93 | public inline function set_from_shape(s:Shape) {
94 | x = s.left;
95 | y = s.top;
96 | width = s.right - x;
97 | height = s.bottom - y;
98 | }
99 | /**
100 | * Sets the size of the World based just large enough to encompass all the members.
101 | */
102 | public function set_from_members() {
103 | var l = 0.0;
104 | var r = 0.0;
105 | var t = 0.0;
106 | var b = 0.0;
107 |
108 | for (m in members) {
109 | for (s in m.shapes) {
110 | if (s.left < l) l = s.left;
111 | if (s.right > r) r = s.right;
112 | if (s.top < t) t = s.top;
113 | if (s.bottom > b) b = s.bottom;
114 | }
115 | }
116 |
117 | set(l, t, r - l, b - t);
118 | }
119 |
120 | public inline function center(?rect:Rect):Rect {
121 | return rect != null ? rect.set(x + (width * 0.5), y + (height * 0.5), width, height) : Rect.get(x + (width * 0.5), y + (height * 0.5), width, height);
122 | }
123 |
124 | public function add(body:Body):Body {
125 | if (body.world == this) return body;
126 | if (body.world != null) body.remove();
127 | body.world = this;
128 | body.dirty = true;
129 | members.push(body);
130 | body.quadtree_data = {id: body.id, bounds: body.bounds(), flag: false};
131 | body.is_static() ? static_quadtree.insert(body.quadtree_data) : quadtree.insert(body.quadtree_data);
132 | return body;
133 | }
134 |
135 | public function remove(body:Body):Body {
136 | quadtree.remove(body.quadtree_data);
137 | static_quadtree.remove(body.quadtree_data);
138 | members.remove(body);
139 | body.world = null;
140 | return body;
141 | }
142 |
143 | public inline function iterator():Iterator return members.iterator();
144 | /**
145 | * Returns a new Array containing every dynamic `Body` in the World.
146 | */
147 | public inline function dynamics():Array return members.filter(b -> return b.is_dynamic());
148 | /**
149 | * Returns a new Array containing every static `Body` in the World.
150 | */
151 | public inline function statics():Array return members.filter(b -> return b.is_static());
152 | /**
153 | * Runs a function on every `Body` in the World
154 | * @param f Function to perform on each `Body`.
155 | * @param recursive Currently not supported.
156 | */
157 | public inline function for_each(f:Body->Void, recursive:Bool = true) for (b in members) f(cast b);
158 | /**
159 | * Runs a function on every dynamic `Body` in the World
160 | * @param f Function to perform on each dynamic `Body`.
161 | * @param recursive Currently not supported.
162 | */
163 | public inline function for_each_dynamic(f:Body->Void, recursive:Bool = true) for (b in members) if (b.is_dynamic()) f(cast b);
164 | /**
165 | * Runs a function on every static `Body` in the World
166 | * @param f Function to perform on each static `Body`.
167 | * @param recursive Currently not supported.
168 | */
169 | public inline function for_each_static(f:Body->Void, recursive:Bool = true) for (b in members) if (b.is_static()) f(cast b);
170 | /**
171 | * Clears the World's members and listeners.
172 | */
173 | public function clear() {
174 | while (members.length > 0) {
175 | var m = members.pop();
176 | if (m != null) m.remove();
177 | }
178 | reset_quadtrees();
179 | listeners.clear();
180 | }
181 | /**
182 | * Disposes the World. DO NOT use the World after disposing it, as it could lead to null reference errors.
183 | */
184 | public function dispose() {
185 | for_each(b -> b.remove());
186 | members = null;
187 | gravity = null;
188 | quadtree.put();
189 | listeners.dispose();
190 | listeners = null;
191 | history = null;
192 | }
193 | /**
194 | * Resets the World's dynamic and static Quadtrees.
195 | */
196 | public function reset_quadtrees() {
197 | init = true;
198 | if (quadtree != null) quadtree.put();
199 | quadtree = QuadTree.get();
200 | if (static_quadtree != null) static_quadtree.put();
201 | static_quadtree = QuadTree.get();
202 | var r = center().to_aabb(true);
203 | quadtree.load(r);
204 | static_quadtree.load(r);
205 | for_each((member) -> {
206 | if (member.is_dynamic()) {
207 | member.dirty = true;
208 | }
209 | else {
210 | member.bounds(member.quadtree_data.bounds);
211 | static_quadtree.update(member.quadtree_data);
212 | }
213 | });
214 | }
215 |
216 | inline function get_count():Int return members.length;
217 |
218 | inline function set_x(value:Float) {
219 | x = value;
220 | if (init) reset_quadtrees();
221 | return x;
222 | }
223 |
224 | inline function set_y(value:Float) {
225 | y = value;
226 | if (init) reset_quadtrees();
227 | return y;
228 | }
229 |
230 | inline function set_width(value:Float) {
231 | width = value;
232 | if (init) reset_quadtrees();
233 | return height;
234 | }
235 |
236 | inline function set_height(value:Float) {
237 | height = value;
238 | if (init) reset_quadtrees();
239 | return height;
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/echo/data/Data.hx:
--------------------------------------------------------------------------------
1 | package echo.data;
2 |
3 | import haxe.ds.Vector;
4 | import echo.math.Vector2;
5 | import echo.util.AABB;
6 | import echo.util.Poolable;
7 |
8 | @:structInit
9 | class BodyState {
10 | public final id:Int;
11 | public final x:Float;
12 | public final y:Float;
13 | public final rotation:Float;
14 | public final velocity_x:Float;
15 | public final velocity_y:Float;
16 | public final acceleration_x:Float;
17 | public final acceleration_y:Float;
18 | public final rotational_velocity:Float;
19 |
20 | public function new(id:Int, x:Float, y:Float, rotation:Float, velocity_x:Float, velocity_y:Float, acceleration_x:Float, acceleration_y:Float,
21 | rotational_velocity:Float) {
22 | this.id = id;
23 | this.x = x;
24 | this.y = y;
25 | this.rotation = rotation;
26 | this.velocity_x = velocity_x;
27 | this.velocity_y = velocity_y;
28 | this.acceleration_x = acceleration_x;
29 | this.acceleration_y = acceleration_y;
30 | this.rotational_velocity = rotational_velocity;
31 | }
32 | }
33 | /**
34 | * Class containing data describing any Collisions between two Bodies.
35 | */
36 | class Collision implements Poolable {
37 | /**
38 | * Body A.
39 | */
40 | public var a:Body;
41 | /**
42 | * Body B.
43 | */
44 | public var b:Body;
45 | /**
46 | * Array containing Data from Each Collision found between the two Bodies' Shapes.
47 | */
48 | public final data:Array = [];
49 |
50 | public static inline function get(a:Body, b:Body):Collision {
51 | var c = pool.get();
52 | c.a = a;
53 | c.b = b;
54 | c.data.resize(0);
55 | c.pooled = false;
56 | return c;
57 | }
58 |
59 | public function put() {
60 | if (!pooled) {
61 | for (d in data) d.put();
62 | pooled = true;
63 | pool.put_unsafe(this);
64 | }
65 | }
66 |
67 | inline function new() {}
68 | }
69 | /**
70 | * Class containing data describing a Collision between two Shapes.
71 | */
72 | class CollisionData implements Poolable {
73 | /**
74 | * Shape A.
75 | */
76 | public var sa:Null;
77 | /**
78 | * Shape B.
79 | */
80 | public var sb:Null;
81 | /**
82 | * The length of Shape A's penetration into Shape B.
83 | */
84 | public var overlap = 0.;
85 |
86 | public var contact_count = 0;
87 |
88 | public final contacts = Vector.fromArrayCopy([Vector2.zero, Vector2.zero]);
89 | /**
90 | * The normal vector (direction) of Shape A's penetration into Shape B.
91 | */
92 | public final normal = Vector2.zero;
93 |
94 | public static inline function get(overlap:Float, x:Float, y:Float):CollisionData {
95 | var c = pool.get();
96 | c.sa = null;
97 | c.sb = null;
98 | c.contact_count = 0;
99 | for (cc in c.contacts) cc.set(0, 0);
100 | c.set(overlap, x, y);
101 | c.pooled = false;
102 | return c;
103 | }
104 |
105 | inline function new() {}
106 |
107 | public inline function set(overlap:Float, x:Float, y:Float) {
108 | this.overlap = overlap;
109 | normal.set(x, y);
110 | }
111 |
112 | public function put() {
113 | if (!pooled) {
114 | pooled = true;
115 | pool.put_unsafe(this);
116 | }
117 | }
118 | }
119 | /**
120 | * Class containing data describing any Intersections between a Line and a Body.
121 | */
122 | class Intersection implements Poolable {
123 | /**
124 | * Line.
125 | */
126 | public var line:Null;
127 | /**
128 | * Body.
129 | */
130 | public var body:Null;
131 | /**
132 | * Array containing Data from Each Intersection found between the Line and each Shape in the Body.
133 | */
134 | public final data:Array = [];
135 | /**
136 | * Gets the IntersectionData that has the closest hit distance from the beginning of the Line.
137 | */
138 | public var closest(get, never):Null;
139 |
140 | public static inline function get(line:Line, body:Body):Intersection {
141 | var i = pool.get();
142 | i.line = line;
143 | i.body = body;
144 | i.data.resize(0);
145 | i.pooled = false;
146 | return i;
147 | }
148 |
149 | public function put() {
150 | if (!pooled) {
151 | for (d in data) d.put();
152 | pooled = true;
153 | pool.put_unsafe(this);
154 | }
155 | }
156 |
157 | inline function new() {}
158 |
159 | inline function get_closest():Null {
160 | if (data.length == 0) return null;
161 | if (data.length == 1) return data[0];
162 |
163 | var closest = data[0];
164 | for (i in 1...data.length) if (data[i] != null && data[i].distance < closest.distance) closest = data[i];
165 | return closest;
166 | }
167 | }
168 | /**
169 | * Class containing data describing an Intersection between a Line and a Shape.
170 | */
171 | class IntersectionData implements Poolable {
172 | public var line:Null;
173 | public var shape:Null;
174 | /**
175 | * The second Line in the Intersection. This is only set when intersecting two Lines.
176 | */
177 | public var line2:Null;
178 | /**
179 | * The position along the line where the line hit the shape.
180 | */
181 | public final hit = Vector2.zero;
182 | /**
183 | * The distance between the start of the line and the hit position.
184 | */
185 | public var distance = 0.;
186 | /**
187 | * The length of the line that has overlapped the shape.
188 | */
189 | public var overlap = 0.;
190 | /**
191 | * The normal vector (direction) of the Line's penetration into the Shape.
192 | */
193 | public final normal = Vector2.zero;
194 | /**
195 | Indicates if normal was inversed and usually occurs when Line penetrates into the Shape from the inside.
196 | **/
197 | public var inverse_normal = false;
198 |
199 | public static inline function get(distance:Float, overlap:Float, x:Float, y:Float, normal_x:Float, normal_y:Float,
200 | inverse_normal:Bool = false):IntersectionData {
201 | var i = pool.get();
202 | i.line = null;
203 | i.shape = null;
204 | i.line2 = null;
205 | i.set(distance, overlap, x, y, normal_x, normal_y, inverse_normal);
206 | i.pooled = false;
207 | return i;
208 | }
209 |
210 | inline function new() {
211 | hit = new Vector2(0, 0);
212 | normal = new Vector2(0, 0);
213 | }
214 |
215 | public inline function set(distance:Float, overlap:Float, x:Float, y:Float, normal_x:Float, normal_y:Float, inverse_normal:Bool = false) {
216 | this.distance = distance;
217 | this.overlap = overlap;
218 | this.inverse_normal = inverse_normal;
219 | hit.set(x, y);
220 | normal.set(normal_x, normal_y);
221 | }
222 |
223 | public function put() {
224 | if (!pooled) {
225 | pooled = true;
226 | pool.put_unsafe(this);
227 | }
228 | }
229 | }
230 |
231 | @:structInit
232 | class QuadTreeData {
233 | /**
234 | * Id of the Data.
235 | */
236 | public var id:Int;
237 | /**
238 | * Bounds of the Data.
239 | */
240 | public var bounds:Null = null;
241 | /**
242 | * Helper flag to check if this Data has been counted during queries.
243 | */
244 | public var flag = false;
245 | }
246 |
247 | enum abstract Direction(Int) from Int to Int {
248 | var TOP = 0;
249 | }
250 |
--------------------------------------------------------------------------------
/echo/data/Types.hx:
--------------------------------------------------------------------------------
1 | package echo.data;
2 |
3 | enum abstract MassType(Float) from Float to Float {
4 | var AUTO = -1;
5 | var STATIC = 0;
6 | }
7 |
8 | enum abstract ShapeType(Int) from Int to Int {
9 | var RECT;
10 | var CIRCLE;
11 | var POLYGON;
12 | }
13 |
14 | enum abstract ForceType(Int) from Int to Int {
15 | var ACCELERATION;
16 | var VELOCITY;
17 | var POSITION;
18 | }
19 |
--------------------------------------------------------------------------------
/echo/math/Matrix2.hx:
--------------------------------------------------------------------------------
1 | package echo.math;
2 |
3 | import echo.math.Types;
4 |
5 | @:dox(hide)
6 | @:noCompletion
7 | class Matrix2Default {
8 | public var m00:Float;
9 | public var m01:Float;
10 |
11 | public var m10:Float;
12 | public var m11:Float;
13 | /**
14 | * Column-Major Orientation.
15 | * /m00, m10/
16 | * /m01, m11/
17 | */
18 | public inline function new(m00:Float, m10:Float, m01:Float, m11:Float) {
19 | this.m00 = m00 + 0.0;
20 | this.m10 = m10 + 0.0;
21 | this.m01 = m01 + 0.0;
22 | this.m11 = m11 + 0.0;
23 | }
24 |
25 | public function toString():String {
26 | return '{ m00:$m00, m10:$m10, m01:$m01, m11:$m11 }';
27 | }
28 | }
29 | /**
30 | * Column-Major Orientation.
31 | * /m00, m10/
32 | * /m01, m11/
33 | */
34 | @:using(echo.math.Matrix2)
35 | @:forward(m00, m10, m01, m11)
36 | abstract Matrix2(Matrix2Type) from Matrix2Type to Matrix2Type {
37 | public static inline final element_count:Int = 4;
38 |
39 | public static var zero(get, never):Matrix2;
40 |
41 | public static var identity(get, never):Matrix2;
42 |
43 | public var col_x(get, set):Vector2;
44 |
45 | public var col_y(get, set):Vector2;
46 | /**
47 | * Gets a rotation matrix from the given radians.
48 | */
49 | public static inline function from_radians(radians:Float) {
50 | var c = Math.cos(radians);
51 | var s = Math.sin(radians);
52 | return new Matrix2(c, -s, s, c);
53 | }
54 |
55 | public static inline function from_vectors(x:Vector2, y:Vector2) return new Matrix2(x.x, y.x, x.y, y.y);
56 |
57 | @:from
58 | public static inline function from_arr(a:Array):Matrix2 @:privateAccess return new Matrix2(a[0], a[1], a[2], a[3]);
59 |
60 | @:to
61 | public inline function to_arr():Array {
62 | var self = this;
63 | return [self.m00, self.m10, self.m01, self.m11];
64 | }
65 |
66 | public inline function new(m00:Float, m10:Float, m01:Float, m11:Float) {
67 | this = new Matrix2Type(m00, m10, m01, m11);
68 | }
69 |
70 | // region operator overloads
71 |
72 | @:op([])
73 | public inline function arr_read(i:Int):Float {
74 | var self:Matrix2 = this;
75 |
76 | switch (i) {
77 | case 0:
78 | return self.m00;
79 | case 1:
80 | return self.m10;
81 | case 2:
82 | return self.m01;
83 | case 3:
84 | return self.m11;
85 | default:
86 | throw "Invalid element";
87 | }
88 | }
89 |
90 | @:op([])
91 | public inline function arr_write(i:Int, value:Float):Float {
92 | var self:Matrix2 = this;
93 |
94 | switch (i) {
95 | case 0:
96 | return self.m00 = value;
97 | case 1:
98 | return self.m10 = value;
99 | case 2:
100 | return self.m01 = value;
101 | case 3:
102 | return self.m11 = value;
103 | default:
104 | throw "Invalid element";
105 | }
106 | }
107 |
108 | @:op(a * b)
109 | static inline function mul(a:Matrix2, b:Matrix2):Matrix2
110 | return new Matrix2(a.m00 * b.m00
111 | + a.m10 * b.m01, a.m00 * b.m10
112 | + a.m10 * b.m11, a.m01 * b.m00
113 | + a.m11 * b.m01, a.m01 * b.m10
114 | + a.m11 * b.m11);
115 |
116 | @:op(a * b)
117 | static inline function mul_vec2(a:Matrix2, v:Vector2):Vector2
118 | return new Vector2(a.m00 * v.x + a.m10 * v.y, a.m01 * v.x + a.m11 * v.y);
119 |
120 | // endregion
121 |
122 | static inline function get_zero():Matrix2 {
123 | return new Matrix2(0.0, 0.0, 0.0, 0.0);
124 | }
125 |
126 | static inline function get_identity():Matrix2 {
127 | return new Matrix2(1.0, 0.0, 0.0, 1.0);
128 | }
129 |
130 | inline function get_col_x():Vector2 {
131 | var self = this;
132 | return new Vector2(self.m00, self.m01);
133 | }
134 |
135 | inline function get_col_y():Vector2 {
136 | var self = this;
137 | return new Vector2(self.m11, self.m11);
138 | }
139 |
140 | inline function set_col_x(vector2:Vector2):Vector2 {
141 | var self = this;
142 | return vector2.set(self.m00, self.m01);
143 | }
144 |
145 | inline function set_col_y(vector2:Vector2):Vector2 {
146 | var self = this;
147 | return vector2.set(self.m10, self.m11);
148 | }
149 | }
150 |
151 | inline function copy_to(a:Matrix2, b:Matrix2):Matrix2 {
152 | b.copy_from(a);
153 | return a;
154 | }
155 |
156 | inline function copy_from(a:Matrix2, b:Matrix2):Matrix2 {
157 | a.m00 = b.m00;
158 | a.m10 = b.m10;
159 | a.m01 = b.m01;
160 | a.m11 = b.m11;
161 | return a;
162 | }
163 |
164 | inline function transposed(m:Matrix2):Matrix2 return new Matrix2(m.m00, m.m01, m.m10, m.m11);
165 |
--------------------------------------------------------------------------------
/echo/math/Matrix3.hx:
--------------------------------------------------------------------------------
1 | package echo.math;
2 |
3 | import echo.math.Types.Matrix3Type;
4 |
5 | @:dox(hide)
6 | @:noCompletion
7 | class Matrix3Default {
8 | public var m00:Float;
9 | public var m01:Float;
10 | public var m02:Float;
11 |
12 | public var m10:Float;
13 | public var m11:Float;
14 | public var m12:Float;
15 |
16 | public var m20:Float;
17 | public var m21:Float;
18 | public var m22:Float;
19 | /**
20 | * Column-Major Orientation.
21 | * /m00, m10, m20/
22 | * /m01, m11, m21/
23 | * /m02, m12, m22/
24 | */
25 | public inline function new(m00:Float, m10:Float, m20:Float, m01:Float, m11:Float, m21:Float, m02:Float, m12:Float, m22:Float) {
26 | this.m00 = m00 + 0.0;
27 | this.m10 = m10 + 0.0;
28 | this.m20 = m20 + 0.0;
29 |
30 | this.m01 = m01 + 0.0;
31 | this.m11 = m11 + 0.0;
32 | this.m21 = m21 + 0.0;
33 |
34 | this.m02 = m02 + 0.0;
35 | this.m12 = m12 + 0.0;
36 | this.m22 = m22 + 0.0;
37 | }
38 |
39 | public function toString():String {
40 | return '{ m00:$m00, m10:$m10, m20:$m20, m01:$m01, m11:$m11, m21:$m21, m02:$m02, m12:$m12, m22:$m22 }';
41 | }
42 | }
43 | /**
44 | * Column-Major Orientation.
45 | * /m00, m10, m20/
46 | * /m01, m11, m21/
47 | * /m02, m12, m22/
48 | */
49 | @:using(echo.math.Matrix3)
50 | @:forward(m00, m10, m20, m01, m11, m21, m02, m12, m22)
51 | abstract Matrix3(Matrix3Type) from Matrix3Type to Matrix3Type {
52 | public static inline final element_count:Int = 9;
53 |
54 | public static var zero(get, never):Matrix3;
55 |
56 | public static var identity(get, never):Matrix3;
57 |
58 | @:from
59 | public static inline function from_arr(a:Array):Matrix3 @:privateAccess return new Matrix3(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]);
60 |
61 | @:to
62 | public inline function to_arr():Array {
63 | var self = this;
64 | return [
65 | self.m00,
66 | self.m10,
67 | self.m20,
68 | self.m01,
69 | self.m11,
70 | self.m21,
71 | self.m02,
72 | self.m12,
73 | self.m22
74 | ];
75 | }
76 |
77 | public inline function new(m00:Float, m10:Float, m20:Float, m01:Float, m11:Float, m21:Float, m02:Float, m12:Float, m22:Float) {
78 | this = new Matrix3Type(m00, m10, m20, m01, m11, m21, m02, m12, m22);
79 | }
80 |
81 | // region operator overloads
82 |
83 | @:op([])
84 | public inline function arr_read(i:Int):Float {
85 | var self:Matrix3 = this;
86 |
87 | switch (i) {
88 | case 0:
89 | return self.m00;
90 | case 1:
91 | return self.m10;
92 | case 2:
93 | return self.m20;
94 | case 3:
95 | return self.m01;
96 | case 4:
97 | return self.m11;
98 | case 5:
99 | return self.m21;
100 | case 6:
101 | return self.m02;
102 | case 7:
103 | return self.m12;
104 | case 8:
105 | return self.m22;
106 | default:
107 | throw "Invalid element";
108 | }
109 | }
110 |
111 | @:op([])
112 | public inline function arr_write(i:Int, value:Float):Float {
113 | var self:Matrix3 = this;
114 |
115 | switch (i) {
116 | case 0:
117 | return self.m00 = value;
118 | case 1:
119 | return self.m10 = value;
120 | case 2:
121 | return self.m20 = value;
122 | case 3:
123 | return self.m01 = value;
124 | case 4:
125 | return self.m11 = value;
126 | case 5:
127 | return self.m21 = value;
128 | case 6:
129 | return self.m02 = value;
130 | case 7:
131 | return self.m12 = value;
132 | case 8:
133 | return self.m22 = value;
134 | default:
135 | throw "Invalid element";
136 | }
137 | }
138 |
139 | @:op(a * b)
140 | static inline function mul(a:Matrix3, b:Matrix3):Matrix3 {
141 | return new Matrix3(a.m00 * b.m00
142 | + a.m10 * b.m01
143 | + a.m20 * b.m02, a.m00 * b.m10
144 | + a.m10 * b.m11
145 | + a.m20 * b.m12,
146 | a.m00 * b.m20
147 | + a.m10 * b.m21
148 | + a.m20 * b.m22, a.m01 * b.m00
149 |
150 | + a.m11 * b.m01
151 | + a.m21 * b.m02, a.m01 * b.m10
152 | + a.m11 * b.m11
153 | + a.m21 * b.m12,
154 | a.m01 * b.m20
155 | + a.m11 * b.m21
156 | + a.m21 * b.m22, a.m02 * b.m00
157 |
158 | + a.m12 * b.m01
159 | + a.m22 * b.m02, a.m02 * b.m10
160 | + a.m12 * b.m11
161 | + a.m22 * b.m12,
162 | a.m02 * b.m20
163 | + a.m12 * b.m21
164 | + a.m22 * b.m22);
165 | }
166 |
167 | @:op(a * b)
168 | static inline function mul_vec3(a:Matrix3, v:Vector3):Vector3 {
169 | return new Vector3(a.m00 * v.x
170 | + a.m10 * v.y
171 | + a.m20 * v.z, a.m01 * v.x
172 | + a.m11 * v.y
173 | + a.m21 * v.z, a.m02 * v.x
174 | + a.m12 * v.y
175 | + a.m22 * v.z);
176 | }
177 |
178 | // endregion
179 |
180 | static inline function get_zero():Matrix3 {
181 | return new Matrix3(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
182 | }
183 |
184 | static inline function get_identity():Matrix3 {
185 | return new Matrix3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0);
186 | }
187 | }
188 |
189 | inline function copy_to(a:Matrix3, b:Matrix3):Matrix3 {
190 | b.copy_from(a);
191 | return a;
192 | }
193 |
194 | inline function copy_from(a:Matrix3, b:Matrix3):Matrix3 {
195 | a.m00 = b.m00;
196 | a.m10 = b.m10;
197 | a.m20 = b.m20;
198 |
199 | a.m01 = b.m01;
200 | a.m11 = b.m11;
201 | a.m21 = b.m21;
202 |
203 | a.m02 = b.m02;
204 | a.m12 = b.m12;
205 | a.m22 = b.m22;
206 | return a;
207 | }
208 |
--------------------------------------------------------------------------------
/echo/math/Types.hx:
--------------------------------------------------------------------------------
1 | package echo.math;
2 |
3 | #if ECHO_USE_HXMATH
4 | typedef Vector2Type = hxmath.math.Vector2;
5 | typedef Vector3Type = hxmath.math.Vector3;
6 | typedef Matrix2Type = echo.math.Matrix2.Matrix2Default;
7 | typedef Matrix3Type = hxmath.math.Matrix3x3;
8 | #elseif ECHO_USE_ZEROLIB
9 | typedef Vector2Type = zero.utilities.Vec2;
10 | typedef Vector3Type = zero.utilities.Vec3;
11 | typedef Matrix2Type = echo.math.Matrix2.Matrix2Default;
12 | typedef Matrix3Type = echo.math.Matrix3.Matrix3Default;
13 | #elseif ECHO_USE_VECTORMATH
14 | typedef Vector2Type = Vec2;
15 | typedef Vector3Type = Vec3;
16 | typedef Matrix2Type = echo.math.Matrix2.Matrix2Default;
17 | typedef Matrix3Type = echo.math.Matrix3.Matrix3Default;
18 |
19 | // TODO - finish type once Mat3 elements are easily accessible
20 | abstract Mat3Impl(Mat3) from Mat3 to Mat3 {
21 | // public var m00(get, set):Float;
22 | // public var m01(get, set):Float;
23 | // public var m02(get, set):Float;
24 | // public var m10(get, set):Float;
25 | // public var m11(get, set):Float;
26 | // public var m12(get, set):Float;
27 | // public var m20(get, set):Float;
28 | // public var m21(get, set):Float;
29 | // public var m22(get, set):Float;
30 | public inline function new(m00:Float, m10:Float, m20:Float, m01:Float, m11:Float, m21:Float, m02:Float, m12:Float, m22:Float) {
31 | this = new Mat3(m00, m01, m02, m10, m11, m12, m20, m21, m22);
32 | }
33 | }
34 | #elseif ECHO_USE_HEAPS
35 | typedef Vector2Type = h2d.col.Point;
36 | typedef Vector3Type = h3d.col.Point;
37 | typedef Matrix2Type = echo.math.Matrix2.Matrix2Default;
38 | typedef Matrix3Type = echo.math.Matrix3.Matrix3Default;
39 | #else
40 | typedef Vector2Type = echo.math.Vector2.Vector2Default;
41 | typedef Vector3Type = echo.math.Vector3.Vector3Default;
42 | typedef Matrix2Type = echo.math.Matrix2.Matrix2Default;
43 | typedef Matrix3Type = echo.math.Matrix3.Matrix3Default;
44 | #end
45 |
--------------------------------------------------------------------------------
/echo/math/Vector3.hx:
--------------------------------------------------------------------------------
1 | package echo.math;
2 |
3 | import echo.math.Types.Vector3Type;
4 |
5 | @:dox(hide)
6 | @:noCompletion
7 | class Vector3Default {
8 | public var x:Float;
9 | public var y:Float;
10 | public var z:Float;
11 |
12 | public inline function new(x:Float, y:Float, z:Float) {
13 | // the + 0.0 helps the optimizer realize it can collapse const float operations (from vector-math lib)
14 | this.x = x + 0.0;
15 | this.y = y + 0.0;
16 | this.z = z + 0.0;
17 | }
18 |
19 | public function toString():String
20 | return '{ x:$x, y:$y, z:$z }';
21 | }
22 |
23 | @:using(echo.math.Vector3)
24 | @:forward(x, y, z)
25 | abstract Vector3(Vector3Type) from Vector3Type to Vector3Type {
26 | @:from
27 | public static inline function from_arr(a:Array):Vector3
28 | return new Vector3(a[0], a[1], a[2]);
29 |
30 | @:to
31 | public inline function to_arr():Array {
32 | var self = this;
33 | return [self.x, self.y];
34 | }
35 |
36 | public inline function new(x:Float, y:Float, z:Float) @:privateAccess this = new Vector3Type(x, y, z);
37 | }
38 |
--------------------------------------------------------------------------------
/echo/shape/Circle.hx:
--------------------------------------------------------------------------------
1 | package echo.shape;
2 |
3 | import echo.data.Data;
4 | import echo.shape.*;
5 | import echo.util.AABB;
6 | import echo.util.Poolable;
7 | import echo.math.Vector2;
8 |
9 | using echo.util.SAT;
10 |
11 | class Circle extends Shape implements Poolable {
12 | /**
13 | * The radius of the Circle, transformed with `scale_x`. Use `local_radius` to get the untransformed radius.
14 | */
15 | public var radius(get, set):Float;
16 | /**
17 | * The diameter of the Circle.
18 | */
19 | public var diameter(get, set):Float;
20 | /**
21 | * The local radius of the Circle, which represents the Circle's radius with no transformations.
22 | */
23 | public var local_radius:Float;
24 | /**
25 | * Gets a Cirlce from the pool, or creates a new one if none are available. Call `put()` on the Cirlce to place it back in the pool.
26 | * @param x
27 | * @param y
28 | * @param radius
29 | * @param rotation
30 | * @return Circle
31 | */
32 | public static inline function get(x:Float = 0, y:Float = 0, radius:Float = 1, rotation:Float = 0, scale_x:Float = 1, scale_y:Float = 1):Circle {
33 | var circle = pool.get();
34 | circle.set(x, y, radius, rotation, scale_x, scale_y);
35 | circle.pooled = false;
36 | return circle;
37 | }
38 |
39 | inline function new() {
40 | super();
41 | type = CIRCLE;
42 | radius = 0;
43 | }
44 |
45 | override function put() {
46 | super.put();
47 | if (!pooled) {
48 | pooled = true;
49 | pool.put_unsafe(this);
50 | }
51 | }
52 |
53 | public inline function set(x:Float = 0, y:Float = 0, radius:Float = 1, rotation:Float = 0, scale_x:Float = 1, scale_y:Float = 1):Circle {
54 | local_x = x;
55 | local_y = y;
56 | local_rotation = rotation;
57 | local_scale_x = scale_x;
58 | local_scale_y = scale_y;
59 | this.radius = radius;
60 | return this;
61 | }
62 |
63 | public inline function load(circle:Circle):Circle return set(circle.x, circle.y, circle.radius);
64 |
65 | override inline function bounds(?aabb:AABB):AABB {
66 | var d = diameter;
67 | return aabb == null ? AABB.get(x, y, d, d) : aabb.set(x, y, d, d);
68 | }
69 |
70 | override inline function volume():Float {
71 | var r = radius;
72 | return Math.PI * r * r;
73 | }
74 |
75 | override function clone():Circle return Circle.get(local_x, local_y, radius);
76 |
77 | override function contains(v:Vector2):Bool return this.circle_contains(v);
78 |
79 | override function intersect(l:Line):Null return this.circle_intersects(l);
80 |
81 | override inline function overlaps(s:Shape):Bool {
82 | var cd = s.collides(this);
83 | if (cd != null) {
84 | cd.put();
85 | return true;
86 | }
87 | return false;
88 | }
89 |
90 | override inline function collides(s:Shape):Null return s.collide_circle(this);
91 |
92 | override inline function collide_rect(r:Rect):Null return r.rect_and_circle(this, true);
93 |
94 | override inline function collide_circle(c:Circle):Null return c.circle_and_circle(this);
95 |
96 | override inline function collide_polygon(p:Polygon):Null return this.circle_and_polygon(p, true);
97 |
98 | // getters
99 | inline function get_radius():Float return local_radius * scale_x;
100 |
101 | inline function get_diameter():Float return radius * 2;
102 |
103 | override inline function get_top():Float return y - radius;
104 |
105 | override inline function get_bottom():Float return y + radius;
106 |
107 | override inline function get_left():Float return x - radius;
108 |
109 | override inline function get_right():Float return x + radius;
110 |
111 | // setters
112 | inline function set_radius(value:Float):Float {
113 | local_radius = value / scale_x;
114 | return value;
115 | }
116 |
117 | inline function set_diameter(value:Float):Float {
118 | radius = value * 0.5;
119 | return value;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/echo/shape/Polygon.hx:
--------------------------------------------------------------------------------
1 | package echo.shape;
2 |
3 | import echo.data.Data;
4 | import echo.shape.*;
5 | import echo.util.AABB;
6 | import echo.util.Poolable;
7 |
8 | using echo.util.SAT;
9 | using echo.math.Vector2;
10 |
11 | class Polygon extends Shape implements Poolable {
12 | /**
13 | * The amount of vertices in the Polygon.
14 | */
15 | public var count(default, null):Int;
16 | /**
17 | * The Polygon's vertices adjusted for it's rotation.
18 | *
19 | * This Array represents a cache'd value, so changes to this Array will be overwritten.
20 | * Use `set_vertice()` or `set_vertices()` to edit this Polygon's vertices.
21 | */
22 | public var vertices(get, never):Array;
23 | /**
24 | * The Polygon's computed normals.
25 | *
26 | * This Array represents a cache'd value, so changes to this Array will be overwritten.
27 | * Use `set_vertice()` or `set_vertices()` to edit this Polygon's normals.
28 | */
29 | public var normals(get, never):Array;
30 |
31 | var local_vertices:Array;
32 |
33 | var _vertices:Array;
34 |
35 | var _normals:Array;
36 |
37 | var _bounds:AABB;
38 |
39 | var dirty_vertices:Bool;
40 |
41 | var dirty_bounds:Bool;
42 | /**
43 | * Gets a Polygon from the pool, or creates a new one if none are available. Call `put()` on the Polygon to place it back in the pool.
44 | * @param x
45 | * @param y
46 | * @param sides
47 | * @param radius
48 | * @param rotation
49 | * @return Polygon
50 | */
51 | public static inline function get(x:Float = 0, y:Float = 0, sides:Int = 3, radius:Float = 1, rotation:Float = 0, scale_x:Float = 1,
52 | scale_y:Float = 1):Polygon {
53 | if (sides < 3) throw 'Polygons require 3 sides as a minimum';
54 |
55 | var polygon = pool.get();
56 |
57 | var rot:Float = (Math.PI * 2) / sides;
58 | var angle:Float;
59 | var verts:Array = new Array();
60 |
61 | for (i in 0...sides) {
62 | angle = (i * rot) + ((Math.PI - rot) * 0.5);
63 | var vector:Vector2 = new Vector2(Math.cos(angle) * radius, Math.sin(angle) * radius);
64 | verts.push(vector);
65 | }
66 |
67 | polygon.set(x, y, rotation, verts, scale_x, scale_y);
68 | polygon.pooled = false;
69 | return polygon;
70 | }
71 | /**
72 | * Gets a Polygon from the pool, or creates a new one if none are available. Call `put()` on the Polygon to place it back in the pool.
73 | * @param x
74 | * @param y
75 | * @param rotation
76 | * @param vertices
77 | * @return Polygon
78 | */
79 | public static inline function get_from_vertices(x:Float = 0, y:Float = 0, rotation:Float = 0, ?vertices:Array, scale_x:Float = 1,
80 | scale_y:Float = 1):Polygon {
81 | var polygon = pool.get();
82 | polygon.set(x, y, rotation, vertices, scale_x, scale_y);
83 | polygon.pooled = false;
84 | return polygon;
85 | }
86 | /**
87 | * Gets a Polygon from the pool, or creates a new one if none are available. Call `put()` on the Polygon to place it back in the pool.
88 | * @param rect
89 | * @return Polygon return _pool.get().set_from_rect(rect)
90 | */
91 | public static inline function get_from_rect(rect:Rect):Polygon return {
92 | var polygon = pool.get();
93 | polygon.set_from_rect(rect);
94 | polygon.pooled = false;
95 | return polygon;
96 | }
97 |
98 | // TODO
99 | // public static inline function get_from_circle(c:Circle, sub_divisions:Int = 6) {}
100 |
101 | override function put() {
102 | super.put();
103 | if (!pooled) {
104 | pooled = true;
105 | pool.put_unsafe(this);
106 | }
107 | }
108 |
109 | public inline function set(x:Float = 0, y:Float = 0, rotation:Float = 0, ?vertices:Array, scale_x:Float = 1, scale_y:Float = 1):Polygon {
110 | local_x = x;
111 | local_y = y;
112 | local_rotation = rotation;
113 | local_scale_x = scale_x;
114 | local_scale_y = scale_y;
115 | set_vertices(vertices);
116 | return this;
117 | }
118 |
119 | public inline function set_from_rect(rect:Rect):Polygon {
120 | count = 4;
121 | for (i in 0...count) if (local_vertices[i] == null) local_vertices[i] = new Vector2(0, 0);
122 | local_vertices[0].set(-rect.ex, -rect.ey);
123 | local_vertices[1].set(rect.ex, -rect.ey);
124 | local_vertices[2].set(rect.ex, rect.ey);
125 | local_vertices[3].set(-rect.ex, rect.ey);
126 | local_x = rect.local_x;
127 | local_y = rect.local_y;
128 | local_rotation = rect.local_rotation;
129 | local_scale_x = rect.local_scale_x;
130 | local_scale_y = rect.local_scale_y;
131 | dirty_vertices = true;
132 | dirty_bounds = true;
133 | return this;
134 | }
135 |
136 | inline function new(?vertices:Array) {
137 | super();
138 | type = POLYGON;
139 | _vertices = [];
140 | _normals = [];
141 | _bounds = AABB.get();
142 | transform.on_dirty = on_dirty;
143 | set_vertices(vertices);
144 | }
145 |
146 | public inline function load(polygon:Polygon):Polygon return set(polygon.local_x, polygon.local_y, polygon.local_rotation, polygon.local_vertices,
147 | polygon.local_scale_x, polygon.local_scale_y);
148 |
149 | override function bounds(?aabb:AABB):AABB {
150 | if (dirty_bounds) {
151 | dirty_bounds = false;
152 |
153 | var verts = vertices;
154 |
155 | var left = verts[0].x;
156 | var top = verts[0].y;
157 | var right = verts[0].x;
158 | var bottom = verts[0].y;
159 |
160 | for (i in 1...count) {
161 | if (verts[i].x < left) left = verts[i].x;
162 | if (verts[i].y < top) top = verts[i].y;
163 | if (verts[i].x > right) right = verts[i].x;
164 | if (verts[i].y > bottom) bottom = verts[i].y;
165 | }
166 |
167 | _bounds.set_from_min_max(left, top, right, bottom);
168 | }
169 |
170 | return aabb == null ? _bounds.clone() : aabb.load(_bounds);
171 | }
172 |
173 | override inline function volume():Float {
174 | var sum = 0.;
175 | var verts = vertices;
176 | var v = verts[verts.length - 1];
177 | for (i in 0...count) {
178 | var vi = verts[i];
179 | sum += vi.x * v.y - v.x * vi.y;
180 | v = vi;
181 | }
182 | return Math.abs(sum) * 0.5;
183 | }
184 |
185 | override function clone():Polygon return Polygon.get_from_vertices(x, y, rotation, local_vertices);
186 |
187 | override function contains(v:Vector2):Bool return this.polygon_contains(v);
188 |
189 | override function intersect(l:Line):Null return this.polygon_intersects(l);
190 |
191 | override inline function overlaps(s:Shape):Bool {
192 | var cd = s.collides(this);
193 | if (cd != null) {
194 | cd.put();
195 | return true;
196 | }
197 | return false;
198 | }
199 |
200 | override inline function collides(s:Shape):Null return s.collide_polygon(this);
201 |
202 | override inline function collide_rect(r:Rect):Null return r.rect_and_polygon(this, true);
203 |
204 | override inline function collide_circle(c:Circle):Null return c.circle_and_polygon(this);
205 |
206 | override inline function collide_polygon(p:Polygon):Null return p.polygon_and_polygon(this, true);
207 |
208 | override inline function get_top():Float {
209 | if (count == 0 || vertices[0] == null) return y;
210 |
211 | var top = vertices[0].y;
212 | for (i in 1...count) if (vertices[i].y < top) top = vertices[i].y;
213 |
214 | return top;
215 | }
216 |
217 | override inline function get_bottom():Float {
218 | if (count == 0 || vertices[0] == null) return y;
219 |
220 | var bottom = vertices[0].y;
221 | for (i in 1...count) if (vertices[i].y > bottom) bottom = vertices[i].y;
222 |
223 | return bottom;
224 | }
225 |
226 | override inline function get_left():Float {
227 | if (count == 0 || vertices[0] == null) return x;
228 |
229 | var left = vertices[0].x;
230 | for (i in 1...count) if (vertices[i].x < left) left = vertices[i].x;
231 |
232 | return left;
233 | }
234 |
235 | override inline function get_right():Float {
236 | if (count == 0 || vertices[0] == null) return x;
237 |
238 | var right = vertices[0].x;
239 | for (i in 1...count) if (vertices[i].x > right) right = vertices[i].x;
240 |
241 | return right;
242 | }
243 |
244 | // todo - Skip AABB
245 | public inline function to_rect():Rect return bounds().to_rect(true);
246 | /**
247 | * Sets the vertice at the desired index.
248 | * @param index
249 | * @param x
250 | * @param y
251 | */
252 | public inline function set_vertice(index:Int, x:Float = 0, y:Float = 0):Void {
253 | if (local_vertices[index] == null) local_vertices[index] = new Vector2(x, y);
254 | else local_vertices[index].set(x, y);
255 |
256 | set_dirty();
257 | }
258 |
259 | public inline function set_vertices(?vertices:Array, ?count:Int):Void {
260 | local_vertices = vertices == null ? [] : vertices;
261 | this.count = (count != null && count >= 0) ? count : local_vertices.length;
262 | if (count > local_vertices.length) for (i in local_vertices.length...count) local_vertices[i] = new Vector2(0, 0);
263 |
264 | set_dirty();
265 | }
266 |
267 | public inline function centroid() {
268 | var ca = 0.;
269 | var cx = 0.;
270 | var cy = 0.;
271 |
272 | var verts = vertices;
273 |
274 | var v = verts[count - 1];
275 | for (i in 0...count) {
276 | var vi = verts[i];
277 | var a = v.x * vi.y - vi.x * v.y;
278 | cx += (v.x + vi.x) * a;
279 | cy += (v.y + vi.y) * a;
280 | ca += a;
281 | v = vi;
282 | }
283 |
284 | ca *= 0.5;
285 | cx *= 1 / (6 * ca);
286 | cy *= 1 / (6 * ca);
287 |
288 | return new Vector2(cx, cy);
289 | }
290 |
291 | function on_dirty(t) {
292 | set_dirty();
293 | }
294 |
295 | inline function set_dirty() {
296 | dirty_vertices = true;
297 | dirty_bounds = true;
298 | }
299 |
300 | inline function transform_vertices():Void {
301 | // clear any extra vertices
302 | while (_vertices.length > count) _vertices.pop();
303 |
304 | for (i in 0...count) {
305 | if (local_vertices[i] == null) continue;
306 | if (_vertices[i] == null) _vertices[i] = new Vector2(0, 0);
307 | var pos = transform.point_to_world(local_vertices[i].x, local_vertices[i].y);
308 | _vertices[i].set(pos.x, pos.y);
309 | }
310 | }
311 | /**
312 | * Compute face normals
313 | */
314 | inline function compute_normals():Void {
315 | for (i in 0...count) {
316 | var v = _vertices[(i + 1) % count].clone();
317 | v -= _vertices[i];
318 | v.rotate_left();
319 |
320 | // Calculate normal with 2D cross product between vector and scalar
321 | if (_normals[i] == null) _normals[i] = v.clone();
322 | else _normals[i].copy_from(v);
323 | _normals[i].normalize();
324 | }
325 | }
326 |
327 | // getters
328 |
329 | inline function get_vertices():Array {
330 | if (dirty_vertices) {
331 | dirty_vertices = false;
332 | transform_vertices();
333 | compute_normals();
334 | }
335 |
336 | return _vertices;
337 | }
338 |
339 | inline function get_normals():Array {
340 | if (dirty_vertices) {
341 | dirty_vertices = false;
342 | transform_vertices();
343 | compute_normals();
344 | }
345 |
346 | return _normals;
347 | }
348 |
349 | // setters
350 | }
351 |
--------------------------------------------------------------------------------
/echo/shape/Rect.hx:
--------------------------------------------------------------------------------
1 | package echo.shape;
2 |
3 | import echo.util.AABB;
4 | import echo.shape.*;
5 | import echo.util.Poolable;
6 | import echo.data.Data;
7 | import echo.math.Vector2;
8 |
9 | using echo.util.SAT;
10 |
11 | class Rect extends Shape implements Poolable {
12 | /**
13 | * The half-width of the Rectangle, transformed with `scale_x`. Use `local_ex` to get the untransformed extent.
14 | */
15 | public var ex(get, set):Float;
16 | /**
17 | * The half-height of the Rectangle, transformed with `scale_y`. Use `local_ey` to get the untransformed extent.
18 | */
19 | public var ey(get, set):Float;
20 | /**
21 | * The width of the Rectangle, transformed with `scale_x`. Use `local_width` to get the untransformed width.
22 | */
23 | public var width(get, set):Float;
24 | /**
25 | * The height of the Rectangle, transformed with `scale_y`. Use `local_height` to get the untransformed height.
26 | */
27 | public var height(get, set):Float;
28 | /**
29 | * The width of the Rectangle.
30 | */
31 | public var local_width(get, set):Float;
32 | /**
33 | * The height of the Rectangle.
34 | */
35 | public var local_height(get, set):Float;
36 | /**
37 | * The half-width of the Rectangle.
38 | */
39 | public var local_ex(default, set):Float;
40 | /**
41 | * The half-height of the Rectangle.
42 | */
43 | public var local_ey(default, set):Float;
44 | /**
45 | * The top-left position of the Rectangle.
46 | */
47 | public var min(get, null):Vector2;
48 | /**
49 | * The bottom-right position of the Rectangle.
50 | */
51 | public var max(get, null):Vector2;
52 | /**
53 | * If the Rectangle has a rotation, this Polygon is constructed to represent the transformed vertices of the Rectangle.
54 | */
55 | public var transformed_rect(default, null):Null;
56 | /**
57 | * Gets a Rect from the pool, or creates a new one if none are available. Call `put()` on the Rect to place it back in the pool.
58 | *
59 | * Note - The X and Y positions represent the center of the Rect. To set the Rect from its Top-Left origin, `Rect.get_from_min_max()` is available.
60 | * @param x The centered X position of the Rect.
61 | * @param y The centered Y position of the Rect.
62 | * @param width The width of the Rect.
63 | * @param height The height of the Rect.
64 | * @param rotation The rotation of the Rect.
65 | * @return Rect
66 | */
67 | public static inline function get(x:Float = 0, y:Float = 0, width:Float = 1, height:Float = 0, rotation:Float = 0, scale_x:Float = 1,
68 | scale_y:Float = 1):Rect {
69 | var rect = pool.get();
70 | rect.set(x, y, width, height, rotation, scale_x, scale_y);
71 | rect.pooled = false;
72 | return rect;
73 | }
74 | /**
75 | * Gets a Rect from the pool, or creates a new one if none are available. Call `put()` on the Rect to place it back in the pool.
76 | * @param min_x
77 | * @param min_y
78 | * @param max_x
79 | * @param max_y
80 | * @return Rect
81 | */
82 | public static inline function get_from_min_max(min_x:Float, min_y:Float, max_x:Float, max_y:Float):Rect {
83 | var rect = pool.get();
84 | rect.set_from_min_max(min_x, min_y, max_x, max_y);
85 | rect.pooled = false;
86 | return rect;
87 | }
88 |
89 | inline function new() {
90 | super();
91 | local_ex = 0;
92 | local_ey = 0;
93 | type = RECT;
94 | transform.on_dirty = on_dirty;
95 | }
96 |
97 | override function put() {
98 | super.put();
99 | if (transformed_rect != null) {
100 | transformed_rect.put();
101 | transformed_rect = null;
102 | }
103 | if (!pooled) {
104 | pooled = true;
105 | pool.put_unsafe(this);
106 | }
107 | }
108 |
109 | public inline function set(x:Float = 0, y:Float = 0, width:Float = 1, height:Float = 0, rotation:Float = 0, scale_x:Float = 1, scale_y:Float = 1):Rect {
110 | local_x = x;
111 | local_y = y;
112 | local_width = width;
113 | local_height = height <= 0 ? width : height;
114 | local_rotation = rotation;
115 | local_scale_x = scale_x;
116 | local_scale_y = scale_y;
117 | set_dirty();
118 | return this;
119 | }
120 |
121 | public inline function set_from_min_max(min_x:Float, min_y:Float, max_x:Float, max_y:Float):Rect {
122 | return set((min_x + max_x) * 0.5, (min_y + max_y) * 0.5, max_x - min_x, max_y - min_y);
123 | }
124 |
125 | public inline function load(rect:Rect):Rect {
126 | local_x = rect.local_x;
127 | local_y = rect.local_y;
128 | local_ex = rect.local_ex;
129 | local_ey = rect.local_ey;
130 | local_rotation = rect.local_rotation;
131 | local_scale_x = rect.local_scale_x;
132 | local_scale_y = rect.local_scale_y;
133 | set_dirty();
134 | return this;
135 | }
136 |
137 | public function to_aabb(put_self:Bool = false):AABB {
138 | if (put_self) {
139 | var aabb = bounds();
140 | put();
141 | return aabb;
142 | }
143 | return bounds();
144 | }
145 |
146 | public function to_polygon(put_self:Bool = false):Polygon {
147 | if (put_self) {
148 | var polygon = Polygon.get_from_rect(this);
149 | put();
150 | return polygon;
151 | }
152 | return Polygon.get_from_rect(this);
153 | }
154 |
155 | override inline function bounds(?aabb:AABB):AABB {
156 | if (transformed_rect != null && rotation != 0) return transformed_rect.bounds(aabb);
157 | return (aabb == null) ? AABB.get(x, y, width, height) : aabb.set(x, y, width, height);
158 | }
159 |
160 | override inline function volume() return width * height;
161 |
162 | override inline function clone():Rect return Rect.get(local_x, local_y, width, height, local_rotation);
163 |
164 | override inline function contains(p:Vector2):Bool return this.rect_contains(p);
165 |
166 | override inline function intersect(l:Line):Null return this.rect_intersects(l);
167 |
168 | override inline function overlaps(s:Shape):Bool {
169 | var cd = transformed_rect == null ? s.collides(this) : transformed_rect.collides(this);
170 | if (cd != null) {
171 | cd.put();
172 | return true;
173 | }
174 | return false;
175 | }
176 |
177 | override inline function collides(s:Shape):Null return s.collide_rect(this);
178 |
179 | override inline function collide_rect(r:Rect):Null return r.rect_and_rect(this);
180 |
181 | override inline function collide_circle(c:Circle):Null return this.rect_and_circle(c);
182 |
183 | override inline function collide_polygon(p:Polygon):Null return this.rect_and_polygon(p);
184 |
185 | override function set_parent(?body:Body) {
186 | super.set_parent(body);
187 | set_dirty();
188 | if (transformed_rect != null) transformed_rect.set_parent(body);
189 | }
190 |
191 | function on_dirty(t) {
192 | set_dirty();
193 | }
194 |
195 | inline function set_dirty() {
196 | if (transformed_rect == null && rotation != 0) {
197 | transformed_rect = Polygon.get_from_rect(this);
198 | transformed_rect.set_parent(parent);
199 | }
200 | else if (transformed_rect != null) {
201 | transformed_rect.local_x = local_x;
202 | transformed_rect.local_y = local_y;
203 | transformed_rect.local_rotation = local_rotation;
204 | transformed_rect.local_scale_x = local_scale_x;
205 | transformed_rect.local_scale_y = local_scale_y;
206 | }
207 | }
208 |
209 | // getters
210 |
211 | inline function get_width():Float return local_width * scale_x;
212 |
213 | inline function get_height():Float return local_height * scale_y;
214 |
215 | inline function get_ex():Float return local_ex * local_scale_x;
216 |
217 | inline function get_ey():Float return local_ey * local_scale_y;
218 |
219 | inline function get_local_width():Float return local_ex * 2;
220 |
221 | inline function get_local_height():Float return local_ey * 2;
222 |
223 | function get_min():Vector2 return new Vector2(left, top);
224 |
225 | function get_max():Vector2 return new Vector2(bottom, right);
226 |
227 | override inline function get_top():Float {
228 | if (transformed_rect == null || rotation == 0) return y - ey;
229 | return transformed_rect.top;
230 | }
231 |
232 | override inline function get_bottom():Float {
233 | if (transformed_rect == null || rotation == 0) return y + ey;
234 | return transformed_rect.bottom;
235 | }
236 |
237 | override inline function get_left():Float {
238 | if (transformed_rect == null || rotation == 0) return x - ex;
239 | return transformed_rect.left;
240 | }
241 |
242 | override inline function get_right():Float {
243 | if (transformed_rect == null || rotation == 0) return x + ex;
244 | return transformed_rect.right;
245 | }
246 |
247 | // setters
248 | inline function set_ex(value:Float):Float {
249 | local_ex = value / scale_x;
250 | return value;
251 | }
252 |
253 | inline function set_ey(value:Float):Float {
254 | local_ey = value / scale_y;
255 | return value;
256 | }
257 |
258 | inline function set_width(value:Float):Float {
259 | local_width = value / scale_x;
260 | return value;
261 | }
262 |
263 | inline function set_height(value:Float):Float {
264 | local_height = value / scale_y;
265 | return value;
266 | }
267 |
268 | inline function set_local_width(value:Float):Float return ex = value * 0.5;
269 |
270 | inline function set_local_height(value:Float):Float return ey = value * 0.5;
271 |
272 | inline function set_local_ex(value:Float):Float {
273 | local_ex = value;
274 | if (transformed_rect != null) transformed_rect.set_from_rect(this);
275 | return local_ex;
276 | }
277 |
278 | inline function set_local_ey(value:Float):Float {
279 | local_ey = value;
280 | if (transformed_rect != null) transformed_rect.set_from_rect(this);
281 | return local_ey;
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/echo/util/AABB.hx:
--------------------------------------------------------------------------------
1 | package echo.util;
2 |
3 | import echo.shape.Rect;
4 | import echo.util.Poolable;
5 | import echo.math.Vector2;
6 |
7 | class AABB implements Poolable {
8 | public var min_x:Float;
9 | public var max_x:Float;
10 | public var min_y:Float;
11 | public var max_y:Float;
12 |
13 | public var width(get, never):Float;
14 | public var height(get, never):Float;
15 | /**
16 | * Gets an AABB from the pool, or creates a new one if none are available. Call `put()` on the AABB to place it back in the pool.
17 | *
18 | * Note - The X and Y positions represent the center of the AABB. To set the AABB from its Top-Left origin, `AABB.get_from_min_max()` is available.
19 | * @param x The centered X position of the AABB.
20 | * @param y The centered Y position of the AABB.
21 | * @param width The width of the AABB.
22 | * @param height The height of the AABB.
23 | * @return AABB
24 | */
25 | public static inline function get(x:Float = 0, y:Float = 0, width:Float = 1, height:Float = 1):AABB {
26 | var aabb = pool.get();
27 | aabb.set(x, y, width, height);
28 | aabb.pooled = false;
29 | return aabb;
30 | }
31 | /**
32 | * Gets an AABB from the pool, or creates a new one if none are available. Call `put()` on the AABB to place it back in the pool.
33 | * @param min_x
34 | * @param min_y
35 | * @param max_x
36 | * @param max_y
37 | * @return AABB
38 | */
39 | public static inline function get_from_min_max(min_x:Float, min_y:Float, max_x:Float, max_y:Float):AABB {
40 | var aabb = pool.get();
41 | aabb.set_from_min_max(min_x, min_y, max_x, max_y);
42 | aabb.pooled = false;
43 | return aabb;
44 | }
45 |
46 | inline function new() {
47 | min_x = 0;
48 | max_x = 1;
49 | min_y = 0;
50 | max_y = 1;
51 | }
52 | /**
53 | * Sets the values on this AABB.
54 | *
55 | * Note - The X and Y positions represent the center of the AABB. To set the AABB from its Top-Left origin, `AABB.set_from_min_max()` is available.
56 | * @param x The centered X position of the AABB.
57 | * @param y The centered Y position of the AABB.
58 | * @param width The width of the AABB.
59 | * @param height The height of the AABB.
60 | * @return AABB
61 | */
62 | public inline function set(x:Float = 0, y:Float = 0, width:Float = 1, height:Float = 1) {
63 | width *= 0.5;
64 | height *= 0.5;
65 | this.min_x = x - width;
66 | this.min_y = y - height;
67 | this.max_x = x + width;
68 | this.max_y = y + height;
69 | return this;
70 | }
71 |
72 | public inline function set_from_min_max(min_x:Float, min_y:Float, max_x:Float, max_y:Float) {
73 | this.min_x = min_x;
74 | this.max_x = max_x;
75 | this.min_y = min_y;
76 | this.max_y = max_y;
77 | return this;
78 | }
79 |
80 | public inline function to_rect(put_self:Bool = false):Rect {
81 | if (put_self) put();
82 | return Rect.get_from_min_max(min_x, min_y, max_x, max_y);
83 | }
84 |
85 | public inline function overlaps(other:AABB):Bool {
86 | return this.min_x < other.max_x && this.max_x >= other.min_x && this.min_y < other.max_y && this.max_y >= other.min_y;
87 | }
88 |
89 | public inline function contains(point:Vector2):Bool {
90 | return min_x <= point.x && max_x >= point.x && min_y <= point.y && max_y >= point.y;
91 | }
92 |
93 | public inline function load(aabb:AABB):AABB {
94 | this.min_x = aabb.min_x;
95 | this.max_x = aabb.max_x;
96 | this.min_y = aabb.min_y;
97 | this.max_y = aabb.max_y;
98 | return this;
99 | }
100 | /**
101 | * Adds the bounds of an AABB into this AABB.
102 | * @param aabb
103 | */
104 | public inline function add(aabb:AABB) {
105 | if (min_x > aabb.min_x) min_x = aabb.min_x;
106 | if (min_y > aabb.min_y) min_y = aabb.min_y;
107 | if (max_x < aabb.max_x) max_x = aabb.max_x;
108 | if (max_y < aabb.max_y) max_y = aabb.max_y;
109 | }
110 |
111 | public inline function clone() {
112 | return AABB.get_from_min_max(min_x, min_y, max_x, max_y);
113 | }
114 |
115 | public function put() {
116 | if (!pooled) {
117 | pooled = true;
118 | pool.put_unsafe(this);
119 | }
120 | }
121 |
122 | function toString() return 'AABB: {min_x: $min_x, min_y: $min_y, max_x: $max_x, max_y: $max_y}';
123 |
124 | // getters
125 |
126 | inline function get_width():Float return max_x - min_x;
127 |
128 | inline function get_height():Float return max_y - min_y;
129 | }
130 |
--------------------------------------------------------------------------------
/echo/util/BitMask.hx:
--------------------------------------------------------------------------------
1 | package echo.util;
2 |
3 | abstract BitMask(Int) to Int {
4 |
5 | @:from
6 | public static function from_int(i:Int):BitMask {
7 | return new BitMask(1 << i);
8 | }
9 |
10 | public function new(value:Int = 0) this = value;
11 |
12 | public inline function remove(mask:BitMask):Int {
13 | return this = this & ~mask;
14 | }
15 |
16 | public inline function add(mask:BitMask):Int {
17 | return this = this | mask;
18 | }
19 |
20 | public inline function contains(mask:BitMask):Bool {
21 | return this & mask != 0;
22 | }
23 |
24 | public inline function clear() {
25 | this = 0;
26 | }
27 |
28 | public inline function is_empty():Bool {
29 | return this == 0;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/echo/util/BodyOrBodies.hx:
--------------------------------------------------------------------------------
1 | package echo.util;
2 |
3 | import haxe.ds.Either;
4 | /**
5 | * Abstract representing a `Body` or and Array of Bodies.
6 | */
7 | abstract BodyOrBodies(Either>) from Either> to Either> {
8 | @:from inline static function from_body(a:Body):BodyOrBodies {
9 | return Left(a);
10 | }
11 |
12 | @:from inline static function from_bodies(b:Array):BodyOrBodies {
13 | return Right(b);
14 | }
15 |
16 | @:to inline function to_body():Null return switch (this) {
17 | case Left(a): a;
18 | default: null;
19 | }
20 |
21 | @:to inline function to_bodies():Null> return switch (this) {
22 | case Right(b): b;
23 | default: null;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/echo/util/Disposable.hx:
--------------------------------------------------------------------------------
1 | package echo.util;
2 |
3 | @:using(echo.util.Disposable)
4 | interface Disposable {
5 | function dispose():Void;
6 | }
7 | /**
8 | * Checks if an object is not null before calling dispose(), always returns null.
9 | *
10 | * @param object An IDisposable object that will be disposed if it's not null.
11 | * @return null
12 | */
13 | inline function dispose(object:Null):T {
14 | if (object != null) {
15 | object.dispose();
16 | }
17 | return null;
18 | }
19 | /**
20 | * dispose every element of an array of IDisposables
21 | *
22 | * @param array An Array of IDisposable objects
23 | * @return null
24 | */
25 | inline function dispose_array(array:Array):Array {
26 | if (array != null) {
27 | for (e in array) dispose(e);
28 | array.splice(0, array.length);
29 | }
30 | return null;
31 | }
32 |
--------------------------------------------------------------------------------
/echo/util/History.hx:
--------------------------------------------------------------------------------
1 | package echo.util;
2 |
3 | /**
4 | * History implementation from: https://code.haxe.org/category/data-structures/ring-array.html
5 | */
6 | @:generic class History {
7 | var re:Ring;
8 | var un:Ring;
9 |
10 | public function new(len) {
11 | re = new Ring(len);
12 | un = new Ring(len);
13 | }
14 |
15 | public function redo():Null {
16 | var r = re.pop();
17 | if (r != null) un.push(r);
18 | return r;
19 | }
20 |
21 | public function undo():Null {
22 | var u = un.pop();
23 | if (u != null) re.push(u);
24 | return u;
25 | }
26 |
27 | public function add(v:T) {
28 | un.push(v);
29 | re.reset();
30 | }
31 | }
32 | /**
33 | * Fixed Ring Array Data Structure from: https://code.haxe.org/category/data-structures/ring-array.html
34 | */
35 | @:generic class Ring {
36 | public var cap(get, never):Int;
37 |
38 | inline function get_cap() return a.length;
39 |
40 | public var len(get, never):Int;
41 |
42 | inline function get_len() return i + left - start;
43 |
44 | var i:Int;
45 | var start:Int;
46 | var left:Int;
47 | var a:haxe.ds.Vector;
48 |
49 | public function new(len) {
50 | a = new haxe.ds.Vector(len);
51 | reset();
52 | }
53 |
54 | public function pop():Null {
55 | if (len <= 0) return null;
56 | if (i == 0) {
57 | i = cap;
58 | left = 0;
59 | }
60 | return a[--i];
61 | }
62 |
63 | public function shift():Null {
64 | if (len <= 0) return null;
65 | if (start == cap) {
66 | start = 0;
67 | left = 0;
68 | }
69 | return a[start++];
70 | }
71 |
72 | public function push(v:T) {
73 | if (i == cap) {
74 | if (left > 0 && start == i) start = 0;
75 | i = 0;
76 | left = cap;
77 | }
78 | if (len == cap) start++;
79 | a[i++] = v;
80 | }
81 |
82 | public function reset() {
83 | i = 0;
84 | start = 0;
85 | left = 0;
86 | }
87 |
88 | public function remove(v:T) {
89 | var cap = this.cap;
90 | var max = this.len;
91 | var j = 0, p = 0;
92 | while (j < max) {
93 | p = (j + start) % cap;
94 | if (v == a[p]) {
95 | if (p == start) {
96 | ++start;
97 | }
98 | else {
99 | if (this.i == 0) {
100 | this.i = cap;
101 | this.left = 0;
102 | }
103 | --max;
104 | while (j < max) {
105 | a[(j + start) % cap] = a[(j + start + 1) % cap];
106 | ++j;
107 | }
108 | --this.i;
109 | }
110 | break;
111 | }
112 | ++j;
113 | }
114 | }
115 |
116 | public inline function toString() {
117 | return '[i: $i, start: $start, len: $len, left: $left]';
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/echo/util/JSON.hx:
--------------------------------------------------------------------------------
1 | package echo.util;
2 |
3 | /**
4 | * Class to provide different Utilities for dealing with Object Data
5 | */
6 | class JSON {
7 | /**
8 | * Copy an object's fields into target object. Overwrites the target object's fields.
9 | * Can work with Static Classes as well (as destination)
10 | *
11 | * Adapted from the DJFlixel Library: https://github.com/johndimi/djFlixel
12 | *
13 | * @param from The Master object to copy fields from
14 | * @param into The Target object to copy fields to
15 | * @return The resulting object
16 | */
17 | public static function copy_fields(from:T, into:T):T {
18 | if (from == null) return into;
19 | if (into == null) into = Reflect.copy(from);
20 | else for (f in Reflect.fields(from)) Reflect.setField(into, f, Reflect.field(from, f));
21 |
22 | return into;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/echo/util/Poolable.hx:
--------------------------------------------------------------------------------
1 | package echo.util;
2 |
3 | #if macro
4 | import haxe.macro.Context;
5 | import haxe.macro.Expr;
6 |
7 | using haxe.macro.TypeTools;
8 | using haxe.macro.ComplexTypeTools;
9 | using Lambda;
10 | #end
11 | /**
12 | * Implementing this interface on a Class will run `PoolableMacros.build`, then remove itself.
13 | **/
14 | @:autoBuild(echo.util.PoolableMacros.build())
15 | interface Poolable {
16 | function put():Void;
17 | }
18 |
19 | @:deprecated('`IPooled` renamed to `IPoolable.')
20 | typedef IPooled = Poolable;
21 |
22 | interface Pool {
23 | function pre_allocate(amount:Int):Void;
24 | function clear():Array;
25 | }
26 | /**
27 | * Generic Pooling container
28 | */
29 | @:generic
30 | class GenericPool implements Pool {
31 | public var length(get, null):Int;
32 |
33 | var members:Array;
34 | var type:Class;
35 | var count:Int;
36 |
37 | public function new(type:Class) {
38 | this.type = type;
39 | members = [];
40 | count = 0;
41 | }
42 |
43 | public function get():T {
44 | if (count == 0) return Type.createInstance(type, []);
45 | return members[--count];
46 | }
47 |
48 | public function put(obj:T):Void {
49 | if (obj != null) {
50 | var i:Int = members.indexOf(obj);
51 | // if the object's spot in the pool was overwritten, or if it's at or past count (in the inaccessible zone)
52 | if (i == -1 || i >= count) {
53 | members[count++] = obj;
54 | }
55 | }
56 | }
57 |
58 | public function put_unsafe(obj:T):Void {
59 | if (obj != null) {
60 | members[count++] = obj;
61 | }
62 | }
63 |
64 | public function pre_allocate(amount:Int):Void {
65 | while (amount-- > 0) members[count++] = Type.createInstance(type, []);
66 | }
67 |
68 | public function clear():Array {
69 | count = 0;
70 | var old_pool = members;
71 | members = [];
72 | return old_pool;
73 | }
74 |
75 | public function get_length() return count;
76 | }
77 |
78 | class PoolableMacros {
79 | #if macro
80 | public static function build():Array {
81 | var pos = Context.currentPos();
82 | var t = Context.getLocalType();
83 | var ct = Context.toComplexType(t);
84 | var cl = t.getClass();
85 |
86 | var append:Array = [
87 | {
88 | name: 'pool',
89 | kind: FVar(macro:echo.util.Poolable.GenericPool<$ct>, macro new echo.util.Poolable.GenericPool<$ct>($p{cl.pack.concat([cl.name])})),
90 | pos: pos,
91 | access: [AStatic, APrivate]
92 | },
93 | {
94 | name: 'get_pool',
95 | kind: FFun({
96 | args: [],
97 | expr: macro return pool,
98 | ret: macro:echo.util.Poolable.Pool<$ct>
99 | }),
100 | pos: pos,
101 | access: [AStatic, APublic]
102 | }
103 | ];
104 |
105 | // Only add this on Classes that directly implemented `Poolable` (and not any children of those classes)
106 | if (Context.getLocalClass().get().interfaces.exists(i -> i.t.get().name == 'Poolable')) append.push({
107 | name: 'pooled',
108 | kind: FProp('default', 'null', macro:Bool, macro false),
109 | pos: pos,
110 | access: [APublic]
111 | });
112 |
113 | return Context.getBuildFields().concat(append);
114 | }
115 | #end
116 | }
117 |
--------------------------------------------------------------------------------
/echo/util/Proxy.hx:
--------------------------------------------------------------------------------
1 | package echo.util;
2 |
3 | #if macro
4 | import haxe.macro.Context;
5 | import haxe.macro.Expr;
6 |
7 | using Lambda;
8 | #end
9 | /**
10 | * Implementing this interface on a Class will run `ProxyMacros.build`, then remove itself.
11 | **/
12 | @:remove
13 | @:autoBuild(echo.util.ProxyMacros.build())
14 | interface Proxy {}
15 |
16 | @:deprecated("`IProxy` renamed to `Proxy`")
17 | typedef IProxy = Proxy;
18 |
19 | class ProxyMacros {
20 | #if macro
21 | /**
22 | * Generates Getters and Setters for all Fields that are marked as such:
23 | * ```
24 | * var example(get, set):Bool;
25 | * ```
26 | *
27 | * If a field has the `@:alias` metadata, it will generate Getters and Setters that get/set the value that is passed into the metadata:
28 | * ```
29 | * @:alias(position.x)
30 | * var x(get, set):Float;
31 | * ```
32 | *
33 | * @return Array
34 | */
35 | public static function build():Array {
36 | var fields = Context.getBuildFields();
37 | var append = [];
38 |
39 | for (field in fields) {
40 | var alias:Null;
41 | if (field.meta != null) {
42 | for (meta in field.meta) {
43 | switch (meta.name) {
44 | case ":alias":
45 | if (meta.params.length > 0) alias = meta.params[0];
46 | else throw "Variables with the `@:alias` metadata need a property as the parameter";
47 | case ":forward":
48 | if (meta.params.length > 0) throw "Variables with the `@:forward` metadata cannot have any parameters";
49 | }
50 | }
51 | }
52 | switch (field.kind) {
53 | case FVar(t, e):
54 | if (alias != null) {
55 | field.kind = FProp('get', 'set', t, e);
56 | if (!fields.exists((f) -> return f.name == 'get_${field.name}')) append.push(getter(field.name, alias));
57 | if (!fields.exists((f) -> return f.name == 'set_${field.name}')) append.push(setter(field.name, alias));
58 | }
59 | case FProp(pget, pset, _, _):
60 | if (pget == 'get' && !fields.exists((f) -> return f.name == 'get_${field.name}')) {
61 | append.push(getter(field.name, alias));
62 | }
63 | if (pset == 'set' && !fields.exists((f) -> return f.name == 'set_${field.name}')) {
64 | append.push(setter(field.name, alias));
65 | }
66 | // Add isVar metadata if needed
67 | if (pget == 'get' && pset == 'set') {
68 | if (field.meta != null
69 | && !field.meta.exists((m) -> return m.name == ':isVar')) field.meta.push({name: ':isVar', pos: Context.currentPos()});
70 | else if (field.meta == null) field.meta = [{name: ':isVar', pos: Context.currentPos()}];
71 | }
72 | default:
73 | }
74 | }
75 |
76 | return fields.concat(append);
77 | }
78 | /**
79 | * TODO - generate a `ClassOptions` structure from a `Class`, containing all public fields of the Class. Maybe add `load_options` method to `Class` that sets all the fields from options?
80 | * @return Array
81 | */
82 | public static function options():Array {
83 | var local_class = Context.getLocalClass().get();
84 | var fields = Context.getBuildFields();
85 | var defaults = fields.filter((f) -> {
86 | for (m in f.meta) if (m.name == ':defaultOp') return true;
87 | return false;
88 | });
89 |
90 | Context.defineType({
91 | pos: Context.currentPos(),
92 | name: '${local_class.name}Options',
93 | fields: defaults,
94 | pack: local_class.pack,
95 | kind: TDStructure
96 | });
97 |
98 | if (defaults.length == 0) return fields;
99 |
100 | // fields.push({
101 | // name: 'defaults',
102 | // kind:
103 | // });
104 |
105 | for (f in fields) {}
106 |
107 | return fields;
108 | }
109 | /**
110 | * Generates a Getter function for a value
111 | * @param name name of the var (`x` will return `get_x`)
112 | * @param alias optional field that the getter will get instead
113 | */
114 | static function getter(name:String, ?alias:Expr):Field return {
115 | name: 'get_${name}',
116 | kind: FieldType.FFun({
117 | args: [],
118 | expr: alias != null ? macro return ${alias} : macro return $i{name},
119 | ret: null
120 | }),
121 | pos: Context.currentPos()
122 | }
123 | /**
124 | * Generates a Setter function for a value
125 | * @param name name of the var (`x` will return `set_x`)
126 | * @param alias optional field that the setter will set instead
127 | */
128 | static function setter(name:String, ?alias:Expr):Field return {
129 | name: 'set_${name}',
130 | kind: FieldType.FFun({
131 | args: [{name: 'value', type: null}],
132 | expr: alias != null ? macro return ${alias} = value : macro return $i{name} = value,
133 | ret: null
134 | }),
135 | pos: Context.currentPos()
136 | }
137 | #end
138 | }
139 |
--------------------------------------------------------------------------------
/echo/util/QuadTree.hx:
--------------------------------------------------------------------------------
1 | package echo.util;
2 |
3 | import haxe.ds.Vector;
4 | import echo.data.Data;
5 | import echo.util.Poolable;
6 | /**
7 | * Simple QuadTree implementation to assist with broad-phase 2D collisions.
8 | */
9 | class QuadTree extends AABB {
10 | /**
11 | * The maximum branch depth for this QuadTree collection. Once the max depth is reached, the QuadTrees at the end of the collection will not spilt.
12 | */
13 | public var max_depth(default, set):Int = 5;
14 | /**
15 | * The maximum amount of `QuadTreeData` contents that a QuadTree `leaf` can hold before becoming a branch and splitting it's contents between children Quadtrees.
16 | */
17 | public var max_contents(default, set):Int = 10;
18 | /**
19 | * The child QuadTrees contained in the Quadtree. If this Vector is empty, the Quadtree is regarded as a `leaf`.
20 | */
21 | public var children:Vector;
22 | /**
23 | * The QuadTreeData contained in the Quadtree. If the Quadtree is not a `leaf`, all of it's contents will be dispersed to it's children QuadTrees (leaving this aryar emyty).
24 | */
25 | public var contents:Array;
26 | /**
27 | * Gets the total amount of `QuadTreeData` contents in the Quadtree, recursively. To get the non-recursive amount, check `quadtree.contents_count`.
28 | */
29 | public var count(get, null):Int;
30 |
31 | public var contents_count:Int;
32 | /**
33 | * A QuadTree is regarded as a `leaf` if it has **no** QuadTree children (ie `quadtree.children.length == 0`).
34 | */
35 | public var leaf(get, never):Bool;
36 | /**
37 | * The QuadTree's branch position in it's collection.
38 | */
39 | public var depth:Int;
40 | /**
41 | * Cache'd list of QuadTrees used to help with memory management.
42 | */
43 | var nodes_list:Array = [];
44 |
45 | function new(?aabb:AABB, depth:Int = 0) {
46 | super();
47 | if (aabb != null) load(aabb);
48 | this.depth = depth;
49 | children = new Vector(4);
50 | contents = [];
51 | contents_count = 0;
52 | }
53 | /**
54 | * Gets an Quadtree from the pool, or creates a new one if none are available. Call `put()` on the Quadtree to place it back in the pool.
55 | *
56 | * Note - The X and Y positions represent the center of the Quadtree. To set the Quadtree from its Top-Left origin, `Quadtree.get_from_min_max()` is available.
57 | * @param x The centered X position of the Quadtree.
58 | * @param y The centered Y position of the Quadtree.
59 | * @param width The width of the Quadtree.
60 | * @param height The height of the Quadtree.
61 | * @return Quadtree
62 | */
63 | public static inline function get(x:Float = 0, y:Float = 0, width:Float = 1, height:Float = 1):QuadTree {
64 | var qt = pool.get();
65 | qt.set(x, y, width, height);
66 | qt.clear();
67 | qt.pooled = false;
68 | return qt;
69 | }
70 | /**
71 | * Gets an Quadtree from the pool, or creates a new one if none are available. Call `put()` on the Quadtree to place it back in the pool.
72 | * @param min_x
73 | * @param min_y
74 | * @param max_x
75 | * @param max_y
76 | * @return Quadtree
77 | */
78 | public static inline function get_from_min_max(min_x:Float, min_y:Float, max_x:Float, max_y:Float):QuadTree {
79 | var qt = pool.get();
80 | qt.set_from_min_max(min_x, min_y, max_x, max_y);
81 | qt.clear();
82 | qt.pooled = false;
83 | return qt;
84 | }
85 | /**
86 | * Puts the QuadTree back in the pool of available QuadTrees.
87 | */
88 | override inline function put() {
89 | if (!pooled) {
90 | pooled = true;
91 | clear();
92 | nodes_list.resize(0);
93 | pool.put_unsafe(this);
94 | }
95 | }
96 | /**
97 | * Attempts to insert the `QuadTreeData` into the QuadTree. If the `QuadTreeData` already exists in the QuadTree, use `quadtree.update(data)` instead.
98 | */
99 | public function insert(data:QuadTreeData) {
100 | if (data.bounds == null) return;
101 | // If the new data does not intersect this node, stop.
102 | if (!data.bounds.overlaps(this)) return;
103 | // If the node is a leaf and contains more than the maximum allowed, split it.
104 | if (leaf && contents_count + 1 > max_contents) split();
105 | // If the node is still a leaf, push the data to it.
106 | // Else try to insert the data into the node's children
107 | if (leaf) {
108 | var index = get_first_null(contents);
109 | if (index == -1) contents.push(data);
110 | else contents[index] = data;
111 | contents_count++;
112 | }
113 | else for (child in children) child.insert(data);
114 | }
115 | /**
116 | * Attempts to remove the `QuadTreeData` from the QuadTree.
117 | */
118 | public function remove(data:QuadTreeData, allow_shake:Bool = true):Bool {
119 | if (leaf) {
120 | var i = 0;
121 | while (i < contents.length) {
122 | if (contents[i] != null && data != null && contents[i].id == data.id) {
123 | contents[i] = null;
124 | contents_count--;
125 | return true;
126 | }
127 | i++;
128 | }
129 | return false;
130 | // return contents.remove(data);
131 | }
132 |
133 | var removed = false;
134 | for (child in children) if (child != null && child.remove(data)) removed = true;
135 | if (allow_shake && removed) shake();
136 |
137 | return removed;
138 | }
139 | /**
140 | * Updates the `QuadTreeData` in the QuadTree by first removing the `QuadTreeData` from the QuadTree, then inserting it.
141 | * @param data
142 | */
143 | public function update(data:QuadTreeData, allow_shake:Bool = true) {
144 | remove(data, allow_shake);
145 | insert(data);
146 | }
147 | /**
148 | * Queries the QuadTree for any `QuadTreeData` that overlaps the `AABB`.
149 | * @param aabb The `AABB` to query.
150 | * @param result An Array containing all `QuadTreeData` that collides with the shape.
151 | */
152 | public function query(aabb:AABB, result:Array) {
153 | if (!overlaps(aabb)) {
154 | return;
155 | }
156 | if (leaf) {
157 | for (data in contents) if (data != null && data.bounds.overlaps(aabb)) result.push(data);
158 | }
159 | else {
160 | for (child in children) child.query(aabb, result);
161 | }
162 | }
163 | /**
164 | * If the QuadTree is a branch (_not_ a `leaf`), this will check if the amount of data from all the child Quadtrees can fit in the Quadtree without exceeding it's `max_contents`.
165 | * If all the data can fit, the Quadtree branch will "shake" its child Quadtrees, absorbing all the data and clearing the children (putting all the child Quadtrees back in the pool).
166 | *
167 | * Note - This works recursively.
168 | */
169 | public function shake():Bool {
170 | if (leaf) return false;
171 | var len = count;
172 | if (len == 0) {
173 | clear_children();
174 | }
175 | else if (len < max_contents) {
176 | nodes_list.resize(0);
177 | nodes_list.push(this);
178 | while (nodes_list.length > 0) {
179 | var node = nodes_list.shift();
180 | if (node != this && node.leaf) {
181 | for (data in node.contents) {
182 | if (contents.indexOf(data) == -1) {
183 | var index = get_first_null(contents);
184 | if (index == -1) contents.push(data);
185 | else contents[index] = data;
186 | contents_count++;
187 | }
188 | }
189 | }
190 | else for (child in node.children) nodes_list.push(child);
191 | }
192 | clear_children();
193 | return true;
194 | }
195 | return false;
196 | }
197 | /**
198 | * Splits the Quadtree into 4 Quadtree children, and disperses it's `QuadTreeData` contents into them.
199 | */
200 | function split() {
201 | if (depth + 1 >= max_depth) return;
202 |
203 | var xw = width * 0.5;
204 | var xh = height * 0.5;
205 |
206 | for (i in 0...4) {
207 | var child = get();
208 | switch (i) {
209 | case 0:
210 | child.set_from_min_max(min_x, min_y, min_x + xw, min_y + xh);
211 | case 1:
212 | child.set_from_min_max(min_x + xw, min_y, max_x, min_y + xh);
213 | case 2:
214 | child.set_from_min_max(min_x, min_y + xh, min_x + xw, max_y);
215 | case 3:
216 | child.set_from_min_max(min_x + xw, min_y + xh, max_x, max_y);
217 | }
218 | child.depth = depth + 1;
219 | child.max_depth = max_depth;
220 | child.max_contents = max_contents;
221 | for (j in 0...contents.length) if (contents[j] != null) child.insert(contents[j]);
222 | children[i] = child;
223 | }
224 |
225 | clear_contents();
226 | }
227 | /**
228 | * Clears the Quadtree's `QuadTreeData` contents and all children Quadtrees.
229 | */
230 | public inline function clear() {
231 | clear_children();
232 | clear_contents();
233 | }
234 | /**
235 | * Puts all of the Quadtree's children back in the pool and clears the `children` Array.
236 | */
237 | inline function clear_children() {
238 | for (i in 0...children.length) {
239 | if (children[i] != null) {
240 | children[i].clear_children();
241 | children[i].put();
242 | children[i] = null;
243 | }
244 | }
245 | }
246 |
247 | inline function clear_contents() {
248 | contents.resize(0);
249 | contents_count = 0;
250 | }
251 | /**
252 | * Resets the `flag` value of the QuadTree's `QuadTreeData` contents.
253 | */
254 | function reset_data_flags() {
255 | for (i in 0...contents.length) if (contents[i] != null) contents[i].flag = false;
256 | for (i in 0...children.length) if (children[i] != null) children[i].reset_data_flags();
257 | }
258 |
259 | // getters
260 |
261 | function get_count() {
262 | reset_data_flags();
263 | // Initialize the count with this node's content's length
264 | var num = 0;
265 | for (i in 0...contents.length) {
266 | if (contents[i] != null) {
267 | contents[i].flag = true;
268 | num += 1;
269 | }
270 | }
271 |
272 | // Create a list of nodes to process and push the current tree to it.
273 | nodes_list.resize(0);
274 | nodes_list.push(this);
275 |
276 | // Process the nodes.
277 | // While there still nodes to process, grab the first node in the list.
278 | // If the node is a leaf, add all its contents to the count.
279 | // Else push this node's children to the end of the node list.
280 | // Finally, remove the node from the list.
281 | while (nodes_list.length > 0) {
282 | var node = nodes_list.shift();
283 | if (node.leaf) {
284 | for (i in 0...node.contents.length) {
285 | if (node.contents[i] != null && !node.contents[i].flag) {
286 | num += 1;
287 | node.contents[i].flag = true;
288 | }
289 | }
290 | }
291 | else for (i in 0...node.children.length) nodes_list.push(node.children[i]);
292 | }
293 | return num;
294 | }
295 |
296 | function get_first_null(arr:Array) {
297 | for (i in 0...arr.length) if (arr[i] == null) return i;
298 | return -1;
299 | }
300 |
301 | inline function get_leaf() return children[0] == null;
302 |
303 | // setters
304 |
305 | inline function set_max_depth(value:Int) {
306 | for (i in 0...children.length) if (children[i] != null) children[i].max_depth = value;
307 | return max_depth = value;
308 | }
309 |
310 | inline function set_max_contents(value:Int) {
311 | for (i in 0...children.length) if (children[i] != null) children[i].max_depth = value;
312 | max_contents = value;
313 | shake();
314 | return max_contents;
315 | }
316 | }
317 |
--------------------------------------------------------------------------------
/echo/util/ext/ArrayExt.hx:
--------------------------------------------------------------------------------
1 | package echo.util.ext;
2 |
3 | inline function dispose_bodies(arr:Array):Array {
4 | for (body in arr) body.dispose();
5 | return arr;
6 | }
7 |
--------------------------------------------------------------------------------
/echo/util/ext/FloatExt.hx:
--------------------------------------------------------------------------------
1 | package echo.util.ext;
2 |
3 | /**
4 | * Checks if two Floats are "equal" within the margin of error defined by the `diff` argument.
5 | * @param a The first Float to check for equality.
6 | * @param b The first Float to check for equality.
7 | * @param diff The margin of error to check by.
8 | * @return returns true if the floats are equal (within the defined margin of error)
9 | */
10 | inline function equals(a:Float, b:Float, diff:Float = 0.00001):Bool
11 | return Math.abs(a - b) <= diff;
12 |
13 | inline function clamp(value:Float, min:Float, max:Float):Float {
14 | if (value < min) return min;
15 | else if (value > max) return max;
16 | else return value;
17 | }
18 | /**
19 | * Converts specified angle in radians to degrees.
20 | * @return angle in degrees (not normalized to 0...360)
21 | */
22 | inline function rad_to_deg(rad:Float):Float
23 | return 180 / Math.PI * rad;
24 | /**
25 | * Converts specified angle in degrees to radians.
26 | * @return angle in radians (not normalized to 0...Math.PI*2)
27 | */
28 | inline function deg_to_rad(deg:Float):Float
29 | return Math.PI / 180 * deg;
30 |
31 | inline extern overload function sign(value:Float):Int return value > 0 ? 1 : value < 0 ? -1 : 0;
32 |
33 | inline extern overload function sign(value:Float, deadzone:Float):Int {
34 | if (Math.abs(value) < deadzone) return 0;
35 | return value <= -deadzone ? -1 : 1;
36 | }
37 |
--------------------------------------------------------------------------------
/echo/util/ext/IntExt.hx:
--------------------------------------------------------------------------------
1 | package echo.util.ext;
2 |
3 | inline function max(a:Int, b:Int):Int {
4 | return b > a ? b : a;
5 | }
6 |
7 | inline function min(a:Int, b:Int):Int {
8 | return b < a ? b : a;
9 | }
10 |
11 | inline extern overload function sign(value:Int):Int return value > 0 ? 1 : value < 0 ? -1 : 0;
12 |
13 | inline extern overload function sign(value:Int, deadzone:Int):Int {
14 | if (Math.abs(value) < deadzone) return 0;
15 | return value <= -deadzone ? -1 : 1;
16 | }
17 |
--------------------------------------------------------------------------------
/echo/util/verlet/Composite.hx:
--------------------------------------------------------------------------------
1 | package echo.util.verlet;
2 |
3 | import echo.util.verlet.Constraints;
4 | /**
5 | * A Composite contains Dots and Constraints. It can be thought of as a "Body" in the Verlet simulation.
6 | */
7 | class Composite {
8 | public var dots:Array = [];
9 | public var constraints:Array = [];
10 |
11 | public function new() {}
12 |
13 | public function add_dot(?x:Float, ?y:Float) {
14 | var dot = new Dot(x, y);
15 | dots.push(dot);
16 | return dot;
17 | }
18 |
19 | public inline function remove_dot(dot:Dot) {
20 | return dots.remove(dot);
21 | }
22 |
23 | public function add_constraint(constraint:Constraint):Constraint {
24 | constraints.push(constraint);
25 | return constraint;
26 | }
27 |
28 | public inline function remove_constraint(constraint:Constraint):Bool {
29 | return constraints.remove(constraint);
30 | }
31 |
32 | public inline function pin(index:Int) add_constraint(new PinConstraint(dots[index]));
33 |
34 | public function bounds(?aabb:AABB):AABB {
35 | var left = dots[0].x;
36 | var top = dots[0].y;
37 | var right = dots[0].x;
38 | var bottom = dots[0].y;
39 |
40 | for (i in 1...dots.length) {
41 | if (dots[i].x < left) left = dots[i].x;
42 | if (dots[i].y < top) top = dots[i].y;
43 | if (dots[i].x > right) right = dots[i].x;
44 | if (dots[i].y > bottom) bottom = dots[i].y;
45 | }
46 |
47 | return aabb == null ? AABB.get_from_min_max(left, top, right, bottom) : aabb.set_from_min_max(left, top, right, bottom);
48 | }
49 |
50 | public inline function clear() {
51 | dots.resize(0);
52 | constraints.resize(0);
53 | }
54 |
55 | public function toString() return 'Dot Group: {dots: $dots, constraints: $constraints}';
56 | }
57 |
--------------------------------------------------------------------------------
/echo/util/verlet/Constraints.hx:
--------------------------------------------------------------------------------
1 | package echo.util.verlet;
2 |
3 | import echo.math.Vector2;
4 |
5 | abstract class Constraint {
6 | public var active:Bool = true;
7 |
8 | public abstract function step(dt:Float):Void;
9 |
10 | public abstract function position_count():Int;
11 |
12 | public abstract function get_position(i:Int):Vector2;
13 |
14 | public inline function iterator() {
15 | return new ConstraintIterator(this);
16 | }
17 |
18 | public inline function get_positions():Array {
19 | return [for (p in iterator()) p];
20 | }
21 | }
22 |
23 | class ConstraintIterator {
24 | var c:Constraint;
25 | var i:Int;
26 |
27 | public inline function new(c:Constraint) {
28 | this.c = c;
29 | i = 0;
30 | }
31 |
32 | public inline function hasNext() {
33 | return i < c.position_count();
34 | }
35 |
36 | public inline function next() {
37 | return c.get_position(i++);
38 | }
39 | }
40 |
41 | class DistanceConstraint extends Constraint {
42 | public var a:Dot;
43 | public var b:Dot;
44 | public var stiffness:Float;
45 | public var distance:Float = 0;
46 |
47 | public function new(a:Dot, b:Dot, stiffness:Float, ?distance:Float) {
48 | if (a == b) {
49 | trace("Can't constrain a particle to itself!");
50 | return;
51 | }
52 |
53 | this.a = a;
54 | this.b = b;
55 | this.stiffness = stiffness;
56 | if (distance != null) this.distance = distance;
57 | else this.distance = a.get_position().distance(b.get_position());
58 | }
59 |
60 | public function step(dt:Float) {
61 | var ap = a.get_position();
62 | var bp = b.get_position();
63 | var normal = ap - bp;
64 | var m = normal.length_sq;
65 | var n = normal * (((distance * distance - m) / m) * stiffness * dt);
66 | a.set_position(ap + n);
67 | b.set_position(bp - n);
68 | }
69 |
70 | public inline function position_count() return 2;
71 |
72 | public inline function get_position(i:Int):Vector2 {
73 | switch (i) {
74 | case 0:
75 | return a.get_position();
76 | case 1:
77 | return b.get_position();
78 | }
79 | throw 'Constraint has no position at index $i.';
80 | }
81 | }
82 |
83 | class PinConstraint extends Constraint {
84 | public var a:Dot;
85 | public var x:Float;
86 | public var y:Float;
87 |
88 | public function new(a:Dot, ?x:Float, ?y:Float) {
89 | this.x = a.x = x == null ? a.x : x;
90 | this.y = a.y = y == null ? a.y : y;
91 | this.a = a;
92 | }
93 |
94 | public function step(dt:Float) {
95 | a.x = x;
96 | a.y = y;
97 | }
98 |
99 | public inline function position_count():Int return 2;
100 |
101 | public inline function get_position(i:Int):Vector2 {
102 | switch (i) {
103 | case 0:
104 | return a.get_position();
105 | case 1:
106 | return new Vector2(x, y);
107 | }
108 | throw 'Constraint has no position at index $i.';
109 | }
110 | }
111 |
112 | class RotationConstraint extends Constraint {
113 | public var a:Dot;
114 | public var b:Dot;
115 | public var c:Dot;
116 | public var radians:Float;
117 | public var stiffness:Float;
118 |
119 | public function new(a:Dot, b:Dot, c:Dot, stiffness:Float) {
120 | this.a = a;
121 | this.b = b;
122 | this.c = c;
123 | this.stiffness = stiffness;
124 | radians = b.get_position().radians_between(a.get_position(), c.get_position());
125 | }
126 |
127 | public function step(dt:Float) {
128 | var a_pos = a.get_position();
129 | var b_pos = b.get_position();
130 | var c_pos = c.get_position();
131 | var angle_between = b_pos.radians_between(a_pos, c_pos);
132 | var diff = angle_between - radians;
133 |
134 | if (diff <= -Math.PI) diff += 2 * Math.PI;
135 | else if (diff >= Math.PI) diff -= 2 * Math.PI;
136 |
137 | diff *= dt * stiffness;
138 |
139 | a.set_position((a_pos - b_pos).rotate(diff) + b_pos);
140 | c.set_position((c_pos - b_pos).rotate(-diff) + b_pos);
141 | a_pos.set(a.x, a.y);
142 | c_pos.set(c.x, c.y);
143 | b.set_position((b_pos - a_pos).rotate(diff) + a_pos);
144 | b.set_position((b.get_position() - c_pos).rotate(-diff) + c_pos);
145 | }
146 |
147 | public inline function position_count() return 3;
148 |
149 | public inline function get_position(i:Int):Vector2 {
150 | switch (i) {
151 | case 0:
152 | return a.get_position();
153 | case 1:
154 | return b.get_position();
155 | case 2:
156 | return c.get_position();
157 | }
158 | throw 'Constraint has no position at index $i.';
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/echo/util/verlet/Dot.hx:
--------------------------------------------------------------------------------
1 | package echo.util.verlet;
2 |
3 | import echo.math.Vector2;
4 | /**
5 | * The Dot is the basic building block of the Verlet simulation, representing a single moving point.
6 | *
7 | * Each Dot stores its latest position, acceleration, and prior position (from the last time the Verlet simulation stepped forward).
8 | */
9 | class Dot {
10 | /**
11 | * The Dot's X position.
12 | */
13 | public var x:Float;
14 | /**
15 | * The Dot's Y position.
16 | */
17 | public var y:Float;
18 | /**
19 | * The Dot's last X position.
20 | */
21 | public var dx:Float;
22 | /**
23 | * The Dot's last Y position.
24 | */
25 | public var dy:Float;
26 | /**
27 | * The Dot's X acceleration.
28 | */
29 | public var ax:Float;
30 | /**
31 | * The Dot's Y acceleration.
32 | */
33 | public var ay:Float;
34 |
35 | public inline function new(x:Float = 0, y:Float = 0) {
36 | dx = this.x = x;
37 | dy = this.y = y;
38 | ax = ay = 0;
39 | }
40 |
41 | public inline function push(x:Float = 0, y:Float = 0) {
42 | ax += x;
43 | ay += y;
44 | }
45 |
46 | public inline function get_position() return new Vector2(x, y);
47 |
48 | public inline function get_last_position() return new Vector2(dx, dy);
49 |
50 | public inline function get_acceleration() return new Vector2(ax, ay);
51 |
52 | public inline function set_position(v:Vector2) {
53 | x = v.x;
54 | y = v.y;
55 | }
56 |
57 | public inline function set_last_position(v:Vector2) {
58 | dx = v.x;
59 | dy = v.y;
60 | }
61 |
62 | public inline function set_acceleration(v:Vector2) {
63 | ax = v.x;
64 | ay = v.y;
65 | }
66 |
67 | public function toString() return 'Dot: {x: $x, y: $y}';
68 | }
69 |
--------------------------------------------------------------------------------
/echo/util/verlet/Verlet.hx:
--------------------------------------------------------------------------------
1 | package echo.util.verlet;
2 |
3 | import echo.util.verlet.Composite;
4 | import echo.util.Disposable;
5 | import echo.util.verlet.Constraints;
6 | import echo.math.Vector2;
7 | import echo.data.Options.VerletOptions;
8 | /**
9 | * A Verlet physics simulation, using Dots, Constraints, and Composites. Useful for goofy Softbody visuals and effects!
10 | *
11 | * This simulation is standalone, meaning it doesn't directly integrate with the standard echo simulation.
12 | */
13 | class Verlet implements Disposable {
14 | /**
15 | * The Verlet World's position on the X axis.
16 | */
17 | public var x:Float;
18 | /**
19 | * The Verlet World's position on the Y axis.
20 | */
21 | public var y:Float;
22 | /**
23 | * Width of the Verlet World, extending right from the World's X position.
24 | */
25 | public var width:Float;
26 | /**
27 | * Height of the Verlet World, extending down from the World's Y position.
28 | */
29 | public var height:Float;
30 | /**
31 | * The amount of acceleration applied to each `Dot` every Step.
32 | */
33 | public var gravity(default, null):Vector2;
34 |
35 | public var drag:Float;
36 |
37 | public var composites(default, null):Array = [];
38 | /**
39 | * The amount of iterations that occur on Constraints each time the Verlet World is stepped. The higher the number, the more stable the Physics Simulation will be, at the cost of performance.
40 | */
41 | public var iterations:Int;
42 | /**
43 | * The fixed Step rate of the Verlet World. The Verlet simulation must be stepped forward at a consistent rate, or it's stability will quickly deteriorate.
44 | */
45 | public var fixed_framerate(default, set):Float;
46 |
47 | public var bounds_left:Bool = false;
48 |
49 | public var bounds_right:Bool = false;
50 |
51 | public var bounds_top:Bool = false;
52 |
53 | public var bounds_bottom:Bool = false;
54 |
55 | var fixed_accumulator:Float = 0;
56 | var fixed_dt:Float;
57 |
58 | public static function rect(x:Float, y:Float, width:Float, height:Float, stiffness:Float, ?distance:Float):Composite {
59 | var r = new Composite();
60 | var tl = r.add_dot(x, y);
61 | var tr = r.add_dot(x + width, y);
62 | var br = r.add_dot(x + width, y + height);
63 | var bl = r.add_dot(x, y + height);
64 |
65 | r.add_constraint(new DistanceConstraint(tl, tr, stiffness, distance));
66 | r.add_constraint(new DistanceConstraint(tr, br, stiffness, distance));
67 | r.add_constraint(new DistanceConstraint(br, bl, stiffness, distance));
68 | r.add_constraint(new DistanceConstraint(bl, tr, stiffness, distance));
69 | r.add_constraint(new DistanceConstraint(bl, tl, stiffness, distance));
70 |
71 | return r;
72 | }
73 |
74 | public static function rope(points:Array, stiffness:Float, ?pinned:Array):Composite {
75 | var r = new Composite();
76 | for (i in 0...points.length) {
77 | var d = new Dot(points[i].x, points[i].y);
78 | r.dots.push(d);
79 | if (i > 0) {
80 | r.constraints.push(new DistanceConstraint(r.dots[i], r.dots[i - 1], stiffness));
81 | }
82 | if (pinned != null && pinned.indexOf(i) != -1) {
83 | r.constraints.push(new PinConstraint(r.dots[i]));
84 | }
85 | }
86 | return r;
87 | }
88 |
89 | public static function cloth(x:Float, y:Float, width:Float, height:Float, segments:Int, pin_mod:Int, stiffness:Float):Composite {
90 | var c = new Composite();
91 | var x_stride = width / segments;
92 | var y_stride = height / segments;
93 |
94 | for (sy in 0...segments) {
95 | for (sx in 0...segments) {
96 | var px = x + sx * x_stride;
97 | var py = y + sy * y_stride;
98 | c.dots.push(new Dot(px, py));
99 |
100 | if (sx > 0) c.constraints.push(new DistanceConstraint(c.dots[sy * segments + sx], c.dots[sy * segments + sx - 1], stiffness));
101 |
102 | if (sy > 0) c.constraints.push(new DistanceConstraint(c.dots[sy * segments + sx], c.dots[(sy - 1) * segments + sx], stiffness));
103 | }
104 | }
105 |
106 | for (x in 0...segments) {
107 | if (x % pin_mod == 0) c.add_constraint(new PinConstraint(c.dots[x]));
108 | }
109 |
110 | return c;
111 | }
112 |
113 | public function new(options:VerletOptions) {
114 | width = options.width;
115 | height = options.height;
116 | x = options.x == null ? 0 : options.x;
117 | y = options.y == null ? 0 : options.y;
118 | gravity = new Vector2(options.gravity_x == null ? 0 : options.gravity_x, options.gravity_y == null ? 0 : options.gravity_y);
119 | drag = options.drag == null ? .98 : options.drag;
120 | iterations = options.iterations == null ? 5 : options.iterations;
121 | fixed_framerate = options.fixed_framerate == null ? 60 : options.fixed_framerate;
122 | }
123 |
124 | public function step(dt:Float, ?colliders:Array) {
125 | fixed_accumulator += dt;
126 | while (fixed_accumulator > fixed_dt) {
127 | for (composite in composites) {
128 | for (d in composite.dots) {
129 | // Integrate
130 | var pos = d.get_position();
131 | var vel:Vector2 = (pos - d.get_last_position()) * drag;
132 | d.set_last_position(pos);
133 | d.set_position(pos + vel + (gravity + d.get_acceleration()) * fixed_dt);
134 |
135 | // Check bounds
136 | if (bounds_bottom && d.y > height + y) d.y = height + y;
137 | else if (bounds_top && d.y < y) d.y = y;
138 | if (bounds_left && d.x < x) d.x = x;
139 | else if (bounds_right && d.x > width + x) d.x = width + x;
140 |
141 | // TODO
142 | // Check collisions
143 | if (colliders != null) for (c in colliders) {}
144 | }
145 |
146 | // Constraints
147 | var fdt = 1 / iterations;
148 | for (i in 0...iterations) {
149 | for (c in composite.constraints) {
150 | if (c.active) c.step(fdt);
151 | }
152 | }
153 | }
154 | fixed_accumulator -= fixed_dt;
155 | }
156 | }
157 |
158 | public inline function add(composite:Composite):Composite {
159 | composites.push(composite);
160 | return composite;
161 | }
162 |
163 | public inline function remove(composite:Composite):Bool {
164 | return composites.remove(composite);
165 | }
166 |
167 | public inline function dispose() {
168 | if (composites != null) for (composite in composites) composite.clear();
169 | composites = null;
170 | }
171 |
172 | inline function set_fixed_framerate(v:Float) {
173 | fixed_framerate = Math.max(v, 0);
174 | fixed_dt = 1 / fixed_framerate;
175 | return fixed_framerate;
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/haxelib.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "echo",
3 | "license": "MIT",
4 | "tags": [
5 | "physics",
6 | "collisions"
7 | ],
8 | "description": "Simple Physics Library written in Haxe",
9 | "contributors": [
10 | "austineast"
11 | ],
12 | "url": "https://austineast.dev/echo",
13 | "releasenote": "revert regression",
14 | "version": "4.2.4"
15 | }
16 |
--------------------------------------------------------------------------------
/hxformat.json:
--------------------------------------------------------------------------------
1 | {
2 | "indentation": {
3 | "character": " ",
4 | "conditionalPolicy": "aligned"
5 | },
6 | "sameLine": {
7 | "anonFunctionBody": "keep",
8 | "ifBody": "same",
9 | "ifElse": "next",
10 | "elseBody": "same",
11 | "elseIf": "same",
12 | "caseBody": "keep",
13 | "doWhile": "same",
14 | "doWhileBody": "same",
15 | "whileBody": "same",
16 | "forBody": "same",
17 | "tryBody": "same",
18 | "catchBody": "same",
19 | "functionBody": "keep"
20 | },
21 | "emptyLines": {
22 | "afterLeftCurly": "remove",
23 | "beforeDocCommentEmptyLines": "none",
24 | "afterImportsUsing": 1,
25 | "afterPackage": 1,
26 | "finalNewline": true,
27 | "classEmptyLines": {
28 | "betweenFunctions": 1
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/release_haxelib.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | rm -f echo.zip
3 | zip -r echo.zip echo *.html *.md *.json *.hxml run.n
4 | haxelib submit echo.zip $HAXELIB_PWD --always
5 |
--------------------------------------------------------------------------------
/sample-hl.hxml:
--------------------------------------------------------------------------------
1 | # class paths
2 | -cp sample
3 |
4 | # entry point
5 | -main Main
6 |
7 | # libraries
8 | -lib echo
9 | -lib heaps
10 | -lib hlsdl
11 | # -lib hldx
12 |
13 | # math lib integrations
14 | # -lib hxmath
15 | # -D ECHO_USE_HXMATH
16 |
17 | # -lib zerolib
18 | # -D ECHO_USE_ZEROLIB
19 |
20 | # -lib vector-math
21 | # -D ECHO_USE_VECTORMATH
22 |
23 | -D ECHO_USE_HEAPS
24 |
25 | #flags
26 | -dce full
27 | -D windowSize=1280x720
28 | # -D HXMATH_USE_HEAPS_STRUCTURES
29 | # -D hl-profile
30 | # -D hl-ver=1.11.0
31 | -D dump=pretty
32 |
33 | # output
34 | -hl bin/sample.hl
35 | -cmd hl bin/sample.hl
--------------------------------------------------------------------------------
/sample.hxml:
--------------------------------------------------------------------------------
1 | # class paths
2 | -cp sample
3 |
4 | # entry point
5 | -main Main
6 |
7 | # libraries
8 | -lib echo
9 | -lib heaps
10 |
11 | # math lib integrations
12 | # -lib hxmath
13 | # -D ECHO_USE_HXMATH
14 |
15 | # -lib zerolib
16 | # -D ECHO_USE_ZEROLIB
17 |
18 | # -lib vector-math
19 | # -D ECHO_USE_VECTORMATH
20 |
21 | # -D ECHO_USE_HEAPS
22 |
23 | # copy assets
24 | --macro util.Assets.copyProjectAssets()
25 |
26 | # flags
27 | # use Heaps math structures with hxmath
28 | # -D HXMATH_USE_HEAPS_STRUCTURES
29 | -dce full
30 | # -debug
31 |
32 | # output
33 | -js bin/sample.js
34 |
--------------------------------------------------------------------------------
/sample/BaseApp.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import echo.World;
4 | import echo.util.Debug;
5 | import util.FSM;
6 |
7 | class BaseApp extends hxd.App {
8 | public var debug:HeapsDebug;
9 |
10 | var sample_states:Array>>;
11 | var fsm:FSM;
12 | var fui:h2d.Flow;
13 | var index:Int = 0;
14 |
15 | function reset_state() return fsm.set(Type.createInstance(sample_states[index], []));
16 |
17 | function previous_state() {
18 | index -= 1;
19 | if (index < 0) index = sample_states.length - 1;
20 | return fsm.set(Type.createInstance(sample_states[index], []));
21 | }
22 |
23 | function next_state() {
24 | index += 1;
25 | if (index >= sample_states.length) index = 0;
26 | return fsm.set(Type.createInstance(sample_states[index], []));
27 | }
28 |
29 | public function getFont() {
30 | return hxd.res.DefaultFont.get();
31 | }
32 |
33 | public function addButton(label:String, onClick:Void->Void, ?parent:h2d.Object) {
34 | var f = new h2d.Flow(parent == null ? fui : parent);
35 | f.padding = 5;
36 | f.paddingBottom = 7;
37 | f.backgroundTile = h2d.Tile.fromColor(0x404040, 1, 1, 0.5);
38 | var tf = new h2d.Text(getFont(), f);
39 | tf.text = label;
40 | f.enableInteractive = true;
41 | f.interactive.cursor = Button;
42 | f.interactive.onClick = function(_) onClick();
43 | f.interactive.onOver = function(_) f.backgroundTile = h2d.Tile.fromColor(0x606060, 1, 1, 0.5);
44 | f.interactive.onOut = function(_) f.backgroundTile = h2d.Tile.fromColor(0x404040, 1, 1, 0.5);
45 | return f;
46 | }
47 |
48 | public function addSlider(label:String, get:Void->Float, set:Float->Void, min:Float = 0., max:Float = 1., int:Bool = false) {
49 | var f = new h2d.Flow(fui);
50 |
51 | f.horizontalSpacing = 5;
52 |
53 | var tf = new h2d.Text(getFont(), f);
54 | tf.text = label;
55 | tf.maxWidth = 70;
56 | tf.textAlign = Right;
57 |
58 | var sli = new h2d.Slider(100, 10, f);
59 | sli.minValue = min;
60 | sli.maxValue = max;
61 | sli.value = get();
62 |
63 | var tf = new h2d.TextInput(getFont(), f);
64 | tf.text = "" + (int ? Std.int(hxd.Math.fmt(sli.value)) : hxd.Math.fmt(sli.value));
65 | sli.onChange = function() {
66 | set(sli.value);
67 | tf.text = "" + (int ? Std.int(hxd.Math.fmt(sli.value)) : hxd.Math.fmt(sli.value));
68 | f.needReflow = true;
69 | };
70 | tf.onChange = function() {
71 | var v = Std.parseFloat(tf.text);
72 | if (Math.isNaN(v)) return;
73 | sli.value = v;
74 | set(v);
75 | };
76 | return sli;
77 | }
78 |
79 | public function addCheck(label:String, get:Void->Bool, set:Bool->Void) {
80 | var f = new h2d.Flow(fui);
81 |
82 | f.horizontalSpacing = 5;
83 |
84 | var tf = new h2d.Text(getFont(), f);
85 | tf.text = label;
86 | tf.maxWidth = 70;
87 | tf.textAlign = Right;
88 |
89 | var size = 10;
90 | var b = new h2d.Graphics(f);
91 | function redraw() {
92 | b.clear();
93 | b.beginFill(0x808080);
94 | b.drawRect(0, 0, size, size);
95 | b.beginFill(0);
96 | b.drawRect(1, 1, size - 2, size - 2);
97 | if (get()) {
98 | b.beginFill(0xC0C0C0);
99 | b.drawRect(2, 2, size - 4, size - 4);
100 | }
101 | }
102 | var i = new h2d.Interactive(size, size, b);
103 | i.onClick = function(_) {
104 | set(!get());
105 | redraw();
106 | };
107 | redraw();
108 | return i;
109 | }
110 |
111 | public function addChoice(text, choices, callb:Int->Void, value = 0, width = 110) {
112 | var font = getFont();
113 | var i = new h2d.Interactive(width, font.lineHeight, fui);
114 | i.backgroundColor = 0xFF808080;
115 | fui.getProperties(i).paddingLeft = 20;
116 |
117 | var t = new h2d.Text(font, i);
118 | t.maxWidth = i.width;
119 | t.text = text + ":" + choices[value];
120 | t.textAlign = Center;
121 |
122 | i.onClick = function(_) {
123 | value++;
124 | value %= choices.length;
125 | callb(value);
126 | t.text = text + ":" + choices[value];
127 | };
128 | i.onOver = function(_) {
129 | t.textColor = 0xFFFFFF;
130 | };
131 | i.onOut = function(_) {
132 | t.textColor = 0xEEEEEE;
133 | };
134 | i.onOut(null);
135 | return i;
136 | }
137 |
138 | public function addText(text = "", ?parent) {
139 | var tf = new h2d.Text(getFont(), parent == null ? fui : parent);
140 | tf.text = text;
141 | return tf;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/sample/Main.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import hxd.Window;
4 | import hxd.Key;
5 | import echo.Echo;
6 | import echo.World;
7 | import echo.util.Debug;
8 | import util.FSM;
9 | import state.*;
10 |
11 | class Main extends BaseApp {
12 | public static var instance:Main;
13 |
14 | public var scene:h2d.Scene;
15 | public var state_text:h2d.Text;
16 | public var gravity_slider:h2d.Slider;
17 | public var iterations_slider:h2d.Slider;
18 | public var playing:Bool = true;
19 |
20 | var width:Int = 640;
21 | var height:Int = 360;
22 | var world:World;
23 | var members_text:h2d.Text;
24 | var fps_text:h2d.Text;
25 |
26 | override function init() {
27 | instance = this;
28 |
29 | // Create a World to hold all the Physics Bodies
30 | world = Echo.start({
31 | width: width,
32 | height: height,
33 | gravity_y: 100,
34 | iterations: 5,
35 | history: 1000
36 | });
37 |
38 | // Reduce Quadtree depths - our World is very small, so not many subdivisions of the Quadtree are actually needed.
39 | // This can help with performance by limiting the Quadtree's overhead on simulations with small Body counts!
40 | world.quadtree.max_depth = 3;
41 | world.static_quadtree.max_depth = 3;
42 |
43 | // Increase max contents per Quadtree depth - can help reduce Quadtree subdivisions in smaller World sizes.
44 | // Tuning these Quadtree settings can be very useful when optimizing for performance!
45 | world.quadtree.max_contents = 20;
46 | world.static_quadtree.max_contents = 20;
47 |
48 | // Set up our Sample States
49 | sample_states = [
50 | PolygonState, StackingState, MultiShapeState, ShapesState, GroupsState, StaticState, LinecastState, Linecast2State, TileMapState, TileMapState2,
51 | BezierState, VerletState
52 | ];
53 | index = 0;
54 | // Create a State Manager and pass it the World and the first Sample
55 | fsm = new FSM(world, Type.createInstance(sample_states[index], []));
56 | // Create a Debug drawer to display debug graphics
57 | debug = new HeapsDebug(s2d);
58 | // Set the Background color of the Scene
59 | engine.backgroundColor = 0x45283c;
60 | // Set the Heaps Scene size
61 | s2d.scaleMode = LetterBox(width, height);
62 | // Get a static reference to the Heaps scene so we can access it later
63 | scene = s2d;
64 | // Add the UI elements
65 | add_ui();
66 | }
67 |
68 | override function update(dt:Float) {
69 | // Draw the World
70 | debug.draw(world);
71 |
72 | if (world.history != null) {
73 | // Press Left to undo
74 | if (Key.isDown(Key.LEFT)) {
75 | world.undo();
76 | playing = false;
77 | }
78 | // Press Right to redo
79 | if (Key.isDown(Key.RIGHT)) {
80 | world.redo();
81 | playing = false;
82 | }
83 | // Press Space to play/pause
84 | if (Key.isPressed(Key.SPACE)) playing = !playing;
85 | }
86 | // Hold Shift for slowmo debugging
87 | var fdt = Key.isDown(Key.SHIFT) ? dt * 0.3 : dt;
88 | // Update the current Sample State
89 | fsm.step(fdt);
90 | // Step the World Forward, with a fixed step rate of 60 per second
91 | if (playing) world.step(fdt, 60);
92 |
93 | // Update GUI text
94 | members_text.text = 'Bodies: ${world.count}';
95 | fps_text.text = 'FPS: ${engine.fps}';
96 | }
97 |
98 | function add_ui() {
99 | fui = new h2d.Flow(s2d);
100 | fui.y = 5;
101 | fui.padding = 5;
102 | fui.verticalSpacing = 5;
103 | fui.layout = Vertical;
104 |
105 | var tui = new h2d.Flow(s2d);
106 | tui.padding = 5;
107 | tui.verticalSpacing = 5;
108 | tui.layout = Vertical;
109 | tui.y = s2d.height - 90;
110 | fps_text = addText("FPS: ", tui);
111 | members_text = addText("Bodies: ", tui);
112 | state_text = addText("Sample: ", tui);
113 | var buttons = new h2d.Flow(tui);
114 | buttons.horizontalSpacing = 2;
115 |
116 | var bui = new h2d.Flow(s2d);
117 | bui.padding = 5;
118 | bui.verticalSpacing = 5;
119 | bui.layout = Vertical;
120 | bui.y = s2d.height - 65;
121 | bui.x = s2d.width - 150;
122 | addText("Arrow Keys: Undo/Redo", bui);
123 | addText("Spacebar: Pause/Play", bui);
124 | addText("Hold Shift: Slowmo", bui);
125 |
126 | addButton("Previous", previous_state, buttons);
127 | addButton("Restart", reset_state, buttons);
128 | addButton("Next", next_state, buttons);
129 | gravity_slider = addSlider("Gravity", () -> return world.gravity.y, (v) -> world.gravity.y = v, -100, 300);
130 | iterations_slider = addSlider("Iterations", () -> return world.iterations, (v) -> world.iterations = Std.int(v), 1, 10, true);
131 | }
132 |
133 | static function main() {
134 | new Main();
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/sample/build.hxml:
--------------------------------------------------------------------------------
1 | -lib heaps
2 | -lib hlsdl
3 | -lib echo
4 |
5 | -main Main
6 |
7 | # math lib integrations
8 | # -lib hxmath
9 | # -D ECHO_USE_HXMATH
10 |
11 | # -lib zerolib
12 | # -D ECHO_USE_ZEROLIB
13 |
14 | # -lib vector-math
15 | # -D ECHO_USE_VECTORMATH
16 |
17 | -dce full
18 | -D standalone
19 | -D windowSize=1280x720
20 | -hl bin/sample.hl
21 |
--------------------------------------------------------------------------------
/sample/ogmo/project.ogmo:
--------------------------------------------------------------------------------
1 | {
2 | "name": "New Project",
3 | "ogmoVersion": "3.3.0",
4 | "levelPaths": ["."],
5 | "backgroundColor": "#282c34ff",
6 | "gridColor": "#3c4049cc",
7 | "anglesRadians": true,
8 | "directoryDepth": 5,
9 | "layerGridDefaultSize": {"x": 8, "y": 8},
10 | "levelDefaultSize": {"x": 320, "y": 240},
11 | "levelMinSize": {"x": 128, "y": 128},
12 | "levelMaxSize": {"x": 4096, "y": 4096},
13 | "levelValues": [],
14 | "defaultExportMode": ".json",
15 | "compactExport": false,
16 | "externalScript": "",
17 | "playCommand": "",
18 | "entityTags": [],
19 | "layers": [
20 | {
21 | "definition": "tile",
22 | "name": "tiles",
23 | "gridSize": {"x": 16, "y": 16},
24 | "exportID": "31998968",
25 | "exportMode": 0,
26 | "arrayMode": 0,
27 | "defaultTileset": "tileset"
28 | }
29 | ],
30 | "entities": [],
31 | "tilesets": [
32 | {"label": "tileset", "path": "terrain-tiles.png", "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANAAAAAgCAMAAABU6AZfAAAAAXNSR0IArs4c6QAAAA9QTFRFAAAA4NvNKysmqJ+UcGtmTGMrswAAAAV0Uk5TAP////8c0CZSAAABLElEQVRYhdXZ6w7DIAgF4JK9/zsvWbKl43YOkljhb0X9rKOtuy4VosJc1glpOD0EDV8qgjTbXTpd27OYqwWRfDqjSMugfL6ogSRDOWP/OoEpxhMMYxe0BZJ08SIOQ3oGJOlYCQeSHI87irNDGiDJ0gAHkBqg9aIgYDDAyUhBgi1o1HKSIHhrIScmPQEiy2rOCUjxHQ3alUQ+iKtCmOOSHgBRP1qOY0lJWtSuIvJAzB7nOZq0H0RsiRrnj5QXkWBilfcv0yNewTrnRtoOggOucb4kkFx+bFiQDjSnZQ6V3geBOzYfNH7LlUQTikJFNKJsF0QzHqy8aMirDy2a8nLKisZ8PpCiOR94nGjQJzglOumQpBFHHmO14sSDxl4ceBTcjFqHOw7ru3He3ykq3lQ4ILU1D1tLAAAAAElFTkSuQmCC", "tileWidth": 16, "tileHeight": 16, "tileSeparationX": 0, "tileSeparationY": 0, "tileMarginX": 0, "tileMarginY": 0}
33 | ]
34 | }
--------------------------------------------------------------------------------
/sample/ogmo/slopes.json:
--------------------------------------------------------------------------------
1 | {
2 | "ogmoVersion": "3.3.0",
3 | "width": 240,
4 | "height": 176,
5 | "offsetX": 0,
6 | "offsetY": 0,
7 | "layers": [
8 | {
9 | "name": "tiles",
10 | "_eid": "31998968",
11 | "offsetX": 0,
12 | "offsetY": 0,
13 | "gridCellWidth": 16,
14 | "gridCellHeight": 16,
15 | "gridCellsX": 15,
16 | "gridCellsY": 11,
17 | "tileset": "tileset",
18 | "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 20, -1, -1, -1, -1, -1, -1, -1, 15, 19, 18, 1, 1, 1, -1, 2, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, 21, 1, 1, -1, 15, 16, 0, -1, -1, -1, -1, -1, -1, -1, -1, 23, 1, 1, -1, -1, -1, -1, -1, -1, -1, 4, 5, 6, 7, -1, 10, 1, 1, -1, -1, -1, -1, -1, -1, -1, 17, 18, 19, 20, -1, 8, 1, 1, -1, -1, 10, 11, -1, -1, -1, -1, -1, -1, -1, -1, 21, 1, 1, -1, -1, 23, 24, -1, -1, 12, -1, -1, -1, -1, -1, 23, 1, 1, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 12, -1, 1, 1, 1, 6, 7, -1, -1, -1, -1, 4, 5, 1, 3, -1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
19 | "exportMode": 0,
20 | "arrayMode": 0
21 | }
22 | ]
23 | }
--------------------------------------------------------------------------------
/sample/ogmo/terrain-tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AustinEast/echo/d889af58823d11067fd8d62ca0cc059cf12a5efb/sample/ogmo/terrain-tiles.png
--------------------------------------------------------------------------------
/sample/profile.hxml:
--------------------------------------------------------------------------------
1 | --no-inline
2 | -D hl-profile
3 | -D hl-ver=1.11.0
4 | build.hxml
5 | -cmd hl --profile 10000 bin/sample.hl
--------------------------------------------------------------------------------
/sample/res/res.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AustinEast/echo/d889af58823d11067fd8d62ca0cc059cf12a5efb/sample/res/res.txt
--------------------------------------------------------------------------------
/sample/state/BaseState.hx:
--------------------------------------------------------------------------------
1 | package state;
2 |
3 | import echo.Body;
4 | import echo.World;
5 | import util.FSM;
6 |
7 | class BaseState extends State {
8 | override public function exit(world:World) world.clear();
9 |
10 | function offscreen(b:Body, world:World) {
11 | var bounds = b.bounds();
12 | var check = bounds.min_y > world.height || bounds.max_x < 0 || bounds.min_x > world.width;
13 | bounds.put();
14 | return check;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/sample/state/BezierState.hx:
--------------------------------------------------------------------------------
1 | package state;
2 |
3 | import echo.data.Data.IntersectionData;
4 | import echo.util.SAT;
5 | import echo.Line;
6 | import h2d.Interactive;
7 | import hxd.Key;
8 | import echo.util.Bezier;
9 | import echo.World;
10 | import echo.math.Vector2;
11 |
12 | class BezierState extends BaseState {
13 | final segments = 100;
14 |
15 | var mode = [Cubic, Quadratic, Linear];
16 | var current_mode = 0;
17 | var controls:Array = [];
18 | var bezier:Bezier;
19 | var line:Line;
20 | var t:Float = 0;
21 | var qt:Float = 0;
22 | var curve_mode_button:Interactive;
23 |
24 | override function enter(parent:World) {
25 | Main.instance.state_text.text = "Sample: Bezier Curves (CLICK to drag Control Points)";
26 | curve_mode_button = Main.instance.addChoice('Curve Mode', ['Cubic', 'Quadratic', 'Linear'], (i) -> {
27 | current_mode = i;
28 | bezier.curve_mode = mode[current_mode];
29 | }, current_mode, 140);
30 |
31 | super.enter(parent);
32 |
33 | var c = parent.center();
34 |
35 | // Create a new Bezier Curve with some control points
36 | bezier = new Bezier([
37 | new Vector2(c.x - 170, c.y + 80),
38 | new Vector2(c.x - 90, c.y + 80),
39 | new Vector2(c.x - 80, c.y - 80),
40 | new Vector2(c.x, c.y - 80),
41 | new Vector2(c.x + 80, c.y - 80),
42 | new Vector2(c.x + 90, c.y + 80),
43 | new Vector2(c.x + 170, c.y + 80),
44 | ]);
45 |
46 | // For each control point in the Bezier Curve, create an h2d.Interactive so we can move the control points around
47 | for (i in 0...bezier.control_count) {
48 | var p = bezier.get_control_point(i);
49 | var interactive = new Interactive(6, 6, Main.instance.scene);
50 | interactive.setPosition(p.x - interactive.width * 0.5, p.y - interactive.height * 0.5);
51 | interactive.isEllipse = true;
52 | interactive.onPush = (e) -> {
53 | interactive.startCapture((e) -> {
54 | var x = Main.instance.scene.mouseX;
55 | var y = Main.instance.scene.mouseY;
56 | interactive.x = x - interactive.width * 0.5;
57 | interactive.y = y - interactive.height * 0.5;
58 | bezier.set_control_point(i, x, y);
59 | });
60 | };
61 | interactive.onRelease = (e) -> {
62 | interactive.stopCapture();
63 | };
64 | controls.push(interactive);
65 | }
66 |
67 | // Create a line to linecast again the Bezier Curve
68 | line = Line.get(c.x, c.y - 120, c.x, c.y + 120);
69 | c.put();
70 | }
71 |
72 | override function step(parent:World, dt:Float) {
73 | super.step(parent, dt);
74 |
75 | t += dt;
76 | qt += dt * 0.25;
77 |
78 | // update control handles
79 | for (i in 0...controls.length) {
80 | var c = bezier.get_control_point(i);
81 | controls[i].setPosition(c.x - controls[i].width * 0.5, c.y - controls[i].height * 0.5);
82 | }
83 |
84 | // Draw the Bezier Curve
85 | Main.instance.debug.draw_bezier(bezier, true, true);
86 |
87 | // Move the linecasting Line
88 | var c = parent.center();
89 | line.dx = line.x = c.x + Math.sin(t) * 160;
90 | c.put();
91 |
92 | // Perform a linecast against the Bezier Curve
93 | var closest:IntersectionData = null;
94 | for (l in bezier.lines) {
95 | var result = SAT.line_intersects_line(line, l);
96 | if (closest == null || result != null && result.distance < closest.distance) closest = result;
97 | }
98 |
99 | // Draw the linecast results
100 | if (closest != null) Main.instance.debug.draw_intersection_data(closest);
101 | else Main.instance.debug.draw_line(line.start.x, line.start.y, line.end.x, line.end.y, Main.instance.debug.intersection_overlap_color);
102 |
103 | // Draw a point along the Bezier Curve
104 | var p = bezier.get_point(0.5 * (1 + Math.sin(2 * Math.PI * qt)));
105 | if (p != null) Main.instance.debug.draw_circle(p.x, p.y, 5, Main.instance.debug.shape_color);
106 | }
107 |
108 | override function exit(world:World) {
109 | curve_mode_button.remove();
110 | super.exit(world);
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/sample/state/GroupsState.hx:
--------------------------------------------------------------------------------
1 | package state;
2 |
3 | import echo.Body;
4 | import echo.World;
5 | import util.Random;
6 |
7 | class GroupsState extends BaseState {
8 | var body_count:Int = 50;
9 | var circles:Array;
10 | var rects:Array;
11 | var floors:Array;
12 | var timer:Float;
13 |
14 | override public function enter(world:World) {
15 | Main.instance.state_text.text = "Sample: Grouped Collisions";
16 | timer = 0;
17 | // create some arrays to hold the different collision groups
18 | circles = [];
19 | rects = [];
20 | floors = [];
21 |
22 | // Add some platforms for the bodies to bounce off of
23 | // Setting the Mass to 0 makes them unmovable
24 | for (i in 0...4) {
25 | var floor = new Body({
26 | mass: STATIC,
27 | x: (world.width / 4) * i + (world.width / 8),
28 | y: world.height - 30,
29 | material: {elasticity: 0.3},
30 | shape: {
31 | type: RECT,
32 | width: world.width / 8,
33 | height: 10
34 | }
35 | });
36 | floors.push(floor);
37 | world.add(floor);
38 | }
39 |
40 | world.listen(circles, rects);
41 | world.listen(circles, floors);
42 | world.listen(rects, floors);
43 | }
44 |
45 | override function step(world:World, dt:Float) {
46 | timer += dt;
47 | if (timer > 0.3 + Random.range(-0.2, 0.2)) {
48 | if (circles.length < body_count) {
49 | var c = make_circle();
50 | circles.push(c);
51 | launch(world.add(c), world, true);
52 | }
53 | else {
54 | var found = false;
55 | for (member in circles) {
56 | if (!found && offscreen(member, world)) {
57 | launch(member, world, true);
58 | found = true;
59 | }
60 | }
61 | }
62 |
63 | if (rects.length < body_count) {
64 | var r = make_rect();
65 | rects.push(r);
66 | launch(world.add(r), world, false);
67 | }
68 | else {
69 | var found = false;
70 | for (member in rects) {
71 | if (!found && offscreen(member, world)) {
72 | launch(member, world, false);
73 | found = true;
74 | }
75 | }
76 | }
77 |
78 | timer = 0;
79 | }
80 | }
81 |
82 | inline function make_circle():Body return new Body({
83 | material: {elasticity: 0.5},
84 | shape: {
85 | type: CIRCLE,
86 | radius: Random.range(16, 32)
87 | }
88 | });
89 |
90 | inline function make_rect():Body return new Body({
91 | material: {elasticity: 0.5},
92 | shape: {
93 | type: RECT,
94 | width: Random.range(32, 64),
95 | height: Random.range(32, 64)
96 | }
97 | });
98 |
99 | inline function launch(b:Body, w:World, left:Bool) {
100 | b.set_position(left ? 20 : w.width - 20, w.height / 2);
101 | b.velocity.set(left ? 130 : -130, hxd.Math.lerp(-60, 20, Main.instance.scene.mouseY / w.height));
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/sample/state/Linecast2State.hx:
--------------------------------------------------------------------------------
1 | package state;
2 |
3 | import echo.Echo;
4 | import echo.math.Vector2;
5 | import echo.Line;
6 | import echo.Body;
7 | import echo.World;
8 | import util.Random;
9 |
10 | class Linecast2State extends BaseState {
11 | var body_count:Int = 50;
12 | var cast_count:Int = 100;
13 | var cast_length:Float = 90;
14 | var dynamics:Array = [];
15 |
16 | override public function enter(world:World) {
17 | Main.instance.state_text.text = "Sample: Linecasting 2";
18 | // Add a bunch of random Physics Bodies to the World
19 | for (i in 0...body_count) {
20 | var b = new Body({
21 | x: Random.range(0, world.width),
22 | y: Random.range(0, world.height),
23 | rotational_velocity: Random.range(-20, 20),
24 | material: {gravity_scale: 0},
25 | shape: {
26 | type: Random.chance() ? POLYGON : CIRCLE,
27 | radius: Random.range(16, 32),
28 | width: Random.range(16, 48),
29 | height: Random.range(16, 48),
30 | sides: Random.range_int(3, 8)
31 | }
32 | });
33 | dynamics.push(b);
34 | world.add(b);
35 | }
36 |
37 | // world.listen();
38 | }
39 |
40 | override function step(world:World, dt:Float) {
41 | var mouse = new Vector2(Main.instance.scene.mouseX, Main.instance.scene.mouseY);
42 | var line = Line.get();
43 | for (i in 0...cast_count) {
44 | line.set_from_vector(mouse, 360 * (i / cast_count), cast_length);
45 | var result = line.linecast(dynamics, world);
46 | if (result != null) Main.instance.debug.draw_intersection(result, false);
47 | else Main.instance.debug.draw_line(line.start.x, line.start.y, line.end.x, line.end.y, Main.instance.debug.intersection_color);
48 | }
49 | line.put();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/sample/state/LinecastState.hx:
--------------------------------------------------------------------------------
1 | package state;
2 |
3 | import echo.Material;
4 | import echo.Line;
5 | import echo.Body;
6 | import echo.World;
7 | import util.Random;
8 |
9 | class LinecastState extends BaseState {
10 | var body_count:Int = 30;
11 | var dynamics:Array = [];
12 | var line:Line;
13 |
14 | override public function enter(world:World) {
15 | Main.instance.state_text.text = "Sample: Linecasting";
16 |
17 | // Create a material for all the shapes to share
18 | var material:Material = {gravity_scale: 0};
19 |
20 | // Add a bunch of random Physics Bodies to the World
21 | for (i in 0...body_count) {
22 | var b = new Body({
23 | x: (world.width * 0.35) * Math.cos(i) + world.width * 0.5,
24 | y: (world.height * 0.35) * Math.sin(i) + world.height * 0.5,
25 | material: material,
26 | shape: {
27 | type: Random.chance() ? POLYGON : CIRCLE,
28 | radius: Random.range(16, 32),
29 | width: Random.range(16, 48),
30 | height: Random.range(16, 48),
31 | sides: Random.range_int(3, 8)
32 | }
33 | });
34 |
35 | dynamics.push(b);
36 | world.add(b);
37 | }
38 |
39 | line = Line.get(world.width / 2, world.height / 2, world.width / 2, world.height / 2);
40 | }
41 |
42 | override function step(world:World, dt:Float) {
43 | line.end.set(Main.instance.scene.mouseX, Main.instance.scene.mouseY);
44 | var result = line.linecast(dynamics, world);
45 | if (result != null) Main.instance.debug.draw_intersection(result);
46 | else Main.instance.debug.draw_line(line.start.x, line.start.y, line.end.x, line.end.y, Main.instance.debug.intersection_color);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/sample/state/MultiShapeState.hx:
--------------------------------------------------------------------------------
1 | package state;
2 |
3 | import echo.Body;
4 | import echo.World;
5 | import util.Random;
6 |
7 | class MultiShapeState extends BaseState {
8 | var body_count:Int = 49;
9 |
10 | override public function enter(world:World) {
11 | Main.instance.state_text.text = "Sample: Bodies With Multiple Shapes";
12 | // Add a bunch of random Physics Bodies with multiple shapes to the World
13 | for (i in 0...body_count) {
14 | var scale = Random.range(0.2, 1.6);
15 | var b = new Body({
16 | x: Random.range(0, world.width),
17 | y: Random.range(0, world.height / 2),
18 | material: {elasticity: 0.3},
19 | // rotational_velocity: 25,
20 | rotation: Random.range(0, 360),
21 | scale_x: scale,
22 | scale_y: scale,
23 | shapes: [
24 | {
25 | type: i % 2 == 0 ? CIRCLE : RECT,
26 | offset_x: 16,
27 | width: 24,
28 | radius: 12
29 | },
30 | {
31 | type: i % 2 == 0 ? CIRCLE : RECT,
32 | offset_x: -16,
33 | width: 24,
34 | radius: 12
35 | },
36 | {
37 | type: i % 2 == 0 ? CIRCLE : RECT,
38 | offset_y: -16,
39 | width: 24,
40 | radius: 12
41 | },
42 | {
43 | type: i % 2 == 0 ? CIRCLE : RECT,
44 | offset_y: 16,
45 | width: 24,
46 | radius: 12
47 | }
48 | ]
49 | });
50 | world.add(b);
51 | }
52 |
53 | // Add a Physics body at the bottom of the screen for the other Physics Bodies to stack on top of
54 | // This body has a mass of 0, so it acts as an immovable object
55 | world.add(new Body({
56 | mass: STATIC,
57 | x: world.width / 2,
58 | y: world.height - 10,
59 | material: {elasticity: 0.5},
60 | shape: {
61 | type: RECT,
62 | width: world.width,
63 | height: 20
64 | }
65 | }));
66 |
67 | // Create a listener for collisions between the Physics Bodies
68 | world.listen();
69 | }
70 |
71 | override function step(world:World, dt:Float) {
72 | // Reset any off-screen Bodies
73 | world.for_each((member) -> {
74 | // member.rotation += 1 * dt;
75 | if (offscreen(member, world)) {
76 | member.velocity.set(0, 0);
77 | member.set_position(Random.range(0, world.width), 0);
78 | }
79 | });
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/sample/state/PolygonState.hx:
--------------------------------------------------------------------------------
1 | package state;
2 |
3 | import echo.Material;
4 | import hxd.Key;
5 | import echo.Body;
6 | import echo.World;
7 | import util.Random;
8 |
9 | class PolygonState extends BaseState {
10 | var body_count:Int = 100;
11 |
12 | override public function enter(world:World) {
13 | Main.instance.state_text.text = "Sample: Stacking Polygons";
14 |
15 | // Create a material for all the shapes to share
16 | var material:Material = {elasticity: 0.7};
17 |
18 | // Add a bunch of random Physics Bodies to the World
19 | for (i in 0...body_count) {
20 | var scale = Random.range(0.3, 1);
21 | var b = new Body({
22 | x: Random.range(60, world.width - 60),
23 | y: Random.range(0, world.height / 2),
24 | rotation: Random.range(0, 360),
25 | material: material,
26 | shape: {
27 | type: POLYGON,
28 | radius: Random.range(16, 32),
29 | width: Random.range(16, 48),
30 | height: Random.range(16, 48),
31 | sides: Random.range_int(3, 8),
32 | scale_x: scale,
33 | scale_y: scale
34 | }
35 | });
36 | world.add(b);
37 | }
38 |
39 | // Add a Physics body at the bottom of the screen for the other Physics Bodies to stack on top of
40 | // This body has a mass of 0, so it acts as an immovable object
41 | world.add(new Body({
42 | mass: STATIC,
43 | x: world.width / 5,
44 | y: world.height - 40,
45 | material: material,
46 | rotation: 5,
47 | shape: {
48 | type: RECT,
49 | width: world.width / 2,
50 | height: 20
51 | }
52 | }));
53 |
54 | world.add(new Body({
55 | mass: STATIC,
56 | x: world.width - world.width / 5,
57 | y: world.height - 40,
58 | material: material,
59 | rotation: -5,
60 | shape: {
61 | type: RECT,
62 | width: world.width / 2,
63 | height: 20
64 | }
65 | }));
66 |
67 | // Create a listener for collisions between the Physics Bodies
68 | world.listen();
69 | }
70 |
71 | override function step(world:World, dt:Float) {
72 | // Reset any off-screen Bodies
73 | world.for_each((member) -> {
74 | if (offscreen(member, world)) {
75 | member.velocity.set(0, 0);
76 | member.set_position(Random.range(0, world.width), 0);
77 | }
78 | });
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/sample/state/ShapesState.hx:
--------------------------------------------------------------------------------
1 | package state;
2 |
3 | import echo.Body;
4 | import echo.World;
5 | import util.Random;
6 |
7 | class ShapesState extends BaseState {
8 | var body_count:Int = 100;
9 | var cursor:Body;
10 | var cursor_speed:Float = 10;
11 | var timer:Float;
12 |
13 | override public function enter(world:World) {
14 | Main.instance.state_text.text = "Sample: Box/Circle/Polygon Collisions";
15 | timer = 0;
16 | // Add some platforms for the bodies to bounce off of
17 | // Setting the Mass to 0 makes them unmovable
18 | for (i in 0...4) {
19 | world.add(new Body({
20 | mass: STATIC,
21 | x: (world.width / 4) * i + (world.width / 8),
22 | y: world.height - 30,
23 | material: {elasticity: 0.3},
24 | shape: {
25 | type: RECT,
26 | width: world.width / 8,
27 | height: 10
28 | }
29 | }));
30 | }
31 |
32 | cursor = new Body({
33 | x: Main.instance.scene.mouseX,
34 | y: Main.instance.scene.mouseY,
35 | shape: {
36 | type: CIRCLE,
37 | radius: 16
38 | }
39 | });
40 | world.add(cursor);
41 |
42 | // Create a listener for collisions between the Physics Bodies
43 | world.listen();
44 | }
45 |
46 | override function step(world:World, dt:Float) {
47 | // Move the Cursor Body
48 | cursor.velocity.set(Main.instance.scene.mouseX - cursor.x, Main.instance.scene.mouseY - cursor.y);
49 | cursor.velocity *= cursor_speed;
50 |
51 | timer += dt;
52 | if (timer > 0.3 + Random.range(-0.2, 0.2)) {
53 | if (world.count < body_count) world.add(new Body({
54 | x: Random.range(0, world.width),
55 | material: {elasticity: 0.3},
56 | rotational_velocity: Random.range(-30, 30),
57 | shape: {
58 | type: Random.chance() ? RECT : POLYGON,
59 | radius: Random.range(16, 32),
60 | width: Random.range(8, 64),
61 | height: Random.range(8, 64),
62 | sides: Random.range_int(5, 8)
63 | }
64 | }));
65 |
66 | timer = 0;
67 | }
68 | // Reset any off-screen Bodies
69 | world.for_each((member) -> {
70 | // Exclude the cursor
71 | if (member.id != cursor.id && offscreen(member, world)) {
72 | member.velocity.set(0, 0);
73 | member.set_position(Random.range(0, world.width), 0);
74 | }
75 | });
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/sample/state/StackingState.hx:
--------------------------------------------------------------------------------
1 | package state;
2 |
3 | import echo.Material;
4 | import echo.Body;
5 | import echo.World;
6 | import util.Random;
7 |
8 | class StackingState extends BaseState {
9 | var body_count:Int = 149;
10 |
11 | override public function enter(world:World) {
12 | Main.instance.state_text.text = "Sample: Stacking Boxes";
13 |
14 | // Create a material for all the shapes to share
15 | var material:Material = {elasticity: 0.7};
16 |
17 | // Add a bunch of random Physics Bodies to the World
18 | for (i in 0...body_count) {
19 | var b = new Body({
20 | x: Random.range(60, world.width - 60),
21 | y: Random.range(0, world.height / 2),
22 | material: material,
23 | shape: {
24 | type: RECT,
25 | width: Random.range(16, 48),
26 | height: Random.range(16, 48),
27 | }
28 | });
29 | world.add(b);
30 | }
31 |
32 | // Add a Physics body at the bottom of the screen for the other Physics Bodies to stack on top of
33 | // This body has a mass of 0, so it acts as an immovable object
34 | world.add(new Body({
35 | mass: STATIC,
36 | x: world.width / 2,
37 | y: world.height - 10,
38 | material: {elasticity: 0.2},
39 | shape: {
40 | type: RECT,
41 | width: world.width,
42 | height: 20
43 | }
44 | }));
45 |
46 | // Create a listener for collisions between the Physics Bodies
47 | world.listen();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/sample/state/StaticState.hx:
--------------------------------------------------------------------------------
1 | package state;
2 |
3 | import echo.Material;
4 | import echo.Body;
5 | import echo.World;
6 | import util.Random;
7 |
8 | class StaticState extends BaseState {
9 | var dynamics:Array;
10 | var statics:Array;
11 | var body_count:Int = 100;
12 | var static_count:Int = 500;
13 | var cursor:Body;
14 | var cursor_speed:Float = 10;
15 | var timer:Float;
16 |
17 | override public function enter(world:World) {
18 | Main.instance.state_text.text = "Sample: Optimized Statics";
19 | timer = 0;
20 |
21 | dynamics = [];
22 | statics = [];
23 |
24 | // Create a material for all the static shapes to share
25 | var static_material:Material = {elasticity: 1};
26 |
27 | for (i in 0...static_count) {
28 | var b = new Body({
29 | mass: STATIC,
30 | x: (world.width * 0.5) * Math.cos(i) + world.width * 0.5,
31 | y: (world.height * 0.5) * Math.sin(i) + world.height * 0.5,
32 | material: static_material,
33 | shape: {
34 | type: CIRCLE,
35 | radius: Random.range(2, 4),
36 | }
37 | });
38 | world.add(b);
39 | statics.push(b);
40 | }
41 |
42 | cursor = new Body({
43 | x: world.width * 0.5,
44 | y: world.height * 0.5,
45 | shape: {
46 | type: CIRCLE,
47 | radius: 16
48 | }
49 | });
50 | world.add(cursor);
51 |
52 | // Create a listener for collisions between the Physics Bodies
53 | world.listen(dynamics, statics);
54 | world.listen(cursor, dynamics);
55 | }
56 |
57 | override function step(world:World, dt:Float) {
58 | // Move the Cursor Body
59 | cursor.velocity.set(Main.instance.scene.mouseX - cursor.x, Main.instance.scene.mouseY - cursor.y);
60 | cursor.velocity *= cursor_speed;
61 |
62 | timer += dt;
63 | if (timer > 0.1 + Random.range(-0.2, 0.2)) {
64 | if (world.count < body_count + static_count) dynamics.push(world.add(new Body({
65 | x: (world.width * 0.5) + Random.range(-world.width * 0.3, world.width * 0.3),
66 | y: (world.height * 0.5) + Random.range(-world.height * 0.3, world.height * 0.3),
67 | max_velocity_length: 300,
68 | elasticity: 1,
69 | shape: {
70 | type: Random.chance() ? RECT : CIRCLE,
71 | radius: Random.range(8, 32),
72 | width: Random.range(8, 48),
73 | height: Random.range(8, 48),
74 | }
75 | })));
76 | timer = 0;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/sample/state/TileMapState.hx:
--------------------------------------------------------------------------------
1 | package state;
2 |
3 | import echo.Material;
4 | import echo.Body;
5 | import echo.World;
6 | import util.Random;
7 |
8 | class TileMapState extends BaseState {
9 | var cursor:Body;
10 | var cursor_speed:Float = 10;
11 | var body_count:Int = 50;
12 | var data = [
13 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
14 | 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
15 | 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
16 | 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
17 | 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
18 | 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
19 | 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
20 | 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
21 | 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
22 | 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
23 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
24 | ];
25 | var tile_width = 32;
26 | var tile_height = 32;
27 | var width_in_tiles = 15;
28 | var height_in_tiles = 12;
29 |
30 | override public function enter(world:World) {
31 | Main.instance.state_text.text = "Sample: Tilemap Generated Colliders";
32 |
33 | // Create a material for all the shapes to share
34 | var material:Material = {elasticity: 0.2};
35 |
36 | // Add a bunch of random Physics Bodies to the World
37 | var bodies = [];
38 | var hw = world.width / 2;
39 | var hh = world.height / 2;
40 | for (i in 0...body_count) {
41 | var b = new Body({
42 | x: Random.range(hw - 120, hw + 120),
43 | y: Random.range(hh - 64, hh + 64),
44 | material: material,
45 | rotation: Random.range(0, 360),
46 | drag_length: 10,
47 | shape: {
48 | type: POLYGON,
49 | radius: Random.range(8, 16),
50 | sides: Random.range_int(3, 8)
51 | }
52 | });
53 | bodies.push(b);
54 | world.add(b);
55 | }
56 |
57 | // Add the Cursor
58 | var center = world.center();
59 | cursor = new Body({
60 | x: center.x,
61 | y: center.y,
62 | shape: {
63 | type: RECT,
64 | width: 16
65 | }
66 | });
67 | center.put();
68 | world.add(cursor);
69 |
70 | // Generate an optimized Array of Bodies from Tilemap data
71 | var tilemap = echo.util.TileMap.generate(data, tile_width, tile_height, width_in_tiles, height_in_tiles, 72, 5);
72 | for (b in tilemap) world.add(b);
73 |
74 | // Create a listener for collisions between the Physics Bodies
75 | world.listen(bodies);
76 |
77 | // Create a listener for collisions between the Physics Bodies and the Tilemap Colliders
78 | world.listen(bodies, tilemap);
79 |
80 | // Create a listener for collisions between the Physics Bodies and the cursor
81 | world.listen(bodies, cursor);
82 | }
83 |
84 | override function step(world:World, dt:Float) {
85 | // Move the Cursor Body
86 | cursor.velocity.set(Main.instance.scene.mouseX - cursor.x, Main.instance.scene.mouseY - cursor.y);
87 | cursor.velocity *= cursor_speed;
88 |
89 | // Reset any off-screen Bodies
90 | world.for_each((member) -> {
91 | if (member != cursor && offscreen(member, world)) {
92 | member.velocity.set(0, 0);
93 | member.set_position(Random.range(0, world.width), 0);
94 | }
95 | });
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/sample/state/TileMapState2.hx:
--------------------------------------------------------------------------------
1 | package state;
2 |
3 | import echo.Material;
4 | import echo.Body;
5 | import echo.World;
6 | import util.Random;
7 |
8 | class TileMapState2 extends BaseState {
9 | var cursor:Body;
10 | var cursor_speed:Float = 10;
11 | var body_count:Int = 30;
12 | var data = [
13 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 20, -1, -1, -1, -1, -1, -1, -1, 15, 19, 18, 1, 1, 1, -1, 2, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, 21,
14 | 1, 1, -1, 15, 16, 0, -1, -1, -1, -1, -1, -1, -1, -1, 23, 1, 1, -1, -1, -1, -1, -1, -1, -1, 4, 5, 6, 7, -1, 10, 1, 1, -1, -1, -1, -1, -1, -1, -1, 17, 18,
15 | 19, 20, -1, 8, 1, 1, -1, -1, 10, 11, -1, -1, -1, -1, -1, -1, -1, -1, 21, 1, 1, -1, -1, 23, 24, -1, -1, 12, -1, -1, -1, -1, -1, 23, 1, 1, 3, -1, -1, -1,
16 | -1, -1, -1, -1, -1, -1, -1, 12, -1, 1, 1, 1, 6, 7, -1, -1, -1, -1, 4, 5, 1, 3, -1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
17 | ];
18 | var tile_width = 32;
19 | var tile_height = 32;
20 | var width_in_tiles = 15;
21 | var height_in_tiles = 12;
22 |
23 | override public function enter(world:World) {
24 | Main.instance.state_text.text = "Sample: Tilemap Generated Colliders (Slopes/Custom)";
25 |
26 | // Create a material for all the shapes to share
27 | var material:Material = {elasticity: 0.2};
28 |
29 | // Add a bunch of random Physics Bodies to the World
30 | var bodies = [];
31 | var hw = world.width / 2;
32 | var hh = world.height / 2;
33 |
34 | for (i in 0...body_count) {
35 | var b = new Body({
36 | x: Random.range(hw - 120, hw + 120),
37 | y: Random.range(world.y + 64, world.y + 72),
38 | material: material,
39 | rotation: Random.range(0, 360),
40 | drag_length: 10,
41 | shape: {
42 | type: POLYGON,
43 | radius: Random.range(8, 16),
44 | sides: Random.range_int(3, 8)
45 | }
46 | });
47 | bodies.push(b);
48 | world.add(b);
49 | }
50 |
51 | // Add the Cursor
52 | var center = world.center();
53 | cursor = new Body({
54 | x: center.x,
55 | y: center.y,
56 | shape: {
57 | type: RECT,
58 | width: 16
59 | }
60 | });
61 | center.put();
62 | world.add(cursor);
63 |
64 | // Generate an optimized Array of Bodies from Tilemap data
65 | var tilemap = echo.util.TileMap.generate(data, tile_width, tile_height, width_in_tiles, height_in_tiles, 72, 5, 1, [
66 | {
67 | index: 2,
68 | slope_direction: TopLeft
69 | },
70 | {
71 | index: 4,
72 | slope_direction: TopLeft,
73 | slope_shape: {angle: Gentle, size: Thin}
74 | },
75 | {
76 | index: 5,
77 | slope_direction: TopLeft,
78 | slope_shape: {angle: Gentle, size: Thick}
79 | },
80 | {
81 | index: 10,
82 | slope_direction: TopLeft,
83 | slope_shape: {angle: Sharp, size: Thin}
84 | },
85 | {
86 | index: 8,
87 | slope_direction: TopLeft,
88 | slope_shape: {angle: Sharp, size: Thick}
89 | },
90 | {
91 | index: 3,
92 | slope_direction: TopRight
93 | },
94 | {
95 | index: 7,
96 | slope_direction: TopRight,
97 | slope_shape: {angle: Gentle, size: Thin}
98 | },
99 | {
100 | index: 6,
101 | slope_direction: TopRight,
102 | slope_shape: {angle: Gentle, size: Thick}
103 | },
104 | {
105 | index: 11,
106 | slope_direction: TopRight,
107 | slope_shape: {angle: Sharp, size: Thin}
108 | },
109 | {
110 | index: 9,
111 | slope_direction: TopRight,
112 | slope_shape: {angle: Sharp, size: Thick}
113 | },
114 | {
115 | index: 15,
116 | slope_direction: BottomLeft
117 | },
118 | {
119 | index: 17,
120 | slope_direction: BottomLeft,
121 | slope_shape: {angle: Gentle, size: Thin}
122 | },
123 | {
124 | index: 18,
125 | slope_direction: BottomLeft,
126 | slope_shape: {angle: Gentle, size: Thick}
127 | },
128 | {
129 | index: 23,
130 | slope_direction: BottomLeft,
131 | slope_shape: {angle: Sharp, size: Thin}
132 | },
133 | {
134 | index: 21,
135 | slope_direction: BottomLeft,
136 | slope_shape: {angle: Sharp, size: Thick}
137 | },
138 | {
139 | index: 16,
140 | slope_direction: BottomRight
141 | },
142 | {
143 | index: 20,
144 | slope_direction: BottomRight,
145 | slope_shape: {angle: Gentle, size: Thin}
146 | },
147 | {
148 | index: 19,
149 | slope_direction: BottomRight,
150 | slope_shape: {angle: Gentle, size: Thick}
151 | },
152 | {
153 | index: 24,
154 | slope_direction: BottomRight,
155 | slope_shape: {angle: Sharp, size: Thin}
156 | },
157 | {
158 | index: 22,
159 | slope_direction: BottomRight,
160 | slope_shape: {angle: Sharp, size: Thick}
161 | },
162 | {
163 | index: 12,
164 | custom_shape: {
165 | type: CIRCLE,
166 | radius: tile_width * 0.5,
167 | offset_x: tile_width * 0.5,
168 | offset_y: tile_height * 0.5
169 | }
170 | }
171 | ]);
172 | for (b in tilemap) world.add(b);
173 |
174 | // Create a listener for collisions between the Physics Bodies
175 | world.listen(bodies);
176 |
177 | // Create a listener for collisions between the Physics Bodies and the Tilemap Colliders
178 | world.listen(bodies, tilemap);
179 |
180 | // Create a listener for collisions between the Physics Bodies and the cursor
181 | world.listen(bodies, cursor);
182 | }
183 |
184 | override function step(world:World, dt:Float) {
185 | // Move the Cursor Body
186 | cursor.velocity.set(Main.instance.scene.mouseX - cursor.x, Main.instance.scene.mouseY - cursor.y);
187 | cursor.velocity *= cursor_speed;
188 |
189 | // Reset any off-screen Bodies
190 | world.for_each((member) -> {
191 | if (member != cursor && offscreen(member, world)) {
192 | member.velocity.set(0, 0);
193 | member.set_position(Random.range(0, world.width), 0);
194 | }
195 | });
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/sample/state/VerletState.hx:
--------------------------------------------------------------------------------
1 | package state;
2 |
3 | import hxd.Timer;
4 | import util.Random;
5 | import echo.util.verlet.Constraints;
6 | import echo.util.verlet.Composite;
7 | import echo.math.Vector2;
8 | import echo.util.verlet.Verlet;
9 | import echo.World;
10 |
11 | using hxd.Math;
12 |
13 | class VerletState extends BaseState {
14 | var verlet:Verlet;
15 | var grass:Array = [];
16 |
17 | override function enter(parent:World) {
18 | super.enter(parent);
19 | Main.instance.state_text.text = "Sample: Softbody Verlet Physics";
20 |
21 | verlet = new Verlet({
22 | width: parent.width,
23 | height: parent.height,
24 | gravity_y: 20
25 | });
26 |
27 | // Constrict composites into th simulation bounds
28 | verlet.bounds_bottom = true;
29 | verlet.bounds_left = true;
30 | verlet.bounds_right = true;
31 |
32 | // Create a couple random rectangles
33 | for (i in 0...4) {
34 | var box = Verlet.rect(Random.range(parent.width * 0.2, parent.width * 0.8), 80, 30, 40, 0.7);
35 | box.dots[0].x += Math.random() * 10;
36 | box.dots[0].y -= Math.random() * 20;
37 | verlet.add(box);
38 | }
39 |
40 | // Create a rope
41 | var rope = Verlet.rope([for (i in 0...10) new Vector2(80 + i * 10, 70)], 0.7, [0]);
42 | verlet.add(rope);
43 |
44 | // Create some grass
45 | var i = 0.;
46 | while (i < parent.width) {
47 | var g = new Composite();
48 | g.add_dot(i, parent.height);
49 | g.add_dot(i, parent.height - Random.range(4, 6));
50 | g.add_dot(i, parent.height - Random.range(11, 16));
51 | g.add_dot(i, parent.height - Random.range(19, 23));
52 | g.pin(0);
53 | g.pin(1);
54 | g.add_constraint(new DistanceConstraint(g.dots[0], g.dots[1], 0.97));
55 | g.add_constraint(new DistanceConstraint(g.dots[1], g.dots[2], 0.97));
56 | g.add_constraint(new DistanceConstraint(g.dots[2], g.dots[3], 0.97));
57 | g.add_constraint(new RotationConstraint(g.dots[0], g.dots[1], g.dots[2], 0.3));
58 | g.add_constraint(new RotationConstraint(g.dots[1], g.dots[2], g.dots[3], 0.1));
59 | verlet.add(g);
60 | grass.push(g);
61 | i += Random.range(3, 7);
62 | }
63 |
64 | // Create a cloth
65 | var cloth = Verlet.cloth(250, 0, 130, 130, 13, 6, .93);
66 | verlet.add(cloth);
67 | }
68 |
69 | override function step(parent:World, dt:Float) {
70 | super.step(parent, dt);
71 |
72 | var w = Math.lerp(-1, 1, Main.instance.scene.mouseX / parent.width);
73 | if (w.isNaN()) w = 0;
74 | for (g in grass) g.dots[2].ax = Math.random() * 60 * w;
75 | if (Main.instance.playing) verlet.step(dt);
76 | Main.instance.debug.draw_verlet(verlet);
77 | }
78 |
79 | override function exit(world:World) {
80 | super.exit(world);
81 | verlet.dispose();
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/sample/test.hxml:
--------------------------------------------------------------------------------
1 | -debug
2 | build.hxml
3 | -cmd hl bin/sample.hl
--------------------------------------------------------------------------------
/sample/util/Assets.hx:
--------------------------------------------------------------------------------
1 | package util;
2 |
3 | import haxe.io.Path;
4 | /**
5 | * Macro for copying assets from one folder to another
6 | */
7 | class Assets {
8 | #if macro
9 | private static function copy(sourceDir:String, targetDir:String):Int {
10 | var numCopied:Int = 0;
11 |
12 | if (!sys.FileSystem.exists(targetDir)) sys.FileSystem.createDirectory(targetDir);
13 |
14 | for (entry in sys.FileSystem.readDirectory(sourceDir)) {
15 | var srcFile:String = Path.join([sourceDir, entry]);
16 | var dstFile:String = Path.join([targetDir, entry]);
17 |
18 | if (sys.FileSystem.isDirectory(srcFile)) numCopied += copy(srcFile, dstFile);
19 | else {
20 | sys.io.File.copy(srcFile, dstFile);
21 | numCopied++;
22 | }
23 | }
24 | return numCopied;
25 | }
26 |
27 | public static function copyProjectAssets() {
28 | var cwd:String = Sys.getCwd();
29 | var assetSrcFolder = Path.join([cwd, "assets"]);
30 | var assetsDstFolder = Path.join([cwd, "bin"]);
31 |
32 | // make sure the assets folder exists
33 | if (!sys.FileSystem.exists(assetsDstFolder)) sys.FileSystem.createDirectory(assetsDstFolder);
34 |
35 | // copy it!
36 | var numCopied = copy(assetSrcFolder, assetsDstFolder);
37 | Sys.println('Copied ${numCopied} project assets to ${assetsDstFolder}!');
38 | }
39 | #end
40 | }
41 |
--------------------------------------------------------------------------------
/sample/util/FSM.hx:
--------------------------------------------------------------------------------
1 | package util;
2 |
3 | class State {
4 | public function new() {}
5 |
6 | public function enter(parent:T) {}
7 |
8 | public function step(parent:T, dt:Float) {}
9 |
10 | public function exit(parent:T) {}
11 | }
12 |
13 | class FSM {
14 | var parent:T;
15 | var current:State;
16 | var requested:State;
17 |
18 | public function new(parent:T, initialState:State) {
19 | this.parent = parent;
20 | requested = initialState;
21 | }
22 |
23 | public function set(state:State):State return requested = state;
24 |
25 | public function step(dt:Float) {
26 | if (requested != null) {
27 | if (current != null) {
28 | current.exit(parent);
29 | }
30 | current = requested;
31 | current.enter(parent);
32 | requested = null;
33 | }
34 |
35 | current.step(parent, dt);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/sample/util/Random.hx:
--------------------------------------------------------------------------------
1 | package util;
2 |
3 | class Random {
4 | /**
5 | * Takes a minimum and maximum Float and returns a random value bewteen them.
6 | * Leave blank to return a value between -1 and 1
7 | * @param min Minimum desired result
8 | * @param max Maximum desired result
9 | */
10 | public static inline function range(?min:Float = -1, ?max:Float = 1):Float return min + Math.random() * (max - min);
11 |
12 | public static inline function range_int(?min:Float = -1, ?max:Float = 1):Int return Std.int(range(min, max));
13 |
14 | public static inline function range_int_to_string(?min:Float, ?max:Float):String return "" + Math.floor(range_int(min, max));
15 |
16 | public static inline function chance(percent:Float = 50):Bool return Math.random() < percent / 100;
17 | }
18 |
--------------------------------------------------------------------------------
/test.hxml:
--------------------------------------------------------------------------------
1 | # class paths
2 | -cp test
3 |
4 | # entry point
5 | -main Main
6 |
7 | # libraries
8 | -lib echo
9 |
10 | # math lib integrations
11 | # -lib hxmath
12 | # -D ECHO_USE_HXMATH
13 |
14 | # -lib zerolib
15 | # -D ECHO_USE_ZEROLIB
16 |
17 | # -lib vector-math
18 | # -D ECHO_USE_VECTORMATH
19 |
20 | # -D ECHO_USE_HEAPS
21 |
22 | --each
23 |
24 | # output
25 | -hl bin/test.hl
26 | --no-output
27 |
28 | --next
29 |
30 | -js bin/test.js
31 | --no-output
32 |
--------------------------------------------------------------------------------
/test/Main.hx:
--------------------------------------------------------------------------------
1 | import echo.Echo;
2 |
3 | class Main {
4 | static function main() {
5 | // Create a World to hold all the Physics Bodies
6 | // Worlds, Bodies, and Listeners are all created with optional configuration objects.
7 | // This makes it easy to construct object configurations, reuse them, and even easily load them from JSON!
8 | var world = Echo.start({
9 | width: 64, // Affects the bounds for collision checks.
10 | height: 64, // Affects the bounds for collision checks.
11 | gravity_y: 20, // Force of Gravity on the Y axis. Also available for the X axis.
12 | iterations: 2 // Sets the number of Physics iterations that will occur each time the World steps.
13 | });
14 |
15 | // Create a Body with a Circle Collider and add it to the World
16 | var a = world.make({
17 | material: {elasticity: 0.2},
18 | shape: {
19 | type: CIRCLE,
20 | radius: 16,
21 | }
22 | });
23 |
24 | // Create a Body with a Rectangle collider and add it to the World
25 | // This Body will be static (ie have a Mass of zero), rendering it as unmovable
26 | // This is useful for things like platforms or walls.
27 | var b = world.make({
28 | mass: STATIC, // Setting this to Static/zero makes the body unmovable by forces and collisions
29 | y: 48, // Set the object's Y position below the Circle, so that gravity makes them collide
30 | material: {elasticity: 0.2},
31 | shape: {
32 | type: RECT,
33 | width: 10,
34 | height: 10
35 | }
36 | });
37 |
38 | // Create a listener and attach it to the World.
39 | // This listener will react to collisions between Body "a" and Body "b", based on the configuration options passed in
40 | world.listen(a, b, {
41 | separate: true, // Setting this to true will cause the Bodies to separate on Collision. This defaults to true
42 | enter: (a, b, c) -> trace("Collision Entered"), // This callback is called on the first frame that a collision starts
43 | stay: (a, b, c) -> trace("Collision Stayed"), // This callback is called on frames when the two Bodies are continuing to collide
44 | exit: (a, b) -> trace("Collision Exited"), // This callback is called when a collision between the two Bodies ends
45 | });
46 |
47 | // Set up a Timer to act as an update loop (at 60fps)
48 | new haxe.Timer(16).run = () -> {
49 | // Step the World's Physics Simulation forward (at 60fps)
50 | world.step(16 / 1000);
51 | // Log the World State in the Console
52 | echo.util.Debug.log(world);
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------