Trouble pushing a box with another box, collision detection

Hello. I have been trying to find a way to get a box to push another box from any of its sides. This seems to almost work, but it keeps failing for some reason. Pushing from the top and right seem to work better than the left and bottom for some reason, but it has to be done very slowly.

I want to be able to use a box to push another box in a straight line left/right, up/down, and I also want to do it by a method that can be expanded so that the pushed box can push any reasonable number of other boxes in a row.

Whenever I find a method that pushes the box somewhat normally, moving the box around to a different side to push it in a different direction doesn’t work as expected, that’s why I was trying to use some way to choose how to constrain the move by which side the dragged box is on before it contacts the target box.

I was able to use a method that would push the box somewhat normally in one direction, but the dragged box would always overlap the pushed box if the mouse is moved quickly, I was hoping to avoid having that happen.

Am I on the right track with this, or is there a better approach?

Box pusher, target;
PVector m;
void setup() {
  //size(screen.width, screen.height);
  size(400, 500);
  pusher = new Box(new PVector(100, 100), new PVector(110, 110), color(127, 192, 92, 40));
  target = new Box(new PVector(width/2, height/2), new PVector(75, 75), color(0, 127, 255));
}
void draw() {
  background(127);
  m = new PVector(mouseX, mouseY);

  pusher.move(m, new PVector(0, 0));

  pusher.displayBox();
  target.displayBox();

  collisionTest();
  target.displayLabel();

  moveTarget();
}
void mousePressed() {
  pusher.checkOver();
}
void mouseDragged() {
}
void mouseReleased() {
  pusher.isLocked = false;
}
void moveTarget() {
  println(target.pushSide, target.lastPushSide);
  if (pusher.xyMax.y >= target.xyMin.y && target.lastPushSide == "N") {
    target.isLocked = true;
    target.move(new PVector(target.loc.x, pusher.xyMax.y), new PVector(0, target.dims.y/2));
  }
  if (pusher.xyMin.x <= target.xyMax.x && target.lastPushSide == "E") {
    target.isLocked = true;
    target.move(new PVector(pusher.xyMin.x, target.loc.y), new PVector(-target.dims.x/2, 0));
  }
  if (pusher.xyMin.y >= target.xyMax.y && target.lastPushSide == "S") {
    target.isLocked = true;
    target.move(new PVector(target.loc.x, pusher.xyMin.y), new PVector(0, -target.dims.y/2));
  }
  if (pusher.xyMax.x >= target.xyMin.x && target.lastPushSide == "W") {
    target.isLocked = true;
    target.move(new PVector(pusher.xyMax.x, target.loc.y), new PVector(target.dims.x/2, 0));
  }
}
void collisionTest() {
  // pusher is the box that is moving
  // target is the box to be pushed
  // north
  if (pusher.xyMax.y <= target.xyMin.y && pusher.xyMin.x < target.xyMax.x && pusher.xyMax.x > target.xyMin.x) {
    //target.highlightCollision(new PVector(target.xyMin.x, target.xyMin.y), new PVector(target.xyMax.x, target.xyMin.y));
    target.pushSide = "N";
  }
  // east
  else if (pusher.xyMin.x >= target.xyMax.x && pusher.xyMax.y > target.xyMin.y && pusher.xyMin.y < target.xyMax.y) {
    //target.highlightCollision(new PVector(target.xyMax.x, target.xyMin.y), new PVector(target.xyMax.x, target.xyMax.y));
    target.pushSide = "E";
  }
  // south
  else if (pusher.xyMin.x <= target.xyMax.x && pusher.xyMin.y > target.xyMax.y && pusher.xyMax.x > target.xyMin.x) {
    //target.highlightCollision(new PVector(target.xyMax.x, target.xyMax.y), new PVector(target.xyMin.x, target.xyMax.y));
    target.pushSide = "S";
  }
  // west
  else if (pusher.xyMin.y <= target.xyMax.y && pusher.xyMax.x < target.xyMin.x && pusher.xyMax.y > target.xyMin.y) {
    //target.highlightCollision(new PVector(target.xyMin.x, target.xyMax.y), new PVector(target.xyMin.x, target.xyMin.y));
    target.pushSide = "W";
  } else {
    target.isLocked = false;
    target.lastPushSide = target.pushSide;
    target.pushSide = "X";
  }
}
class Box {
  PVector loc, dims, xyMin, xyMax;
  String pushSide, lastPushSide;
  color c;
  boolean isLocked;

