Luna Tech Series: A Deep Dive into Unity Configurable Joints

Vladimir T
Luna Labs

--

ConfigurableJoint is one of the most powerful parts of Unity’s physics engine, combining functionality from every other joint. With more than 40 configuration options available, you can create almost anything from a door hinge to a ragdoll and anything in between.

But, the official Unity documentation can be quite limiting. You often have to lead your way through endless properties with fuzzy and misleading descriptions. Having to replicate this behaviour in your own physics engine doesn’t make it any easier.

So, doing what our team here at Luna Labs does best, we worked very hard to resolve this challenge — which led us to this second article of the Luna Tech Series.

After weeks of trial and error, our team was able to create a perfectly working test scene with hundreds of joints whilst gaining a pretty good understanding of how to use them.

Before we share everything you need to know about ConfigurableJoint, let’s review the basics — especially for those of you who’re a little rusty 😉

Buggy ragdolls — a perfect way to start the morning

The basics: anchors and connected body

The two main properties of any joint are anchor and connectedAnchor. They’re the Romeo and Juliet of the joints world — two points that are meant to be together. Unity tries to move the object with a joint until these points are co-located, and will always succeed unless you decide to use springs or free motion along some axis (we will get to that later). Keep in mind that, by default, anchor is a position in the object’s space and connectedAnchor is a position in world space. So, when the object moves, anchor moves as well; whereas, connectedAnchor doesn’t.

Take a look at the example below:

Unity is moving object to pull Anchor and Connected Anchor together

As you can see, it’s possible to move an object by moving the connectedAnchor of an attached joint, which is not extremely useful. To make it more exciting, you can use a connectedBody. When a connectedBody is set, the position of the connectedAnchor will be relative to the position of the connectedBody. So when you move this connectedBody, Unity will recalculate the position of the connectedAnchor, and try to pull the anchor and connectedAnchor together. As a result, your object moves towards the connectedBody.

As you can see below, this even works in reverse:

It works. Sweet!

If you enable autoConfigureConnectedAnchor, Unity will completely ignore the value of connectedAnchor that you set in the editor and will calculate it automatically upon initialization to make sure that the anchor and connectedAnchor are at the same point.

Here’s our own joint code to make things crystal clear:

This was the easiest part! Now, onwards to some fancier stuff.

Linear Motion

Here, we’ll cover three similar properties: xMotion, yMotion, and zMotion.

Imagine the world position of a connectedAnchor is (1, 2, 3) and the world position of an anchor is (-1, -2, -3). There’s obviously a mismatch here as remember, the main purpose of any joint is to make sure that a connectedAnchor and anchor are at the same point.

To make things right, a joint should change the position of an anchor or connectedAnchor. The only way to do it is to move either our body by (2, 4, 6) or the connected body by (-2, -4, -6). However, you can fine-tune that error correction process by using motion properties.

And each property can take one of the following values:

  • Locked: When the entire error should be corrected. If you set yMotion to Locked in the example above, then one of the bodies will be moved by 4 units (or both of them by 2 units towards each other).
  • Limited: When you still want to correct the error, but only if it exceeds a specified threshold. We will discuss Limited motion shortly.
  • Free: When you’re fine with the error along that axis and don’t want to correct it.

Here are the results of using Locked and Free motion at the same time:

xMotion is free and yMotion is locked — only error along Y axis was corrected

And what it looks like when we update our code:

// ... skipping almost everything for brevityif ( this.xMotion == ConfigurableJointMotion.Free ) {
positionError.x = 0;
}
if ( this.yMotion == ConfigurableJointMotion.Free ) {
positionError.y = 0;
}
if ( this.zMotion == ConfigurableJointMotion.Free ) {
positionError.z = 0;
}
ourBody.position += positionError;

As you can see, if you replicate the condition shown on the picture above, you’ll get completely different results — the body will rotate instead of rising.

Why is that? You’ll find out in the next topic.

Axes

There’s one extremely important nuance — linear limits use objects local axes, not the global ones. This means that the code I wrote above is only valid if the object has zero rotation around every axis. This also has one more important implication — let’s see what will actually happen in the scene from the previous section:

Once again: xMotion is free and yMotion is locked

Although our cube hasn’t moved a bit, it rotated around Z instead. Why move the cube up to correct the error along the Y axis when you can just move theY axis itself?

If you inspect the image closely, you can see that the error was fully corrected — projecting the anchor and connectedAnchor on our new Y axis will give the same value.

But, why did the joint do that? Let’s answer a much broader question — why do joints do what they do?

Remember the example I went over in the previous section? Where we had a connectedAnchor at (1, 2, 3) and an anchor at (-1, -2, -3)?

Yes, a reasonable way to resolve this situation is to move our body by (2, 4, 6). But another solution would be to move our body by (1002, 1004, 1006) and connectedBody by (1000, 1000, 1000).

You can easily see that the anchor and connectedAnchor will end up in the same place, therefore fully correcting the error. This is a correct solution, but is it reasonable? Probably not. But what is reasonable in the joints world?

