Halt! You should have a good understanding of the basic tutorials before venturing further.
Ok, so you're having fun throwing things around in the Box2D world but you'd like them stick to something when they hit, rather than just fall to the ground. Common examples might be ninja stars and arrows.
This topic will cover three main points:
- Modeling an arrow in flight
- Deciding whether the arrow should stick into something it hits
- Attaching the arrow to the thing it hit
In the first of these we will simulate the way an arrow turns to face the direction it is moving in due to wind-drag on the feathered end. This is not a very generic behavior so it may not be useful for that many situations (for example, if your projectiles are ninja stars you wont care about this) but it's a nice touch to add some realism.
Deciding whether the arrow should stick in will be done by looking at the impulse that Box2D uses to separate it from the target, in the PostSolve function of the collision listener. This is a more generically useful point, not only for 'arrows sticking in' but if you consider this as a 'how much damage was done' question then it becomes useful in many more situations.
Once we've decided whether the arrow should stick in, we can attach it either with a weld joint or by actually making it part of the body that it hit, by destroying the original arrow and adding a new shape to the target.
Arrow in flight
If you went to the moon and fired an arrow at a 45 degree angle, it would fly in a nice parabola and land on its tail at 45 degrees. Actually this is what bodies in Box2D do too, because there is no air friction simulated so everything is essentially in a vacuum. To make the arrow behave as expected we need to manually add some friction or 'drag' to make it rotate.
We can simulate drag as a small force, applied to the tail of the arrow in the opposite direction to its movement, and scaled according to how fast it is moving. In reality the whole arrow is subject to a small amount of resistance as it moves through the air but compared to the drag on the tail vanes this is so small we can ignore it. The size of the drag force depends on what angle the arrow is pointing, compared to its direction of flight. For our purposes we will say that the drag is zero when the arrow is exactly pointing in the flight direction, and at its maximum when the arrow is at 90 degrees to the flight direction. To find a good value for the maximum we will just try a number and adjust it until it looks ok :)
The values in between can be found by using the dot product of the arrow direction and the flight direction. Actually, I think 'one minus the dot product' is what we want: Box2D sticky projectiles
Update: Astute observers will point out that when the arrow is facing backwards relative to the direction of movement, the value of 'one minus the dot product' will be between 1 and 2, and the arrow will be flipped around more quickly than it should be. Although this doesn't cause any major problems it's not really what was intended - the correct way is to take the absolute value of the dot product.
The arrow can be any sensible shape, a long box would do fine but if you wanted to make the center of mass a little more real-world like, you could make a slightly non-symmetrical polygon like this:
const vertices = [
new b2Vec2(-1.4, 0),
new b2Vec2(0, -0.1),
new b2Vec2(0.6, 0),
new b2Vec2(0, 0.1)
];
const polygon = b2MakePolygon(vertices, 4);
With this setup, the direction the arrow is pointing is (1,0) in the local coordinates. Putting it all together:
const pointingDirection = b2Body_GetWorldVector(arrowBodyId, new b2Vec2(1, 0));
const flightDirection = b2Body_GetLinearVelocity(arrowBodyId);
const flightSpeed = Math.sqrt(flightDirection.x * flightDirection.x + flightDirection.y * flightDirection.y);
flightDirection.x /= flightSpeed;
flightDirection.y /= flightSpeed;
const dot = flightDirection.x * pointingDirection.x + flightDirection.y * pointingDirection.y;
const dragForceMagnitude = (1 - Math.abs(dot)) * flightSpeed * flightSpeed * dragConstant * b2Body_GetMass(arrowBodyId);
const arrowTailPosition = b2Body_GetWorldPoint(arrowBodyId, new b2Vec2(-1.4, 0));
b2Body_ApplyForce(arrowBodyId, new b2Vec2(
dragForceMagnitude * -flightDirection.x,
dragForceMagnitude * -flightDirection.y
), arrowTailPosition, true);
When creating the arrow body:
b2Body_SetAngularDamping(arrowBodyId, 3);
Deciding whether to stick
Here's the contact event handling structure:
const contactEvents = b2World_GetContactEvents(worldId);
for (let i = 0; i < contactEvents.beginContacts.length; i++) {
const contact = contactEvents.beginContacts[i];
const impulse = contact.normalImpulse;
}
To handle different target materials we specify a hardness parameter. The arrow will stick if the contact normal impulse is greater than this value.
const targetParameters = {
hardness: 1
};
// Store in shape user data
b2Shape_SetUserData(shapeId, targetParameters);
For handling sticky collisions:
const stickyCollisions = [];
function handleContact(contact) {
const shapeA = b2Shape_GetBody(contact.shapeIdA);
const shapeB = b2Shape_GetBody(contact.shapeIdB);
const targetInfoA = b2Shape_GetUserData(contact.shapeIdA);
const targetInfoB = b2Shape_GetUserData(contact.shapeIdB);
if (targetInfoA && contact.normalImpulse > targetInfoA.hardness) {
stickyCollisions.push({
targetBody: shapeA,
arrowBody: shapeB
});
}
else if (targetInfoB && contact.normalImpulse > targetInfoB.hardness) {
stickyCollisions.push({
targetBody: shapeB,
arrowBody: shapeA
});
}
}
// In step function
for (const collision of stickyCollisions) {
b2Body_Disable(collision.arrowBody);
}
stickyCollisions.length = 0;
The arrows just freeze where they hit so it doesn't look right when they hit the dynamic targets, but this is a quick way to check if we've made sensible settings for the hardness vs impulse parameters.
Attaching with weld joint
Okay, so now we know which target body to attach the arrow to, it's just a matter of doing it. Let's go with the easiest way first, a weld joint. The weld joint is probably the simplest of the joints, all it does is hold two bodies in place relative to each other, exactly what we need for this situation.
for (const collision of stickyCollisions) {
const worldCoordsAnchorPoint = b2Body_GetWorldPoint(collision.arrowBody, {x: 0.6, y: 0});
const weldJointDef = b2DefaultWeldJointDef();
weldJointDef.bodyIdA = collision.targetBody;
weldJointDef.bodyIdB = collision.arrowBody;
weldJointDef.localAnchorA = b2Body_GetLocalPoint(weldJointDef.bodyIdA, worldCoordsAnchorPoint);
weldJointDef.localAnchorB = b2Body_GetLocalPoint(weldJointDef.bodyIdB, worldCoordsAnchorPoint);
weldJointDef.referenceAngle = b2Body_GetRotation(weldJointDef.bodyIdB).angle -
b2Body_GetRotation(weldJointDef.bodyIdA).angle;
b2CreateWeldJoint(worldId, weldJointDef);
}
stickyCollisions.length = 0;
Creating new shape method
const vertices = [
new b2Vec2(-1.4, 0),
new b2Vec2(0, -0.1),
new b2Vec2(0.6, 0),
new b2Vec2(0, 0.1)
];
const targetTransform = b2Body_GetTransform(collision.targetBody);
const arrowTransform = b2Body_GetTransform(collision.arrowBody);
// Transform vertices to target body space
const transformedVertices = vertices.map(v => {
const worldPoint = b2TransformPoint(arrowTransform, v);
return b2Body_GetLocalPoint(collision.targetBody, worldPoint);
});
const polygon = b2MakePolygon(transformedVertices, 4);
const shapeDef = b2DefaultShapeDef();
shapeDef.density = 1;
shapeDef.shape = polygon;
b2CreatePolygonShape(collision.targetBody, shapeDef);
b2DestroyBody(collision.arrowBody);
This time the main difference is that now each arrow has become one with its target, so that it is part of the same rigid body.
In most cases you will notice that the arrows do not stick in very far at all with the method we've looked at here, because by the time the joint is created, the collision response to bounce the arrow back out of the target has already been calculated and applied. This can look a bit odd when the arrow hits the target at a shallow angle, because it gets deflected to an even more shallow angle and then sticks onto the target in a position where their edges are barely touching. For fast-moving projectiles, consider using pre-contact events instead and disabling the collision response if the arrow speed is high enough.
Credits
This tutorial is adapted from an original piece of work created by Chris Campbell and is used under license.