How to do a knob click?

I have a knob with RigidBody. It is constrained against movement in X, Y, or Z, and also constrained against rotation in Y or Z. It is allowed to rotate around X.

When the player presses the button down, code sets the RigidBody to isKinematic == true, and then rotates the knob around X in response to mouse movement. The effect is to let the player spin the knob via click-and-drag of the mouse.

When the player releases the mouse button, code sets the RigidBody to isKinematic == false. Also, the code keeps the amount of rotation applied in the previous FixedUpdate, converts this to an equivalent angular velocity, and sets the RigidBody’s angularVelocity to this amount. The effect is for the knob to continue spinning for a while due to its angular momentum after the player releases the mouse button if the mouse was in motion when the player released it.

This all works spectacularly well. Now I want to add a sound effect that will cause the knob to emit a “clink” every time it rotates by a certain number of degrees. I tried this trick:

        if (Quaternion.Angle(latestClink, transform.localRotation) > clinkAngle)
        {
            audioSource.PlayOneShot(clink, 1);

            latestClink = transform.localRotation;
        }

This works, after a fashion. Every time the knob’s localRotation exceeds clinkAngle when measured against the localRotation as of the previous clink, it sounds a clink and stores the localRotation into lastestClink. The next time the localRotation creates an angle greater than clinkAngle when measured against what the localRotation was at the last clink (which is stored in latestClink), it does all this again.

There are (at least) two problems with this: First, the angle of the knob for each clink isn’t precise. The clinks happen after the knob has rotated more than clinkAngle away from the last clink. That’s going to be some number near clinkAngle, but always greater and never necessarily by the same amount (depending on how fast the knob is spinning). Second, when the knob makes the clink sound, if the player reverses direction immediately and spins the knob back the other way, no clink will be heard until the knob spins more than clinkAngle again. The player is probably going to expect there to be a clink sound whenever the knob passes through a certain angle. That is, if the player is turning the knob clockwise and hears a clink. the player will expect that immediately turning the knob counterclockwise will generate the same clink again, kind of like the gizmos that make the clicking sounds on the Wheel of Fortune.


My trick does a nice job of simulating a sort of hidden mechanism, perhaps the tumblers in a safe. I can use that when it is appropriate, but it’s not like the gizmos on the Wheel of Fortune. If I knew the exact angle of rotation, I could just track that and sound a clink every time it crossed a multiple of clinkAngle. But the wacky way Quaternions get decomposed into Euler angles makes that unreliable. I could add up the angular changes of the knob, by summing the rotations when it is in isKinematic == true, and the angular velocities at each FixedUpdate when isKinematic == false, but that’s going to accumulate round-off error pretty fast.

How do I tell when a RigidBody has rotated past a multiple of a fixed angle?

If you know the RB isn’t tumbling through space, the easiest way is probably to use one the two axes that are orthogonal to the axis of rotation, and feed its values into Mathf.Atan2().

If RB is your rigidbody and it is rotating around the Z axis, then we could use the .up (Y) or the .right (X) axis.

Let’s use the .up…

somewhere we’ll also have a private float previousAngle;

Vector3 up = RB.transform.up;

float angleNow = Mathf.Atan2( up.y, up.x) * Mathf.Rad2Deg;

float difference = Mathf.DeltaAngle( previousAngle, angleNow);

previousAngle = angleNow;

Now… difference alone MIGHT get you what you want. But another way to get absolutely right-on-an-angle ticks might be to quantize it down from 0 to 360 into however many pie slices you have around, and when it’s not the same pie slice, make the tick.

This way if you just barely wiggle it back and forth across the boundary, you’d get the tick every time it crossed.

private int previousPieSlice;

and building on the angleNow we calculated above,

int pieSlice = (int)(angleNow / DegreesPerPieSlice);

if (pieSlice != previousPieSlice)
{
  // Tick!
}

previousPieSlice = pieSlice;

BUT… it might be that your ear hears higher speeds as wrong. If this doesn’t work you might have to go and actually implement the low-level DSP audio callback and make ticks according to the current rotational speed. This would involve implementing this method, and luckily they have a good example of code using it to make a metronome:

https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnAudioFilterRead.html

Hey, that’s a great solution! The up/right/forward vectors have no complications like the Euler angles do. Do you think the overhead in the Atan2 method is anything to worry about?

I suppose a “mechanical” alternative would be to add a GameObject with a collider and actually have the gizmo and pins like a real clicking wheel would have. One could use just two pins, really, and have them jump back or forward on each collision (which would also be when the sound was played).

There are some approximations for Atan2 on the 'net, so even if it is a slow method, I could probably use one of those. Great thing about games is that you can get away with tricks they’d never accept in rocket science.

Thanks for the good idea!

1 Like

Not a chance that you’d even see a single Mathf.Atan2() impacting your frame rate… Transcendental functions in modern hardware are pretty quickity-quick. You would have to do a lot of them every frame before you saw an issue.

And… you can even get away with rocket science in games! :slight_smile:

Continues to amaze me that this is so true. When I got into computer graphics, we put a lot of effort into table-based approximations for trig functions, sometimes even using polynomial interpolations between table entries (because the per-process address limit in those days was 2Mb, so your tables couldn’t be all that big). But, I remember reading the specs when the first 8087 FPUs came out. Seeing it claim a shorter time for floating square-roots than for floating division was kind of a shock. I’ve never really been able to believe these things are as fast as they are. But they are, aren’t they?

1 Like

What’s crazy is how slow main memory access is and how rarely does anyone concern themselves with it.

Check out this presentation:

https://www.youtube.com/watch?v=rX0ItVEVjHc

The entire presentation is full of great little tidbits, but the specifics comparing speeds of transcendentals vs main memory access starts around 28:30 or so.

Yeah, I imagine wide data paths help with that, as does all that cache he was talking about, and a lot of other considerations I left behind me decades ago. Had to revisit a lot of that when I did some multi-core, multi-threaded coding recently. You can get some truly mind-bending errors if you share data between different cores that each have their own caches, and you don’s use a fence or other such hocus pocus. Trouble is, when you do use the necessary mechanisms, that’s when the relative sluggishness of main memory to cache shows itself, because those mechanisms often rely on things like cache flushes to get their jobs done. Still, it all does run jaw-droppingly fast, today.

Back in c.1982, I laboriously wrote a Bresenham’s Algorithm routine in 6502 machine code (yes, I said “machine code,” as I wrote it by hand in assembler, then had to assemble it myself so I could use Atari BASIC to poke the code into memory numerically). It drew lovely lines on an external graphics card I had running on my Atari 800, but you could still see the image accumulate. At the speeds of that era, you couldn’t get near what we do today. Now… optimize? Hell, not only do people not optimize, every modern “Elements of [insert language here] Style” book explicitly says not to optimize (because we are dopes who can’t be trusted to do it without causing problems).

So, I will just call Atan2 and be done with it.