The answer is simple — using the least amount of energy possible to do what you need to do. For instance, moving the body by (2, 4, 6) is much more energy-efficient than moving it by (1002, 1004, 1006). And, slightly rotating the body is more efficient than moving it.

It’s that simple — a joint will rotate the body instead of lifting it up.

But we’re not done yet. There are still two properties to review: axis and secondaryAxis.

They both define the direction of the X and Y axes in an object’s local space and have reasonable defaults of (1, 0, 0) and (0, 1, 0). The Z axis is calculated implicitly by taking the cross product of the axis and secondaryAxis. Conveniently enough, (1, 0, 0) × (0, 1, 0) = (0, 0, 1) so defaults work as you would expect. You can point those axes in different directions, but xMotion will always control movement along axis and yMotion will always control movement along secondaryAxis.

Armed with that knowledge, let’s update our code once again:

// ... skipping almost everything for brevity// Calculating world direction of our axes
var xAxisDirection = ourBody.transform.TransformDirection(
this.axis
);
var yAxisDirection = ourBody.transform.TransformDirection(
this.secondaryAxis
);
var zAxisDirection = Vector3.Cross(
xAxisDirection,
yAxisDirection
);
// Removing error components along axes marked as "Free"
if ( this.xMotion == ConfigurableJointMotion.Free ) {
positionError -= xAxisDirection *
Vector3.Dot( positionError, xAxisDirection );
}
if ( this.yMotion == ConfigurableJointMotion.Free ) {
positionError -= yAxisDirection *
Vector3.Dot( positionError, yAxisDirection );
}
if ( this.zMotion == ConfigurableJointMotion.Free ) {
positionError -= zAxisDirection *
Vector3.Dot( positionError, zAxisDirection );
}
ourBody.position += positionError;

Linear limits

In this section, I’ll be discussing the effects of using ConfigurableJointMotion.Limited. This tells the joint that it’s okay to have some errors along with one of the axes. The maximum absolute value of allowed error can be specified using a linearLimit property.

This struct consists of three fields:

  • limit, which contains the allowed error value.
  • bounciness, which determines whether a body should bounce back when it hits the limit.
  • contactDistance, which prevents you from abrupt stops and slowing down when the object approaches the limit.

The very same limit is applied to all Limited axes. Let’s imagine that you set limit to 3. If only one axis is Limited and others are Locked, then the object will be able to travel up and down along Limited axis no further than 3 units away. Two Limited axes will give you a circle with radius 3 and three Limited axes will give you a sphere with the same radius.

Movement is limited along X and Y and locked along Z. Anchor can be anywhere within the neon yellow circle.

By default, hitting linear limit will abruptly stop the object. If you want the object to bounce, you can use the bounciness property. The body will bounce back with the velocity = velocityAlongAxis * bounciness, so setting bounciness to 1 will result in almost full energy conservation (simulation is not ideal, so you will gain some energy on each hit).

You can also attach a virtual string to limits using linearLimitSpring. As usual, you can control the spring’s stiffness via the spring property. You can also control the damping ratio via the damper property.

But when a spring is set to any non-zero value, thebounciness will be completely ignored because those two can’t really work together.

From left to right — regular limit, limit with bounciness, limit with spring, limit with spring and damper

It’ll be quite tedious to implement springs in Unity, so we’ll skip that part and move to angular stuff.

Angular movement

ConfigurableJoint has a lot of tools that allow you to control a body’s rotation as well.

During initialisation, a joint tries to preserve the rotation difference between your body and the connectedBody. That means that if you rotate one of the bodies, the other one will rotate as well — to keep the rotation difference the same. And if the connectedBody is empty, the joint assumes that rotation difference is zero, effectively preventing your body from rotating at all.

As for anchor, connectedAnchor and autoConfigureConnectedAnchor, you can forget about them as they have nothing to do with angular movements. Regardless of whether the body is above, below or on the other side of the scene from a connectedBody — the difference in rotations is all that matters.

Similar to the linear movement, angular error is split into three parts — around axis, around secondaryAxis, and around their cross product.

You can decide what to do with each error component — whether that means to ignore it completely with Free motion, to fully fix it with Locked motion, or to allow for some wiggle room with Limited motion. Movement is controlled via the angularXMotion, angularYMotion, and angularZMotion properties.

Whilst linearLimit and linearLimitSpring properties control linear limits along all axes, angular limits are way more flexible with the following six properties:

Although they are all pretty self-explanatory, we should put some of them in action.

X axis is the most advanced — you can control both minimum and maximum rotations around it as well as attach springs to both limits.

Take a look at the picture below. For convenience, I’ve set the axis to (0, 0, 1), so the local X axis is pointing in the direction of the world Z axis.

highAngularXLimit.limit is set to 30, allowing the body to rotate up to 30 degrees clockwise and lowAngularXLimit.limit is set to -90, allowing the body to rotate up to 90 degrees counterclockwise.

