Okay,
Warning - this is messy and unoptimized, but it could help you get started. Also, being that it's disorganized code, i'll be cutting out a lot of stuff that's specific to my game. I'm not the best with words, so hopefully my explanations below are not too convoluted. 🙂
So first...
Creating Fixtures and Joints
In order to avoid creating a fixture for every slot, we decide ahead of time which slots we want to map over to the physics world. This is good for performance optimization and for avoiding very mesh-based animation slots (such as wings or cloth).
Another important note - we designate one slot as the central slot. This will match up as the body part that we primarily will steer around and to which we would apply any necessary rotation forces for rotating the entire character. In this case our central slot is named "bodBounds."
Also, make sure to do all this after you've added the skeleton to the scene and placed it in its setup pose.
vector<string> slots = // place the slot names that you want to map to box2d
map<string,b2Body*> boneBodyMap; // to temporarily keep track of slot-to-body mappings
b2Body* bodyCenter = NULL;
struct imgInfo{...} // this is a class that holds the skeleton's bodies and joints for easy access later
for( string name : slots )
{
spSlot* slot = node->findSlot(name.data());
if( slot )
{
spSlot* slot = node->findSlot(name.data());
if( slot )
{
float worldVertices[1000];
float minX = FLT_MAX, minY = FLT_MAX, maxX = -FLT_MAX, maxY = -FLT_MAX;
if (slot->attachment)
{
// Create body
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position = b2Vec2(position.x,position.y);
bodyDef.gravityScale = 0;
bodyDef.bullet = isBullet;
bodyDef.fixedRotation = isFixedRotation;
b2Body* body = m_world->CreateBody(&bodyDef);
body->SetUserData(imgInfo);
imgInfo->bodies.push_back(body);
boneBodyMap.insert(pair<string,b2Body*>(string(slot->bone->data->name), body));
int verticesCount = 0;
switch( slot->attachment->type )
{
/* ... removed code... */
case SP_ATTACHMENT_SKINNED_MESH:
{
spSkinnedMeshAttachment* mesh = (spSkinnedMeshAttachment*)slot->attachment;
spSkinnedMeshAttachment_computeWorldVertices(mesh, slot, worldVertices);
verticesCount = mesh->uvsCount;
break;
}
case SP_ATTACHMENT_REGION:
{
if( verticesCount == 0 )
{
spRegionAttachment* attachment = (spRegionAttachment*)slot->attachment;
spRegionAttachment_computeWorldVertices( attachment, slot->bone, worldVertices );
verticesCount = 8;
}
// NOTE: pass-through here
}
case SP_ATTACHMENT_BOUNDING_BOX:
if( verticesCount == 0 )
{
spBoundingBoxAttachment* attachment = (spBoundingBoxAttachment*)slot->attachment;
spBoundingBoxAttachment_computeWorldVertices(attachment, slot->bone, worldVertices);
verticesCount = attachment->verticesCount;
}
// NOTE: pass-through here
case SP_ATTACHMENT_MESH:
if( verticesCount == 0 )
{
spMeshAttachment* mesh = (spMeshAttachment*)slot->attachment;
spMeshAttachment_computeWorldVertices(mesh, slot, worldVertices);
verticesCount = mesh->verticesCount;
}
b2Vec2 shapePoints[8];
b2Vec2 previousStart, previousEnd;
// box 2d doesn't like tons of vertices - so use
if( verticesCount > 16 )
{
/* we PolyPartition library for this - but there's a lot of room for optimization */
}
else
{
// create the fixture using the world vertices
b2FixtureDef fixtureDef;
b2PolygonShape polygonShape;
int iP = 0;
for (int ii = 0; ii < verticesCount; ) {
shapePoints[iP].x = worldVertices[ii]*node->getScaleX();
++ii;
shapePoints[iP].y = worldVertices[ii]*node->getScaleY();
++ii;
++iP;
}
polygonShape.Set( shapePoints, verticesCount/2 );
fixtureDef.shape = &polygonShape;
fixtureDef.density = density;
fixtureDef.friction = friction;
fixtureDef.restitution = restitution;
fixtureDef.filter.categoryBits = PHYSICS_FILTER_CATEGORY_ENEMY;
fixtureDef.filter.maskBits = PHYSICS_FILTER_MASK_ENEMY;
setFilterBits(fixtureDef, aiDefinition->type);
b2Fixture *fixture = body->CreateFixture(&fixtureDef);
}
break;
}
// sub-bodies get a low damping value so the whole character doesn't freak out when animating
body->SetAngularDamping(0.1);
body->SetLinearDamping(0.1);
// locate our central body and store it for use later
if( strcmp(slot->data->name,"bodBounds") == 0 )
{
bodyCenter = body;
}
}
}
}
//// Now to build the joints
// TODO - optimize - a second pass like this is not necessary
slots.erase(slots.begin()); // remove the central slot
for( string name : slots )
{
spSlot* slot = node->findSlot(name.data());
if( slot )
{
b2Body* body = boneBodyMap.at(string(slot->bone->data->name));
b2Body* parentBody = boneBodyMap.at(string(slot->bone->parent->data->name));
b2RevoluteJointDef jointDef;
jointDef.Initialize(body, parentBody, b2Vec2(position.x+slot->bone->worldX*node->getScaleX(), position.y+slot->bone->worldY*node->getScaleY()));
jointDef.collideConnected = false;
jointDef.enableMotor = true;
jointDef.maxMotorTorque = 10000.0f;
b2RevoluteJoint* joint = (b2RevoluteJoint*)m_world->CreateJoint(&jointDef);
joint->SetUserData(slot);
imgInfo->joints.push_back(joint);
imgInfo->initialJointAngles.push_back(slot->bone->rotation);
}
}
}
/** removed code */
// Now assign damping values you want for the central body
bodyCenter->SetAngularDamping(angularDamping);
bodyCenter->SetLinearDamping(linearDamping);
bodyCenter->SetUserData(imgInfo);
// adjust for current rotation
for( b2Body* b : imgInfo->bodies )
{
// note: negative rotation
b->SetTransform(bodyCenter->GetPosition(), -CC_DEGREES_TO_RADIANS(node->getRotation()));
}
Now you should have something like in the image below. This is a debug view of one of our bad guys. Outlines in blue are spine slot bounding boxes. In beige are the box2d fixtures. Note that we did not map all the body parts - just the torso, neck, and head.
Updating Animations - Rotations and Central Body Movement
At every frame, update the physics side to keep in sync with the animation:
vector<b2Joint*>& joints = imgInfo->joints;
vector<float>& initialJointAngles = imgInfo->initialJointAngles;
Node* node = imgInfo->sprite;
b2Body* body = imgInfo->body;
spSlot* bodySlot = imgInfo->bodySlot;
////////////// synchronize joints with animation bones
for( int i = 0; i < joints.size(); ++i )
{
b2Joint* j = joints.at(i);
b2RevoluteJoint* joint = (b2RevoluteJoint*)j;
spSlot* slot = (spSlot*)joint->GetUserData();
float32 angleError = joint->GetJointAngle() + slot->bone->rotation/180.0f*M_PI - initialJointAngles.at(i)/180.0f*M_PI;
if( node->getScaleY() < 0 )
angleError = joint->GetJointAngle() - slot->bone->rotation/180.0f*M_PI + initialJointAngles.at(i)/180.0f*M_PI;
if( angleError > M_PI*2 || angleError < M_PI*-2 )
{
// TODO - optimize
while (angleError <= -M_PI) angleError += M_PI*2.0f;
while (angleError > M_PI) angleError -= M_PI*2.0f;
}
// angular movement
joint->SetMaxMotorTorque(10000000);
joint->SetMotorSpeed(angleError*-FPS_SPINE_ANIMATIONS);
}
////////////// synchronize bod movement
spBone* bone = bodySlot->bone;
float nodeAngle = imgInfo->sprite->getRotation()/180.0f*M_PI;
float xx = bone->worldX*imgInfo->sprite->getScaleX() - imgInfo->previousX;
float yy = bone->worldY*imgInfo->sprite->getScaleY() - imgInfo->previousY;
float x = xx*sinf(nodeAngle) + yy*sinf(nodeAngle);
float y = xx*cosf(nodeAngle) + yy*cosf(nodeAngle);
imgInfo->previousX = bone->worldX*imgInfo->sprite->getScaleX();
imgInfo->previousY = bone->worldY*imgInfo->sprite->getScaleY();
// velocity to move the calculated distance in one frame
float dx = x*FPS_SPINE_ANIMATIONS;
float dy = y*FPS_SPINE_ANIMATIONS;
b2Vec2 v = body->GetLinearVelocity();
// don't exceed desired velocity
if( dx > 0 && v.x >= dx )
dx = 0;
else if( dx < 0 && v.x <= dx )
dx = 0;
if( dy > 0 && v.y >= dy )
dy = 0;
else if( dy < 0 && v.y <= dy )
dy = 0;
float mass = body->GetMass();
float impulseX = mass*dx;
float impulseY = mass*dy;
b2Vec2 linearImpulse(impulseX,impulseY);
// linear movement
body->ApplyLinearImpulse(linearImpulse, body->GetWorldCenter(), true);
Ragdoll
Once your animated character needs to ragdoll, stop updating the physics side. Let the physics take over and essentially apply its rotations to the animation instead.
Notes and stuff...
So this works well for us, but some characters' animations cause major stability problems. Synchronizing the central body is the cause of most of these issues. We like keeping that in though because it allows us to do things like synchronized hovering (synchronized with the wings of a character for example).
Okay, so that's all I have for now. I hope that this is remotely useful. If you have ideas for improvements, please let me know.
If time permits, I'll try to do this again with some video examples or something..
And while I'm at it, some self promotion :p
The game we're working on (almost at alpha): A Quiver of Crows - http://www.aquiverofcrows.com/