There are three problems with the clamping line (and a few unrelated ones), the first is:
rotation
is returning a Quaternion
:
Feel free to skip the next one two three... paragraphs:* These have always been pet-peeves of mine in Unity. From what they've said, they're working on it, but my first pet-peeve is that Unity probably has some of the worst naming conventions you'll find in successful C# product. Part of it stems from supporting 3 languages (or 2 depending on what what you consider support [Boo is going to stop being documented and available in the editor menu]), but part of it just seems to them having their hands tied by existing code. But the presence of their new IL modification tools might change that in the future...
The other pet-peeve is Unity's use of mutable structs. They cause nothing but trouble, because modifying structs just doesn't work how people expect it to, since they're value types. But I don't need to get into this because it's been examined to death. (MSDN also makes it pretty clear it's a bad practice).
That being said Unity is totally justified in using them. This is just one time when the performance demands of games "overrides" ideal design. The performance cost of immutable structs (i.e. the cost of requiring a brand new struct to be allocated every time a position, rotation, or any other struct based value was modified) would just be too great.
Now the relevance of my mini-rant monologue: a Quaternion
represents a rotation quaternion. Games often use rotation quaternions mainly because they aren't subject to gimbal lock. And Unity quaternions internally (the editor shows rotations as Euler angles by default because you can't make any meaningful changes to them without very advanced math).
But it uses the same names for Quaternion
member names and types for the components x
,y
and z
as it does for Vector3
's. Vector3
a very common class that represents, amongst many other things Euler angles (in it's defense those are common names for those components of a Quaternion
). So if you look around you can see that has caused lots of trouble for people, since you can easily modify a Quaternion
thinking it's a Vector3
holding a rotation. If Unity didn't use mutable structs you'd have to assign a Quaternion
, which would make the error clear, but as I said, it's hands are tied. In reality you only modify a Quaternion
in Unity using certain functions, or operators.
So what you probably want is transform.eulerAngles
.
It allows you to set the for rotation in degrees. Just remember, as the docs mention, it's only for setting absolute value (like you're trying to do).
So now you'll have...
Mathf.Clamp(transform.eulerAngles.x, -60, 60);
...but that won't work because of the second problem:
Mathf.Clamp
is a pure function.
It won't (and can't) modify the variables you pass to it, instead it returns the clamped value (Unity doesn't have it marked with PureAttribute
because of the .NET version it uses)
Now you'd expect to able to use something like this...
transform.eulerAngles.x = Mathf.Clamp(transform.rotation.x, -60, 60);
...but unless you're in UnityScript, that won't work (and with good reason, UnityScript allowing that is a flaw). The final problem is:
transform
and eulerAngles
are properties.
So you're trying to modify a member of a struct that was returned by the property. That means trying to assign a value to a copy of the memeber that will be discarded at the end of the statement. To possibly oversimplify it, what that would be doing is equivalent here to:
GetTransformSomehow().GetRotationSomehow().x = 20;
That makes it clear why such an assignment won't work (it wouldn't mean anything so the compiler prohibits it).
One of the correct ways to do this follows. I choose an object initializer and the call to Mathf.Clamp
inside it because it makes it abundantly clear to your what the idea is at a glance, you could take advantage the mutable structs (shudder) and skip the extra Vector3
, but I felt that might be distracting here (calling transform.eulerAngles = currentEulerAngles
might have looked strange):
var currentEulerAngles = transform.eulerAngles; //Store the value we have
transform.eulerAngles = new Vector3();
{
x = Mathf.Clamp(currentRotation.x, -60, 60), //Set x to the value that got returned
y = currentEulerAngles.y, //Set y to the same value
z = currentEulerAngles.z //Set z to the same value
};
So now you have code that should clamp the transform, but I suspect you'll find it either:
- Doesn't work at all
- Or is acting very strangely (maybe twitching)
Even if it works, there are some problems that stem from this line:
rigidbody.AddTorque(rotUpDown*mouseSensitivity, rotLeftRight*mouseSensitivity, 0);
Edit: I didn't notice the mention of vehicular controls in the question, so it's possible you can skip this next problem. I'm no longer 100% sure what the question is asking because of the mention of both a first person camera and vehicular controls. If the transform for the camera, and the rigidbody for the vehicle, I'd recommend separating them, and the following will still apply. But if both belong to the vehicle, I'm not sure how the camera is supposed to behave (a screenshot or diagram could help clear that up).
First off, I doubt you actually want to use a Rigidbody
for a first person camera. Even the most realistic first person camera won't be behaving in a manner dictated by physics simulation. The effects you see some games like bobbing and inertia are simulated using animations and speed offsets. So you should be using Transform.Rotate
(docs), and if you find yourself looking for "physics-like" use techniques like:
Decreasing mouseSensitivity
. That's the simplest way to reduce responsiveness, like when a character is sleepy.
Basing the rotation on a clamped "velocity" that decays over time (not the same as Rigidbody
velocity, but it simulates inertia when turning)
Lerping the camera to a position on the character's model each frame. This simulates momentum when jumping or falling, and if the character's model is animated, it can add extra realism. If there's no model, Lerp the camera to a position relative to the character's collider.
Another possible problem with using Rigidbody
is that it's behavior is non-deterministic. Whether or not this matters to your game depends on it's nature, but generally speaking, people expect the same control movement will produce the same in-game camera movement every time they make it. But using a Rigidbody
means that each time they move the controls, they're "rolling the dice". There's no chance to get overly accustomed to precise movements because they're vary in result from machine to machine, and even sometimes from second to second. It can also make networking more complex because you can no longer reproduce a set of movements strictly from the input the player passed.
That being said, this next problem applies to either approach:
With your current code, the camera will move slower on faster devices. What you always want in 3D movement is framerate-independant movement (on 2D it's sometimes debatable). The fix (or at least, one fix) is simple, just use the ForceMode
of Impulse
. This prevents multiplying the number of pixels moved in the last frame (how much the velocity should change) by the time between frames. This way, no matter how fast the game is running, moving the same amount of pixels results in the same change in velocity:
rigidbody.AddTorque(_verticalInput*mouseSensitivity, _horizontalInput*mouseSensitivity, 0, ForceMode.Impulse);
You will also need to be apply rotation or torque in a different order than you have now, because your current code is modifying the rotation after you clamp it.
Edit: This next problem might also not be applicable.
Here I'm going to show the Transform
method because the code for if you use a Rigidbody
should actually be split between FixedUpdate
and Update
, and it adds a few complications.
//Now framerate doesn't affect the controls
float rotLeftRight = Input.GetAxis("Mouse X") * Time.deltaTime;
float rotUpDown = Input.GetAxis("Mouse Y") * Time.deltaTime;
//Rotate the transform. Usually the multiplication of Time.deltaTimewould be inside this call, but for simplicity I multiplied it above
transform.Rotate(rotUpDown*mouseSensitivity, rotLeftRight*mouseSensitivity, 0);
var currentEulerAngles = transform.eulerAngles; //Store the value we have after rotations
transform.eulerAngles = new Vector3
{
x = Mathf.Clamp(currentRotation.x, -60, 60), //Set x to the value that got returned
y = currentEulerAngles.y, //Set y to the same value
z = currentEulerAngles.z //Set z to the same value
};
Edit: I failed to read the last part of the question, and I guess I don't get to skip out on tackling the FixedUpdate
issue (you should look at the Unity tutorials, they go over most of the stuff I mentioned here [and are probably better at explaining it]).
The idea behind why you should use FixedUpdate
is wide enough, and well documented enough, that I'd recommend doing a little research. But the simple rule that the documentation for FixedUpdate
mentions is "[it] should be used instead of Update when dealing with Rigidbody
".
FixedUpdate
can get called multiple times a frame, but more importantly for this code, these calls occur before Update. So you'll need to move part of the code to FixedUpdate
, but the code that gets input needs to stay in Update
. This is because, as I mentioned , FixedUpdate
will execute before Update
, but input gets processed at the start of Update
. That means you'll end up with weird bugs, like missing input on some frames, and the inability to use button based inputs.
I'd personally separate the Input calls from this script and put it in another object that this script references. Not only does it take care of sharing the data between the methods, it lets you do all kinds of cool things like replay data, A.I. control, and more importantly, different controls without having to change this script. But for simplicity I'll just use a private field:
private float _verticalInput = 0f;
private _horizontalInput = 0f;
void Update()
{
_horizontalInput = Input.GetAxis("Mouse X");
_verticalInput = Input.GetAxis("Mouse Y");
var currentEulerAngles = transform.eulerAngles; //Store the value we have after rotations
//Update is called after Fixed Update, so we can do this
transform.eulerAngles = new Vector3
{
x = Mathf.Clamp(currentRotation.x, -60, 60), //Set x to the value that got returned
y = currentEulerAngles.y, //Set y to the same value
z = currentEulerAngles.z //Set z to the same value
};
}
void FixedUpdate()
{
rigidbody.AddTorque(_verticalInput*mouseSensitivity, _horizontalInput*mouseSensitivity, 0, ForceMode.Impulse);
}