Mouse Grabbing

Learn how to grab and drop objects with the mouse

Source Code

Grab and Drop Objects

Being able to grab objects with the mouse is really useful when learning Box2D. Box2D has even implemented a Joint called Mouse Joint especially for this purpose. This tutorial is pretty advanced, if you do not understand everything it’s ok, you can come later and read it again when you have a better understanding of Box2D. We are going to use two advanced features : Joint and AABB Query Callback. I will not enter into the details of those two features, you will learn to use them in future tutorials.

So how do grabbing and dropping objects work ? It’s a process that happens in three steps
 
  • Step 1 : Click on an object and hold the mouse button down, this will grab the object
  • Step 2 : Move the mouse while holding the mouse button down, the object will follow your mouse
  • Step 3 : Release the mouse button, this will drop the object

A Little Preparation

Before we start, let’s prepare our Scene. We are going to create a floor so our boxes don’t fall off the screen. We start with the code of the previous tutorial SFML Integration. Inside the method Init() we adjust the Debug Draw flags to draw only bodies shapes and centers of mass and we add a function createFloorBody(). Inside the event callback onMouseButton(), we adjust the code so our boxes are created when we do a CTRL + Left Click.

void init()
{
    //setup the DebugDraw
    mDebugDraw.setRenderWindow(&getRenderWindow());
    mDebugDraw.SetFlags(b2Draw::e_shapeBit | b2Draw::e_centerOfMassBit); // Modification 1
    mPhysicWorld.SetDebugDraw(&mDebugDraw);
    //create a rigid floor
    createFloorBody(); //Modification 2
}

The code for creating the floor is shown below (Expand the section Create Floor body). It’s almost the same as creating boxes. There are two differences,

  • First, the body type is set to static (b2_staticBody) instead of dynamic (b2_dynamicBody)
  • Second, we use an Edge Shape instead of a Polygon Shape.

You will learn more about shapes and bodies later. After those modifications, you should be able to create boxes and let them lay down on the floor as shown in the picture below.

void createFloorBody()
{
    b2Vec2 point1 = ng::sf_to_b2(sf::Vector2f(0.f, 600.f), ng::SCALE);
    b2Vec2 point2 = ng::sf_to_b2(sf::Vector2f(1080.f, 600.f), ng::SCALE);
     //create an empty body
    b2BodyDef bodyDef;
    bodyDef.position      = b2Vec2(0.f, 0.f);
    bodyDef.angle         = 0.f;
    bodyDef.allowSleep    = true;
    bodyDef.fixedRotation = false;
    bodyDef.gravityScale  = 1.f;
    bodyDef.type          = b2_staticBody;
    b2Body* boxBody       = mPhysicWorld.CreateBody(&bodyDef);
    //create a box shape
    b2EdgeShape edgeshape;
    edgeshape.Set(point1, point2);
    //create a fixture and provide the shape to the body
    b2FixtureDef fixtureDef;
    fixtureDef.shape        = &edgeshape;
    fixtureDef.isSensor     = false;
    fixtureDef.density      = 1.f;
    fixtureDef.friction     = 0.1f;
    fixtureDef.restitution  = 0.1f;
    boxBody->CreateFixture(&fixtureDef);
     //clean everything
    boxBody = nullptr;
}

Step 1 : Grab Objects

The first step is to grab objects, it’s the most complicated part, so let’s go slowly.

  • Phase 1 : Capture the mouse click, when you make a mouse left-click, the function mouseGrabObject(mouse_position) will be called inside the method onMouseButton().
void onMouseButton(const sf::Mouse::Button& button, const bool& isPressed, const sf::Vector2f& position)
{
    if (button == sf::Mouse::Left && isPressed)
    {
        mouseGrabObject(ng::sf_to_b2(position, ng::SCALE));
    }
}
  • Phase 2 : Find the object you’ve clicked on. To do that, we use Box2D feature AABB Query. The way it works is pretty simple, you draw a rectangle (the AABB) and every object touching that rectangle gets selected. It’s the same idea as rectangle selection in applications like Photoshop or Gimp. In order to have a precise selection, we create a very tiny box around the mouse position (0.001 floats for each side) for our aabb. Then we call the Physical World method  QueryAABB() with our aabb and a custom Query Callback called MouseQueryCallback. I will not enter into the detail of the Query Callback, you will learn more about it later.
void mouseGrabObject(const b2Vec2& position)
{
    // Make a small box.
    b2AABB aabb;
    b2Vec2 distance;
    distance.Set(0.001f, 0.001f);
    aabb.lowerBound = position - distance;
    aabb.upperBound = position + distance;
    // Query the world for overlapping shapes.
    MouseQueryCallback callback(position);
    mPhysicWorld.QueryAABB(&callback, aabb);
}
  • Phase 3 : Grab the selected object using a Mouse Joint. As you will learn later, a Joint is something created between two bodies. Now that we have selected the object to grab, we need another object to create a joint. For this purpose, we use a Dummy body called mMouseGround. The mouse ground is created inside the method Init(), it has no special properties, it only exists so we can create some a Mouse Joint.
void init()
{
    //mouse grabbing
    b2BodyDef bodyDef;
    mMouseGround = mPhysicWorld.CreateBody(&bodyDef);
}
void mouseGrabObject(const b2Vec2& position)
{
    /*
        select object using AABB Query here
    */
    if (callback.mFixture)
    {
        b2Body* body        = callback.mFixture->GetBody();
        b2MouseJointDef jointDef;
        jointDef.bodyA      = mMouseGround;
        jointDef.bodyB      = body;
        jointDef.target     = position;
        jointDef.maxForce   = 1000.0f * body->GetMass();
        mMouseJoint         = (b2MouseJoint*)mPhysicWorld.CreateJoint(&jointDef);
        body->SetAwake(true);
        body = nullptr;
    }
}

Step 2 : Move Objects

Now that we have created a Mouse Joint on the selected object, make it follow the mouse movement is pretty simple. Using the event callback onMouseMoved(), we capture the mouse movements and call the function mouseMoveObject(mouse_position). Inside this function, we simply update the Mouse Joint with the new mouse position using SetTarget(new_position).

void onMouseMoved(const sf::Vector2f& position)
{
    mouseMoveObject(ng::sf_to_b2(position, ng::SCALE));
}
void mouseMoveObject(const b2Vec2& position)
{
    if (mMouseJoint)
    {
        mMouseJoint->SetTarget(position);
    }
}

Step 3 : Drop Objects

To drop objects, we simply destroy the Mouse Joint. Inside the event callback onMouseButton(), we detect when the mouse left button is released and we call the function mouseDropObject(). Inside mouseDropObject(), we let the Physical World destroy the joint, freeing our object from the mouse.

That’s all, you can now enjoy moving objects with your mouse !?

void onMouseButton(const sf::Mouse::Button& button, const bool& isPressed, const sf::Vector2f& position)
{
    if (button == sf::Mouse::Left && !isPressed)
    {
        mouseDropObject();
    }
}
void mouseDropObject()
{
    if(mMouseJoint)
    {
        mPhysicWorld.DestroyJoint(mMouseJoint);
        mMouseJoint = nullptr;
    }
}