  Box(PVector loc, PVector dims, color c) {
    this.loc = loc;
    this.dims = dims;
    this.c = c;
    xyMin = PVector.sub(loc, PVector.div(dims, 2));
    xyMax = PVector.add(xyMin, dims);
    isLocked = false;
    pushSide = "";
    lastPushSide = "";
  }
  void displayBox() {
    rectMode(CENTER);
    fill(c);
    strokeWeight(1);
    stroke(255);
    rect(loc.x, loc.y, dims.x, dims.y);
  }
  void displayLabel() {
    textAlign(CENTER, CENTER);
    textSize(18);
    fill(255);
    text(pushSide, loc.x, loc.y);
  }
  void update() {
    xyMin = PVector.sub(loc, PVector.div(dims, 2));
    xyMax = PVector.add(xyMin, dims);
  }
  void move(PVector newLoc, PVector offset) {
    if (isLocked) {
      loc = PVector.add(newLoc, offset);
      update();
    }
  }
  boolean mOver() {
    if (m.x > xyMin.x - 1 && m.y > xyMin.y - 1 && m.x < xyMax.x && m.y < xyMax.y) {
      return true;
    } else {
      return false;
    }
  }
  void checkOver() {
    if (mOver()) {
      isLocked = true;
    } else {
      isLocked = false;
    }
  }
  void highlightCollision(PVector a, PVector b) {
    strokeWeight(5);
    stroke(255, 0, 0);
    line(a.x, a.y, b.x, b.y);
  }
}

2 Likes

Pushing is an issue in physics simulations because when an object moves, it actually teleports by small distances. When you try to push a box with another box, they are actually just overlapping, and then reacting according to your algorithms. There are a bunch of ways that established physics engines deal with this, but I’ll save you the trouble.

Just use box2D, it’s simple enough to learn in a couple of hours, and it’s a really well behaved engine. Ive created physics engines using SAT detection and triangulation for complex polygons, and to be honest it was a waste of my time. People have been optimizing these things for years, so it makes more sense to use an established engine.

3 Likes

Thanks. I noticed there are a few variants of box2d, jbox2d, and pbox2d. They both seem really old, are they still working? Is pbox2d a good place to start learning this? I’ve only been using processing for a few months and I’ve never worked with a physics engine before.

If you go into Contributions Manager in PDE you will see the link to the recommended Box2D for Processing 3.x – Box2D For Processing 0.4.

To me, this is the part that shouts “I want a physics engine”. A solver will handle these cascading interactions like pushing a row for you – even if you really want to dig into trying to implement that yourself, studying the Box2D source is still once good starting point.

This is good to know. I definitely am not up for reinventing that wheel.

Does anyone know of a good tutorial or some examples of simple Box2D interactions that involve one or two objects and not a particle system? I’m having trouble relating the examples I’m finding to what I’m trying to accomplish.

You can check out Daniel Shiffman’s chapter on phyaics engines in Nature of Code which he has made available for free.

Personally I didn’t quite understand that tutorial, so if you are like me you might want to read my little tutorial at eeyorelife.github.io and if you have any questions or feedback don’t hesitate to ask.

The link to my website also provides a link to examples that is very valuable when trying to learn to code with Box2D. Good luck!

2 Likes

Thanks… that’s exactly what I have been reading, Shiffman’s book. Lots of neat particle sprays and balls flying all over the place, but I just want to make a box draggable by the mouse, and let it push some other boxes around. I will check out your tutorial.

Okay, after looking at the “Mouse” example from the Box2D library, it seems like that could be fairly close to what I was hoping to do.

But I don’t want the box I’m dragging to have motion on it’s own after the mouse is released. I’d even prefer not to have it rotate. I want it to behave like it’s a domino on a table, you push it around, you let go, it sits where you left it. If it’s used to push other dominoes, they just slide and stop the moment they are not pushed anymore.

The idea of a “spring” that lets you fling the box around the screen and watch it bounce and spin and send other objects flying in a realistic manner is impressive, but what if you just want to simulate objects on a table, moved around gently with precision? Is that spring mouse joint basically the right way to get the mouse to move a box?

I noticed that the friction setting seems only to apply to the object coming into contact with other objects or “boundaries.” Is there a way to introduce a friction as though a box was on a table so that it’s momentum could be controlled that way?

*** Never mind
If it’s not too much trouble, and I feel bad asking for this much help, could someone add Box2D to my example at the top of this post? With the big box controlled by the mouse to push the smaller box around without either box rotating, and assuming that the view is essentially like a top view of two dominoes on a table, so not having gravity pull them on their own, and neither box having any noticeable momentum after being moved.

