Luna Tech Series: A Deep Dive into Unity Configurable Joints
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 😉
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:
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:
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 setyMotion
toLocked
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 discussLimited
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:
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:
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.
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.
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 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.
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
.
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.