Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.

Resolved Tank turret with a joint

Discussion in 'Physics for ECS' started by Tigrian, Mar 30, 2023.

  1. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    Hi!

    I'm trying to make a tank turret with an angular velocity motor joint (so with a physic body on both the tank and the turret), on the whole it works well, I calculate the speed of the turret according to its rotational distance with the direction of the camera, and put it in the target velocity of the velocity joint but I have a slight problem:

    When the tank rotates, the turret starts to spin.

    I tried to modify the springFrequency and springDamping values of the Hinge joint associated with the angular velocity motor, but nothing works. I would like my joint to be very stiff, and to be only slightly impacted (if at all) by the rotation of the tank body.

    If anyone knows a way to do this (without completely blocking the rotation of the turret when the tank is turning), or knows a better way to rotate the turret, let me know.

     
  2. TheOtherMonarch

    TheOtherMonarch

    Joined:
    Jul 28, 2012
    Posts:
    827
    You can do this without a joint and some basic math. That is what I do for my tank turrets.
     
  3. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    I'm curious to know how you handle it in ECS, because that's what I was doing in mono too. I have two problems:

    1. Since I don't put joints, the turret is a child of the hull's physic body, which means that its collider is merged into the hull's compound collider, and which means that rotating the turret's localTransform is not enough to rotate its collider. But, this can be solved by also rotating the collider in the compound apparently (I'll look into that).
    2. What concerns me most is what SteveeHavok said in this thread, where he was against this solution: PhysicsCollider not updating with parent changes. - Unity Forum.
      I'm particularly worried about the collision misses, since turret collisions are very important in a tank game (and some tanks have a pretty big turret).
     
  4. TheOtherMonarch

    TheOtherMonarch

    Joined:
    Jul 28, 2012
    Posts:
    827
    You need to not use a CompoundCollider.
     
  5. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    I may have looked at the physic package documentation too quickly, but so far, from my understanding, if I put a collider as a child of a physic body (in a subscene), it is automatically merged into a compound collider with the other colliders associated with that physic body. If you have a solution to avoid this I'm interested.
     
  6. n3b

    n3b

    Joined:
    Nov 16, 2014
    Posts:
    56
    @Tigrian you can use a fixed joint and then update BallAndSocket (my bad, it's FixedAngle) constraint orientation every frame. But I'm not sure if ECS actually allows you to modify joints (I use Physics directly with no ECS).

    Updating compound collider is not a big deal either, technically you can just rebuild colliders every frame (I believe you plan only several tanks in the world) or modify collider API to be able to update BVH/masses. But I'd add to the SteveeHavok's the fact, that such transform changes happen outside of the solver cycles, implying unwanted penetrations that can make physics go nuts.
     
    Last edited: Apr 1, 2023
    Tigrian likes this.
  7. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    @n3b I would have loved such a solution to work, but I just checked, unfortunately no way to change the orientation of a ballAndSocket constraint. I like the idea though, I'm looking at the list of available constraints, see if there is one that can do the job.

    Otherwise, I managed in a hacky way to almost have my turret not spin: I added to the joint motor angular speed value 2 times the y-angle speed of the tank's hull (to compensate exactly for the velocity given to the turret by the tank's rotation). This works perfectly except when the tank stops rotating, the turret is then very slightly wobbly for a second, which gives an unnatural feeling, especially when the rotation of the hull stops abruptly. I'll try to find where it comes from.

    This effect is undesirable enough to me to abandon the idea of the joint, if the reason of this wobbliness is not to be found. And as @TheOtherMonarch recommend not to use a joint, I'm still looking for a solution to make the turret collider move without issues : In my case, hitting or not hitting the turret of a tank with a shell is essential in gameplay, it is excluded that collisions can be missed, or recorded wrongly.
     
  8. TheOtherMonarch

    TheOtherMonarch

    Joined:
    Jul 28, 2012
    Posts:
    827
    You can do this in a number of different ways. I am still experimenting myself with which way is best. I will probably end up with something similar to what @scottjdaley does during baking. But I cannot give you an answer what is best just yet.

    https://forum.unity.com/threads/phy...being-merged-with-parents-on-convert.1310672/

    Compound colliders cause other issues as well such as not knowing what part of the tank gets hit by a projectile. So it absolutely needs to be worked around.
     
    Last edited: Apr 1, 2023
    Tigrian likes this.
  9. n3b

    n3b

    Joined:
    Nov 16, 2014
    Posts:
    56
    @Tigrian I just checked and found this method (see attachment) so you definitely can update joint constraints

    @TheOtherMonarch and you definitely can acquire the child of compound collider from hit info
    Code (CSharp):
    1. var key = hit.ColliderKey;
    2. collider.Value.GetChild(ref key, out var childCollider);
    or child index
    Code (CSharp):
    1. key.PopSubKey(collider.Value.NumColliderKeyBits, out var childIndex);
     

    Attached Files:

    TheOtherMonarch likes this.
  10. n3b

    n3b

    Joined:
    Nov 16, 2014
    Posts:
    56
    @Tigrian oh, it's even simpler - you only need to update
    BodyAFromJoint or
    BodyBFromJoint depending on which one is the turret, both have setters (i.e. no need to update constraints)
     
  11. TheOtherMonarch

    TheOtherMonarch

    Joined:
    Jul 28, 2012
    Posts:
    827
    That is very limited. The real issue I am having with compound colliders was that I did not want all hitboxes to be part of the same ridgebody and collide with the terrain etc.
     
  12. n3b

    n3b

    Joined:
    Nov 16, 2014
    Posts:
    56
    That is what collision filters are for. You can have different filters for every single child of compound collider - whether those are wheels or hitboxes.
     
  13. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    For the SetConstraints, I'm limited to the property that are in the constraint struct, i.e. in the case of a ballAndSocket, these (target is only there for the motorized joints).
    Code (CSharp):
    1.         public static Constraint BallAndSocket(float3 impulseEventThreshold, float springFrequency = DefaultSpringFrequency, float springDamping = DefaultSpringDamping)
    2.         {
    3.             return new Constraint
    4.             {
    5.                 ConstrainedAxes = new bool3(true),
    6.                 Type = ConstraintType.Linear,
    7.                 Min = 0.0f,
    8.                 Max = 0.0f,
    9.                 SpringFrequency = springFrequency,
    10.                 SpringDamping = springDamping,
    11.                 MaxImpulse = impulseEventThreshold,
    12.                 Target = float3.zero
    13.             };
    14.         }
    So, no way to change it's orientation.

    And, in the case of BodyAFromJoint, this might do, but I am limited to a Body Frame.
    Code (CSharp):
    1. new BodyFrame
    2. {
    3.     Axis = new float3(0f,1f,0f),
    4.     PerpendicularAxis = new float3(1f, 0f, 0f),
    5.     Position = float3.zero
    6. }
    7.  
    I don't know what the perpendicular axis do, maybe rotating this one on the Y axis will do the trick, but I'm sceptical.

    Alternatively, the BodyFrame can be converted to a RigidTransform, but as the name implies, I think this is for read-only purposes (it's not an actual transform but rather a copy of the transform, this code is from the BodyFrame struct) :
    Code (CSharp):
    1. public RigidTransform AsRigidTransform() => new RigidTransform(ValidateAxes(), Position);
     
  14. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    Thank you for this information, it will certainly prove useful.
     
  15. TheOtherMonarch

    TheOtherMonarch

    Joined:
    Jul 28, 2012
    Posts:
    827
    My recollection is all the filters get merged together with compound collider.
     
  16. n3b

    n3b

    Joined:
    Nov 16, 2014
    Posts:
    56
    Sorry, I gave you a false hint here. The first thing - I mixed up BallAndSocket and FixedAngle (the first one limits linear motions). The second - these two constraints have no info about orientation, but they do keep orientation and position of two bodies using BodyAFromJoint and BodyBFromJoint as a reference.

    BodyFrame has an implicit conversion from RigidTransform, so this should be a straightforward solution
    Code (CSharp):
    1. joint.BodyBFromJoint = new RigidTransform(newOrientationInJointSpace, positionInJointSpace);
    Alternatively you could use PositionMotor joint (not the velocity one). It contains 3 constraints:
    Code (CSharp):
    1. m_Constraints = new ConstraintBlock3
    2.             {
    3.                 Length = 3,
    4.                 A = Constraint.MotorPlanar(target, math.abs(maxImpulseOfMotor)),
    5.                 B = Constraint.FixedAngle(),
    6.                 C = Constraint.Cylindrical(0, float2.zero)
    7.             }
    where the first one is responsible for the second body orientation, which you can update like this (it will keep the rest 2 constraints intact):
    Code (CSharp):
    1.  
    2. joint.SetConstraints(new FixedList512Bytes<Constraint>{Length = 1, [0] = Constraint.MotorPlanar(targetAngle, maxImpulse)});
    3.  
     
  17. n3b

    n3b

    Joined:
    Nov 16, 2014
    Posts:
    56
    Each child keeps its original filter (you can check that by collider.Value.GetCollisionFilter(childColliderKey)), also BoundingVolumeHierarchy keeps each leaf union for Aabb overlap queries

    edit: my bad, CompoundCollider BVH doesn't keep filters yet children do
     
    Last edited: Apr 1, 2023
  18. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    Nice, i'll try the implicit conversion as RigidTransform, this should do the trick. I'll tell you what my results are.

    About the position motor, did you meant the rotational motor? Because the position motor modifies position, so inputing an angle into its target would not make sense.
    If it is the case, well, unfortunately, this is what I tried first, and I encounter a problem listed in the known issues of the physics ecs package (Unity Physics overview | Unity Physics | 1.0.0-pre.65 (unity3d.com)):
    The turret was constantly rotating and did not react to changes in the targetAngle. But even if it did work, I don't know how to limit the turret rotation speed precisely if I just input a target angle to the joint (historical tanks usually have a very well defined maximum turret rotation speed).
     
  19. n3b

    n3b

    Joined:
    Nov 16, 2014
    Posts:
    56
    ah, right, it's the rotation one
    Code (CSharp):
    1. joint.SetConstraints(new FixedList512Bytes<Constraint>{Length = 1, [0] = Constraint.MotorTwist(target, math.abs(maxImpulseOfMotor))});
    You mentioned that you used "angular velocity motor joint", that one is different
    Code (CSharp):
    1. public enum ConstraintType : byte
    2.     {
    3.         /// <summary>   An enum constant representing the linear type. </summary>
    4.         Linear,
    5.         /// <summary>   An enum constant representing the angular type. </summary>
    6.         Angular,
    7.         /// <summary>   An enum constant representing the rotation motor type. </summary>
    8.         RotationMotor,
    9.         /// <summary>   An enum constant representing the angular velocity motor type. </summary>
    10.         AngularVelocityMotor,
    11.         /// <summary>   An enum constant representing the position motor type. </summary>
    12.         PositionMotor,
    13.         /// <summary>   An enum constant representing the linear velocity motor type. </summary>
    14.         LinearVelocityMotor
    15.     }
    I'm not sure if you actually hit that known issue, or it's a misconfiguration case. Regarding max velocity, as I can see in RotationMotor constraint there is a maxImpulse field which is self explanatory :)
    MotionVelocity contains this method
    Code (CSharp):
    1. public void ApplyAngularImpulse(float3 impulse)
    2.         {
    3.             AngularVelocity += impulse * InverseInertia;
    4.         }
    means
    Code (CSharp):
    1. var maxImpulse = (requiredMaxAngularVelocity / InverseInertia)[indexOfRotationAxis];
    2. joint.SetConstraints(new FixedList512Bytes<Constraint>{Length = 1, [0] = Constraint.MotorTwist(targetAngle, math.abs(maxImpulse))});
    Assuming that you have to rotate body while there is a user input, I'd go with a fixed joint instead. But there is a catch - you have to cover cases when there is an obstacle that blocks turret rotation yourself, otherwise turret will start rotating tank. In such cases something like motor with limited impulse is preferable.
     
  20. TheOtherMonarch

    TheOtherMonarch

    Joined:
    Jul 28, 2012
    Posts:
    827
    Which means that instead of just ignoring colliders tons of extra work / random access needs to happen hardly very optimized. Just for raycasts.

    Not to mention that impacts with colliders will happen and affect the ridgebody solver. What a terrible design by Unity.

    And don't get me started on use cases involving a hitbox inside another hitbox. For example, an engine hitbox inside an armor hitbox.
     
    Last edited: Apr 2, 2023
  21. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    Setup with angular velocity motor joint, with compensation by using a the y Angular velocity (from the PhysicsVelocity component) of the hull added to the to the target turret speed. It works, but the turret has a little weird wobbly bug when the tank stop turning (in video, you can see it at 0:9, 0:17, 0:21 and 0:31):

    Setup with Rotational Motor joint, with maxVelocity set to math.radians(24), and thus maxImpulse to math.abs((maxVelocity/InverseInertia)[indexOfRotationAxis]), target is set directly as the camera pivot angle. The bug of the known issues seams to appear only when the turret goes to the rear of the vehicle (which correspond to the change in target angle from 2*PI to 0 or 0 to 2*PI ), it just becomes mad and starts spinning.


    As you can see in the video, the turret move instanly with camera change, which made me think that the maxvelocity must be multiplied by a deltaTime. by reducing the maxvelocity, the turret start to rotate around the target angle (a bit too far to the left, then to the right, and so on).
     
  22. n3b

    n3b

    Joined:
    Nov 16, 2014
    Posts:
    56
    That's true, quaternions represent rotations up to PI radians, so rotation motor will always produce invalid correction when initial error (orientation before step) and future error (after step) are on the opposite sides
    Code (CSharp):
    1. // Calculate the error, adjust by tau and damping, and apply an impulse to correct it
    2.             float futureError = CalculateError(futureMotionBFromA);
    3.             float solveError = JacobianUtilities.CalculateCorrection(futureError, InitialError, Tau, Damping);
    Code (CSharp):
    1. float CalculateError(quaternion motionBFromA)
    2.         {
    3.             float angle = 2.0f * math.acos(motionBFromA.value.w);
    4.             quaternion targetAsQuaternion = quaternion.Euler(Target);
    5.             float targetAngle = 2.0f * math.acos(targetAsQuaternion.value.w);
    6.             return angle - targetAngle;
    7.         }
    Regarding the wobbliness with velocity motor, are you using default values for spring frequency and damping or custom ones? If the former, how do you calculate velocity for the motor?
     
  23. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    I'm using the default ones, I'll show you some code on how I calculate the velocity target (but it might be a mess to read):

    The camera is first rotated by input, and a component store the camera pivot (which has the same position of the turret) x and y rotation :

    Code (CSharp):
    1. var x = -data.Value.y * speed.Value * SystemAPI.Time.DeltaTime;
    2. var y = data.Value.x * speed.Value * SystemAPI.Time.DeltaTime;
    3. angle.Angle.x = ((angle.Angle.x + x)%360f);
    4. angle.Angle.y = ((angle.Angle.y + y)%360f);
    Then, I compute the turret y-localRotation, by comparing the angle between the forward direction of the turret and the forward direction of the hull,
    Code (CSharp):
    1. //FloatUtilities.Angle is float3 equivalent to Vector3.Angle
    2. var angle = FloatUtilities.Angle(localTransform.ValueRO.Forward(), _componentLookup[hullEntityReference.ValueRO.Value].Forward());
    3.  
    4. var sign = -math.sign(math.dot(_componentLookup[hullEntityReference.ValueRO.Value].Up(), math.cross(localTransform.ValueRO.Forward(),_componentLookup[hullEntityReference.ValueRO.Value].Forward())));
    5.  
    6. angleY.ValueRW.Angle = angle * sign;
    Afterwards, I calculate the turret speed based on a turn rate that is computed with previous turnrate

    Code (CSharp):
    1.  
    2. //_cameraAngle is a componentLookup of CameraAngle, the angle stored in first step.
    3. //AngleY is the angle of turret calculated above.
    4. var turretRingTargetAngle =_cameraAngle[cameraPivot.ValueRO.CameraPivot].Angle.y - angleY.ValueRO.Angle;
    5.  
    6. var sign = math.sign(turretRingTargetAngle);
    7. turretRingTargetAngle = math.abs(turretRingTargetAngle);
    8.  
    9. var currentSlowdownAng = math.abs(data.ValueRO.RotationSpeed * speed.ValueRO.PreviousTurnRate.y) * data.ValueRO.Deceleration;
    10.  
    11. float targetTurnRate =
    12.     math.clamp(
    13.         turretRingTargetAngle / (data.ValueRO.RotationSpeed * Time.fixedDeltaTime + currentSlowdownAng),
    14.         0f, 1f) * sign;
    15.  
    16. var turnRate = turretRingTargetAngle > currentSlowdownAng ? FloatUtilities.MoveTowards(speed.ValueRO.PreviousTurnRate.y, targetTurnRate, SystemAPI.Time.fixedDeltaTime /data.ValueRO.Acceleration) : FloatUtilities.MoveTowards(speed.ValueRO.PreviousTurnRate.y, targetTurnRate, SystemAPI.Time.fixedDeltaTime / data.ValueRO.Deceleration);
    17. speed.ValueRW.PreviousTurnRate.y = turnRate;
    18.  
    19. //Write in TurretRotationSpeedComponent.
    20. speed.ValueRW.Speed = data.ValueRO.RotationSpeed * turnRate;
    21.  
    And then I apply in a different system (Because I want this system to only rely on TurretRotationSpeed component, to have AI Tank). Notice that in order for the turret not to spin, I add 2 times the angular velocitity of the hull physicsbody.

    Code (CSharp):
    1. var constraints = joint.ValueRW.GetConstraints();
    2. var c0 = constraints[0];
    3. c0.Target = new float3(_turretRotationSpeedLookup[bodyPair.ValueRO.EntityA].Speed + 2f * _physicsVelocityLookup[bodyPair.ValueRO.EntityB].Angular.y,0f,0f);
    4. constraints[0] = c0;
    5. joint.ValueRW.SetConstraints(constraints);
    6.  
     
  24. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    It may be worth noting that the input is done in classic update (InitializationSystemGroup), while the update of the joints is done in fixedStep, in this group :
    Code (CSharp):
    1.     [UpdateInGroup(typeof(PhysicsSimulationGroup))]
    2.     [UpdateAfter(typeof(PhysicsCreateBodyPairsGroup))]
    3.     [UpdateBefore(typeof(PhysicsCreateContactsGroup))]
    As I'm trying to debug this, I got some inconsistent result due to the fact these don't update at the same time, even though I set the framerate to 60 fps, and the physic step to 0.167
     
  25. n3b

    n3b

    Joined:
    Nov 16, 2014
    Posts:
    56
    I'd go with something like this:
    Code (CSharp):
    1. const float maxTurretVelocity = 0.5f; // rad/sec
    2.             // camera orientation in turret space
    3.             var c = math.mul(math.conjugate(turretWorldTransform.rot), cameraWorldTransform.rot);
    4.             // Extract the angle of rotation around the y-axis
    5.             var forward = math.forward(c);
    6.             var deltaAngle = math.atan2(forward.x, forward.z);
    7.             // max required velocity
    8.             deltaAngle *= invertedDeltaTime;
    9.             var requiredTurretVelocity = math.clamp(deltaAngle, -maxTurretVelocity, maxTurretVelocity);
    10.             // represents relative angular velocity between two bodies
    11.             var constraintTarget = new float3(0, requiredTurretVelocity, 0);
    It doesn't take into account the fact that every joint applies impulse to both bodies, so there will be some undershoot after step but it should be negligible.

    edit: I've updated angle extraction
     
    Last edited: Apr 3, 2023
  26. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    Much much simpler indeed :), thanks to that, I've deleted one system, two componentLookup, some components and the weird block of calculation with the previous turnRate. I really love how elegant this solution is, as you get the same effect with turret deceleration as it get closer to target

    But unfortunately, there is still the wobbling when the tank stop turning abruptly, I've tried a solution that reduce the effect but costs me one more componentLookup. I gather the input data for moving the tank from a struct called TankMoveData (this can be user input or AI Inputs), which is on the Hull entity. By doing this componentLookup to get the input as soon as the rotation input get to 0, I can multiply by a fine tune compensation value the target velocity, and this for a couple physics steps (Ideally, I make it stop when the angular velocity y of the hull has reached 0), limiting the over/undershoot. I'm not a fan of this way of doing things, as it does not completely remove the wobbling (sometimes it is still there). Please tell me if you find the wobbling in the video disturbing from a player point of view.

    Anyway, here is the code, can certainly be improved, so tell me what you think about it, and if you believe this kind of solution is worth it, or what alternative could be used to remove this wobbling :

    Code (CSharp):
    1. if (!_turretRotationSpeedLookup.HasComponent(bodyPair.ValueRO.EntityA)) continue;
    2. var speed = _turretRotationSpeedLookup[bodyPair.ValueRO.EntityA].Speed;
    3. var hullAngularVelocityY = _physicsVelocityLookup[bodyPair.ValueRO.EntityB].Angular.y;
    4. var tankMoveData = _tankMoveDataLookup[bodyPair.ValueRO.EntityB];
    5.  
    6. var target = speed+ 2f * hullAngularVelocityY;
    7. if (compensation.ValueRO.IsTurning && math.abs(tankMoveData.Value.x) < 0.05f)
    8. {
    9.     if (math.abs(hullAngularVelocityY) < 0.01f || compensation.ValueRO.StepCount > compensation.ValueRO.MaxStepCount)
    10.     {
    11.         compensation.ValueRW.IsTurning = false;
    12.         compensation.ValueRW.StepCount = 0;
    13.     }
    14.     else
    15.     {
    16.         compensation.ValueRW.StepCount++;
    17.         target *= compensation.ValueRO.CompensationValue;
    18.     }
    19. }
    20. else
    21. {
    22.     compensation.ValueRW.IsTurning = math.abs(tankMoveData.Value.x)>0.05f;
    23. }
    24. var constraints = joint.ValueRW.GetConstraints();
    25.  
    26. var c0 = constraints[0];
    27. c0.Target = new float3(target,0f,0f);
    28. constraints[0] = c0;
    29.  
    30. joint.ValueRW.SetConstraints(constraints);
     
  27. n3b

    n3b

    Joined:
    Nov 16, 2014
    Posts:
    56
    So without this smoothing do you see any wobbling when hull is not moving? Are there overshoots by the turret itself?
    Might make sense to integrate turret orientation to compensate velocity change
    Code (CSharp):
    1. // we do this before sim, so hullAngularVelocity is the one that is expected to be after sim step, i.e. with user input applied
    2.             var angularVelocity = hullAngularVeloctiy + turretAngularVelocity;
    3.             var turretOrientation = turretWorldTransform.rot;
    4.             Integrator.IntegrateOrientation(ref turretOrientation, angularVelocity, timestep);
    5.             // camera orientation in turret space
    6.             var c = math.mul(math.conjugate(turretOrientation), cameraWorldTransform.rot);
    7.             ...
    Also I'm not yet sure about this but you might want to track the sign change of requiredTurretVelocity. i.e. if in prev frame it was negative and in current it's positive or vice versa. This happens on overshoots during hull velocity change so it could be necessary to increase clamp by hull velocity delta.

    edit: probably I'm wrong about hullAngularVeloctiy. the turretAngularVelocity is what affects future turret position with no constraints applied, and we want to compensate it with hull velocity change - so probably hullAngularVelocity should be the difference between prev and future hull velocity.
     
    Last edited: Apr 3, 2023
  28. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    Without this smoothing, I get about the same result as in the video called "Turret Angular velocity motor + compensation in targetVelocity". That means no overshoot that can be catch by human eye when the hull is not moving or moving in a straigh line (with camera that moved to different direction and turret moving of course).

    When the hull is turning, I believe the turret is a bit lagging behind the hull movement : it must be about 1 degree or less behind ("behind" in hull angular velocity direction) camera's camera pivot Y rotation <=> constantly undershooting by 1 degrees. It is not very noticeable, so I'll do debugs to know if it is really the case.

    And of course, when the hull stop turning, the turret overshoot by a couple degrees in front ("front" meaning the turret overshoot in the same direction than previous hull angular velocity direction), which is really noticeable. Actually, it is not exact same result than in previous video : In previous video the turret was wobbly, meaning once the overshoot at occured, the turret started to slightly "vibrate", going a bit right then a bit left... and so on.
    Now the turret overshoot first by a couple degree, and then just correct itself, going back to the right rotation, without "overshooting while correcting the overshoot". I hope I'm clear in my explanations, if not I'll do an other video :D.

    I'll try the different Integrator method setups you suggested, see if one works. I'll track the sign aswell.
     
  29. n3b

    n3b

    Joined:
    Nov 16, 2014
    Posts:
    56
    Sounds like expected flaws of velocity motor when you actually need the rotation one if it worked correctly. You are calculating error prior to simulation and are trying to compensate velocity based on that, while there are many things may happen during solver iterations which not being taken into account (including collisions).

    If it's critical - there is a way to fix rotation motor before Unity released fix, but you'd need to copy and edit physics package.
     
  30. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    Not critical at all, I have other area I can expand on, and come back to the turret when rotational motor is fix. But I'm curious anyway, if it is not a too much laborious fix for you to write, I'll be glad to know it. I'm okay with dealing with a copy of physic package for test purposes, but if it takes more than a couple lines to change in physics package, don't bother, I can wait the fix.
     
  31. n3b

    n3b

    Joined:
    Nov 16, 2014
    Posts:
    56
    Well I think error calculation has to be relative to target quaternion, so RotationMotorJacobian.CalculateError instead of
    Code (CSharp):
    1. float CalculateError(quaternion motionBFromA)
    2.         {
    3.             float angle = 2.0f * math.acos(motionBFromA.value.w);
    4.             quaternion targetAsQuaternion = quaternion.Euler(Target);
    5.             float targetAngle = 2.0f * math.acos(targetAsQuaternion.value.w);
    6.             return angle - targetAngle;
    7.         }
    should be
    Code (CSharp):
    1. float CalculateError(quaternion motionBFromA)
    2.         {
    3.             var targetAsQuaternion = quaternion.Euler(Target);
    4.             var delta = math.mul(math.conjugate(targetAsQuaternion), motionBFromA);
    5.             var forward = math.forward(delta);
    6.             return math.atan2(forward.x, forward.z);
    7.         }
    (targetAsQuaternion can be built during Build stage)

    The math guys probably can find a less expensive solution given that the rotation axis is constant, but I don't want to spend time on that (in your case it won't bottleneck you).

    What makes me think about your case more and more is the fact that the joint defines max impulse like a limit that motor can apply, while we ignore friction of the ring system completely. i.e. when hull stops rotating the friction should stop turret rotation too, but the joint negative impulse is limited by joint constraint, so it can't catch up with the hull. And the problem is that maxImpulse is the same solver input as velocity is - you can't adjust it between solver iterations.
    So if for example hull hits obstacle during step - it will stop rotating, but the turret will try to catch up using same maxImpulse - that will introduce another overshoot.
     
  32. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    Thank you very much for your help and your detailled explanations. Both you and TheOtherMonarch helped me very much on this. I see now where the problem lies, so I won't take more of your time. I will try different setups, including the no joint one and will post the final results when I get something good :D
     
  33. n3b

    n3b

    Joined:
    Nov 16, 2014
    Posts:
    56
    You are welcome! I think I got it after all :D With functional rotation motor - maxImpulse defines friction of the system, and the velocity limit can be implemented similar to example of velocity motor, but instead of clamped veloctiy you calculate clamped per step distance that will gradually increase/decrease constraint target.
     
  34. daniel-holz

    daniel-holz

    Unity Technologies

    Joined:
    Sep 17, 2021
    Posts:
    241
    @n3b : Thanks for the great contribution here and sorry for the delay in joining in.
    I have just confirmed that there is indeed an issue with the relative velocity calculation in the angular velocity motor. We will be looking at this issue internally.

    @Tigrian: I apologize for the issues this has caused you in authoring your tank. However, for this particular case, the position-based RotationMotor might actually be better suited since it can not suffer from any velocity drift by definition, ensuring that your turret always remains perfectly fixed at the target angle relative to the chassis. The RotationMotor acts like a spring-damper and has a target angle while the AngularVelocityMotor is a pure damper and has a target velocity. Therefore, the RotationMotor will always attempt to reach a target angle, while the angular velocity motor will only try to honor the specified target velocity. This allows you to drive the rotation motor to the exact location you desire and prevent it from changing this location unless the game requires it.
    So you could indeed manually update the target angle in the RotationMotor gradually over time using a simple first order euler integrator as was mentioned above:

    newAngle = oldAngle + timeStep * desiredRotationSpeed


    @n3b: As for the fix you suggest above, may I ask what version of the code this is from? We have made some fixes in some recent release with the rotation motor and the code you are showing might be the old one. In the latest version, the RotationMotorJacobian.CalculateError function is as follows:

    Code (CSharp):
    1. private float CalculateError(quaternion motionBFromA)
    2. {
    3.     // Calculate the relative joint frame rotation
    4.     quaternion jointBFromA = math.mul(math.mul(math.inverse(MotionBFromJoint), motionBFromA), MotionAFromJoint);
    5.  
    6.     // extract current axis and angle between the two joint frames
    7.     ((Quaternion)jointBFromA).ToAngleAxis(out var angle, out var axis);
    8.     // filter out any "out of rotation axis" components between the joint frames and make sure we are accounting
    9.     // for a potential axis flip in the to-angle-axis calculation.
    10.     angle *= axis[AxisIndex];
    11.  
    12.     return math.radians(angle) - Target;
    13. }
     
    Last edited: Apr 5, 2023
  35. daniel-holz

    daniel-holz

    Unity Technologies

    Joined:
    Sep 17, 2021
    Posts:
    241
    @Tigrian : Above you pointed out a known issue with the rotational motor that is mentioned in the documentation:
    I believe that this has been fixed in the last few releases and is a leftover (see latest code above). We will confirm and update the known issues sections accordingly.
     
  36. n3b

    n3b

    Joined:
    Nov 16, 2014
    Posts:
    56
    Hey @daniel-holz thanks for the info! I do indeed use outdated version of physics since it is heavily modified and also my own constraints instead of motors. But I believe Tigrian mentioned they're on the latest and one of the videos demonstrates issues with PI+ angles
     
    Last edited: Apr 5, 2023
  37. daniel-holz

    daniel-holz

    Unity Technologies

    Joined:
    Sep 17, 2021
    Posts:
    241
    @Tigrian:
    Could you please share the code you are using to update the target rotation angle and the package version?

    I'd like to reproduce the behaviour you are experiencing on my end.
    The wrap around in the Unity Physics joint might simply not be accounted for in the internal joint error calculation, which causes a singularity around 2 pi (360 degrees). I want to make sure though that this is not induced by the target angle calculation in some way, e.g. setting a >360 degree target angle after the first wrap around which would potentially induce a requested full 360 spin in the joint. That could explain the fact that the joint starts spinning faster and faster.

    To reproduce this exactly, your angle control code would be very helpful.

    Btw, in order to reduce the wobblyness when the turret starts or stops rotating you could limit the acceleration as well as the velocity in your controller by calculating the new rotation velocity using a single Euler integration (new_v = old_v + timestep * max_acceleration), then clamping the velocity to some maximum (new_v = max(new_v, max_v) and with the new velocity updating your turret angle with a second Euler integration (new_angle = old_angle + timestep * new_v).
    When stopping the turret simply do the same but with a negative acceleration and negative minimum velocity.
     
  38. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    Hi! using this with the rotational motor joint allowed to remove the wobbling when the tank stop turning, so this is gone. I still have a problem when the turret cross 2PI or -2PI, where it starts to spin.This might be due to my implementation, so here is the code (thanks to n3b for the required velocity code), I had the turret "vibrating" at rest without the 0.05f factor in the clamp, that is why it is there. :

    Code (CSharp):
    1. [RequireMatchingQueriesForUpdate]
    2. [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    3. [UpdateAfter(typeof(PhysicsSystemGroup))]
    4. [BurstCompile]
    5. public partial struct TurretRotationSystem : ISystem
    6. {
    7.     private ComponentLookup<LocalToWorld> _localToWorlds;
    8.     [BurstCompile]
    9.     public void OnCreate(ref SystemState state)
    10.     {
    11.         state.RequireForUpdate<TurretJointData>();
    12.         _localToWorlds = SystemAPI.GetComponentLookup<LocalToWorld>(true);
    13.     }
    14.     [BurstCompile]
    15.     public void OnDestroy(ref SystemState state)
    16.     {
    17.     }
    18.  
    19.  
    20.     [BurstCompile]
    21.     public void OnUpdate(ref SystemState state)
    22.     {
    23.         _localToWorlds.Update(ref state);
    24.         foreach (var (joint,turretAngle,data,cameraPivot, bodyPair) in SystemAPI.Query<RefRW<PhysicsJoint>, RefRW<TurretJointAngle>,RefRO<TurretJointData>,RefRO<CameraDrivenTurret>, RefRO<PhysicsConstrainedBodyPair>>())
    25.         {
    26.             // camera orientation in turret space
    27.             var c = math.mul(math.conjugate(_localToWorlds[bodyPair.ValueRO.EntityA].Rotation), _localToWorlds[cameraPivot.ValueRO.CameraPivot].Rotation);
    28.             // Extract the angle of rotation around the y-axis
    29.             var forward = math.forward(c);
    30.             var deltaAngle = math.atan2(forward.x, forward.z);
    31.             // max required velocity
    32.             deltaAngle *= 1/SystemAPI.Time.fixedDeltaTime;
    33.             var requiredTurretVelocity = math.clamp(0.05f * deltaAngle, -data.ValueRO.RotationSpeed, data.ValueRO.RotationSpeed);
    34.             // represents relative angular velocity between two bodies
    35.             var targetAngle = (turretAngle.ValueRO.Angle + requiredTurretVelocity * SystemAPI.Time.fixedDeltaTime);
    36.             var constraints = joint.ValueRW.GetConstraints();
    37.             var c0 = constraints[0];
    38.             c0.Target = new float3(targetAngle,0f,0f);
    39.             constraints[0] = c0;
    40.             joint.ValueRW.SetConstraints(constraints);
    41.             turretAngle.ValueRW.Angle = targetAngle;
    42.         }
    43.      
    44.     }
    45. }
     
  39. daniel-holz

    daniel-holz

    Unity Technologies

    Joined:
    Sep 17, 2021
    Posts:
    241
    That's great to hear!
    Looking at your code you probably need to make the angle wrap around at 2pi and -2pi. So just do an fmod of your new target angle with 2pi before setting it in the joint and you should be good.
     
  40. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    Hum, there is still a problem, it spins a little bit before returning to normal. I'll try to post you a video. I'm doing this
    Code (CSharp):
    1. var targetAngle = (turretAngle.ValueRO.Angle + requiredTurretVelocity * SystemAPI.Time.fixedDeltaTime) %(2*math.PI);
     
    Last edited: Apr 6, 2023
  41. daniel-holz

    daniel-holz

    Unity Technologies

    Joined:
    Sep 17, 2021
    Posts:
    241
    Hmmm... Yeah looks like this alone doesn't work. There seems to be still some sort of singularity around 2pi in the joint.
    We'll have to investigate this.

    Maybe just to confirm that the values you are providing make sense could you log them and post them here around the point that singularity occurs?
     
    Last edited: Apr 6, 2023
  42. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    Here is a video with a text updated to the joint target angle
     
    daniel-holz likes this.
  43. daniel-holz

    daniel-holz

    Unity Technologies

    Joined:
    Sep 17, 2021
    Posts:
    241
    Hi @Tigrian . Thanks for that video. It's clear that there is some bug around +-2pi. We logged the issue internally and will look into it.
    Likely the error that is calculated when moving past the +-2pi mark (previous location not yet past 2pi and new target location past +-2pi) will jump unintentionally. So we might just need to modify the CalculateError function to account for this crossing point. Probably just using a "DeltaAngle" function.

    Edit: I just tried this real quick and it shows promise. Could you try to replace the last line in the
    RotationMotorJacobian.CalculateError function by this line please?
    Code (CSharp):
    1. return math.radians(Mathf.DeltaAngle(math.degrees(Target), angle));
    That would replace the line
    Code (CSharp):
    1. return math.radians(angle) - Target;
    That might do it until we have finalized the fix.
     
    Last edited: Apr 9, 2023
  44. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    Hi @daniel-holz. Here is a video of the result, there is a problem around 0 (in fact around +- 2Pik without the fmod). It does not happend all the time, but it is there.
    I'm sorry the angle is not displayed anymore, I remove the setup to display it, If needed I can do it again, I just wanted to warn you quickly about it.


    I've tried setting the target to 0 if it comes too close to 0, and the wobbling happend aswell, even though the target is exaclty 0.
     
  45. daniel-holz

    daniel-holz

    Unity Technologies

    Joined:
    Sep 17, 2021
    Posts:
    241
    Ok. I think we are getting somewhere. We have this issue logged internally and we will continue working out the fix.
    Your control code is very helpful for this purpose. Thank you.

    In the meantime, I was also able to reproduce the angular velocity motor issue internally, and we are also closing in on a fix for this one.

    I'll keep you posted with the developments.
     
    Tigrian likes this.
  46. daniel-holz

    daniel-holz

    Unity Technologies

    Joined:
    Sep 17, 2021
    Posts:
    241
    @Tigrian Quick update here. We found the issue with the angular velocity motor not correctly dealing with the relative velocity between two moving rigid bodies, and the fix is completed. It will be added to a future release.

    This should fix your original tank case.
     
  47. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    Good to know, thanks for the update!
     
    daniel-holz likes this.
  48. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    106
    Hi @daniel-holz, I can confirm that the fix works, my turret takes into account the angular velocity of the hull with the angular velocity joint. No need for strange compensation with the angular velocity of the hull, and no more wobbling when the tank stops turning. I just enter an angular velocity value and my turret turns at that value, whatever the hull does. It's perfect, thank you very much for the fix.
     
    daniel-holz likes this.
  49. daniel-holz

    daniel-holz

    Unity Technologies

    Joined:
    Sep 17, 2021
    Posts:
    241
    I'm glad to hear that. Thanks for confirming that it works now.
    Happy creating! :)
     
    layker90524 likes this.
  50. MarcGFJ

    MarcGFJ

    Joined:
    May 16, 2019
    Posts:
    23
    Hello, I'm trying to use a rotational motor with Unity Physics similar to this post, but while everyone mentionned 2PI and -2PI, I'm having issues with PI and -PI. In fact, the authoring documentation even states that angles below -180 and 180 aren't recommended: https://docs.unity3d.com/Packages/com.unity.physics@1.0/manual/custom-motors.html

    So my issue arises when I make my turret turn around, and it reaches PI. It started spinning out of control beyond PI. So I changed my code to normalize the angle so that I always feed an angle withing -PI..PI, but that still doesn't work: as soon as it wraps, the motor still goes crazy instead of taking the shorter path. What should I do to stop this from happening?

    Here is the code I use (for testing, not production), with a single joint in the scene:

    Code (CSharp):
    1.     [UpdateInGroup(typeof(BeforePhysicsSystemGroup))]
    2.     partial class MyMotorSystem : SystemBase
    3.     {
    4.         private const float TAU = math.PI * 2;
    5.  
    6.         static float Repeat(float x, float d)
    7.         {
    8.             return x - math.floor(x / d) * d;
    9.         }
    10.  
    11.         static float NormalizeAngle(float a)
    12.         {
    13.             a = Repeat(a, TAU);
    14.             if (a >= math.PI)
    15.             {
    16.                 a -= TAU;
    17.             }
    18.             return a;
    19.         }
    20.  
    21.         protected override void OnUpdate()
    22.         {
    23.             var camera = Camera.main;
    24.             var forward = camera.transform.forward;
    25.  
    26.             Entities.ForEach((ref PhysicsJoint joint) =>
    27.             {
    28.                 var dir2d = math.normalizesafe(new float2(forward.x, forward.z));
    29.                 target = math.atan2(dir2d.y, -dir2d.x) - math.PI * 0.5f;
    30.                 float target = NormalizeAngle(target);
    31.  
    32.                 if (joint[0].Target.x != target)
    33.                 {
    34.                     Debug.Log($"Changing target to {target}");
    35.  
    36.                     var constraints = joint.GetConstraints();
    37.                     var constraint = constraints[0];
    38.                     constraint.Target = new float3(target, 0f, 0f);
    39.                     constraints[0] = constraint;
    40.                     joint.SetConstraints(constraints);
    41.                 }
    42.  
    43.             }).Run();
    44.         }
    45.     }
    I had a look at `RotationMotorJacobian.cs` and `CalculateError` returns `math.radians(angle) - Target`, which seems like it won't account properly for wrapping angles.

    Modifying the code like so seems to fix it:
    Code (CSharp):
    1.             //return math.radians(angle) - Target;
    2.             return -DeltaAngle(math.radians(angle), Target);
    3.         }
    4.  
    5.         static float Repeat(float x, float d)
    6.         {
    7.             return x - math.floor(x / d) * d;
    8.         }
    9.  
    10.         static float DeltaAngle(float current, float target)
    11.         {
    12.             float num = Repeat(target - current, math.PI * 2f);
    13.             if (num > math.PI)
    14.                 num -= math.PI * 2f;
    15.             return num;
    16.         }
     
    Last edited: Nov 29, 2023