Polishing a Character Controller collision detection to make it flush with surfaces - c#

I've been looking through the Source SDK and read through how it handles player collision with the space around it. Inspired by that, I've been writing a character controller that should have similar behavior.
I have code to control a Transform transform by computing its velocity vecVelocity and trying to move it along that vector. I represent the player by a capsule of radius capsuleRadius and sphere centers capsuleStart, capsuleEnd, but I haven't given it a proper collider as of yet
When trying to deal with collisions, I came up with the following function
void TryPlayerMove()
{
Vector3 transDirection;
float transMagnitude;
Vector3 translation;
Vector3 currentMotion = Vector3.zero;
int i, j, maxHits = 4; //Bump up to 4 times before giving up
RaycastHit capsule;
bool collision;
Vector3[] collNormal = new Vector3[maxHits];
float collDistance;
//Find starting translation vector
translation = vecVelocity * Time.deltaTime;
//Store initial displacement
Vector3 oldTranslation = translation;
for (i = 0; i < maxHits; i++)
{
//Find translation vector magnitude and direction
transDirection = translation.normalized;
transMagnitude = translation.magnitude;
//If we wouldn't move anyway, feel free to break the loop
if (transMagnitude == 0)
{
break;
}
//Shoot a capsule to desired endpoint
collision = Physics.CapsuleCast(capsuleStart + currentMotion, capsuleEnd + currentMotion, capsuleRadius, transDirection, out capsule, transMagnitude);
if (collision)
{
//If we hit something, hug it and take off what is left of translation in that direction
collNormal[i] = capsule.normal.normalized;
collDistance = capsule.distance;
//currentMotion += transDirection * collDistance; //WHY DOESN'T THIS WORK???
translation = ClipVector(translation*(1f-collDistance/transMagnitude), collNormal[i]);
Debug.Log(transDirection.magnitude);
//If we're going towards something we've hit before, stop moving so we don't go into weird corner loops
for (j = 0; j < i; j++)
{
if (Vector3.Dot(translation, collNormal[j]) < 0)
{
translation = Vector3.zero;
break;
}
}
}
else
{
//Just move
currentMotion += translation;
break;
}
}
//Translate the player character and take note of its velocity for future computation
vecVelocity = currentMotion / Time.deltaTime;
transform.Translate(currentMotion, Space.World);
}
and the clipping function just makes sure our vector is REALLY not pointing at the collider
Vector3 ClipVector(Vector3 inputVector, Vector3 normalVector)
{
float projection;
Vector3 outputVector;
//Determine how much to take out
projection = Vector3.Dot(inputVector, normalVector);
//Subtract the perpendicular component
outputVector = inputVector - normalVector * projection;
//Iterate once more just to make sure
float adjust = Vector3.Dot(outputVector, normalVector);
if (adjust < 0f)
{
outputVector -= normalVector * adjust;
}
return outputVector;
}
This works fine as it is, and the controls respond quite as I would expect, but with a cosmetic fault: each time the player "collides" with something, it does so stopping at a different distance from the object, I've logged it and it fluctuates around 5-10 percent of the capsule's radius away from the player.
In the TryPlayerMove() function, there is a command that I would expect to make it hug the collider, it's commented and decorated with a note of desperation
//currentMotion += transDirection * collDistance; //WHY DOESN'T THIS WORK???
Whenever I uncomment this, the controller invariably goes through absolutely any collider it touches and completely messes up whatever motion it was supposed to have. I have no idea why this happens, though.
How could I implement a functionality for my player to hug the colliders it touches, seeing this doesn't seem to work at all?

I found a hacky fix for it out of the blue. I have a float deltaGroundI use to check for ground. I changed the command to
currentMotion += transDirection * (collDistance-deltaGround/20f);
but the 20 could have been any reasonably sized number. This makes it much more consistent but introduces some jitter in player motion when I try to ram myself against a wall. Still taking more suggestions.
I had tried Mathf.Epsilon but it didn't fix anything, by the way.

Related

Controlling a moving non-kinematic rigidbody along a path

