深入了解 Unity 可配置关节 Configurable Joints)

  本文内容翻译自Luna Tech Series: A Deep Dive into Unity Configurable Joints   并在自己理解的基础上加入了部分注解。

  附:Unity 可配置关节 官方说明

 

  ConfigurableJoint(可配置关节) 是Unity物理引擎中非常强大的一个部分,该关节包含其他关节类型的所有功能。其它关节包括角色关节 (Character Joint) 、固定关节 (Fixed Joint)、铰链关节 (Hinge Joint)、弹簧关节 (Spring Joint) ,它们不在本文讨论范围内。通过超过40个可配置的选项,你几乎可以创建从门铰链到布娃娃等所有需要两者系在一起的东西。

   遗憾的是,Unity官方文档对该部分的讲解十分的有限,难免看完之后仍有疑惑。一大堆的属性带来无尽的自由,同时又让你深陷泥潭。你不得不在这一堆充满迷惑性的属性中不断摸索,不断试错。想要把它应用于自己的项目十分的困难。

  所以我们团队(Luna Labs )尽自己最大的努力的面对了这项挑战,使我们完成了 Luna Tech Series的第二篇文章。(致敬!!!)

  通过几个星期的研究试错,我们创建了一个能完美跑起来,包含上百个关节的测试场景。并对如何使用这些关节有了非常不错的理解。

  在介绍所有信息之前,先来看一下ConfigurableJoint的基本内容,特别对于对该内容比较生疏的人。

  有bug的布偶娃娃——开启早晨的完美方式(Buggy ragdolls — a perfect way to start the morning.  直译,有点生硬。英语高手指点一下。)

  

基础:锚点和连接体(anchors and connected body)

  锚点(anchor)和连接锚点(connectedAnchor)是所有关节里的两个主要属性。他们是关节世界的罗密欧与朱丽叶——两个千方百计紧紧粘连在一起的点。Unity会尝试通过关节移动对象(object),直到这两个点在同一位置为止。它一般都会成功,除非你使用了弹簧(springs )或沿某个轴的自由运动(free motion)(我们将在后面介绍)。另外,请记住,默认情况下,锚点是对象空间(object’s space,也就是local space)中的位置,连接锚点是世界空间(world space)中的位置。所以,对象移动时,锚点也会移动,但连接锚点不会。

  (译者注:这里所说的“连接锚点是世界空间中的位置”,我认为并不准确,我认为连接锚点是连接体的一个相对位置,也就是说,连接锚点的坐标是关于连接体的相对坐标。原文这么说,可能是当连接体为空时,看起来像是世界空间坐标。Unity官方对连接体(connectedBody)这样描述:关节连接到的另一个刚体对象。可将此属性设置为 None 来表示关节连接到空间中的固定位置,而不是另一个刚体。也就是说,可以认为,当连接体为空时,有一个连接体在世界空间中(0,0,0)的位置,那连接锚点设置的坐标就刚好看起来是世界坐标。Unity会让锚点和连接锚点靠近。另外,此时,由于“空的连接体”无法移动,所以会把锚点移到连接锚点的位置。当连接体不为空时,就可以很显然的看出来:连接锚点就是连接体的一个相对位置。另外,锚点应该就是本体(body)的一个相对位置。简而言之,锚点(anchor)是相对本体的位置,连接锚点(connectedAnchor)是相对连接体的位置。)

来看下面这个例子:

  unity移动Object把锚点和连接锚点拉到一起(Unity is moving object to pull Anchor and Connected Anchor together)

  译者注:左边锚点和连接锚点是分开的,运行后,Unity会把物体拉上去,使锚点和连接锚点重合。但要注意,想更好表现这个效果,不要勾选自动配置连接锚点(autoConfigureConnectedAnchor ),勾选了的话,会把连接锚点直接拉到锚点位置。另外,也先别设置连接体(connectedBody)。这里可以理解为:连接锚点和锚点使劲靠近,但此时连接锚点不能动,所以,相对的,把物体拉上去了。

  如你所见,可以给一个物体绑定关节然后通过连接锚点来移动物体。当然,这不是非常有用,平常也不会这么用。要更好地使用它的话,你可以使用连接体(connectedBody)。当设置了连接体后,连接锚点的坐标会关联到连接体的位置。所以,当你移动连接体的时候,Unity会重新计算连接锚点,并尝试把锚点和连接锚点拉到一起。结果,你的物体就会移向该连接体。

    看,生效了,美滋滋!(It works. Sweet!)

  如果你勾选了自动配置连接锚点(autoConfigureConnectedAnchor),Unity会完全忽略你在编辑器上设置的连接锚点(connectedAnchor)的值,并在初始化时自动计算它使它和锚点在相同的位置。(译者注:经测试,去掉勾选,随意设置一个连接锚点的位置,再勾选上自动配置。运行Unity,物体不会动,Unity只会直接把连接锚点拉到锚点位置。)