Or if anyone knows of a similar example I could look at.


Okay, I think I figured it out. It’s amazing how this even eliminates the need to use any logic to make sure tiles are being moved in a valid way. Amazing to me anyway.

import shiffman.box2d.*;
import org.jbox2d.common.*;
import org.jbox2d.dynamics.joints.*;
import org.jbox2d.collision.shapes.*;
import org.jbox2d.dynamics.*;

Box2DProcessing box2d;
ArrayList boundaries;
Box[][] tiles;
Box box;
Spring spring;
Surface surface;
void setup() {
  size(321, 321);
  box2d = new Box2DProcessing(this);
  box2d.createWorld();
  box2d.setGravity(0, 0);
  addBoundaries();
  surface = new Surface(width/2, height/2);
  tiles = new Box[3][3];
  initTiles();
  bindTilesToSurface();
  spring = new Spring();
}
void draw() {
  background(255);
  box2d.step();
  surface.display();
  spring.update(mouseX, mouseY);
  displayWalls();
  displayTiles();
  //spring.display();
}
void mousePressed() {
  int n = 0;
  for (int c = 0; c < tiles.length; c++) {
    for (int r = 0; r < tiles[0].length && n < 8; r++) {
      if (tiles[c][r].contains(mouseX, mouseY)) {
        spring.bind(mouseX, mouseY, tiles[c][r]);
      }
      n++;
    }
  }
}
void mouseReleased() {
  spring.destroy();
}
void displayWalls() {
  for (int i = 0; i < boundaries.size(); i++) {
    Boundary wall = (Boundary) boundaries.get(i);
    wall.display();
  }
}
void displayTiles() {
  int n = 0;
  for (int c = 0; c < tiles.length; c++) {
    for (int r = 0; r < tiles[0].length && n < 8; r++) {
      tiles[c][r].display();
      n++;
    }
  }
}
void addBoundaries() {
  boundaries = new ArrayList();
  boundaries.add(new Boundary(width/2, height-5, width, 10, 0));
  boundaries.add(new Boundary(width/2, 5, width, 10, 0));
  boundaries.add(new Boundary(width-5, height/2, 10, height, 0));
  boundaries.add(new Boundary(5, height/2, 10, height, 0));
}
void initTiles() {
  int n = 0;
  for (int c = 0; c < tiles.length; c++) {
    for (int r = 0; r < tiles[0].length && n < 8; r++) {
      tiles[c][r] = new Box(50 + 100 * c, 50 + 100 * r);
      n++;
    }
  }
}
void bindTilesToSurface() {
  int n = 0;
  for (int c = 0; c < tiles.length; c++) {
    for (int r = 0; r < tiles[0].length && n < 8; r++) {
      tiles[c][r].bind(surface);
      n++;
    }
  }
}
class Boundary {
  float x, y, w, h;
  Body b;
  Boundary(float x, float y, float w, float h, float a) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    PolygonShape ps = new PolygonShape();
    float box2dW = box2d.scalarPixelsToWorld(w/2);
    float box2dH = box2d.scalarPixelsToWorld(h/2);
    ps.setAsBox(box2dW, box2dH);
    BodyDef bd = new BodyDef();
    bd.type = BodyType.STATIC;
    bd.angle = a;
    bd.position.set(box2d.coordPixelsToWorld(x, y));
    b = box2d.createBody(bd);
    b.createFixture(ps, 1);
  }
  void display() {
    fill(75);
    noStroke();
    rectMode(CENTER);
    float a = b.getAngle();
    pushMatrix();
    translate(x, y);
    rotate(-a);
    rect(0, 0, w, h);
    popMatrix();
  }
}
class Box {
  FrictionJoint frictionJoint;
  Body body;
  float x, y;
  float w, h;
  Box(float x, float y) {
    this.x = x;
    this.y = y;
    w = 100;
    h = 100;
    makeBody(new Vec2(x, y));
  }
  boolean contains(float x, float y) {
    Vec2 worldPoint = box2d.coordPixelsToWorld(x, y);
    Fixture f = body.getFixtureList();
    boolean inside = f.testPoint(worldPoint);
    return inside;
  }
  void makeBody(Vec2 center) {
    BodyDef bd = new BodyDef();
    bd.type = BodyType.DYNAMIC;
    bd.position.set(box2d.coordPixelsToWorld(center));
    bd.fixedRotation = true;
    body = box2d.createBody(bd);

    PolygonShape ps = new PolygonShape();
    Vec2[] vertices = new Vec2[8];
    vertices[0] = box2d.vectorPixelsToWorld(new Vec2(-50, -48));
    vertices[1] = box2d.vectorPixelsToWorld(new Vec2(-50, 48));
    vertices[2] = box2d.vectorPixelsToWorld(new Vec2(-48, 50));
    vertices[3] = box2d.vectorPixelsToWorld(new Vec2(48, 50));
    vertices[4] = box2d.vectorPixelsToWorld(new Vec2(50, 48));
    vertices[5] = box2d.vectorPixelsToWorld(new Vec2(50, -48));
    vertices[6] = box2d.vectorPixelsToWorld(new Vec2(48, -50));
    vertices[7] = box2d.vectorPixelsToWorld(new Vec2(-48, -50));
    ps.set(vertices, vertices.length);

    FixtureDef fd = new FixtureDef();
    fd.shape = ps;
    fd.density = 1;
    fd.friction = 0;
    fd.restitution = 0.0;
    body.createFixture(fd);
  }
  void display() {
    Vec2 pos = box2d.getBodyPixelCoord(body);
    float a = body.getAngle();
    pushMatrix();
    translate(pos.x, pos.y);
    rotate(-a);
    fill(255, 255, 240);
    stroke(125, 125, 110);
    strokeWeight(2);
    rectMode(CENTER);
    rect(0, 0, w, h, 5);
    popMatrix();
  }
  void killBody() {
    box2d.destroyBody(body);
  }
  void bind(Surface surface) {
    FrictionJointDef fj = new FrictionJointDef();
    fj.bodyA = body;
    fj.bodyB = surface.body;
    fj.maxForce = 1000;
    fj.maxTorque = 0;
    fj.collideConnected = false;
    frictionJoint = (FrictionJoint) box2d.world.createJoint(fj);
  }
}
class Spring {
  MouseJoint mouseJoint;
  Spring() {
    mouseJoint = null;
  }
  void update(float x, float y) {
    if (mouseJoint != null) {
      Vec2 mouseWorld = box2d.coordPixelsToWorld(x, y);
      mouseJoint.setTarget(mouseWorld);
    }
  }
  void display() {
    if (mouseJoint != null) {
      Vec2 v1 = new Vec2(0, 0);
      mouseJoint.getAnchorA(v1);
      Vec2 v2 = new Vec2(0, 0);
      mouseJoint.getAnchorB(v2);

      v1 = box2d.coordWorldToPixels(v1);
      v2 = box2d.coordWorldToPixels(v2);
      stroke(0);
      strokeWeight(1);
      line(v1.x, v1.y, v2.x, v2.y);
    }
  }
  void bind(float x, float y, Box box) {
    MouseJointDef md = new MouseJointDef();
    md.bodyA = box2d.getGroundBody();
    md.bodyB = box.body;
    Vec2 mp = box2d.coordPixelsToWorld(x, y);
    md.target.set(mp);
    md.maxForce = 1000.0 * box.body.m_mass;
    md.frequencyHz = 5.0;
    md.dampingRatio = 0.9;
    mouseJoint = (MouseJoint) box2d.world.createJoint(md);
  }
  void destroy() {
    if (mouseJoint != null) {
      box2d.world.destroyJoint(mouseJoint);
      mouseJoint = null;
    }
  }
}
class Surface {
  FrictionJoint frictionJoint;
  Body body;
  float x, y, w, h;
  Surface(float x, float y) {
    this.x = x;
    this.y = y;
    w = width;
    h = height;
    makeBody(new Vec2(x, y), w, h);
  }
  void display() {
    Vec2 pos = box2d.getBodyPixelCoord(body);
    rectMode(CENTER);
    pushMatrix();
    translate(pos.x, pos.y);
    fill(0, 127, 0);
    noStroke();
    rect(0, 0, w, h);
    popMatrix();
  }
  void makeBody(Vec2 center, float w, float h) {
    BodyDef bd = new BodyDef();
    bd.type = BodyType.STATIC;
    bd.position.set(box2d.coordPixelsToWorld(center));
    bd.fixedRotation = true;
    body = box2d.createBody(bd);

    PolygonShape ps = new PolygonShape();
    float box2dW = box2d.scalarPixelsToWorld(w/2);
    float box2dH = box2d.scalarPixelsToWorld(h/2);
    ps.setAsBox(box2dW, box2dH);

    FixtureDef fd = new FixtureDef();
    fd.shape = ps;
    body.createFixture(fd);
  }
}