91 |
92 |
93 |
94 |
95 |
101 |
102 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/sample/Main.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import hxd.Key;
4 | import echo.Echo;
5 | import echo.World;
6 | import echo.util.Debug;
7 | import util.FSM;
8 | import state.*;
9 |
10 | class Main extends BaseApp {
11 | public static var instance:Main;
12 |
13 | public var scene:h2d.Scene;
14 | public var state_text:h2d.Text;
15 | public var gravity_slider:h2d.Slider;
16 | public var iterations_slider:h2d.Slider;
17 | public var playing:Bool = true;
18 |
19 | var width:Int = 640;
20 | var height:Int = 360;
21 | var world:World;
22 | var members_text:h2d.Text;
23 | var fps_text:h2d.Text;
24 |
25 | override function init() {
26 | instance = this;
27 |
28 | // Create a World to hold all the Physics Bodies
29 | world = Echo.start({
30 | width: width,
31 | height: height,
32 | gravity_y: 100,
33 | iterations: 5,
34 | history: 1000
35 | });
36 |
37 | // Reduce Quadtree depths - our World is very small, so not many subdivisions of the Quadtree are actually needed.
38 | // This can help with performance by limiting the Quadtree's overhead on simulations with small Body counts!
39 | world.quadtree.max_depth = 3;
40 | world.static_quadtree.max_depth = 3;
41 |
42 | // Increase max contents per Quadtree depth - can help reduce Quadtree subdivisions in smaller World sizes.
43 | // Tuning these Quadtree settings can be very useful when optimizing for performance!
44 | world.quadtree.max_contents = 20;
45 | world.static_quadtree.max_contents = 20;
46 |
47 | // Set up our Sample States
48 | sample_states = [
49 | PolygonState, StackingState, MultiShapeState, ShapesState, GroupsState, StaticState, LinecastState, Linecast2State, TileMapState, TileMapState2,
50 | BezierState, VerletState
51 | ];
52 | index = 0;
53 | // Create a State Manager and pass it the World and the first Sample
54 | fsm = new FSM(world, Type.createInstance(sample_states[index], []));
55 | // Create a Debug drawer to display debug graphics
56 | debug = new HeapsDebug(s2d);
57 | // Set the Background color of the Scene
58 | engine.backgroundColor = 0x45283c;
59 | // Set the Heaps Scene size
60 | s2d.scaleMode = LetterBox(width, height);
61 | // Get a static reference to the Heaps scene so we can access it later
62 | scene = s2d;
63 | // Add the UI elements
64 | add_ui();
65 | }
66 |
67 | override function update(dt:Float) {
68 | // Draw the World
69 | debug.draw(world);
70 |
71 | if (world.history != null) {
72 | // Press Left to undo
73 | if (Key.isDown(Key.LEFT)) {
74 | world.undo();
75 | playing = false;
76 | }
77 | // Press Right to redo
78 | if (Key.isDown(Key.RIGHT)) {
79 | world.redo();
80 | playing = false;
81 | }
82 | // Press Space to play/pause
83 | if (Key.isPressed(Key.SPACE)) playing = !playing;
84 | }
85 | // Hold Shift for slowmo debugging
86 | var fdt = Key.isDown(Key.SHIFT) ? dt * 0.3 : dt;
87 | // Update the current Sample State
88 | fsm.step(fdt);
89 | // Step the World Forward
90 | if (playing) world.step(fdt);
91 |
92 | // Update GUI text
93 | members_text.text = 'Bodies: ${world.count}';
94 | fps_text.text = 'FPS: ${engine.fps}';
95 | }
96 |
97 | function add_ui() {
98 | fui = new h2d.Flow(s2d);
99 | fui.y = 5;
100 | fui.padding = 5;
101 | fui.verticalSpacing = 5;
102 | fui.layout = Vertical;
103 |
104 | var tui = new h2d.Flow(s2d);
105 | tui.padding = 5;
106 | tui.verticalSpacing = 5;
107 | tui.layout = Vertical;
108 | tui.y = s2d.height - 90;
109 | fps_text = addText("FPS: ", tui);
110 | members_text = addText("Bodies: ", tui);
111 | state_text = addText("Sample: ", tui);
112 | var buttons = new h2d.Flow(tui);
113 | buttons.horizontalSpacing = 2;
114 |
115 | var bui = new h2d.Flow(s2d);
116 | bui.padding = 5;
117 | bui.verticalSpacing = 5;
118 | bui.layout = Vertical;
119 | bui.y = s2d.height - 65;
120 | bui.x = s2d.width - 150;
121 | addText("Arrow Keys: Undo/Redo", bui);
122 | addText("Spacebar: Pause/Play", bui);
123 | addText("Hold Shift: Slowmo", bui);
124 |
125 | addButton("Previous", previous_state, buttons);
126 | addButton("Restart", reset_state, buttons);
127 | addButton("Next", next_state, buttons);
128 | gravity_slider = addSlider("Gravity", () -> return world.gravity.y, (v) -> world.gravity.y = v, -100, 300);
129 | iterations_slider = addSlider("Iterations", () -> return world.iterations, (v) -> world.iterations = Std.int(v), 1, 10, true);
130 | }
131 |
132 | static function main() {
133 | new Main();
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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, body.rotational_drag, accel_rot, 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/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 |
--------------------------------------------------------------------------------
/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
248 | abstract Direction(Int) from Int to Int {
249 | var TOP = 0;
250 | }
251 |
--------------------------------------------------------------------------------
/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 | var init:Bool;
56 |
57 | public function new(options:WorldOptions) {
58 | members = options.members == null ? [] : options.members;
59 | init = false;
60 | width = options.width < 1?throw("World must have a width of at least 1") : options.width;
61 | height = options.height < 1?throw("World must have a width of at least 1") : options.height;
62 | x = options.x == null ? 0 : options.x;
63 | y = options.y == null ? 0 : options.y;
64 | gravity = new Vector2(options.gravity_x == null ? 0 : options.gravity_x, options.gravity_y == null ? 0 : options.gravity_y);
65 | reset_quadtrees();
66 |
67 | listeners = new Listeners(options.listeners);
68 | iterations = options.iterations == null ? 5 : options.iterations;
69 | if (options.history != null) history = new History(options.history);
70 | }
71 | /**
72 | * Sets the size of the World. Only Bodies within the world bound will be collided
73 | * @param x The x position of the world bounds
74 | * @param y The y position of the world bounds
75 | * @param width The width of the world bounds
76 | * @param height The height of the world bounds
77 | */
78 | public inline function set(x:Float, y:Float, width:Float, height:Float) {
79 | init = false;
80 | this.x = x;
81 | this.y = y;
82 | this.width = width;
83 | this.height = height;
84 | init = true;
85 | reset_quadtrees();
86 | }
87 | /**
88 | * Sets the size of the World based on a given shape.
89 | * @param s The shape to use as the boundaries of the World
90 | */
91 | public inline function set_from_shape(s:Shape) {
92 | x = s.left;
93 | y = s.top;
94 | width = s.right - x;
95 | height = s.bottom - y;
96 | }
97 | /**
98 | * Sets the size of the World based just large enough to encompass all the members.
99 | */
100 | public function set_from_members() {
101 | var l = 0.0;
102 | var r = 0.0;
103 | var t = 0.0;
104 | var b = 0.0;
105 |
106 | for (m in members) {
107 | for (s in m.shapes) {
108 | if (s.left < l) l = s.left;
109 | if (s.right > r) r = s.right;
110 | if (s.top < t) t = s.top;
111 | if (s.bottom > b) b = s.bottom;
112 | }
113 | }
114 |
115 | set(l, t, r - l, b - t);
116 | }
117 |
118 | public inline function center(?rect:Rect):Rect {
119 | 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);
120 | }
121 |
122 | public function add(body:Body):Body {
123 | if (body.world == this) return body;
124 | if (body.world != null) body.remove();
125 | body.world = this;
126 | body.dirty = true;
127 | members.push(body);
128 | body.quadtree_data = {id: body.id, bounds: body.bounds(), flag: false};
129 | body.is_static() ? static_quadtree.insert(body.quadtree_data) : quadtree.insert(body.quadtree_data);
130 | return body;
131 | }
132 |
133 | public function remove(body:Body):Body {
134 | quadtree.remove(body.quadtree_data);
135 | static_quadtree.remove(body.quadtree_data);
136 | members.remove(body);
137 | body.world = null;
138 | return body;
139 | }
140 |
141 | public inline function iterator():Iterator return members.iterator();
142 | /**
143 | * Returns a new Array containing every dynamic `Body` in the World.
144 | */
145 | public inline function dynamics():Array return members.filter(b -> return b.is_dynamic());
146 | /**
147 | * Returns a new Array containing every static `Body` in the World.
148 | */
149 | public inline function statics():Array return members.filter(b -> return b.is_static());
150 | /**
151 | * Runs a function on every `Body` in the World
152 | * @param f Function to perform on each `Body`.
153 | * @param recursive Currently not supported.
154 | */
155 | public inline function for_each(f:Body->Void, recursive:Bool = true) for (b in members) f(cast b);
156 | /**
157 | * Runs a function on every dynamic `Body` in the World
158 | * @param f Function to perform on each dynamic `Body`.
159 | * @param recursive Currently not supported.
160 | */
161 | public inline function for_each_dynamic(f:Body->Void, recursive:Bool = true) for (b in members) if (b.is_dynamic()) f(cast b);
162 | /**
163 | * Runs a function on every static `Body` in the World
164 | * @param f Function to perform on each static `Body`.
165 | * @param recursive Currently not supported.
166 | */
167 | public inline function for_each_static(f:Body->Void, recursive:Bool = true) for (b in members) if (b.is_static()) f(cast b);
168 | /**
169 | * Clears the World's members and listeners.
170 | */
171 | public function clear() {
172 | while (members.length > 0) {
173 | var m = members.pop();
174 | if (m != null) m.remove();
175 | }
176 | reset_quadtrees();
177 | listeners.clear();
178 | }
179 | /**
180 | * Disposes the World. DO NOT use the World after disposing it, as it could lead to null reference errors.
181 | */
182 | public function dispose() {
183 | for_each(b -> b.remove());
184 | members = null;
185 | gravity = null;
186 | quadtree.put();
187 | listeners.dispose();
188 | listeners = null;
189 | history = null;
190 | }
191 | /**
192 | * Resets the World's dynamic and static Quadtrees.
193 | */
194 | public function reset_quadtrees() {
195 | init = true;
196 | if (quadtree != null) quadtree.put();
197 | quadtree = QuadTree.get();
198 | if (static_quadtree != null) static_quadtree.put();
199 | static_quadtree = QuadTree.get();
200 | var r = center().to_aabb(true);
201 | quadtree.load(r);
202 | static_quadtree.load(r);
203 | for_each((member) -> {
204 | if (member.is_dynamic()) {
205 | member.dirty = true;
206 | }
207 | else {
208 | member.bounds(member.quadtree_data.bounds);
209 | static_quadtree.update(member.quadtree_data);
210 | }
211 | });
212 | }
213 |
214 | inline function get_count():Int return members.length;
215 |
216 | inline function set_x(value:Float) {
217 | x = value;
218 | if (init) reset_quadtrees();
219 | return x;
220 | }
221 |
222 | inline function set_y(value:Float) {
223 | y = value;
224 | if (init) reset_quadtrees();
225 | return y;
226 | }
227 |
228 | inline function set_width(value:Float) {
229 | width = value;
230 | if (init) reset_quadtrees();
231 | return height;
232 | }
233 |
234 | inline function set_height(value:Float) {
235 | height = value;
236 | if (init) reset_quadtrees();
237 | return height;
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------