下面是我们自己写的一段用于更透彻理解关节的模拟代码:

public class OurOwnJoint : MonoBehaviour {
    // Here is our configuration params
    public Rigidbody connectedBody;
    public Vector3 anchor;
    public bool autoConfigureConnectedAnchor;
    public Vector3 connectedAnchor;

    // Here we're performing some initialization.
    void Start() {
        var ourBody = GetComponent<Rigidbody>();
        // If autoConfigureConnectedAnchor is true, 
        // we should ignore value of connectedAnchor.
        if ( this.autoConfigureConnectedAnchor ) {
            // Anchor is relative to our body - let's calculate it position in the world.
            var anchorWorldPosition = ourBody.transform.TransformPoint( this.anchor );
            // If we have a connectedBody then let's store position relative to it.
            // Otherwise we can just use world position of this.anchor.
            this.connectedAnchor = this.connectedBody != null ?
                this.connectedBody.transform.InverseTransformPoint( anchorWorldPosition ) :
                anchorWorldPosition;
        }
    }

    // Here we're fixing a position error
    void FixedUpdate() {
        var ourBody = GetComponent<Rigidbody>();
        // Finding a world position of this.anchor.
        var anchorWorldPosition = ourBody.transform.TransformPoint( this.anchor );
        // Finding a world position of this.connectedAnchor. Note, that it's
        // already stored in this.connectedAnchor if this.connectedBody is null.
        var connectedAnchorWorldPosition = this.connectedBody != null ?
            this.connectedBody.transform.TransformPoint( this.connectedAnchor ) :
            this.connectedAnchor;
        // Here is how far we should move our body in order to co-locate 
        // this.anchor and this.connectedAnchor
        var positionError = connectedAnchorWorldPosition - anchorWorldPosition;
        // This is a simplification - real joint will move connectedBody as well
        ourBody.position += positionError;
    }
}
Here’s our own joint code to make things crystal clear:

  这是最基本最简单的部分,下面我们来看一些吊炸天的东西。

 

