/*******************************************************************************
* Copyright 2015 See AUTHORS file.
* <p/>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.mygdx.game.objects;
import static com.mygdx.game.utilities.Constants.PI;
import static com.mygdx.game.utilities.Constants.PI0_25;
import static com.mygdx.game.utilities.Constants.PI0_5;
import java.util.Iterator;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.model.Node;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.physics.bullet.collision.btBoxShape;
import com.badlogic.gdx.physics.bullet.collision.btCollisionShape;
import com.badlogic.gdx.physics.bullet.dynamics.btConeTwistConstraint;
import com.badlogic.gdx.physics.bullet.dynamics.btFixedConstraint;
import com.badlogic.gdx.physics.bullet.dynamics.btHingeConstraint;
import com.badlogic.gdx.physics.bullet.dynamics.btRigidBody;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ArrayMap;
import com.badlogic.gdx.utils.ObjectMap;
import com.mygdx.game.blender.objects.BlenderEmpty;
import com.mygdx.game.settings.GameSettings;
/**
* @author jsjolund
*/
public abstract class Ragdoll extends GameCharacter {
/**
* Stores connection data between a rigid body and the model nodes which follow
* or control the body.
*/
private class RigidBodyNodeConnection {
// Stores the offset from the center of a rigid body to the node which connects to it
// Used when multiple nodes should follow one body under ragdoll control
public ArrayMap<Node, Vector3> bodyNodeOffsets = new ArrayMap<Node, Vector3>();
// The node this bone should follow in animation mode
public Node followNode = null;
}
/**
* Maps a ragdoll rigid body to one or more model nodes. Also stores the position offset between them.
*/
public final ArrayMap<btRigidBody, RigidBodyNodeConnection> bodyPartMap = new ArrayMap<btRigidBody, RigidBodyNodeConnection>();
/**
* Stores which nodes have been mapped to a ragdoll rigid body
*/
private final Array<Node> ragdollMappedNodes = new Array<Node>();
/**
* Temporary storage for the capsule body world transform
*/
private final Matrix4 capsuleTransform = new Matrix4();
/**
* Temporary transform used when the body is under ragdoll control
*/
private final Matrix4 resetRotationTransform = new Matrix4();
/**
* Temporary storage for node translation during calculations
*/
private final Vector3 nodeTranslation = new Vector3();
/**
* Temporary storage for capsule translation
*/
private final Vector3 capsuleTranslation = new Vector3();
private final Vector3 tmpVec = new Vector3();
private final Matrix4 tmpMatrix = new Matrix4();
private boolean ragdollControl = false;
/**
* Constructs a ragdoll out of rigid bodies using physics constraints.
*
* @param model Model to instantiate
* @param name Name of model
* @param location World position at which to place the model instance
* @param rotation The rotation of the model instance in degrees
* @param scale Scale of the model instance
* @param shape Collision shape with which to construct a rigid body
* @param mass Mass of the body
* @param belongsToFlag Flag for which collision layers this body belongs to
* @param collidesWithFlag Flag for which collision layers this body collides with
* @param callback If this body should trigger collision contact callbacks.
* @param noDeactivate If this body should never 'sleep'
* @param ragdollEmpties Blender empties with body part definitions
* @param armatureNodeId The id of the root node in the model animation armature
* @param steerSettings Steerable settings
*/
public Ragdoll(Model model,
String name,
Vector3 location,
Vector3 rotation,
Vector3 scale,
btCollisionShape shape,
float mass,
short belongsToFlag,
short collidesWithFlag,
boolean callback,
boolean noDeactivate,
Array<BlenderEmpty> ragdollEmpties,
String armatureNodeId,
SteerSettings steerSettings) {
super(model, name, location, rotation, scale, shape, mass,
belongsToFlag, collidesWithFlag, callback, noDeactivate, steerSettings);
createRagdoll(ragdollEmpties, armatureNodeId);
}
@Override
public void update(float deltaTime) {
super.update(deltaTime);
if (ragdollControl) {
updateArmatureToBodies();
} else {
updateBodiesToArmature();
}
}
@Override
public void dispose() {
super.dispose();
for (btRigidBody body : bodyPartMap.keys) {
body.dispose();
}
bodyPartMap.clear();
}
/**
* Updates the nodes of the model instance to follow the rigid body parts
*/
private void updateArmatureToBodies() {
// Let dynamicsworld control ragdoll. Loop over all ragdoll part collision shapes
// and their node connection data.
for (Iterator<ObjectMap.Entry<btRigidBody, RigidBodyNodeConnection>> iterator1
= bodyPartMap.iterator(); iterator1.hasNext(); ) {
ObjectMap.Entry<btRigidBody, RigidBodyNodeConnection> bodyEntry = iterator1.next();
btRigidBody partBody = bodyEntry.key;
RigidBodyNodeConnection connection = bodyEntry.value;
capsuleTransform.getTranslation(capsuleTranslation);
// Loop over each node connected to this collision shape
for (Iterator<ObjectMap.Entry<Node, Vector3>> iterator2
= connection.bodyNodeOffsets.iterator(); iterator2.hasNext(); ) {
ObjectMap.Entry<Node, Vector3> nodeEntry = iterator2.next();
// A node which is to follow this collision shape
Node node = nodeEntry.key;
// The offset of this node from the untranslated collision shape origin
Vector3 offset = nodeEntry.value;
// Set the node to the transform of the collision shape it follows
partBody.getWorldTransform(node.localTransform);
// Calculate difference in translation between the node/ragdoll part and the
// base capsule shape.
node.localTransform.getTranslation(nodeTranslation);
// Calculate the final node transform
node.localTransform.setTranslation(nodeTranslation.sub(capsuleTranslation)).translate(tmpVec.set(offset).scl(-1));
}
}
// Calculate the final transform of the model.
modelInstance.calculateTransforms();
}
/**
* Updates the rigid body parts to follow nodes of the model instance
*/
private void updateBodiesToArmature() {
// Ragdoll parts should follow the model animation.
// Loop over each part and set it to the global transform of the armature node it should follow.
capsuleTransform.set(modelTransform);
for (Iterator<ObjectMap.Entry<btRigidBody, RigidBodyNodeConnection>> iterator
= bodyPartMap.iterator(); iterator.hasNext(); ) {
ObjectMap.Entry<btRigidBody, RigidBodyNodeConnection> entry = iterator.next();
RigidBodyNodeConnection data = entry.value;
btRigidBody body = entry.key;
Node followNode = data.followNode;
Vector3 offset = data.bodyNodeOffsets.get(followNode);
body.proceedToTransform(tmpMatrix.set(capsuleTransform)
.mul(followNode.globalTransform).translate(offset));
}
}
/**
* Enable or disable ragdoll control. During ragdoll control, the animation armature nodes follow
* the simulation of the rigid bodies. Otherwise, the rigid bodies follow the armature nodes.
*
* @param setRagdollControl
*/
public void setRagdollControl(boolean setRagdollControl) {
if (setRagdollControl) {
updateBodiesToArmature();
// Ragdoll follows animation currently, set it to use physics control.
// Animations should be paused for this model.
ragdollControl = true;
// Get the current translation of the base collision shape (the capsule)
capsuleTransform.getTranslation(capsuleTranslation);
// Reset any rotation of the model caused by the motion state from the physics engine,
// but keep the translation.
modelInstance.transform = resetRotationTransform.idt().inv().setToTranslation(capsuleTranslation);
// Set the velocities of the ragdoll collision shapes to be the same as the base shape.
for (btRigidBody bodyPart : bodyPartMap.keys()) {
bodyPart.setLinearVelocity(body.getLinearVelocity().scl(1, 0, 1));
bodyPart.setAngularVelocity(body.getAngularVelocity());
bodyPart.setGravity(GameSettings.GRAVITY);
}
// We don't want to use the translation, rotation, scale values of the model when calculating the
// model transform, and we don't want the nodes inherit the transform of the parent node,
// since the physics engine will be controlling the nodes.
for (Node node : ragdollMappedNodes) {
node.isAnimated = true;
node.inheritTransform = false;
}
} else {
// Ragdoll physics control is enabled, disable it, reset nodes and ragdoll components to animation.
ragdollControl = false;
modelInstance.transform = motionState.transform;
// Reset the nodes to default model animation state.
for (Node node : ragdollMappedNodes) {
node.isAnimated = false;
node.inheritTransform = true;
}
modelInstance.calculateTransforms();
// Disable gravity to prevent problems with the physics engine adding too much velocity
// to the ragdoll
for (btRigidBody bodyPart : bodyPartMap.keys()) {
bodyPart.setGravity(Vector3.Zero);
}
}
}
/**
* @param bodyPart The rigid body which is to be synchronized with a node
* @param node The node which is to be synchronized with a body
* @param nodeBodyOffset The offset from the node to rigid body origin
*/
private void addPart(btRigidBody bodyPart, Node node, Vector3 nodeBodyOffset) {
if (!bodyPartMap.containsKey(bodyPart)) {
bodyPartMap.put(bodyPart, new RigidBodyNodeConnection());
}
RigidBodyNodeConnection conn = bodyPartMap.get(bodyPart);
conn.bodyNodeOffsets.put(node, nodeBodyOffset);
if (!ragdollMappedNodes.contains(node, true)) {
ragdollMappedNodes.add(node);
}
}
/**
* @param bodyPart The rigid body which is to be synchronized with a node
* @param node The node which is to be synchronized with a body
*/
private void addPart(btRigidBody bodyPart, Node node) {
if (!bodyPartMap.containsKey(bodyPart)) {
bodyPartMap.put(bodyPart, new RigidBodyNodeConnection());
}
RigidBodyNodeConnection conn = bodyPartMap.get(bodyPart);
conn.followNode = node;
// Set the follow offset to the middle of the armature bone
Vector3 offsetTranslation = new Vector3();
node.getChild(0).localTransform.getTranslation(offsetTranslation).scl(0.5f);
conn.bodyNodeOffsets.put(node, offsetTranslation);
if (!ragdollMappedNodes.contains(node, true)) {
ragdollMappedNodes.add(node);
}
}
/**
* @param empties Blender empties containing rigid body dimension data
* @param armatureNodeId The name of the root skeleton/armature node
*/
private void createRagdoll(Array<BlenderEmpty> empties, String armatureNodeId) {
Node armature = modelInstance.getNode(armatureNodeId, true, true);
// Load mass and shape half extent data from Blender json
ArrayMap<String, Vector3> halfExtMap = new ArrayMap<String, Vector3>();
ArrayMap<String, Float> massMap = new ArrayMap<String, Float>();
for (BlenderEmpty empty : empties) {
Vector3 halfExtents = new Vector3(empty.scale);
halfExtents.x = Math.abs(halfExtents.x);
halfExtents.y = Math.abs(halfExtents.y);
halfExtents.z = Math.abs(halfExtents.z);
halfExtMap.put(empty.name, halfExtents);
float partMass = Float.parseFloat(empty.custom_properties.get("mass"));
massMap.put(empty.name, super.mass * partMass);
}
ArrayMap<String, btCollisionShape> shapeMap = new ArrayMap<String, btCollisionShape>();
ArrayMap<String, btRigidBody> bodyMap = new ArrayMap<String, btRigidBody>();
// Create rigid bodies using the previously loaded mass and half extents.
// Put them along with the shapes into maps.
for (Iterator<ObjectMap.Entry<String, Vector3>> iterator = halfExtMap.iterator(); iterator.hasNext(); ) {
ObjectMap.Entry<String, Vector3> entry = iterator.next();
String partName = entry.key;
Vector3 partHalfExt = entry.value;
float partMass = massMap.get(partName);
btCollisionShape partShape = new btBoxShape(partHalfExt);
shapeMap.put(partName, partShape);
InvisibleBody phyCmp = new InvisibleBody(partName,
partShape, partMass, new Matrix4(), this.belongsToFlag,
this.collidesWithFlag, false, true);
phyCmp.constructionInfo.dispose();
bodyMap.put(partName, phyCmp.body);
this.addPart(phyCmp.body, armature.getChild(partName, true, true));
}
// Abdomen is the at the top of the armature hierarchy
this.addPart(bodyMap.get("abdomen"), armature, new Vector3(0, halfExtMap.get("abdomen").y * 1.6f, 0));
final Matrix4 localA = new Matrix4();
final Matrix4 localB = new Matrix4();
btHingeConstraint hingeC;
btConeTwistConstraint coneC;
btFixedConstraint fixedC;
String a, b;
// TODO: This part could probably be automated somehow...
// Set the ragdollConstraints
a = "abdomen";
b = "chest";
localA.setFromEulerAnglesRad(0, PI0_25, 0).trn(0, halfExtMap.get(a).y, 0);
localB.setFromEulerAnglesRad(0, PI0_25, 0).trn(0, -halfExtMap.get(b).y, 0);
this.constraints.add(hingeC = new btHingeConstraint(bodyMap.get(a), bodyMap.get(b), localA, localB));
hingeC.setLimit(-PI0_25, PI0_5);
a = "chest";
b = "neck";
localA.setFromEulerAnglesRad(0, 0, 0).trn(0, halfExtMap.get(a).y, 0);
localB.setFromEulerAnglesRad(0, 0, 0).trn(0, -halfExtMap.get(b).y, 0);
this.constraints.add(fixedC = new btFixedConstraint(bodyMap.get(a), bodyMap.get(b), localA, localB));
a = "neck";
b = "head";
localA.setFromEulerAnglesRad(-PI0_5, 0, 0).trn(0, halfExtMap.get(a).y, 0);
localB.setFromEulerAnglesRad(-PI0_5, 0, 0).trn(0, -halfExtMap.get(b).y, 0);
this.constraints.add(coneC = new btConeTwistConstraint(bodyMap.get(a), bodyMap.get(b), localA, localB));
coneC.setLimit(PI0_25, PI0_25, PI0_25);
a = "abdomen";
b = "left_thigh";
localA.setFromEulerAnglesRad(0, PI, 0).scl(-1, 1, 1).trn(halfExtMap.get(a).x * 0.5f, -halfExtMap.get
("abdomen").y, 0);
localB.setFromEulerAnglesRad(0, 0, 0).scl(-1, 1, 1).trn(0, -halfExtMap.get(b).y, 0);
this.constraints.add(coneC = new btConeTwistConstraint(bodyMap.get(a), bodyMap.get(b), localA, localB));
coneC.setLimit(PI0_25, PI0_25, PI0_25);
coneC.setDamping(10);
a = "abdomen";
b = "right_thigh";
localA.setFromEulerAnglesRad(0, PI, 0).trn(-halfExtMap.get(a).x * 0.5f, -halfExtMap.get
("abdomen").y, 0);
localB.setFromEulerAnglesRad(0, 0, 0).trn(0, -halfExtMap.get(b).y, 0);
this.constraints.add(coneC = new btConeTwistConstraint(bodyMap.get(a), bodyMap.get(b), localA, localB));
coneC.setLimit(PI0_25, PI0_25, PI0_25);
coneC.setDamping(10);
a = "left_thigh";
b = "left_shin";
localA.setFromEulerAnglesRad(-PI0_5, 0, 0).trn(0, halfExtMap.get(a).y, 0);
localB.setFromEulerAnglesRad(-PI0_5, 0, 0).trn(0, -halfExtMap.get(b).y, 0);
this.constraints.add(hingeC = new btHingeConstraint(bodyMap.get(a), bodyMap.get(b), localA, localB));
hingeC.setLimit(0, PI0_25 * 3);
a = "right_thigh";
b = "right_shin";
localA.setFromEulerAnglesRad(-PI0_5, 0, 0).trn(0, halfExtMap.get(a).y, 0);
localB.setFromEulerAnglesRad(-PI0_5, 0, 0).trn(0, -halfExtMap.get(b).y, 0);
this.constraints.add(hingeC = new btHingeConstraint(bodyMap.get(a), bodyMap.get(b), localA, localB));
hingeC.setLimit(0, PI0_25 * 3);
// TODO: causes shoulder rotation
a = "chest";
b = "left_upper_arm";
localA.setFromEulerAnglesRad(0, PI, 0).trn(halfExtMap.get(a).x + halfExtMap.get(b).x, halfExtMap.get(a).y, 0);
localB.setFromEulerAnglesRad(PI0_25, 0, 0).trn(0, -halfExtMap.get(b).y, 0);
this.constraints.add(coneC = new btConeTwistConstraint(bodyMap.get(a), bodyMap.get(b), localA, localB));
coneC.setLimit(PI0_5, PI0_5, 0);
coneC.setDamping(10);
// TODO: as above
a = "chest";
b = "right_upper_arm";
localA.setFromEulerAnglesRad(0, PI, 0).trn(-halfExtMap.get(a).x - halfExtMap.get(b).x, halfExtMap.get(a).y, 0);
localB.setFromEulerAnglesRad(-PI0_25, 0, 0).trn(0, -halfExtMap.get("right_upper_arm").y, 0);
this.constraints.add(coneC = new btConeTwistConstraint(bodyMap.get(a), bodyMap.get(b), localA, localB));
coneC.setLimit(PI0_5, PI0_5, 0);
coneC.setDamping(10);
a = "left_upper_arm";
b = "left_forearm";
localA.setFromEulerAnglesRad(PI0_5, 0, 0).trn(0, halfExtMap.get(a).y, 0);
localB.setFromEulerAnglesRad(PI0_5, 0, 0).trn(0, -halfExtMap.get(b).y, 0);
this.constraints.add(hingeC = new btHingeConstraint(bodyMap.get(a), bodyMap.get(b), localA, localB));
hingeC.setLimit(0, PI0_5);
a = "right_upper_arm";
b = "right_forearm";
localA.setFromEulerAnglesRad(PI0_5, 0, 0).trn(0, halfExtMap.get(a).y, 0);
localB.setFromEulerAnglesRad(PI0_5, 0, 0).trn(0, -halfExtMap.get(b).y, 0);
this.constraints.add(hingeC = new btHingeConstraint(bodyMap.get(a), bodyMap.get(b), localA, localB));
hingeC.setLimit(0, PI0_5);
}
}