An arcade-oriented physics engine for Haxe - a framework-agnostic port of Phaser v2 arcade physics.
Online demo: https://jeremyfa.github.io/arcade/
- Fast 2D arcade physics - Optimized for performance with AABB collision detection
- Framework agnostic - Use with any Haxe game framework or custom engine
- Groups & batch operations - Efficiently manage and collide collections of bodies
- QuadTree optimization - Automatic spatial partitioning for large numbers of objects
- Flexible collision callbacks - Process and respond to collisions with custom logic
- World bounds - Define and collide with world boundaries
- Rich physics properties - Velocity, acceleration, drag, bounce, gravity, and more
- Circle collision - Support for both rectangular and circular collision bodies
- Angular motion - Rotation with angular velocity and acceleration
Add to your Haxe project:
haxelib install arcadeOr use the development version:
haxelib git arcade https://github.com/jeremyfa/arcade.gitimport arcade.World;
import arcade.Body;
import arcade.Group;
class Main {
var world:World;
var player:Body;
var platforms:Group;
static function main() {
new Main();
}
public function new() {
// Create a physics world
world = new World(0, 0, 800, 600);
world.gravityY = 300;
// Create player body
player = new Body(400, 100, 32, 32);
player.velocityX = 100;
player.bounceY = 0.8;
player.collideWorldBounds = true;
// Create platforms
platforms = new Group();
var ground = new Body(400, 550, 800, 100);
ground.immovable = true;
platforms.add(ground);
}
public function update(deltaTime:Float) {
world.elapsed = deltaTime;
// Step 1: Pre-update all bodies
player.preUpdate(world, player.x, player.y, player.width, player.height, player.rotation);
for (platform in platforms.objects) {
platform.preUpdate(world, platform.x, platform.y, platform.width, platform.height, platform.rotation);
}
// Step 2: Perform collision detection
world.collide(player, platforms);
// Step 3: Post-update all bodies
player.postUpdate(world);
for (platform in platforms.objects) {
platform.postUpdate(world);
}
}
}The arcade physics engine requires a specific update sequence each frame:
- Pre-Update Phase (
preUpdate()) - Updates velocities and calculates new positions - Collision Phase (
collide()/overlap()) - Detects and resolves collisions - Post-Update Phase (
postUpdate()) - Finalizes positions and updates state
Important: All bodies must complete each phase before moving to the next. Never mix these phases!
// CORRECT - Proper update sequence
public function update(delta:Float) {
world.elapsed = delta;
// Phase 1: Pre-update ALL bodies
for (body in allBodies) {
body.preUpdate(world, body.x, body.y, body.width, body.height, body.rotation);
}
// Phase 2: Perform ALL collision checks
world.collide(player, platforms);
world.collide(enemies, platforms);
world.overlap(player, pickups, collectPickup);
// Phase 3: Post-update ALL bodies
for (body in allBodies) {
body.postUpdate(world);
}
}
// WRONG - Don't do this!
public function badUpdate(delta:Float) {
player.preUpdate(world, player.x, player.y, player.width, player.height, player.rotation);
world.collide(player, platforms);
player.postUpdate(world); // NO! Other bodies haven't pre-updated yet
enemy.preUpdate(world, enemy.x, enemy.y, enemy.width, enemy.height, enemy.rotation); // Too late, player already post-updated
// This will cause incorrect collision behavior
}Since arcade physics is framework-agnostic, you'll need to sync physics bodies with your visual objects (sprites, images, etc.). Here's a pattern for managing this relationship:
// Example: Physics-enabled game object
class GameObject {
public var body:Body;
public var visual:YourVisualObject; // Could be a sprite, image, etc.
public function new(x:Float, y:Float, width:Float, height:Float) {
// Create physics body
body = new Body(x, y, width, height);
// Create visual representation
visual = new YourVisualObject();
visual.x = x;
visual.y = y;
visual.width = width;
visual.height = height;
}
public function syncVisualToBody() {
// Update visual position from body
visual.x = body.x;
visual.y = body.y;
// Update rotation if needed
if (body.allowRotation) {
visual.rotation = body.rotation;
}
}
}
// Complete game update example
class Game {
var world:World;
var gameObjects:Array<GameObject> = [];
public function update(delta:Float) {
world.elapsed = delta;
// Phase 1: Sync visuals TO bodies (for input/AI movement)
for (obj in gameObjects) {
// Update body from visual if needed (e.g., mouse dragging)
if (obj.visual.beingDragged) {
obj.body.x = obj.visual.x;
obj.body.y = obj.visual.y;
}
}
// Phase 2: Pre-update physics
for (obj in gameObjects) {
var body = obj.body;
body.preUpdate(world, body.x, body.y, body.width, body.height, body.rotation);
}
// Phase 3: Collision detection
// Bodies are now at their new positions, resolve collisions
for (i in 0...gameObjects.length) {
for (j in i+1...gameObjects.length) {
world.collide(gameObjects[i].body, gameObjects[j].body);
}
}
// Phase 4: Post-update physics
for (obj in gameObjects) {
obj.body.postUpdate(world);
}
// Phase 5: Sync bodies TO visuals
for (obj in gameObjects) {
obj.syncVisualToBody();
}
}
}The Body class provides dx and dy properties that track position changes during the physics update:
class PhysicsVisual {
public var body:Body;
public var visual:Dynamic;
public function preUpdate(world:World) {
// Sync body position from visual (e.g., after manual movement)
body.x = visual.x;
body.y = visual.y;
// Run physics pre-update
body.preUpdate(world, body.x, body.y, body.width, body.height, body.rotation);
}
public function postUpdate(world:World) {
// Run physics post-update
body.postUpdate(world);
// Apply physics deltas to visual
// dx and dy contain the position change from this frame
visual.x += body.dx;
visual.y += body.dy;
// Apply rotation delta if needed
if (body.allowRotation) {
visual.rotation += body.deltaZ();
}
}
}
// Example with a complete physics-enabled sprite system
class Sprite {
public var x:Float;
public var y:Float;
public var rotation:Float;
public var scaleX:Float = 1;
public var scaleY:Float = 1;
public var anchorX:Float = 0.5;
public var anchorY:Float = 0.5;
public var width:Float;
public var height:Float;
// Add any rendering-specific properties
}
class PhysicsSprite {
public var sprite:Sprite;
public var body:Body;
public function new(x:Float, y:Float, width:Float, height:Float) {
sprite = new Sprite();
sprite.x = x;
sprite.y = y;
sprite.width = width;
sprite.height = height;
body = new Body(x, y, width, height);
}
public function updatePhysics(world:World) {
// Account for anchor and scale when syncing position
var scaleX = Math.abs(sprite.scaleX);
var scaleY = Math.abs(sprite.scaleY);
var anchorX = sprite.scaleX < 0 ? 1 - sprite.anchorX : sprite.anchorX;
var anchorY = sprite.scaleY < 0 ? 1 - sprite.anchorY : sprite.anchorY;
// Pre-update: sync body to sprite position
body.x = sprite.x - sprite.width * scaleX * anchorX;
body.y = sprite.y - sprite.height * scaleY * anchorY;
body.width = sprite.width * scaleX;
body.height = sprite.height * scaleY;
body.rotation = sprite.rotation;
body.preUpdate(world, body.x, body.y, body.width, body.height, body.rotation);
}
public function applyPhysics(world:World) {
body.postUpdate(world);
// Post-update: apply physics deltas to sprite
sprite.x += body.dx;
sprite.y += body.dy;
if (body.allowRotation) {
sprite.rotation += body.deltaZ();
}
}
}The World class manages the physics simulation, including gravity, bounds, and collision detection.
// Create a world with bounds
var world = new World(0, 0, 800, 600);
// Set gravity
world.gravityX = 0;
world.gravityY = 500;
// Configure world bounds collision
world.checkCollisionDown = true;
world.checkCollisionUp = true;
world.checkCollisionLeft = true;
world.checkCollisionRight = true;
// Set elapsed time for physics calculations
world.elapsed = 1.0 / 60.0; // 60 FPSThe Body class represents a physics-enabled object with position, velocity, and collision properties.
// Create a body
var body = new Body(x, y, width, height);
// Set velocity
body.velocityX = 150;
body.velocityY = -200;
// Set acceleration
body.accelerationX = 0;
body.accelerationY = 300; // Gravity
// Configure collision properties
body.bounceX = 0.5;
body.bounceY = 0.8;
body.collideWorldBounds = true;
body.immovable = false;
// Set mass and drag
body.mass = 1.0;
body.dragX = 50;
body.dragY = 0;
// Angular motion
body.angularVelocity = 45; // degrees per second
body.angularAcceleration = 10;
body.angularDrag = 20;
// Physics update sequence (call each frame):
// 1. preUpdate() - Updates velocity and position
// 2. collision detection - Resolve collisions
// 3. postUpdate() - Finalize position and stateGroups allow you to manage collections of bodies and perform batch operations.
// Create groups
var enemies = new Group();
var bullets = new Group();
// Add bodies to groups
for (i in 0...10) {
var enemy = new Body(100 + i * 50, 200, 32, 32);
enemies.add(enemy);
}
// Sort bodies for optimized collision
enemies.sortLeftRight();The library provides two main collision methods:
collide- Bodies physically push each other and transfer velocityoverlap- Detects overlaps without affecting motion
// Collide two bodies (with physics response)
if (world.collide(body1, body2)) {
trace("Collision detected!");
}
// Check overlap without physics response
if (world.overlap(body1, body2)) {
trace("Bodies are overlapping!");
}
// Collide with callbacks
world.collide(player, enemies, function(player:Body, enemy:Body) {
trace("Player hit enemy!");
});
// Group vs Group collision
world.collide(bullets, enemies, function(bullet:Body, enemy:Body) {
bullet.enable = false; // Disable bullet
enemy.enable = false; // Disable enemy
});
// Process callback for conditional collisions
world.collide(player, platforms, null, function(player:Body, platform:Body):Bool {
// Only collide if player is falling
return player.velocityY > 0;
});class Player {
var body:Body;
public function new(x:Float, y:Float) {
body = new Body(x, y, 32, 48);
body.maxVelocityX = 200;
body.maxVelocityY = 400;
body.dragX = 100;
}
public function updateInput(left:Bool, right:Bool, jump:Bool) {
// Horizontal movement
if (left) {
body.velocityX = -150;
} else if (right) {
body.velocityX = 150;
}
// Jump (only if on ground)
if (jump && body.blockedDown) {
body.velocityY = -300;
}
}
}
// In your game loop:
class Game {
var world:World;
var player:Player;
var platforms:Group;
public function update(delta:Float) {
world.elapsed = delta;
// Step 1: Handle input
player.updateInput(leftPressed, rightPressed, jumpPressed);
// Step 2: Pre-update physics
player.body.preUpdate(world, player.body.x, player.body.y, player.body.width, player.body.height, player.body.rotation);
for (platform in platforms.objects) {
platform.preUpdate(world, platform.x, platform.y, platform.width, platform.height, platform.rotation);
}
// Step 3: Collision detection
world.collide(player.body, platforms);
// Step 4: Post-update physics
player.body.postUpdate(world);
for (platform in platforms.objects) {
platform.postUpdate(world);
}
}
}class BouncingBalls {
var world:World;
var balls:Group;
public function new() {
world = new World(0, 0, 800, 600);
world.gravityY = 500;
// Create bouncing balls
balls = new Group();
for (i in 0...5) {
var ball = new Body(
100 + i * 100, // x position
100, // y position
16, 16 // size
);
ball.velocityX = -100 + Math.random() * 200;
ball.velocityY = -100 + Math.random() * 200;
ball.bounceX = 1.0; // Perfect elastic bounce
ball.bounceY = 1.0;
ball.collideWorldBounds = true;
balls.add(ball);
}
}
public function update(delta:Float) {
world.elapsed = delta;
// Step 1: Pre-update all balls
for (ball in balls.objects) {
ball.preUpdate(world, ball.x, ball.y, ball.width, ball.height, ball.rotation);
}
// Step 2: Collision detection
world.collide(balls, balls);
// Step 3: Post-update all balls
for (ball in balls.objects) {
ball.postUpdate(world);
}
}
}class PlatformerGame {
var world:World;
var player:Body;
var platforms:Group;
var enemies:Group;
public function new() {
// Setup world
world = new World(0, 0, 800, 600);
world.gravityY = 800;
// Create player
player = new Body(100, 400, 32, 48);
player.bounceY = 0.1;
player.collideWorldBounds = true;
player.dragX = 100;
// Create platforms
platforms = new Group();
// Ground
var ground = new Body(400, 580, 800, 40);
ground.immovable = true;
platforms.add(ground);
// Floating platforms
var platform1 = new Body(200, 400, 200, 20);
platform1.immovable = true;
platforms.add(platform1);
var platform2 = new Body(500, 300, 150, 20);
platform2.immovable = true;
platforms.add(platform2);
// Create enemies
enemies = new Group();
var enemy = new Body(500, 250, 24, 24);
enemy.velocityX = 50;
enemy.bounceX = 1;
enemies.add(enemy);
}
public function update(deltaTime:Float) {
world.elapsed = deltaTime;
// Handle player input
handlePlayerInput();
// Step 1: Pre-update all bodies
player.preUpdate(world, player.x, player.y, player.width, player.height, player.rotation);
for (enemy in enemies.objects) {
enemy.preUpdate(world, enemy.x, enemy.y, enemy.width, enemy.height, enemy.rotation);
}
// Immovable platforms still need preUpdate
for (platform in platforms.objects) {
platform.preUpdate(world, platform.x, platform.y, platform.width, platform.height, platform.rotation);
}
// Step 2: Collision detection
world.collide(player, platforms);
world.collide(enemies, platforms);
world.collide(player, enemies, onPlayerHitEnemy);
// Step 3: Post-update all bodies
player.postUpdate(world);
for (enemy in enemies.objects) {
enemy.postUpdate(world);
}
for (platform in platforms.objects) {
platform.postUpdate(world);
}
}
function handlePlayerInput() {
// Example input handling
if (leftKey) player.velocityX = -200;
else if (rightKey) player.velocityX = 200;
if (jumpKey && player.blockedDown) {
player.velocityY = -400;
}
}
function onPlayerHitEnemy(player:Body, enemy:Body) {
if (player.velocityY > 0 && player.y < enemy.y) {
// Player jumped on enemy
enemy.enable = false;
player.velocityY = -200; // Bounce
} else {
// Player hit by enemy
trace("Game Over!");
}
}
}// Create a circular body
var circle = new Body(400, 300, 32, 32);
circle.setCircle(16); // Radius of 16 pixels
// Circle vs Rectangle collision works automatically
var rect = new Body(450, 300, 40, 40);
world.collide(circle, rect);class CollisionExample {
var world:World;
var player:Body;
var coins:Group;
var platforms:Group;
var score:Int = 0;
public function update(delta:Float) {
world.elapsed = delta;
// Step 1: Pre-update all bodies
player.preUpdate(world, player.x, player.y, player.width, player.height, player.rotation);
for (coin in coins.objects) {
coin.preUpdate(world, coin.x, coin.y, coin.width, coin.height, coin.rotation);
}
for (platform in platforms.objects) {
platform.preUpdate(world, platform.x, platform.y, platform.width, platform.height, platform.rotation);
}
// Step 2: Collision detection with callbacks
world.collide(player, coins, function(player:Body, coin:Body) {
// Collect coin
coin.enable = false;
score++;
});
// Process callback for one-way platforms
world.collide(player, platforms, null, function(player:Body, platform:Body):Bool {
// Only collide from above
return player.y < platform.y && player.velocityY > 0;
});
// Step 3: Post-update all bodies
player.postUpdate(world);
for (coin in coins.objects) {
coin.postUpdate(world);
}
for (platform in platforms.objects) {
platform.postUpdate(world);
}
}
}class AdvancedPhysics {
var world:World;
var spinner:Body;
var seeker:Body;
var target:Body;
var projectiles:Group;
public function new() {
world = new World(0, 0, 800, 600);
// Rotating sprite
spinner = new Body(400, 300, 64, 64);
spinner.angularVelocity = 180; // 180 degrees per second
spinner.immovable = true;
// Seeking missile
seeker = new Body(100, 100, 24, 24);
seeker.dragX = 50;
seeker.dragY = 50;
// Target
target = new Body(700, 500, 32, 32);
target.immovable = true;
// Projectiles group
projectiles = new Group();
}
public function update(delta:Float) {
world.elapsed = delta;
// Update seeker AI
updateSeekerAI();
// Step 1: Pre-update
spinner.preUpdate(world, spinner.x, spinner.y, spinner.width, spinner.height, spinner.rotation);
seeker.preUpdate(world, seeker.x, seeker.y, seeker.width, seeker.height, seeker.rotation);
target.preUpdate(world, target.x, target.y, target.width, target.height, target.rotation);
for (proj in projectiles.objects) {
proj.preUpdate(world, proj.x, proj.y, proj.width, proj.height, proj.rotation);
}
// Step 2: Collisions
world.collide(seeker, target, function(s:Body, t:Body) {
trace("Target hit!");
s.enable = false;
});
world.collide(projectiles, spinner, function(proj:Body, spin:Body) {
// Projectile bounces off spinner
var angle = Math.atan2(proj.y - spin.y, proj.x - spin.x);
var speed = Math.sqrt(proj.velocityX * proj.velocityX + proj.velocityY * proj.velocityY);
proj.velocityX = Math.cos(angle) * speed;
proj.velocityY = Math.sin(angle) * speed;
});
// Step 3: Post-update
spinner.postUpdate(world);
seeker.postUpdate(world);
target.postUpdate(world);
for (proj in projectiles.objects) {
proj.postUpdate(world);
}
}
function updateSeekerAI() {
var angle = Math.atan2(
target.y - seeker.y,
target.x - seeker.x
);
var speed = 200;
seeker.accelerationX = Math.cos(angle) * speed;
seeker.accelerationY = Math.sin(angle) * speed;
seeker.maxVelocityX = 150;
seeker.maxVelocityY = 150;
// Face the target
seeker.rotation = angle * 180 / Math.PI;
}
public function fireProjectile(x:Float, y:Float, velocityX:Float, velocityY:Float) {
var proj = new Body(x, y, 8, 8);
proj.velocityX = velocityX;
proj.velocityY = velocityY;
proj.collideWorldBounds = true;
proj.bounceX = 1;
proj.bounceY = 1;
projectiles.add(proj);
}
}The physics engine automatically uses QuadTree spatial partitioning when groups contain more than 10 objects:
// Configure QuadTree settings
world.maxObjects = 10; // Max objects per quad
world.maxLevels = 4; // Max subdivision levels
// Adjust threshold for automatic QuadTree usage
world.maxObjectsWithoutQuadTree = 10;
// Disable QuadTree globally
world.skipQuadTree = true;
// Disable QuadTree for specific body
body.skipQuadTree = true;- Use Groups - Always group similar objects for efficient collision checks
- Sort Groups - Use appropriate sort direction for your game layout
- Limit Active Bodies - Disable bodies outside the view with
body.enable = false - Immovable Objects - Set
immovable = truefor static platforms and walls. SetallowGravity = falseon those if your world has gravity - Follow Update Order - Always: preUpdate → collisions → postUpdate
- Batch Operations - Process all bodies in each phase before moving to the next
Properties:
gravityX/Y: Float- Global gravityboundsX/Y/Width/Height: Float- World boundariescheckCollisionUp/Down/Left/Right: Bool- Enable world bounds collision per edgeoverlapBias: Float- Separation bias to prevent tunneling (default: 4)forceX: Bool- Always separate on X axis firstsortDirection: SortDirection- Default sort for groupsisPaused: Bool- Pause all physics updateselapsed: Float- Time step in secondselapsedMS: Float- Time step in milliseconds
Methods:
collide(obj1, ?obj2, ?callback, ?processCallback): Booloverlap(obj1, ?obj2, ?callback, ?processCallback): Boolsort(group): Void- Sort group bodies
Properties:
x/y: Float- Positionwidth/height: Float- SizevelocityX/Y: Float- Current velocityaccelerationX/Y: Float- AccelerationdragX/Y: Float- Drag coefficientmaxVelocityX/Y: Float- Velocity limitsbounceX/Y: Float- Bounce coefficient (0-1)mass: Float- Mass for collisiongravityX/Y: Float- Body-specific gravitygravityScaleX/Y: Float- Gravity multiplierrotation: Float- Rotation in degreesangularVelocity: Float- Rotation speedangularAcceleration: Float- Rotation accelerationangularDrag: Float- Rotation dragenable: Bool- Enable/disable bodyimmovable: Bool- Prevent movement from collisionscollideWorldBounds: Bool- Collide with world edgesblockedUp/Down/Left/Right: Bool- Blocked directionstouchingUp/Down/Left/Right: Bool- Current touchesdx/dy: Float(readonly) - Position change this frameprevX/prevY: Float- Previous frame positionisCircle: Bool- Whether using circular collisionradius: Float- Radius when using circular collision
Methods:
preUpdate(world): Void- Update velocity and position (call before collisions)postUpdate(world): Void- Finalize position and state (call after collisions)setCircle(radius): Void- Use circular collisiondeltaX(): Float- Get X position change this framedeltaY(): Float- Get Y position change this framedeltaZ(): Float- Get rotation change this framedestroy(): Void- Clean up body
Properties:
objects: Array<Body>- Array of bodies in groupsortDirection: SortDirection- Sort direction override
Methods:
add(body): Void- Add body to groupremove(body): Void- Remove body from groupsortLeftRight(): Void- Sort by X positionsortRightLeft(): Void- Sort by X position (reverse)sortTopBottom(): Void- Sort by Y positionsortBottomTop(): Void- Sort by Y position (reverse)
This library was ported and modified for Haxe by Jérémy Faivre from the existing work of:
- Richard Davey rich@photonstorm.com, author of the original arcade physics library for Phaser v2
- Timo Hausmann, author of the initial QuadTree implementation