线性运动(Linear Motion

  这里,我们涵盖了三个相似的移动属性:xMotionyMotionzMotion. 想象一下,连接锚点的世界坐标是(1, 2, 3),锚点的世界坐标是(-1, -2, -3)。任何关节(Joint)的主要目标就是使锚点和连接锚点处于同一位置,这坐标的位置关系明显与我们的目标不符。想要使它们符合预期,关节就要去改变锚点(anchor)或者连接锚点(connectedAnchor)的位置。我们要么使本体(body,就是绑定Configurable Joint的对象)移动(-2,-4,-6),要么使连接体(connected body)移动(2,4,6),又或者两者都往它们之间移动。当然,你也可以通过微调移动属性(motion properties)来纠正这个偏差(这里的偏差就是指锚点和连接锚点的差值)。

  每个移动属性(Motion)都可以设置成下面这些值:

  • Locked: 锁定,偏差会被完全纠正。例如,上面这个例子,你把yMotion 设置成锁定,会使其中一个物体在Y方向移动4个单位,或者分别向对方移动两个单位,总之,会让它们在一起。具体怎么移动,视当时情况而定。 
  • Limited: 受限制的,如果你想在超过一定阈值时再纠正这个偏差,可以设置成受限制的。我们接下来还会详细讨论它。
  • Free: 放任自由的。你对对应轴上的偏差感到满意,并不想纠正它时使用。(译者注:意思就是在对应轴上,你不作任何限制。受咋咋地,像极了受尽996摧残后放假的你~)

  下面演示锁定和自由参数设置:

  xMotion 设置成自由,yMotion 设置成锁定,只在y轴方向上纠正了偏差(xMotion is free and yMotion is locked — only error along Y axis was corrected)

  就像我们在Update里加入了这一段代码:

// ... skipping almost everything for brevity
if ( 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;
And what it looks like when we update our code:

  如你所见,如果你直接复制上面的代码,你会得到完全不一样的结果——物体会旋转,而不是上升。

  为什么会这样呢?你会在下面的内容中找到答案。

 

坐标轴(Axes

  这里有一个非常重要的细节,线性限制(linear limits)使用的是本地坐标系,而不是世界坐标系。这就意味着,我上面写的代码只有在所有坐标轴上旋转都为0时才有效。这里还有一种更为重要的情况:让我们来看看前面的场景实际上会发生什么!

  

  再试一遍:xMotion 设置成自由,yMotion 设置成锁定,(Once again: xMotion is free and yMotion is locked)

  (译者注:这种情况,在一个物体时,即连接体为空时,是不会出现的。只有当连接体不为空,且满足一定条件时,才会出现。条件就是连接体与本体x坐标不一致时,且没有锁定绕z轴的旋转,没有锁定y轴方向的移动(可以通过Rigidbody的Freeze Position和Frezee Rotation 锁定)。当连接体放到(0,0,0)位置时,把它的y轴移动和绕z轴旋转锁定之后,就和连接体为空表现完全一样了。这也验证了我上面的观点,连接体为空,就相当于连接体位于世界坐标的(0,0,0)点。然后,通过这里的测试,可以看出,这个“空的连接体”是位于原点,不能移动,不能旋转的连接体。

  尽管我们的立方体没有移动一点,但是绕着z轴发生了旋转。当你只能移动y轴本身时,为什么沿着y轴向上移动立方体去纠正偏差?(译者注:这一段原文可能表述有误,我觉得应该是“当你沿y轴移动立方体就能纠正偏差,为什么要绕z轴旋转来纠正误差?”放上原文,高手指点一下。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?)

  如果你把图片拉近来看,你会发现这个偏差其实已经完全被纠正了——把锚点和连接锚点放到我们新的坐标系中,y的值已经一样了(x轴是自由的,不会纠错,只有y轴锁定会完全纠错)。

  但是,为什么关节(Joint)会这么做?再扩展一下,为什么关节们(Joints)要这么做?

  还记得上面我提过的例子吗?我们在(1,2,3)有一个连接锚点,以及在(-1,-2,-3)有一个锚点。

  是的,一个解决这种情况的合理方式是:我们的本体(body)移动(2,4,6)。但其它的解决方案也可能是本体移动(1002,1004,1006),连接体(connectedBody )移动(1000,1000,1000)。很明显,这样也可以使锚点和连接锚点最终停在相同的位置,使偏差得到完全纠正。这是一个正确的结果,但这样合理吗?明显不合理。那么在关节世界里,怎么才是合理的?

  答案很简单——花费最小的能量去做你需要做的事情。例如,移动(2,4,6)就比移动(1002,1004,1006)节能得多。同理,你稍微转个身,比起移动更节能得多,不是吗?(译者注:好像挺有道理的哈。)就是这么简单,如果可以,关节会选择旋转它,而不是移动它。

  等等,还没完,还有两个属性需要讨论一下:第一轴向(axis)和第二轴向(secondaryAxis)。它们定义了本地空间的x轴、y轴方向。默认情况下axis = (1,0,0),secondaryAxis = (0,1,0) 。z轴方向通过x轴,y轴方向向量叉乘得到:(1, 0, 0) × (0, 1, 0) = (0, 0, 1)。所以,一般使用默认值就能符合你的期望。你可以修改轴向,但xMotion 始终会控制移动沿着第一轴向(axis)进行,yMotion 始终会控制移动沿着第二轴向(secondaryAxis)进行。

  为了巩固知识,我们再一次更新代码

// ... 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;
Armed with that knowledge, let’s update our code once again:

 

 

线性限制(Linear Limits

  在这一部分将讨论移动属性(Motion)设置成受限制( Limited)的情况。这个选项告诉关节在对应的轴上允许一定的偏差存在。这个最大偏差的绝对值可以通过线性限制(linearLimit属性指定。

  这个属性结构体包含三个字段:

  • limit:限制,允许最大的偏差值。移动限制的边界,可以理解为可移动偏离关节原点的最大距离。
  • bounciness:弹跳强度,决定当位置超出限制时是否反弹。值的大小确定反弹的力度。
  • contactDistance:碰触距离,可以防止逼近极限时突然停止。(译者注:Unity官方解释为——确定解算器可“看到”关节限制的空间前方的距离。)

  它会应用于所有受限制的(Limited)的轴向上。想象一下,我们把限制设置成3。如果只有一个轴向是受限制的,其它轴向是锁定的(Locked),那么对象将在受限制的轴向上来回不超过3个单位的移动。同理,两个轴向受限将提供一个圆形移动区域;而三个轴向都设置成受限制的话,将在一个半径为3的球形区域移动。

  移动属性在x轴、y轴上受限制而在z轴方向锁定,则锚点可以在黄色圆形区域内的任意位置(Movement is limited along X and Y and locked along Z. Anchor can be anywhere within the neon yellow circle.)

  默认情况下,到达线性限制(linear limit)阈值会立即停止物体移动。如果你想它有弹跳效果,你可以使用反弹(bounciness) 属性。设置后,本体(body)到达极限时会以一定的速度(velocity = velocityAlongAxis * bounciness)反弹。当设置反弹强度为1时,几乎不会有能量损耗。(但经过测试,每次撞击极限都会有一定的能损。)

  你也可以通过线性限制弹力(linearLimitSpring)属性给本体添加一个虚拟弹力。通常,你可以通过弹力(spring)属性设置弹力的刚度,通过阻尼器(damper)属性设置阻尼比。

  注意:一旦设置了线性限制弹力(即 LinearLimitSpring下的 spring != 0)之后,线性限制(LinearLimit)下的反弹属性将完全失效。因为它们无法一同运作。

  从左到右,常规设置线性限制,线性限制+反弹,线性限制+弹力,  线性限制+ 弹力+阻尼(From left to right — regular limit, limit with bounciness, limit with spring, limit with spring and damper)

  在Unity中实现弹簧很乏味,所以我们略过它们直接跳到角度部分。(It’ll be quite tedious to implement springs in Unity, so we’ll skip that part and move to angular stuff.)

 

 角度偏移(Angular movement)

  可配置关节(ConfigurableJoint)也提供了很多参数让你可以控制物体的旋转。

  在初始化期间,Unity关节会尝试保留本体与连接体之间的旋转差异。这意味着,你旋转其中一个物体,另一个物体也会跟着旋转——保持相同的旋转差异。另外,如果连接体为空,关节会认为旋转差异为0,可以有效地防止本体旋转。

  对于锚点(anchor),连接锚点(connectedAnchor ),自动配置连接锚点(autoConfigureConnectedAnchor),你可以暂时放下。它们对角度偏移没有影响。而不管本体在连接体的上方、下方还是其它方向——最最重要的东西是它们之前的旋转差异。

  和线性移动一样,角度偏移也分为三个部分——围绕着第一轴向(axis),第二轴向(secondaryAxis),以及它们的叉积作旋转偏移。

  角度的偏移通过x轴角度偏移(angularXMotion), y轴角度偏移(angularYMotion), 以及z轴角度偏移(angularZMotion) 属性控制。你可以决定每一个轴向上出现偏差时的行为——设置成Free可以完全忽略偏差;设置成Locked可以完全纠正偏差;设置成Limited 对偏差作一定限制。

  另外,线性限制(linearLimit)、线性弹力限制(linearLimitSpring)属性控制着所有轴向的线性偏移。而角度偏移的限制更灵活一些,它有6个属性来控制:

  • lowAngularXLimit
  • highAngularXLimit
  • angularYLimit
  • angularZLimit
  • angularXLimitSpring
  • angularYZLimitSpring

  虽然这些属性已经是不言自明的了,我们还是稍作说明:

  x轴是最高级的——你可以控制它的最大和最小旋转范围,还可以对这些限制添加弹力。

  看一眼下图,为了方便,我把第一轴向(axis)设置成(0,0,1),这样就把本地的x轴指向变换到世界空间的z轴方向上。把最高x轴角度偏移限制(highAngularXLimit.limit)设置成30——允许本体(body)顺时针旋转30度;把最低x轴角度偏移限制(lowAngularXLimit.limit)设置成-90——允许本体(body)逆时针方向旋转90度;

  有点摇晃,庆幸的是,你可以看到逆时针旋转的角度是要大一些(It’s a little shaky, but hopefully, you can see that you can rotate the body much further in a counterclockwise direction)

  这对于相对初始旋转角度的旋转非常有价值。假设初始角度是45度,最高限制是30度,最低限制是-90度,那么,你的本体(body)将可以围绕轴向(axis)在[15, 135] 度的区间内任意旋转。

  x轴的旋转限制都有弹跳(bounciness )属性。当然,最低x轴角度偏移限制的弹跳(lowAngularXLimit.bounciness)会同时作用于两个限制属性,而最高x轴角度偏移限制(highAngularXLimit.bounciness)会被忽略。x轴角度限制弹力(angularXLimitSpring )会非常好的符合你的期望。但要注意,它比所有的弹跳(bounciness )属性的优先级都要高,也就是设置了角度限制弹力后,弹跳属性就无效了。

  从左到右,依次设置bounce = 0,bounce = 1,设置限制弹力(From left to right — limit with bounce = 0, limit with bounce = 1, limit with spring)

  其它轴的自由度没那么高,你不能分别控制最高、最低限制。

  例如,设置y轴角度偏移限制(angularYLimit.limit) 为45度,等价于最高限制为45度,最低限制为-45度。

  你对每个轴都可以使用弹力(spring )属性。再次说明,一旦使用了弹力属性。弹跳属性(angularYLimit.bounciness / angularZLimit.bounciness)将会被忽略。

 

线性驱动器(Linear drives)

  你也可以通过驱动属性—— 目标位置(targetPosition),  x轴驱动(xDrive), y轴驱动(yDrive), z轴驱动(zDrive)——添加三个方向的弹力。

  弹力将作用于本地轴向(axis、secondaryAxis 以及它们的叉积)方向。并通过目标位置属性(targetPosition)定义弹力作用的目标位置。

  也就是说,你把目标位置设置成(0,10,0),那么你的物体将会沿着y轴向下偏移,并在(0,-10,0)附近落脚。如果你想要在某个方向添加弹力,你需要同时设置对应的目标位置(targetPosition)以及对应驱动器的弹力属性(positionSpring)。(译者注:这里为什么会向下偏移呢?我的理解是,这个目标位置其实是锚点(anchor)的偏移向量,而锚点和物体的偏移方向是相反的。也就是说,锚点偏移量为(0,10,0)就相当于物体的偏移量为(0,-10,0)。)

  例如,你需要在第二轴向(secondaryAxis)上添加一个弹力,你需要把目标位置的y轴(targetPosition.y)和y轴驱动器的弹力(yDrive.positionSpring)都设置成非零值。

  驱动阻尼(drive.positionDamper)和其它的阻尼原理差不多。驱动最大弹力限制(drive.maximumForce)允许你定义把你的物体拉到目标位置的弹力的最大值。

  注意:驱动属性(drives)会配合运动属性(motion)工作,但不会覆盖运动属性。即运动属性的优先级高于驱动属性。例如,你设置目标位置x轴(targetPosition.x)的值为-10,以及x轴驱动的弹力(xDrive.positionSpring)为50 。但你把x轴的运动属性(xMotion)设置成了锁定的(Locked)。那么,你的这个驱动就不会生效。

  你可以通过目标速度(targetVelocity)属性,给关节添加一个想要的速度。

  与目标位置(targetPosition)相似,设置目标速度为(0, -5, 0)将会使速度结果为(0,5,0)。目标速度应用在驱动弹力(drive.positionSpring)和阻尼(drive.positionDamper)不同时为0的情况。当驱动弹力不为0时,弹力会尝试把物体拉回目标位置,最终会达到平衡而停止移动。

  如果你想要驱动作为一个动力源(motor),可以把驱动弹力设置为0,阻尼比设置成非0值(实际值是多少可以根据需要设置)。这样,弹力失效,物体将获得目标速度且不会受到阻力(译者注:这里忽略了重力,实际上会受到重力的影响)。

 

角度驱动器(Angular drives)

  角度驱动会有些棘手。我们从设置关节旋转的四元数目标旋转(targetRotation)属性开始。与目标位置相似,实际上你会获取一个相反的值。也就是说你的物体会尝试-targetRotation的变换。如果你想要你的对象摆动(0°, 30°, 0°),你需要一个相反的角度(0°, -30°, 0°),并把它转成四元数,约为(0, -0.258819, 0, 0.9659258)。

  有两种方式可以实现旋转:第一是应用一个弹力直接把本体(body)旋转到目标旋转属性位置;第二是应用两个弹力,其中一个沿着x轴,另外一个去调整剩下的旋转差异。下图显示了两个粗略近似的结果,你只需要选择你认为合适的旋转驱动模式(rotationDriveMode)。如果选择XYAndZ,则会创建两个弹力(分别由angularXDriveangularYZDrive属性定义)。如果你选择Slerp,则只会创建一个弹力(由slerpDrive定义)。

  左边使用XYAndZ模式,右边使用Slerp模式。可以得到几乎一样的结果  (XYAndZ (left) vs Slerp (right) drive modes. The outcome is mostly the same)

  和线性驱动类似,目标旋转角速度(targetAngularVelocity)可以给角度驱动添加动力。

  惊喜!目标旋转角速度实现上就是对象的旋转角速度,这和目标速度(targetVelocity)属性情况相反。

  到这,基本上把神奇的可配置骨骼的属性总结完了。在结束之前,我们还会把余下的实践中比较少用但有价值的东西讨论一下。

 

剩余的其它属性(All the remaining properties)

  我们从投影模式(projectionMode), 投影距离( projectionDistance), 和投影角度( projectionAngle )开始。你可以通过它们处理无法解决的关节——你可以点击PhysX guide去了解更多信息。(译者注:原文:You can use them to deal with unsolvable joints — check out this PhysX guide to learn more.   有点迷~)。

  译者注:Unity官方对这些属性的解释是

  projectionMode:此属性定义了当关节意外地超过自身的约束(由于物理引擎无法协调模拟中当前的作用力组合)时如何快速恢复约束。选项为 None 和 Position and Rotation

  Projection Distance:关节超过约束的距离,必须超过此距离才能让物理引擎尝试将关节拉回可接受位置。

  Projection Angle:关节超过约束的旋转角度,必须超过此角度才能让物理引擎尝试将关节拉回可接受位置。

  并且在关节和布娃娃稳定性一节有这样一句描述:

  在极端情况下(例如在墙内不完整生成布娃娃或用大力推动布娃娃),关节解算器无法将布娃娃的刚体组件保持在一起。这种情况下可能导致拉伸。要解决此问题,请使用 ConfigurableJoint.projectionMode 或 CharacterJoint.enableProjection 在关节上启用投影。)

  然后,有一个在世界空间中配置(configuredInWorldSpace)的属性。根据Unity文档,“如果激活该属性,所有的目标值将在世界空间计算,而不是对象空间。”(“if enabled, all target values will be calculated in world space instead of the object’s local space.”)然而,在我的测试场景中得到的结果却是:这个参数只会影响第一轴向(axis)和第二轴向(secondaryAxis)属性。

  最后,还有一个交换本体(swapBodies)属性。如果启用此属性,则会使关节表现得好像组件已附加到连接的刚体(Connected Body)。(If enabled, the two connected rigidbodies will be swapped, as if the joint was attached to the other body.)但是这里,我遇到的唯一情况是驱动的目标位置(targetPosition)和目标旋转(targetRotation)属性使用负值(-targetPosition / -targetRotation)代替。

  乡亲们! 就这样了!希望对你有用,可配置关节可以让你的游戏更具机械性。

  你可以随心所欲地在评论区提出你的问题,建议及要求。也不要忘了在 Medium, Twitter, Facebook, 和 Instagram 关注Luna Labs。否则,你会错过我们之后在Unity方面发表见解的文章。

 

——————————————————————————————————————————————————————————

啊啊啊!终于翻译完了。原来翻译技术文章这么难啊!

 

 

  

  

posted @ 2020-09-29 19:49  吃斤欢乐豆  阅读(7116)  评论(4编辑  收藏  举报