- Home /
Rigidbody FPS Controller using hit.normal problem
Hello Unity users, I'm making an FPS so I would like to achieve the best FPS controller that handles smooth slopes limit, anti-slope sliding, ect... So I wrote this script : (see gifs below) using UnityEngine; using System.Collections;
[RequireComponent (typeof (Rigidbody))]
public class Player_Movement: MonoBehaviour {
[Header("Components")]
public Rigidbody rb;
[Header("Stats")]
public float speed;
public float runFactor;
public bool grounded;
[Header("Offsets")]
public LayerMask layers;
public float speedScale;
[Header("Run-Time values")]
public Vector2 rawAxis;
public Vector3 normalizedAxis;
public Vector3 smoothedAxis;
public Vector3 localAxis;
RaycastHit hit;
public Vector3 force;
void Start (){
}
void Update (){
rawAxis = new Vector2 (Input.GetAxisRaw ("Horizontal"), Input.GetAxisRaw ("Vertical"));
normalizedAxis = new Vector3 (rawAxis.x, 0f, rawAxis.y).normalized;
smoothedAxis = new Vector3 (Mathf.SmoothStep (smoothedAxis.x, normalizedAxis.x, Time.deltaTime * 15f), 0f, Mathf.SmoothStep (smoothedAxis.z, normalizedAxis.z, Time.deltaTime * 15f));
localAxis = transform.TransformDirection (smoothedAxis);
}
void FixedUpdate () {
float totalSpeed = speed * speedScale;
var draggedVel = Vector3.zero;
draggedVel.x = rb.velocity.x * 0.75f;
draggedVel.y = rb.velocity.y;
draggedVel.z = rb.velocity.z * 0.75f;
rb.velocity = draggedVel;
Debug.DrawRay (transform.position, Vector3.down * 1f);
if(Physics.Raycast(transform.position, Vector3.down, out hit, 100f, layers)){
Debug.DrawRay (hit.point, hit.normal * 0.5f, Color.magenta, 2f);
}
force = Vector3.Cross (Vector3.Cross (hit.normal, localAxis), hit.normal) * totalSpeed;
rb.AddForce (force, ForceMode.VelocityChange);
}
void OnDrawGizmos(){
Gizmos.color = Color.green;
Gizmos.DrawSphere (hit.point + force, 0.3f);
}
}
When I'm walking down slopes, it's working like a charm, but I go up a slope it's doing a weird thing, and I don't know why.
The green sphere is the result of "hit.point + force".
If someone could help me it would be appreciable, yours sincerely, Tom.
I replied, but the moderation queue means you won't see it for a while. It seems that comments on a user's post don't have that queue, so I will give a similar answer here.
I tested this code in game, found the problem, and fixed it. The problem is actually a subtle one. Your math is fine, your method is fine. But you apply the force even when the character is airborne (which is half of the frames or something, knowing PhysX), you will simply keep going up forever and ever because you are getting the same global force value forever and ever.
Your rigid body constantly loses contact with the ground, but it doesn't know that, so it applies whatever the last successful force was, and it keeps doing it because the hit.normal is valid. Even when you lose contact with the ground (y > 100f), the character continues flying, because your hit.normal still exists, it is just using the last one that existed.
I have two suggestions here. First, a suggested workaround. Then, a suggestion about coding these kinds of things.
$$anonymous$$gested workaround: To fix this, I did the following. -Created a global boolean airborne = false. -Set, in FixedUpdate, raycast distance to 2f. -In FixedUpdate(), added: if (!airborne) rb.AddForce(...); -Used OnCollisionEnter() and OnCollisionEnd() to set airborne to false and true respectively.
$$anonymous$$gestion about coding: Don't make the variables you are using in an update calculation globals unless you have a good reason, because it makes debugging harder. You can have global variables be set by the ones you're using so you can view them normally (e.g inspectorForce = force). Nothing will help you find a problem faster than a wall of null reference exceptions.
Answer by oswin_c · Oct 17, 2016 at 05:00 AM
I tested it in my Unity and fixed it.
The problem is actually a subtle one. Your math is fine (I think -- I replaced the vector triple product with a quaternion.fromtorotation), your work is fine. Your mistake is subtle -- your force vector is a global variable, and so is your raycasthit.
As you move up a slope, any time when your rigidbody loses connection with the terrain (This will happen CONSTANTLY, because of computational errors) , you just keep applying that force. You keep applying the same force vector again and again forever, causing your character to fly in the direction of the last force vector it had when it lost contact with the ground.
Making the problem worse, is the choice of 100f as a raycast distance, causing your raycast to keep updating as if you were still on the ground. I cannot think of a good reason to have a raycast to get a floor normal be greater than your character's height.
To fix it, I did the following:
Defined a boolean airborne, defaulted to false. In FixedUpdate(), only add a force to the rigidbody if airborne is false. Also, set the raycasthit to 2f. Added the following below FixedUpdate():
void OnCollisionEnter(Collision asdf)
{
airborne = false;
}
void OnCollisionEnd(Collision asdf)
{
airborne = true;
}
This causes the rigidbody to stop applying a force, so the player moves up the slope just fine.
Word of warning: You are going down a dark path with a rigid body character controller. The Unity character controller can be extended to work with most games. If you MUST use a rigid body character controller (I had to make one because, and only because, my game is set on a space station and gravity is not a constant, but changes based on position), be wary. First, your method of dealing with slopes works great for terrain, but test it on hard edges (e.g ramps) before you get too excited. Second... steps. Steps. With rigid bodies. Should be easy, right?
Welcome to hell.
Thanks for your anwser! I'll read it completely when i'll get back from school.
Thank you so much for your anwser, It might be my only answer I got from my Unity Answer experience :'(, anyway I didn't decide to choose the dark path, I chose the Character Controller method, it needs more vector knowledge, but I can do it. Here is my actual code :
using UnityEngine;
using System.Collections;
[RequireComponent (typeof (CharacterController))]
public class Player_$$anonymous$$ovement: $$anonymous$$onoBehaviour {
[Header("Components")]
public CharacterController cc;
[Header("Stats")]
public float speed;
public float runFactor;
public bool grounded;
[Header("Offsets")]
public Layer$$anonymous$$ask layers;
public float speedScale;
public float jumpForce;
[Header("Run-Time values")]
public Vector2 rawAxis;
public Vector3 normalizedAxis;
public Vector3 smoothedAxis;
public Vector3 localAxis;
RaycastHit hit;
public Vector3 force;
public float verticalForce;
void Start (){
}
void Update (){
rawAxis = new Vector2 (Input.GetAxisRaw ("Horizontal"), Input.GetAxisRaw ("Vertical"));
normalizedAxis = new Vector3 (rawAxis.x, 0f, rawAxis.y).normalized;
smoothedAxis = new Vector3 ($$anonymous$$athf.SmoothStep (smoothedAxis.x, normalizedAxis.x, Time.deltaTime * 15f), 0f, $$anonymous$$athf.SmoothStep (smoothedAxis.z, normalizedAxis.z, Time.deltaTime * 15f));
localAxis = transform.TransformDirection (smoothedAxis);
grounded = cc.isGrounded;
float totalSpeed = speed * speedScale;
Debug.DrawRay (transform.position, Vector3.down * 1f);
if (Physics.Raycast (transform.position, Vector3.down, out hit, 2f, layers)) {
Debug.DrawRay (hit.point, hit.normal * 0.5f, Color.magenta, 2f);
}
force = Vector3.Cross (Vector3.Cross (hit.normal, localAxis), hit.normal) * totalSpeed;
if (grounded) {
verticalForce = 0f;
if (Input.Get$$anonymous$$eyDown ($$anonymous$$eyCode.Space)) {
verticalForce = jumpForce;
}
}
verticalForce += $$anonymous$$athf.Sqrt (Time.deltaTime) * Physics.gravity.y / 2f; //2f is a scale
var forceWithGravity = new Vector3(force.x, verticalForce, force.z);
cc.$$anonymous$$ove (forceWithGravity * Time.deltaTime);
}
void OnDrawGizmos(){
Gizmos.color = Color.green;
Gizmos.DrawSphere (hit.point + force, 0.3f);
Gizmos.DrawCube (hit.point + force, new Vector3 (0.05f, 5f, 0.05f));
}
}
I just changed the method "FixedUpdate" to "Update", I learnt that it's more efficient.
If you are using a character controller, you do not need to use vector triple products to deal with slopes. All of that math is already encoded in the character controller.
All you need to do is use: cc.$$anonymous$$ove(localAxis*TotalSpeed*Time.deltaTime). No fussing with vectors, no vector triple product, no nothing.
Your answer