I am working on an endless runner game for Android by Unity. I dont want to use a kinematic rigidbody. So physics is involved but the rigidbody is supposed to run along a predefined path by default. (and jumps or changes lanes by user actions). Moving straight is easy. I have done that but I want to have the next stage in the game where there are turns. It seems to work but it sometimes gets jittery and the turning isnt as smooth as I want it to be. And if I increase the speed, the player gets wonky. Could you please help me to optimize the code to get a smoother turns no matter what the speed is.
As far as I searched I couldnt find an answer on internet probably people are using kinematic rigidbodies more often in order not to deal with physics. So I use .AddForce and .AddTorque. I now use prefabs with predefined turns (road pieces). So it is spawned as the player moves along. Each road prefab has a spline (a free asset based on Unity 2015 procedural spline generation video I suppose) for the moving path. So the player is picking up a node along the spline and sets it as target and uses its rotation to turn towards using the AddTorque.
Maybe it is easier if I switch to kinematic rigidbody. Maybe that is ideal but I insist on doing this for the sake of learning physics and some people might find it useful for another project as there isnt enough resources on this.
void FixedUpdate()
{
if (!jump)
{
//maxangle = Mathf.Clamp(r.velocity.magnitude * 2f,3,15f);
maxangle = r.velocity.magnitude;
r.constraints = RigidbodyConstraints.None;
r.constraints = RigidbodyConstraints.FreezeRotationZ | RigidbodyConstraints.FreezeRotationX;
TurnToTarget(transform, sample.Rotation,target, maxangle);
r.constraints = RigidbodyConstraints.None;
r.constraints = RigidbodyConstraints.FreezeRotationZ | RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationY;
}
//Debug.Log(currentroad.transform.name + maxangle);
if (!GameManager.gameManager.dead && running)
{
r.isKinematic = false;
//Debug.Log(transform.position.y);
var speed = r.velocity.magnitude;
Vector3 directionOfTarget = (target - transform.position).normalized;
if (speed < runspeed)
{
//r.velocity += Vector3.forward * 1f;
Debug.Log(r.velocity.z+ " " + r.velocity.magnitude);
Debug.Log(directionOfTarget);
r.AddForce(directionOfTarget* (runspeed-speed), ForceMode.VelocityChange);
}
if (transform.position.y > 2.7f)
{
r.mass = 50000f;
Physics.gravity = new Vector3(0, -100f, 0);
}
if (grounded)
{
r.mass = 10f;
Physics.gravity = new Vector3(0, -10f, 0);
}
private void TurnToTarget(Transform transform, Quaternion targetrot, Vector3 movePoint, float maxTurnAccel)
{
Vector3 directionOfTarget = (movePoint -transform.position).normalized;
Vector3 directionInEulers = targetrot.eulerAngles;
Vector3 offsetInEulers = ClampHeading(directionInEulers) - ClampHeading(transform.eulerAngles);
offsetInEulers = ClampHeading(offsetInEulers);
//optional
Vector3 angularVelocity = r.angularVelocity / Time.fixedDeltaTime;
if (offsetInEulers.sqrMagnitude < Mathf.Pow(maxTurnAccel, 2))
{
if (offsetInEulers.y < 0)
{
if (angularVelocity.y < offsetInEulers.y)
{
offsetInEulers.y = -offsetInEulers.y;
}
}
else
{
if (angularVelocity.y > offsetInEulers.y)
{
offsetInEulers.y = -offsetInEulers.y;
}
}
if (offsetInEulers.x > 0)
{
if (angularVelocity.x < -offsetInEulers.x)
{
offsetInEulers.x = -offsetInEulers.x * 2;
}
}
else
{
if (angularVelocity.x > -offsetInEulers.x)
{
offsetInEulers.x = -offsetInEulers.x * 2;
}
}
if (offsetInEulers.z > 0)
{
if (angularVelocity.z < -offsetInEulers.z)
offsetInEulers.z = -offsetInEulers.z * 2;
}
else
{
if (angularVelocity.z > -offsetInEulers.z)
offsetInEulers.z = -offsetInEulers.z * 2;
}
}
offsetInEulers = ClampVector(offsetInEulers, -maxTurnAccel, maxTurnAccel);
//Debug.Log(currentroad + " " + offsetInEulers + " " + r.angularVelocity + " " + directionOfTarget + " " + ClampHeading(directionInEulers)+" " +transform.eulerAngles);
r.AddRelativeTorque(transform.up * offsetInEulers.y);
//r.AddTorque(offsetInEulers*r.velocity.magnitude);
}
You could look into splines. You can reduce the amount of computation to move a character along a path by calculating how many points along that path are needed for movement to appear smooth as the character is moved from point to point.
Sometimes blurring effects are used to reduce the number of polygons that need to be drawn, when a character is moving fast.
First
First thing to note is in this code:
if (transform.position.y > 2.7f)
{
r.mass = 50000f;
Physics.gravity = new Vector3(0, -100f, 0);
}
if (grounded)
{
r.mass = 10f;
Physics.gravity = new Vector3(0, -10f, 0);
}
If looks like you're manipulating mass to achieve the affect of "stopping". If the player is in the air, you slam them into the ground with a high gravity value to slow them down rapidly. Manipulating mass of an object in motion can cause a great deal of issues, especially if you're applying a force in the same physics frame. I see you use ForceMode.VelocityChange to counter this problem, so kudos to you on that. However, when you AddRelativeTorque to an object, its impact depends heavily on the mass.
Changing the mass of an object directly does not automatically scale the current linear and angular momentum that an object has. Instead, when you increase the mass to 50,000f you are increasing the momentum by:
50,000 / (prior mass).
Changing mass on-the-fly is usually bound to cause problems. I would recommend finding a different solution to force your player to the ground that does not involve manipulating mass (and to a lesser concern, gravity). Perhaps lerping the rigidbody.position downard, or applying a downward impulse force may achieve the same effect without the risk for bugs.
Second
All of this logic is occurring in the FixedUpdate() cycle. This is separate from the Update() cycle that runs every frame. I see that you are accessing some frame-specific data, notably transform.position and transform.eulerAngles. It is possible that 2 physics cycles occur before the next frame cycle occurs, resulting in something like the following:
Update() - transform is updated
FixedUpdate() - reads most recent transform data
Update() - transform is updated
FixedUpdate() - reads most recent transform data
FixedUpdate() - reads most recent transform data
Update() - transform is updated
Since some of your game logic is based on the transform data, the FixedUpdate() can sometimes double-act before it can be updated, resulting in jittery movements.
I would recommend avoiding any transform reads in FixedUpdate(), and instead using RigidBody.position. Changing the rotation is a bit more nuanced, but RigidBody.MoveRotation may serve useful here as well.

How to make camera relative movement

I'm learning unity and c#, and want to make my movement to be camera relative movement instead of world relative movement. How do I do that?
I'm learning unity and c#, my unity version is 2018.3.12f1. I would be happy for help.
just to let know, instead of moving the cam I'm rotating the player.
void Update()
{
float AxisY = Player.transform.eulerAngles.y;
/* Movement starts here */
Vector3 Movement = new Vector3 (Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)) { //running code
Player.transform.position += Movement * running_speed * Time.deltaTime;
} else {
Player.transform.position += Movement * speed * Time.deltaTime;
}
/*Movement ends here */
/* Rotation controller starts here */
Quaternion target = Quaternion.Euler(Player.transform.eulerAngles.x, Player.transform.eulerAngles.y, Player.transform.eulerAngles.z);
/*if (Player.transform.eulerAngles.x != 0 || Player.transform.eulerAngles.z != 0 || Player.transform.eulerAngles.y != 0) {
Player.transform.rotation = Quaternion.Euler(0,0,0);
}*/
if (Input.GetKey(KeyCode.E))
{
Debug.Log("E got pressed");
//float AxisYPositive = Player.transform.eulerAngles.y;
AxisY = AxisY+1;
Player.transform.rotation = Quaternion.Euler(0, AxisY, 0);
} else if (Input.GetKey(KeyCode.Q))
{
Debug.Log("Q got pressed");
//float AxisYNegetive = Player.transform.eulerAngles.y;
AxisY=AxisY-1;
Player.transform.rotation = Quaternion.Euler(0, AxisY, 0);
}
}
}
The player's movement is world relative, how to make the movement camera relative?
If you want to make the movements relative to the gameObject, call the method Transform.Rotate() on the transform of the gameObject you want to rotate rather than modifying its Quaternion directly. Just make sure the final argument is set to Space.Self.
if (Input.GetKey(KeyCode.E))
{
Debug.Log("E got pressed");
//float AxisYPositive = Player.transform.eulerAngles.y;
AxisY = AxisY+1;
Player.transform.Rotate(Quaternion.Euler(0, AxisY, 0), Space.Self);
}
In general you don't want to directly mess with objects transform.rotation, at least not unless you at least somewhat understand quaternions (I don't!).
I can see a few issues with your code, but the common thread seems to be that you don't really understand how transforms work. Specifically, you might want to look into World/Local space.
The usual way to control a player goes roughly like this:
void DoMovement(Transform player)
{
//If you move first your controls might feel 'drifty', especially at low FPS.
Turn(player);
Move(player);
}
void Turn(Transform player)
{
float yaw = Input.GetAxis("Yaw") * time.deltaTime; //Aka turn left/right
player.Rotate(0, yaw, 0, Space.Self);
// Space.Self is the default value, but I put it here for clarity.
//That means the player will rotate relative to themselves,
//...instead of relative to the world-axis, like in your code.
}
You didn't ask about movement, but as-is your character will always move relative to the world. The below should make it move relative to the camera.
Transform _cameraTransform; //Assumes this is set druing Start()
void Move(Transform player)
{
var forwardMove = _cameraTransform.Forward; //Get whatever direction is 'forward' for camera
forwardMove.Y = 0; //Don't want movement up and down.
forwardMove = forwardMove.normalized; //Normalize sets the 'power' of the vector to 1.
//If you set Y to 0 and don't normalize you'll go slower when camera looks down
//...than when camera is flat along the plane
player.position += forwardMove * Input.GetAxis("Vertical") * time.deltaTime;
//Here you could do the same for strafe/side to side movement.
//Would be same as above, but using the transform.right and Horizontal axis
}
Now, I'm making some assumptions here since you haven't specified what kind of game it is and what kind of controls you want. I'm assuming you have a character running around on a mostly flat plane (no aircraft/spaceship controls), and that the camera is attached to the player. This might not not actually be the case.
In any case I advice you to check out the tutorials, especially the Roll-a-Ball tutorial which I have found is good for beginners to get a grasp on basic players controls that are not just world-relative. The other tutorials, too, are pretty good if you think they're interesting.
Aside from the official Unity tuts a ton of decent to amazing tutorials out there, including video tutorials, so for something like this you could just search for <game type> tutorial and pick whatever seems good to you. While getting started I advice you to avoid the shortest videos, as you will likely benefit greatly from explanation that only fits in longer videos. Of course, that doesn't mean you should pick the longest videos either.
In case someone needs to move an object and don't care about colliders, you can use transform.Translate and assign to his second parameter relativeTo your camera (or any transform) to automatically calculate the translation relative to the object assigned.

