arcade
An arcade-oriented physics engine for Haxe - a framework-agnostic port of Phaser v2 arcade physics.
Online demo: https://jeremyfa.github.io/arcade/
Features
- 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
Installation
Add to your Haxe project:
haxelib install arcade
Or use the development version:
haxelib git arcade https://github.com/jeremyfa/arcade.git
Quick Start
import 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);
}
}
}
Core Concepts
Physics Update Cycle
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
}
Integrating with Visual Objects
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();
}
}
}
Advanced Integration Pattern
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();
}
}
}
World
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 FPS
Body
The 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 state
Groups
Groups 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();
Collision Detection
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;
});
Examples
Basic Movement
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);
}
}
}
Bouncing Balls
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);
}
}
}
Platformer Example
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!");
}
}
}
Circular Bodies
// 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);
Using Callbacks
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);
}
}
}
Advanced Physics
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);
}
}
Performance Optimization
QuadTree
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;
Optimization Tips
- 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 - Follow Update Order - Always: preUpdate → collisions → postUpdate
- Batch Operations - Process all bodies in each phase before moving to the next
API Reference
World
Properties:
- gravityX/Y: Float - Global gravity
- boundsX/Y/Width/Height: Float - World boundaries
- checkCollisionUp/Down/Left/Right: Bool - Enable world bounds collision per edge
- overlapBias: Float - Separation bias to prevent tunneling (default: 4)
- forceX: Bool - Always separate on X axis first
- sortDirection: SortDirection - Default sort for groups
- isPaused: Bool - Pause all physics updates
- elapsed: Float - Time step in seconds
- elapsedMS: Float - Time step in milliseconds
Methods:
- collide(obj1, ?obj2, ?callback, ?processCallback): Bool
- overlap(obj1, ?obj2, ?callback, ?processCallback): Bool
- sort(group): Void - Sort group bodies
Body
Properties:
- x/y: Float - Position
- width/height: Float - Size
- velocityX/Y: Float - Current velocity
- accelerationX/Y: Float - Acceleration
- dragX/Y: Float - Drag coefficient
- maxVelocityX/Y: Float - Velocity limits
- bounceX/Y: Float - Bounce coefficient (0-1)
- mass: Float - Mass for collision
- gravityX/Y: Float - Body-specific gravity
- gravityScaleX/Y: Float - Gravity multiplier
- rotation: Float - Rotation in degrees
- angularVelocity: Float - Rotation speed
- angularAcceleration: Float - Rotation acceleration
- angularDrag: Float - Rotation drag
- enable: Bool - Enable/disable body
- immovable: Bool - Prevent movement from collisions
- collideWorldBounds: Bool - Collide with world edges
- blockedUp/Down/Left/Right: Bool - Blocked directions
- touchingUp/Down/Left/Right: Bool - Current touches
- dx/dy: Float (readonly) - Position change this frame
- prevX/prevY: Float - Previous frame position
- isCircle: Bool - Whether using circular collision
- radius: 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 collision
- deltaX(): Float - Get X position change this frame
- deltaY(): Float - Get Y position change this frame
- deltaZ(): Float - Get rotation change this frame
- destroy(): Void - Clean up body
Group
Properties:
- objects: Array<Body> - Array of bodies in group
- sortDirection: SortDirection - Sort direction override
Methods:
- add(body): Void - Add body to group
- remove(body): Void - Remove body from group
- sortLeftRight(): Void - Sort by X position
- sortRightLeft(): Void - Sort by X position (reverse)
- sortTopBottom(): Void - Sort by Y position
- sortBottomTop(): Void - Sort by Y position (reverse)
Credits
This library was ported and modified for Haxe by Jérémy Faivre from the existing work of:
- Richard Davey
, author of the original arcade physics library for Phaser v2 - Timo Hausmann, author of the initial QuadTree implementation