Skip to content

Physics Legacy

fallahn edited this page Oct 4, 2017 · 2 revisions

This is legacy documentation

Current Wiki index can be found here

xygine provides the option to use Box2D for collision detection and physics simulation. By default Box2D is not invoked by xygine unless an instance of xy::Physics::World exists. The best place to create this is in the same scope as xy::Scene as physics components in the scene graph rely on the existence of the physics world. The physics world class has a few options, including the ability to set the gravity (which defaults to that approximately of Earth's), setting the pixel scale - that is the number of SFML pixels mapped per metre in the physics world, as well as inheriting from sf::Drawable. The latter is useful because it means you can directly draw physics debug output as easily as any other SFML drawable object:

renderTarget.draw(physicsWorld);

The physics world class requires a valid message bus object on construction, which allows physics events to be broadcast via messages. These messages can then be interpreted by any part of xygine. (see Callbacks and Events, below). Once a physics world has been created entities may then have a RigidBody component attached to them. Each entity may only have one body attached, and once attached the transform of the entity can no longer be updated manually. Entity positions, therefore, should first be set if they need to be spawned in a particular location, before attaching the RigidBody component. The component will then be initialised at this position. RigidBody components can be any of the three types of components offered by Box2D - Static, Dynamic or Kinematic. As xygine relies entirely on Box2D for its default physics the Box2D documentation can offer valuable insight to the design and use of xygine's physics system, and details the types of available rigid bodies. The RigidBody component can be created via the appropriate factory function:

auto rigidBody = xy::Component::create<xy::Physics::RigidBody>(messageBus, xy::Physics::BodyType::Dynamic);

Once the body is created it is ready to be configured. RigidBody objects on their own do not interact with the world, they require CollisionShape objects attached to them to define their shape, density and mass, and can optionally have Joint objects attached to them, to join them to other RigidBody components.


Collision Shapes

CollisionEdgeShape

Created with a list of points relative to the body position CollisionEdgeShapes can be used to create a chain or loop of edges, which have double sided collision. Useful for outlining irregular shapes, such as terrain in a side scroller.

CollisionRectangleShape

A box shape created with a width and a height, and placed at a position relative to the parent rigid body.

CollisionPolygonShape

Similar to edge shapes polygon shapes are created with a list of points, positioned relatively to the parent RigidBody. Polygon shapes must be convex, and are always closed-looped. They cannot be hollow. Complex concave shapes can be created by attaching multiple polygon shapes to a single body.

CollisionCircleShape

A simple circle shape with a radius property. Circles are positioned about their centre, and placed relatively to their parent body.

Collision shapes can be created on the stack, configured, then added to a RigidBody component. The shapes are copied into the component, so can be reconfigured and attached to multiple components. Letting the shape go out of scope will not affect any of the bodies to which they are attached. RigidBody::addCollisionShape<T>() returns a pointer to the newly created collision shape attached to the component, so settings of a particular instance can be updated. Just be aware that once a body is destroyed so are any shapes attached to it, and pointers to these will become invalid. Using the messaging system is it possible to receive destruction notifications and nullify pointers when necessary. All shapes have friction, density and restitution (ie bounciness) properties, and can also be configured as sensors. Sensors have no effect on the physics simulation but still raise events when colliding. Collision types can be filtered using the Box2D filter masks. These are explained in the Box2D manual.

//collision shape size and position are relative
//to the position of the parent RigidBody
xy::Physics::CollisionRectangleShape rs({400.f, 120.f});
body->addCollisionShape(rs);

//resize shape and add it to the same body
rs.setRect(sf::FloatRect(400.f, 0.f, 200.f, 120.f));
body->addCollisionShape(rs);

//add the same shape to another body
otherBody->addCollisionShape(rs);

Joints

Distance

Maintains a fixed distance between the two bodies to which it is attached

Friction

Particularly useful for top-down style games where no gravity is use, friction joints apply translational and rotational friction to bodies to which they are attached.

Hinge

Allows two attached bodies to rotate around a fixed point.

Slider

Bodies attached to a slider can be moved back and forth along a single specified axis. The axis may have limits defined, or be free to move indefinitely.

Wheel

Attaches a second body to the first at a given point, around which it rotates like a wheel. Also provides suspension like behaviour, which can be disabled if necessary by setting the spring frequency to zero.

Rope

Limits the maximum distance between two bodies. Useful for preventing flexing when joining Edge shapes together.

Weld

Attempts to fix and hold two bodies together at a single point.

Motor

Translates and/or rotates two attached bodies to a given distance along an axis, or rotation.

Joints are created similarly to CollisionShapes. They can be stack allocated and preconfigured, needing at least one RigidBody on construction. Joints are then attached to a second body using RigidBody::addJoint(). This function clones the Joint object so it may be reused, and returns a pointer to the newly created joint. Not all properties of this joint may be initialised immediately, depending on whether or not the RigidBody component is already attached to its parent entity. Care should be taken when storing these pointers as Joints which are destroyed may leave them dangling. When a Joint is destroyed an event is broadcast over the message bus, so it is suggested that these are handled to nullify any remaining pointers. Joints are usually constructed with one or two anchor points in world space coordinates, describing where on the bodies relative to the world they should attach.

//create a joint which will attach myBody to another
xy::Physics::HingeJoint joint(myBody, {100.f, 100.f}, {500.f, 100.f});
joint.setLimits(90.f, 270.f);
jointLimitEnabled(true);

//get a pointer to the created joint with myOtherBody and enable its motor
auto pJoint = myOtherBody.addJoint(joint);
pJoint->setMotorSpeed(300.f);
pJoint->motorEnabled(true);

//use original joint to attach myBody to myThirdBody
myThirdBody.addJoint(joint); //this motor is not enabled

//-----------//

//....message handler function....
if(msg.id == xy::Message::Type::PhysicsMessage)
{
    auto& msgData = msg.getData<xy::Message::PhysicsEvent>();
    if(msgData.event == xy::Message::PhysicsEvent::JointDestroyed
      && msgData.joint == pJoint)
    {
        pJoint = nullptr;
    }
}

Callbacks and Events

The physics world monitors collisions and destruction of Joints and CollisionShapes throughout the simulation. xygine automatically raises events via the message bus in the form of Message::PhysicsEvents which can then be handled elsewhere in your application. For example when a Joint or CollisionShape is destroyed the appropriate message is sent out containing a pointer to the object about to be destroyed. This can be used to nullify any pointers you may have retained when the RigidBody attached to the Joint or CollisionShape is destroyed (and therefore destroys all its attachments). Message bus events happen outside the physics update loop so acting on these events will not affect the immediate timing of the simulation. This, however, means the event messages are delayed by a single frame, which may be unacceptable in some cases. If you require a callback executed immediately upon a collision or destruction, custom functions can be supplied to be performed as soon as the collision or destruction event occurs. This is done with one of the callback aliases which can be used to supply either an existing function or a lambda expression.

JointDestroyedCallback callback = [](xy::Physics::Joint& joint)
{
    //do stuff with joint
};
physicsWorld.addJointDestructionCallback(callback);

ContactCallback ccb = std::bind(&MyClass::contactCallback, this, std::placeholders::_1);
physicsWorld.addContactBeginCallback(ccb);

Whenever a callback is invoked the Joint, CollisionShape or Contact involved is passed in as a parameter, so that it can be used within the function. Callback functions are executed during the physics update and therefore some care should be taken that they are not too onerous on the CPU.

Contacts

Contacts are event structures which are created by collisions. These contacts contain data about which CollisionShapes are colliding, as well as properties such as friction or restitution. Contact Begin, End, PreSolve and PostSolve events may all have callbacks supplied to them, in which the Contact of the current collision is available. It is possible to modify some of the Contact properties during PreSolve events, but it is not recommended during other events. The Box2D manual explains this in more detail.