How to create movement like Snake vs Block in Unity3D

I am trying to re-create the popular SnakeVSBlock mobile game. Most of the physics are pretty straight forward but I cannot seem to re-create the snake movement. What's special about the snake movement in the game is that the previous node follows the next ones path perfectly, both in X and in Y. This is my current code which does work as a snake movement but does not follow the exact path of the next node. I'm wondering if anyone has a solution to this.
public void Move()
{
if (Input.GetMouseButton(0) && GameManager.Instance.IsGameOver() == false && Nodes.Count > 0)
{
Vector3 curPos = Input.mousePosition;
curPos.z = -1;
curPos = Camera.main.ScreenToWorldPoint(curPos);
Nodes[0].GetComponent<Rigidbody2D>().velocity = new Vector2(curPos.x - Nodes[0].position.x, 0) * 20;
if (Nodes[0].position.x > 2.56f)
{
Nodes[0].position = new Vector2(2.56f,Nodes[0].position.y);
}
else if (Nodes[0].position.x < -2.56f)
{
Nodes[0].position = new Vector2(-2.56f, Nodes[0].position.y);
}
for (int i = 1; i < Nodes.Count; i++)
{
Vector2 newPos = new Vector2(Nodes[i-1].position.x, Nodes[i].position.y);
if (GameManager.Instance.GetPoweredUp())
{
Nodes[i].position = Vector2.Lerp(Nodes[i].position, newPos, Time.deltaTime * 14);
}
else
{
Nodes[i].position = Vector2.Lerp(Nodes[i].position, newPos, Time.deltaTime * 10);
}
}
}
UPDATE: I changed the basic logic for the snake movement in my game. Instead of having the snake move upwards, the snake stays in the same position and the blocks move downwards. However I still can't obtain the smooth snake movement like in the real games. Are there any other suggestions?
If I understand what you want to do, you should approach this in the reverse order.
So, if you have the nodes 0 to n:
Start from the n-th node
Assing this node the position of the (n-1)-th node
Then for each i node, assign it the position of the i-1 node
When you moved all the nodes, move the node 0 according to the user input.
It should look like this:
public void Move()
{
int n = Nodes.Count;
if (n > 0)
{
for (int i = n - 1; i > 0; i--)
{
_curNode = Nodes[i];
_nextNode = Nodes[i - 1];
Vector3 pos = _nextNode.transform.position;
pos.z = Nodes[0].position.z;
_curNode.tranform.position = pos;
}
float horizontalInput = Input.GetAxis("Horizontal");
Nodes[0].Translate(Vector3.right * Time.deltaTime * horizontalInput * 10);
Nodes[0].Translate(Vector3.up * Time.deltaTime * 4);
if (Nodes[0].position.x > 2.90)
{
Nodes[0].position = new Vector2(2.90f, Nodes[0].position.y);
}
else if (Nodes[0].position.x < -2.90)
{
Nodes[0].position = new Vector2(-2.90f, Nodes[0].position.y);
}
}
}
Also, unless you somehow increment the two LerpTime variables, you're misusing the Lerp function, and you don't need it in this context either.
This is an awesomely clear guide on what Lerp really means and how to use it properly:
http://lunarlabs.pt/blog/post/the-art-of-lerp
The old mobile game "snake" where you collect little squares has the same sort of movement. The solution then was that the parts of the snake don't actually move, just each frame you add a bit to the head and take one off the tail.
The reason why that approach works with snake but not immediately in this snake vs blocks game is that the former is discrete, and the latter is continuous.
One way to solve this is that each body part could always be lerping towards the one above it and when it gets there, it teleports back. This would give the illusion of the snake moving forward, when actually the body parts are stuck in place, just like the old snake game.
This works on paper but I have no idea how it will perform. However, if you are really desperate you could consider giving this a try.

Unity: How do I restrict the character's position or velocity to an exact number, making it unable to go past that point?

I'm still working on "perfect" ladder movement, and what I was able to come up with is a way to calculate the exact, specific distance the character needs to move while it is on the ladder to reach the point with which it will collide perfectly with the ground above the ladder.
I know this value is exactly specific because I allow my character to land on the ladder landing when the game starts (position.y = 3.6235), and when I print the movement value in the console I get exactly 3.6235.
I'm not sure that I'm implementing this correctly in code, though, as I've noticed that this value is still barely above "0" in the console once my character is all the way up the ladder. Is Mathf.Clamp() not the correct function to limit movement, or maybe I'm using it incorrectly?
public void ClimbUpLadder(ref Vector3 deltaMovement)
{
float rayLength = raycastOrigins.centerRayLength * 2;
RaycastHit2D[] hits = Physics2D.RaycastAll(new Vector2(raycastOrigins.center.x + deltaMovement.x,
(raycastOrigins.center.y - raycastOrigins.centerRayLength + skinWidth) + deltaMovement.y), Vector2.up,
rayLength, climbMask);
Debug.DrawRay(new Vector2(raycastOrigins.center.x + deltaMovement.x,
(raycastOrigins.center.y - raycastOrigins.centerRayLength + skinWidth) + deltaMovement.y), Vector2.up * rayLength, Color.green);
for (int i = 0; i < hits.Length; i++)
{
if (hits[i])
{
if (hits[i].collider.tag == "Ladder")
{
IsClimbingLadder = true;
}
if (i >= 1 && hits[i].collider.tag == "platformOneWay")
{
//This gives us the exact distance needed to finish climbing
GameObject platform = hits[i].collider.gameObject;
Transform platformTransform = platform.GetComponent<Transform>();
float finalMoveDistance = (platformTransform.position.y - characterTransform.position.y) + platformTransform.position.y;
deltaMovement.y = Mathf.Clamp(deltaMovement.y, 0, finalMoveDistance);
print(finalMoveDistance);
}
}
}
}
Unfortunately, after I set deltaMovement.y to this value it says that finalMoveDistance is around .9 or 1.0, so I still move slightly too far up the ladder. Do you think setting the character's transform.position.y directly is the best way to smooth out the movement? My goal is to eliminate any bounce when transitioning from climbing the ladder to walking on the ground again.
I think you're making a mistake here:
float finalMoveDistance = (platformTransform.position.y - characterTransform.position.y) + platformTransform.position.y;
Why do you add the platformTransform twice?
Try this:
float finalMoveDistance = (platformTransform.position.y - characterTransform.position.y);

Determine if clockwise or counter-clockwise rotation is faster in Unity C#

I'm writing a C# script in Unity, trying to make a spaceship in my top-down game rotate to face the mouse position. For some reason I'm having trouble, though, and I'm not sure what the problem is. Basically, if the ship is facing a small positive angle, say 10 degrees, and the mouse is at 350 degrees, the ship SHOULD rotate clockwise because that's the faster way to go. The ship rotates the correct direction except in this one case (when your ship is above the horizontal and your mouse is below).
This is a visual example of what I want in this case, vs what happens. Link: http://i.imgur.com/lvmzd68.png
This is the method which is supposed to return true if a counterclockwise rotation would be faster than a clockwise rotation.
bool turnCounterFaster() {
//Returns true if turning left would be faster than turning right to get to the ideal angle
float targetAngle = getAngleToMouse ();
float currentAngle = rigidbody2D.rotation;
while (currentAngle > 360f) {
//Debug.Log ("Current angle was "+currentAngle);
currentAngle -= 360f;
//Debug.Log ("now is:"+currentAngle);
}
if (targetAngle < currentAngle) {
//It's possible the target angle is a small angle, if you're a large angle it's shorter to turn counterclokwise
float testDiff = Mathf.Abs((targetAngle+360) - currentAngle);
if(testDiff < Mathf.Abs (targetAngle-currentAngle)){
//Turning counter clockwise is faster
//Debug.Log ("(false) edge case Current "+currentAngle+" target "+targetAngle);
return false;
}
//Debug.Log ("(true) target < current Current "+currentAngle+" target "+targetAngle);
return true;
}
return false;
}
And this is the method that gets the angle between the player and the mouse. These scripts are both attached to the player object.
float getAngleToMouse(){
Vector3 v3Pos;
float fAngle;
// Project the mouse point into world space at
// at the distance of the player.
v3Pos = Input.mousePosition;
v3Pos.z = (transform.position.z - Camera.main.transform.position.z);
v3Pos = Camera.main.ScreenToWorldPoint(v3Pos);
v3Pos = v3Pos - transform.position;
fAngle = Mathf.Atan2 (v3Pos.y, v3Pos.x) * Mathf.Rad2Deg;
if (fAngle < 0.0f)
fAngle += 360.0f;
//Debug.Log ("ANGLE: "+fAngle + " and ship angle: "+rigidbody2D.rotation);
return fAngle;
}
And this is the script that actually causes the ship to turn in either direction. The addTurnThrust method appears to be working correctly, and I've tested it with keyboard input and noticed no problems.
if (turnCounterFaster() == false) {
addTurnThrust(turnAcceleration,true);
} else {
addTurnThrust(-1*turnAcceleration,true);
}
I'm sure there's something incredibly obvious but I'm at my witts end here. Any advice would be greatly appreciated!
Calculate sin(currentAngle - targetAngle). If it is positive, currentAngle needs to move clockwise. If the sine is negative, currentAngle needs to move counterclockwise. If it's zero, then the two angles are either the same or 180° opposite. This works even if the angles aren't normalized to [0,360).

Categories