It’s a little shaky, but hopefully, you can see that you can rotate the body much further in a counterclockwise direction

It’s worth repeating that everything is relative to the initial rotation. If it’s equal to 45 degrees, highAngularXLimit.limit is 30 degrees and lowAngularXLimit.limit is -90 degrees then your body will be able to rotate anywhere from 15 to 135 degrees around that axis.

Both X limits have a bounciness property, however just lowAngularXLimit.bounciness is used for both limits and highAngularXLimit.bounciness is ignored.

angularXLimitSpring works pretty much as you’d expect, and again it has more priority than any bounciness setting.

From left to right — limit with bounce = 0, limit with bounce = 1, limit with spring

The remaining axes are less flexible. You can’t control lower and upper limits separately.

For example, setting angularYLimit.limit to 45 degrees is equivalent to setting the upper limit to 45 degrees and the lower limit to -45 degrees.

You can apply spring to both axes at the same time using a angularYZLimitSpring property. Again, when the spring is active, it discards angularYLimit.bounciness and angularZLimit.bounciness.

Linear drives

You can attach up to three springs using drive properties — targetPosition, xDrive, yDrive, and zDrive.

Springs are located along axes defined by axis, secondaryAxis and their cross product. They are also positioned at -targetPosition.x, -targetPosition.y, and -targetPosition.z.

This means setting targetPosition to (0, 10, 0) will force your body to move down along the Y axis, so it will settle somewhere around (0, -10, 0). In order to activate the spring along some axis, you need to set both corresponding components of the targetPosition and positionSpring property of corresponding drive.

For instance, spring along secondaryAxis will only be active when targetPosition.y and yDrive.positionSpring are both non-zero.

The drive.positionDamper property works similarly to other damper property out there. The drive.maximumForce property allows you to limit the force that Spring applies to your body to reach the desired position.

Drives work together with motion properties instead of overriding them. For example, setting targetPosition.x to -10 and xDrive.positionSpring to 50 will have no effect if you set xMotion to Locked.

You can specify the desired velocity along the joint’s axes when you use the targetVelocity property.

Similar to targetPosition, bodies with targetVelocity of (0, -5, 0) will end up with velocity equal to(0, 5, 0). targetVelocity is used when either drive.positionDamper or drive.positionSpring is non-zero. When drive.positionSpring is non-zero, the spring will try to pull the body back to the targetPosition, and will eventually stop moving.

If you want to use drive as a motor, set drive.positionSpring to zero and drive.positionDamper to something other than zero (the exact value doesn’t matter). The spring will then be disabled and the body will gain targetVelocity without any resistance.

Angular drives

Angular drives are a bit trickier. Let’s start with the targetRotation property, which is a quaternion with the desired rotation of the joint. Similar to targetPosition, it’s actually opposite of what you’ll get.

Your body will try to achieve -targetRotation instead. If you want your body to oscillate around (0°, 30°, 0°) , you will need to take the opposite of that — (0°, -30°, 0°) and convert it to quaternion, which is approximately (0, -0.258819, 0, 0.9659258).

There are two ways to achieve the desired rotation: first is by applying one spring that will try to rotate your body directly into targetRotation. Second, it’s by applying two springs — one around the X axis and the second one to fix the rest of the rotation difference.

The outcome is roughly the same in both cases. You just choose the method that looks best to you using rotationDriveMode. Setting it to XYAndZ will create two springs defined by angularXDrive and angularYZDrive properties. Setting it to Slerp will leave you with just one spring defined by slerpDrive.

XYAndZ (left) vs Slerp (right) drive modes. The outcome is mostly the same

Just like the linear drive, targetAngularVelocity can be used to attach a motor to the angular drive.

Surprise, surprise! targetAngularVelocity will actually then become the body’s angularVelocity, which is the exact opposite of how targetVelocity works.

That almost concludes our dive into the magical world of configurable joints. Before I go, I’ll briefly review the rest of the properties, which are less useful in practice, but still worth checking 😉

All the remaining properties

Let’s start with projectionMode, projectionDistance, and projectionAngle . You can use them to deal with unsolvable joints — check out this PhysX guide to learn more.

Then, there’s configuredInWorldSpace . According to Unity docs, “if enabled, all target values will be calculated in world space instead of the object’s local space.” However, my test scene shows that it only affects axis and secondaryAxis properties.

Finally, we have swapBodies , where the Unity docs indicate the following:

If enabled, the two connected rigidbodies will be swapped, as if the joint was attached to the other body.

But here, the only effect I’ve come across is that Drives starts using -targetPosition and -targetRotation instead of targetPosition and targetRotation.

That’s all, folks! Hope you found this useful, configurable joints can bring a lot of interesting mechanics into your game.

Feel free to post your questions, corrections, and requests for future articles in the comments section. Don’t forget to also follow Luna Labs on Medium, Twitter, Facebook, and Instagram!

Otherwise, you may miss the next round of Unity insights from either my colleagues or myself.

--

--