Skip to content

Instantly share code, notes, and snippets.

@AustinEast
Forked from 01010111/FlxEcho.hx
Last active March 19, 2021 19:58
Show Gist options
  • Save AustinEast/524db026a4fea298a0eebf19459131cc to your computer and use it in GitHub Desktop.
Save AustinEast/524db026a4fea298a0eebf19459131cc to your computer and use it in GitHub Desktop.
Quick Flixel <-> Echo integration

2021 Update - The code in this Gist has been compiled into a haxelib for a more convenient experience! Please upgrade as this Gist will no longer be updated! Check out the repo here: https://github.com/AustinEast/echo-flixel

Quick and easy integration of Echo Physics with Haxeflixel! To get started follow these steps:

  1. Copy the content from 01-Project.xml to your own Project.xml.
  2. Create a new directory named util in the root of source code directory.
  3. Create a new file named FlxEcho.hx in the util directory.
  4. Copy the content from 02-FlxEcho.hx into the FlxEcho.hx file.

After that, you're ready to go! Check out 03-PlayState.hx to see a simple example of how to use FlxEcho.

<!-- Adds an `object` field with `FlxObject` type to echo's `Body` class -->
<haxeflag name="--macro" value="echo.Macros.add_data('object','flixel.FlxObject')"/>
package util;
import echo.Body;
import echo.Echo;
import echo.World;
import echo.data.Options.BodyOptions;
import echo.data.Options.ListenerOptions;
import echo.data.Options.WorldOptions;
import echo.util.AABB;
import flixel.FlxBasic;
import flixel.FlxG;
import flixel.FlxObject.*;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.group.FlxGroup;
import flixel.util.FlxColor;
using Math;
using Std;
using hxmath.math.Vector2;
#if FLX_DEBUG
import echo.util.Debug.OpenFLDebug;
import flixel.system.ui.FlxSystemButton;
import openfl.display.BitmapData;
#end
class FlxEcho extends FlxBasic
{
/**
* Gets the FlxEcho instance, which contains the current Echo World. May be Null if `FlxEcho.init` has not been called.
*/
public static var instance(default, null):FlxEcho;
/**
* Toggles whether the physics simulation updates or not.
*/
public static var updates:Bool;
/**
* Set this to `true` to have each physics body's acceleration reset after updating the physics simulation. Useful if you want to treat acceleration as a non-constant force.
*/
public static var reset_acceleration:Bool;
/**
* Toggles whether the physics' debug graphics are drawn. Also Togglable through the Flixel Debugger (click the "E" icon). If Flixel isnt ran with Debug mode, this does nothing.
*/
public static var draw_debug(default, set):Bool;
public var world(default, null):World;
public var groups:Map<FlxGroup, Array<Body>>;
public var bodies:Map<FlxObject, Body>;
#if FLX_DEBUG
public static var debug_drawer:OpenFLDebug;
static var draw_debug_button:FlxSystemButton;
static var icon_data = [
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]];
#end
/**
* Init the Echo physics simulation
* @param options The attributes that define the physics `World`
* @param force Set to `true` to force the physics `World` to get recreated
*/
public static function init(options:WorldOptions, force:Bool = false)
{
if (force && instance != null)
{
FlxG.plugins.remove(instance);
FlxG.signals.preStateSwitch.remove(on_state_switch);
instance.destroy();
instance = null;
}
if (instance == null)
{
instance = new FlxEcho(options);
FlxG.plugins.add(instance);
FlxG.signals.preStateSwitch.add(on_state_switch);
}
updates = true;
reset_acceleration = false;
#if FLX_DEBUG
var icon = new BitmapData(11, 11, true, FlxColor.TRANSPARENT);
for (y in 0...icon_data.length) for (x in 0...icon_data[y].length) if (icon_data[y][x] > 0) icon.setPixel32(x, y, FlxColor.WHITE);
if (draw_debug_button == null)
{
draw_debug_button = FlxG.debugger.addButton(RIGHT, icon, () -> draw_debug = !draw_debug, true, true);
}
draw_debug = draw_debug;
#end
}
/**
* Add physics body to FlxObject
*/
public static function add_body(object:FlxObject, ?options:BodyOptions):Body
{
var old_body = instance.bodies.get(object);
if (old_body != null)
{
old_body.dispose();
}
if (options == null) options = {};
if (options.x == null) options.x = object.x + object.width * 0.5;
if (options.y == null) options.y = object.y + object.height * 0.5;
if (options.shape == null && options.shapes == null && options.shape_instance == null && options.shape_instances == null) options.shape = {
type: RECT,
width: object.width,
height: object.height
}
var body = new Body(options);
body.object = object;
instance.bodies.set(object, body);
instance.world.add(body);
return body;
}
/**
* Adds FlxObject to FlxGroup, and the FlxObject's associated physics body to the FlxGroup's associated physics group
*/
public inline static function add_to_group(object:FlxObject, group:FlxGroup)
{
group.add(object);
if (!instance.groups.exists(group)) instance.groups.set(group, []);
if (instance.bodies.exists(object)) instance.groups[group].push(instance.bodies[object]);
}
/**
* Creates a physics listener
*/
public static function listen(a:FlxBasic, b:FlxBasic, ?options:ListenerOptions)
{
get_listener_options(options);
var a_is_object = a.is(FlxObject);
var b_is_object = b.is(FlxObject);
if (!a_is_object) add_group_bodies(cast a);
if (!b_is_object) add_group_bodies(cast b);
instance.world.listen(!a_is_object ? instance.groups[cast a] : instance.bodies[cast a],
!b_is_object ? instance.groups[cast b] : instance.bodies[cast b], options);
}
/**
* Performs a one-time collision check
*/
public static function check(a:FlxBasic, b:FlxBasic, ?options:ListenerOptions)
{
get_listener_options(options);
var a_is_object = a.is(FlxObject);
var b_is_object = b.is(FlxObject);
if (!a_is_object) add_group_bodies(cast a);
if (!b_is_object) add_group_bodies(cast b);
instance.world.check(!a_is_object ? instance.groups[cast a] : instance.bodies[cast a],
!b_is_object ? instance.groups[cast b] : instance.bodies[cast b], options);
}
/**
* Get the physics body associated with a FlxObject
*/
public static inline function get_body(object:FlxObject):Body
return instance.bodies[object];
/**
* Sets a physics body to a FlxObject
*/
public static function set_body(object:FlxObject, body:Body):Body
{
var old_body = instance.bodies.get(object);
if (old_body != null)
{
old_body.dispose();
old_body.object = null;
}
body.object = object;
instance.bodies.set(object, body);
instance.world.add(body);
return body;
}
/**
* Removes the physics body from the simulation
*/
public static function remove_body(body:Body):Bool
{
for (o => b in instance.bodies) if (b == body)
{
body.remove();
instance.bodies.remove(o);
return true;
}
return false;
}
/**
* Get the FlxObject associated with a physics body
*/
public static function get_object(body:Body):FlxObject
{
return body.object;
}
/**
* Removes (and optionally disposes) the physics body associated with the FlxObject
*/
public static function remove_object(object:FlxObject, dispose:Bool = true):Bool
{
var body = instance.bodies.get(object);
if (body == null) return false;
if (dispose)
{
body.dispose();
body.object = null;
}
return instance.bodies.remove(object);
}
/**
* Associates a FlxGroup to a physics group
*/
public static inline function add_group_bodies(group:FlxGroup)
{
if (!instance.groups.exists(group)) instance.groups.set(group, []);
}
/**
* Gets a FlxGroup's associated physics group
*/
public static inline function get_group_bodies(group:FlxGroup):Null<Array<Body>>
{
return instance.groups.get(group);
}
/**
* Removes the FlxGroup's associated physics group from the simulation
*/
public static inline function remove_group_bodies(group:FlxGroup)
{
return instance.groups.remove(group);
}
/**
* Removes the FlxObject from the FlxGroup, and the FlxObject's associated physics body from the FlxGroup's associated physics group
*/
public static inline function remove_from_group(object:FlxObject, group:FlxGroup):Bool
{
group.remove(object);
if (!instance.groups.exists(group) || !instance.bodies.exists(object)) return false;
return instance.groups[group].remove(instance.bodies[object]);
}
/**
* Clears the physics world - all Bodies, Listeners, and any associated FlxObjects and FlxGroups
*/
public static function clear()
{
for (body in instance.bodies) body.dispose();
instance.bodies.clear();
instance.groups.clear();
instance.world.clear();
}
static function get_listener_options(?options:ListenerOptions)
{
if (options == null) options = {};
var temp_stay = options.stay;
options.stay = (a, b, c) ->
{
if (temp_stay != null) temp_stay(a, b, c);
if (options.separate == null || options.separate) for (col in c) set_touching(get_object(a), [CEILING, WALL, FLOOR]
[col.normal.dot(Vector2.yAxis).round() + 1]);
}
#if ARCADE_PHYSICS
var temp_condition = options.condition;
options.condition = (a, b, c) ->
{
for (col in c) square_normal(col.normal);
if (temp_condition != null) return temp_condition(a, b, c);
return true;
}
#end
return options;
}
static inline function update_body_object(body:Body)
{
if (body.object == null) return;
body.object.setPosition(body.x, body.y);
if (body.object.isOfType(FlxSprite))
{
var sprite:FlxSprite = cast body.object;
sprite.x -= sprite.origin.x;
sprite.y -= sprite.origin.y;
}
body.object.angle = body.rotation;
if (reset_acceleration) body.acceleration.set(0, 0);
}
static inline function set_touching(object:FlxObject, touching:Int)
{
if (object.touching & touching == 0) object.touching += touching;
}
static function square_normal(normal:Vector2)
{
var len = normal.length;
var dot_x = normal.dot(Vector2.xAxis);
var dot_y = normal.dot(Vector2.yAxis);
if (dot_x.abs() > dot_y.abs()) dot_x > 0 ? normal.set(1, 0) : normal.set(-1, 0); else
dot_y > 0 ? normal.set(0, 1) : normal.set(0, -1);
normal.normalizeTo(len);
}
static function on_state_switch()
{
clear();
draw_debug = false;
#if FLX_DEBUG
if (draw_debug_button != null)
{
FlxG.debugger.removeButton(draw_debug_button);
draw_debug_button = null;
}
#end
}
static function set_draw_debug(v:Bool)
{
#if FLX_DEBUG
if (draw_debug_button != null) draw_debug_button.toggled = !v;
if (v)
{
if (debug_drawer == null)
{
debug_drawer = new OpenFLDebug();
debug_drawer.camera = AABB.get();
debug_drawer.draw_quadtree = false;
debug_drawer.canvas.scrollRect = null;
}
FlxG.addChildBelowMouse(debug_drawer.canvas);
}
else if (debug_drawer != null)
{
debug_drawer.clear();
FlxG.removeChild(debug_drawer.canvas);
}
#end
return draw_debug = v;
}
public function new(options:WorldOptions)
{
super();
groups = [];
bodies = [];
world = Echo.start(options);
}
override public function update(elapsed:Float)
{
if (updates)
world.step(elapsed);
for (body in bodies) update_body_object(body);
}
#if FLX_DEBUG
@:access(flixel.FlxCamera)
override function draw()
{
super.draw();
if (!draw_debug || debug_drawer == null || world == null) return;
// TODO - draw with full FlxG.cameras list
debug_drawer.camera.set_from_min_max(FlxG.camera.scroll.x, FlxG.camera.scroll.y, FlxG.camera.scroll.x + FlxG.camera.width,
FlxG.camera.scroll.y + FlxG.camera.height);
debug_drawer.draw(world);
var s = debug_drawer.canvas;
s.x = s.y = 0;
s.scaleX = s.scaleY = 1;
FlxG.camera.transformObject(s);
}
#end
override function destroy()
{
super.destroy();
for (body in bodies) body.dispose();
bodies.clear();
groups.clear();
world.dispose();
bodies = null;
groups = null;
world = null;
}
}
package states;
import flixel.FlxG;
import flixel.FlxState;
import flixel.FlxSprite;
import flixel.FlxObject.*;
import flixel.math.FlxPoint;
import flixel.group.FlxGroup;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import echo.util.TileMap;
using Math;
using util.FlxEcho;
using hxmath.math.Vector2;
using flixel.util.FlxArrayUtil;
using flixel.util.FlxSpriteUtil;
class PlayState extends FlxState
{
var player:Box;
var level_data = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
];
override function create() {
// First thing we want to do before creating any physics objects is init() our Echo world.
FlxEcho.init({
width: level_data[0].length * 16, // Make the size of your Echo world equal the size of your play field
height: level_data.length * 16,
gravity_y: 800
});
// Normal, every day FlxGroups!
var terrain = new FlxGroup();
add(terrain);
var bouncers = new FlxGroup();
add(bouncers);
// We'll use Echo's TileMap utility to generate physics bodies for our Tilemap - making sure to ignore any tile with the index 2 or 3 so we can create objects out of them later
var tiles = TileMap.generate(level_data.flatten2DArray(), 16, 16, level_data[0].length, level_data.length, 0, 0, 1, null, [2,3]);
for (tile in tiles) {
var bounds = tile.bounds(); // Get the bounds of the generated physics body to create a Box sprite from it
var bluebox = new Box(bounds.min_x, bounds.min_y, bounds.width.floor(), bounds.height.floor(), 0xFF0080FF);
bounds.put(); // Make sure to "put()" the bounds so that they can be reused later. This can really help with memory management!
bluebox.set_body(tile); // Attach the Generated physics body to the Box sprite
bluebox.add_to_group(terrain); // Instead of `group.add(object)` we use `object.add_to_group(group)`
}
// We'll step through our level data and add objects that way
for (j in 0...level_data.length) for (i in 0...level_data[j].length) {
switch (level_data[j][i]) {
case 2:
// Orange boxes will act like springs!
var orangebox = new Box(i * 16, j * 16, 16, 16, 0xFFFF8000);
// We'll set the origin and offset here so that we can animate our orange block later
orangebox.origin.y = 16;
orangebox.offset.y = -8;
orangebox.add_body({ mass: 0 }); // Create a new physics body for the Box sprite. We'll pass in body options with mass set to 0 so that it's static
orangebox.add_to_group(bouncers);
case 3:
player = new Box(i * 16, j * 16, 8, 12, 0xFFFF004D, true);
player.add_body();
add(player);
default: continue;
}
}
// lets add some ramps too! They'll belong to the same collision group as the blue boxes we made earlier.
for (i in 0...8) {
var ramp = new Ramp(16, 112 + i * 16, 16 + i * 16, 128 - i * 16, NW);
ramp.add_to_group(terrain);
}
// Our first physics listener collides our player with the terrain group.
player.listen(terrain);
// Our second physics listener collides our player with the bouncers group.
player.listen(bouncers, {
// We'll add this listener option - every frame our player object is colliding with a bouncer in the bouncers group we'll run this function
stay: (a, b, c) -> { // where a is our first physics body (`player`), b is the physics body it's colliding with (`orangebox`), and c is an array of collision data.
// for every instance of collision data
for (col in c) {
// This checks to see if the normal of our collision is pointing downward - you could use it for hop and bop games to see if a player has stomped on an enemy!
if (col.normal.dot(Vector2.yAxis).round() == 1) {
// set the player's velocity to go up!
a.velocity.y = -400;
// animate the orange box!
var b_object:FlxSprite = cast b.get_object();
b_object.scale.y = 1.5;
FlxTween.tween(b_object.scale, { y: 1 }, 0.5, { ease: FlxEase.elasticOut });
}
}
}
});
}
}
class Box extends FlxSprite {
var control:Bool;
public function new(x:Float, y:Float, w:Int, h:Int, c:Int, control:Bool = false) {
super(x, y);
makeGraphic(w, h, c);
this.control = control;
}
override function update(elapsed:Float) {
if (control) controls();
super.update(elapsed);
}
function controls() {
var body = this.get_body();
body.velocity.x = 0;
if (FlxG.keys.pressed.LEFT) body.velocity.x -= 128;
if (FlxG.keys.pressed.RIGHT) body.velocity.x += 128;
if (FlxG.keys.justPressed.UP && isTouching(FLOOR)) body.velocity.y -= 256;
}
}
class Ramp extends FlxSprite {
public function new(x:Float, y:Float, w:Int, h:Int, d:RampDirection) {
trace('$x / $y / $w / $h');
super(x, y);
makeGraphic(w, h, 0x00FFFFFF);
var verts = [ [0, 0], [w, 0], [w, h], [0, h] ];
switch d {
case NE: verts.splice(0, 1);
case NW: verts.splice(1, 1);
case SE: verts.splice(3, 1);
case SW: verts.splice(2, 1);
}
this.drawPolygon([ for (v in verts) FlxPoint.get(v[0], v[1]) ], 0xFFFF0080);
this.add_body({
mass: 0,
shape: {
type: POLYGON,
vertices: [ for (v in verts) new Vector2(v[0] - w * 0.5, v[1] - h * 0.5) ],
}
});
}
}
enum RampDirection {
NE;
NW;
SE;
SW;
}
@Gioele-Bencivenga
Copy link

I was upgrading from @01010111's version to this one and was having some problems, so for anyone coming after me:

  • update your haxe and openfl
  • add using Math; and using Std; to your imports
  • remove FlxG.update(elapsed); from your PlayState

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment