Dash against slopes using physics in Unity 2D - c#

I'm working on a 2D project in Unity.
The character controller is physics based, so I use rigidbody to move the player. Everything is working fine except when I try to apply a high speed movement to the character, like a dash.
This is how the code looks like.
I just check if the player is dashing, so I increase the Vector2 movement in a certain amount.
private void DashMovement() {
if (isDashing) {
movement.x *= dashFactor;
}
}
I'm also calculating the ground angle, so I set the movement vector to follow the ground inclination.
private void OnSlopeMovement() {
if (isGrounded && !isJumping) {
float moveDistance = Mathf.Abs(movement.x);
float horizontalOnSlope = Mathf.Cos(groundAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign(movement.x);
float verticalOnSlope = Mathf.Sin(groundAngle * Mathf.Deg2Rad) * moveDistance;
if (horizontalOnSlope != 0)
movement.x = horizontalOnSlope;
if (isGrounded && verticalOnSlope != 0)
movement.y = verticalOnSlope;
}
SetMaxFallVelocity();
}
So I set the rigidbody velocity for making it move.
private void Move() {
movement.x *= Time.fixedDeltaTime;
if(isGrounded && !isJumping) movement.y *= Time.fixedDeltaTime;
Vector3 targetVelocity = new Vector2(movement.x, movement.y);
PlayerController.rb2d.velocity = Vector3.SmoothDamp(PlayerController.rb2d.velocity, targetVelocity, ref velocity, movementSmoothing);
}
The problem appears when I apply a speed high enough. I understand this issue is because of physics.
I think the ray that checks the ground and is used to calculate the groundAngle doesn't work fast enough to keep track of that movement, so I can not keep the player fixed on the ground.
I would like to find a solution without making the player kinematic, or stopping the dash on slopes.
This is how it looks ingame.
And this is how the rigidbody movement remain right over the ground, following the slopes angle.
EDIT:
This is how I get the ground angle:
private void GroundAngle() {
Vector2 rayOrigin = feetCollider.bounds.center;
rayOrigin.y += 0.1f;
Vector2 rayDirection = (Input.GetAxisRaw("Horizontal") == 0) ? Vector2.right : new Vector2(Input.GetAxisRaw("Horizontal"), 0);
int groundCollisions = Physics2D.RaycastNonAlloc(rayOrigin, Vector2.down, groundResults, Mathf.Infinity, groundMask);
if (groundCollisions > 0) {
groundAngle = Vector2.Angle(groundResults[0].normal, rayDirection) - 90f;
//Debug.DrawRay(rayOrigin, Vector2.down, Color.green);
if (groundAngle > 0 && !isDashing) {
rayOrigin.x += Input.GetAxisRaw("Horizontal") * .125f;
Physics2D.RaycastNonAlloc(rayOrigin, Vector2.down, groundResults, Mathf.Infinity, groundMask);
groundAngle = Vector2.Angle(groundResults[0].normal, rayDirection) - 90f;
//Debug.DrawRay(rayOrigin, Vector2.down, Color.blue);
}
}
}
Thanks to #Ruzhim for the help. I just post a first "solution" for the problem.
According to Ruzhim advises, I've used him code this way.
private void SetPositionAfterTick() {
if (isDashMovement) {
Vector2 currentPosition = new Vector2(transform.position.x, transform.position.y);
currentPosition.y = feetCollider.bounds.min.y;
Vector2 feetPosAfterTick = currentPosition + PlayerController.rb2d.velocity * Time.deltaTime;
float maxFloorCheckDist = .1f;
RaycastHit2D groundCheckAfterTick = Physics2D.Raycast(feetPosAfterTick + Vector2.up * maxFloorCheckDist, Vector2.down, maxFloorCheckDist * 5f);
if (groundCheckAfterTick) {
Vector2 wantedFeetPosAfterTick = groundCheckAfterTick.point;
if (wantedFeetPosAfterTick != feetPosAfterTick) {
//PlayerController.rb2d.transform.position = (wantedFeetPosAfterTick + new Vector2(0f, feetCollider.bounds.min.y - PlayerController.rb2d.position.y));
PlayerController.rb2d.velocity = Vector2.zero;
}
}
}
}
This is how it looks like.
This is good enough to continue polishing that mechanic. I still need to set the position in some way. The rigidbody's position calculation is not working as it
is raised right now, as the condition (wantedFeetPosAfterTick != feetPosAfterTick) is always true, so the character goes throw the floor and fall.
As you can see, I also need to control the down slopes movement, as it uses the slopes movement sometimes, and dash straight forward others.

This is how asker Rubzero implemented the below code to work for them:
private void SetPositionAfterTick() {
if (isDashMovement) {
Vector2 currentPosition = new Vector2(transform.position.x, transform.position.y);
currentPosition.y = feetCollider.bounds.min.y;
Vector2 feetPosAfterTick = currentPosition + PlayerController.rb2d.velocity * Time.deltaTime;
float maxFloorCheckDist = .1f;
RaycastHit2D groundCheckAfterTick = Physics2D.Raycast(feetPosAfterTick + Vector2.up * maxFloorCheckDist,
Vector2.down, maxFloorCheckDist * 5f);
if (groundCheckAfterTick) {
Vector2 wantedFeetPosAfterTick = groundCheckAfterTick.point;
if (wantedFeetPosAfterTick != feetPosAfterTick) {
//PlayerController.rb2d.transform.position = (wantedFeetPosAfterTick + new Vector2(0f, feetCollider.bounds.min.y -
PlayerController.rb2d.position.y));
PlayerController.rb2d.velocity = Vector2.zero;
}
}
}
}
This is how it looks like.
This is good enough to continue polishing that mechanic. I still need
to set the position in some way. The rigidbody's position calculation
is not working as it is raised right now, as the condition
(wantedFeetPosAfterTick != feetPosAfterTick) is always true, so the
character goes throw the floor and fall.
As you can see, I need to control the down slopes movement, as it uses
the slopes movement sometimes, and dash straight forward others.
I agree with AresCaelum; using physics to do slope movement is pretty much the opposite of what you want to be doing if you don't want to preserve momentum when you're done going up/down the slope. Specifically, your problem is here:
float moveDistance = Mathf.Abs(movement.x);
float horizontalOnSlope = Mathf.Cos(groundAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign(movement.x);
float verticalOnSlope = Mathf.Sin(groundAngle * Mathf.Deg2Rad) * moveDistance;
This is a problem because the more the player moves horizontally in a frame, the more they will move vertically based on the slope of the ramp they are on. However, this assumption doesn't hold if they should only be traveling up the ramp during only part of the movement during the frame. So, you need a way to handle that situation.
One solution is to use a raycast from where the player would be then if it's above the floor, alter the vertical velocity so that it would place them at that floor's position instead.
First, determine if slope movement has occurred in a physics frame...
private bool slopeMovementOccurred = false;
void FixedUpdate() {
slopeMovementOccurred = false;
// ...
}
private void OnSlopeMovement() {
if (isGrounded && !isJumping) {
slopeMovementOccurred = true;
// ...
}
SetMaxFallVelocity();
}
... and if it has, determine where the player is going to be after the physics update. Then do a physics2d raycast from above that position (by some amount) downward (double the previous amount) to find where the player's position should be, and then change the rb2d.velocity such that it will place the player exactly at the height they should be at.
Assuming you can calculate some kind of Vector2 feetOffset that has the local position of the player's feet:
void FixedUpdate() {
// ...
StickToSlopeLanding();
}
void StickToSlopeLanding() {
if (slopeMovementOccurred) {
Vector2 curVelocity = PlayerController.rb2d.velocity;
Vector2 feetPosAfterTick = PlayerController.transform.position
+ PlayerController.feetOffset
+ curVelocity * Time.deltaTime;
float maxFloorCheckDist = 1.0f;
// determine where the player should "land" after this frame
RaycastHit2D groundCheckAfterTick = Physics2D.Raycast(
feetPosAfterTick + Vector2.up * maxFloorCheckDist,
-Vector2.up, maxFloorCheckDist * 2f);
if (groundCheckAfterTick.collider != null) {
Vector2 wantedFeetPosAfterTick = groundCheckAfterTick.point;
// if basic physics won't take them to landing position
if (wantedFeetPosAfterTick != feetPosAfterTick) {
Vector2 wantedVelocity = curVelocity
+ Vector2.up
* ((wantedFeetPosAfterTick.y - feetPosAfterTick.y)
/ Time.deltaTime);
// adjust velocity so that physics will take them to landing position
PlayerController.rb2d.velocity = wantedVelocity;
// optionally, set a flag so that next frame
// it knows the player should be grounded
}
}
}
}
Hopefully this gets you towards a solution that will work.
Note: you may need to also move the rigidbody so that it doesn't try to clip through the corner at the top of the ramp, and you can determine where to put the rigidbody using another raycast, setting the velocity from that point to be horizontal:
void StickToSlopeLanding() {
if (slopeMovementOccurred) {
Vector2 curVelocity = PlayerController.rb2d.velocity;
Vector2 feetPosAfterTick = PlayerController.transform.position
+ PlayerController.feetOffset
+ curVelocity * Time.deltaTime;
float maxFloorCheckDist = 1.0f;
// determine where the player should "land" after this frame
RaycastHit2D groundCheckAfterTick = Physics2D.Raycast(
feetPosAfterTick + Vector2.up * maxFloorCheckDist,
-Vector2.up, maxFloorCheckDist * 2f);
if (groundCheckAfterTick.collider != null) {
Vector2 wantedFeetPosAfterTick = groundCheckAfterTick.point;
// if basic physics won't take them to landing position
if (wantedFeetPosAfterTick != feetPosAfterTick) {
// look for corner of ramp+landing.
// Offsets ensure we don't raycast from inside/above it
float floorCheckOffsetHeight = 0.01f;
float floorCheckOffsetWidth = 0.5f;
RaycastHit2D rampCornerCheck = Physics2D.Raycast(
wantedFeetPosAfterTick
- floorCheckOffsetHeight * Vector2.up
- floorCheckOffsetWidth * Mathf.Sign(movement.x) * Vector2.right,
Mathf.Sign(movement.x) * Vector2.right);
if (rampCornerCheck.collider != null) {
// put feet at x=corner position
Vector2 cornerPos = Vector2(rampCornerCheck.point.x,
wantedFeetPosAfterTick.y);
PlayerController.rb2d.position = cornerPos
- PlayerController.feetOffset;
// adjust velocity so that physics will take them from corner
// to landing position
Vector2 wantedVelocity = (wantedFeetPosAfterTick - cornerPos)
/ Time.deltaTime;
PlayerController.rb2d.velocity = wantedVelocity;
// optionally, set a flag so that next frame
// it knows the player should be grounded
}
}
}
}
}

Related

How to add relative movement to multiple virtual cameras?

I am using a Cinemachine state driver to transition between my 8 directional cameras orbiting my player. Right now my player script is set to a basic isometric character controller:
Player.cs
public float speed = 5f;
Vector3 forward;
Vector3 right;
// Start is called before the first frame update
void Start()
{
forward = Camera.main.transform.forward;
forward.y = 0;
forward = Vector3.Normalize(forward);
right = Quaternion.Euler(new Vector3(0, 90, 0)) * forward;
}
// Update is called once per frame
void Update()
{
if (Input.anyKey)
{
Move();
}
}
void Move ()
{
Vector3 direction = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
Vector3 rightMovement = right * speed * Time.deltaTime * Input.GetAxis("Horizontal");
Vector3 upMovement = forward * speed * Time.deltaTime * Input.GetAxis("Vertical");
Vector3 heading = Vector3.Normalize(rightMovement + upMovement);
transform.forward += heading;
transform.position += rightMovement;
transform.position += upMovement;
}
I want the players move direction to reflect the direction of the camera. For example, using W (from WASD) will always move the player up. Could I edit the script to pick up the direction of each of my virtual cameras and add that to my player controller or is there a better way?
To solve this problem you have to change the movement of the keys according to the angle of the camera. This is done as follows with transform.TransformDirection. When the movement is synchronized with the direction of the camera, it causes the W key to press the character towards the ground, because the angle in front of the camera is inside the ground. To solve the problem, we set y to zero and then normalize the axis.
public float speed = 10f;
void Update()
{
var moveInput = new Vector3(Input.GetAxis("Horizontal"), 0f , Input.GetAxis("Vertical"));
moveInput = Camera.main.transform.TransformDirection(moveInput);
moveInput.y = 0;
moveInput = moveInput.normalized;
transform.position += moveInput * Time.deltaTime * speed;
}

How to make a curved flight path?

How to make a bullet fly in an arc when shooting and hit the player.
As in this picture . I tried to use formulas from physics, the body is thrown at an angle to the horizon, that's what came of it . But the bullet flies away into the void
velocity = Mathf.Round(Vector3.Distance(lastpos, transform.position) / Time.deltaTime);
lastpos = transform.position;
Vector3 direction = PlayeCar.position - Vector3.zero;
float angle = Mathf.Atan2(direction.y, direction.x); // радианы
float x = velocity * Mathf.Cos(angle) + gravity * (time* time) / 2;
float y = velocity * Mathf.Sin(angle);
transform.position = new Vector2(x, y)
;
Sample orientative method could be (The script would be attached to the gun that shots, so when the code referes to transform, refers to the gun's transfom):
public void LaunchTarget()
{
Vector3 initialSpeed = transform.forward; //shot direction, gun's forward
GameObject go = GameObject.Instantiate(m_bullet);//reference to bullet prefab
//in case you need to randomize the shot speed
initialSpeed = transform.forward * Random.Range(m_minLaunchSpeed, m_maxLaunchSpeed);
//set the initial position of the bullet in the gun
go.transform.position = transform.position;
// shoot (give speed to the bullets rigidbody)
go.GetComponent<Rigidbody>().velocity = initialSpeed;
//initially disabled soas to see the bullet when shot
go.SetActive(true);
}
Hope that helps
I did it !!!! at the time of the bullet’s flight, I turn it toward the player in an intermittent manner. And after a while I delete it
void Start()
{
rigidbodyBullet = GetComponent<Rigidbody2D>();
Player = GameObject.Find("carPlayer").GetComponent<Transform>();
_distance = Vector3.Distance(Player.position, transform.position);
Invoke("DestroyArcBullet", 1.5f);
limitDistance = _distance / 1.3f;
}
void Update()
{
transform.position += transform.up * Time.deltaTime * 5f;
_distance = Vector3.Distance(Player.position, transform.position);
if (_distance < limitDistance)
{
var turn = Quaternion.Lerp(transform.rotation,
Quaternion.LookRotation(Vector3.forward, Player.position - transform.position), Time.deltaTime * 2);
rigidbodyBullet.MoveRotation(turn.eulerAngles.z);
}
if (this.transform.position.y + this.transform.position.y <= -10 || this.transform.position.y + this.transform.position.y >= 10
|| this.transform.position.x + this.transform.position.x <= -10 || this.transform.position.x + this.transform.position.x >= 10)
{
Destroy(gameObject);
}
}

Unity 3d - player is moving constantly after touching a wall

After my player object touches the wall it sometimes starts to move and rotate on it's own. I tried to increase player object weight and it helps, but i don't think it's a good approach as tiny movement never dissapears.
Player is rigidbody with box collider attached.
isKinematic - false;
useGravity - true;
XYZ rotation is fixed and Y coordinate position is fixed.
Walls have box collider but have no rigidbody.
Ground has no collider as Y position is fixed and player object doesn't touch the ground.
Game is network based so i have photon rigidbody view component attached to player as well.
Code (C#) which moves player:
public void Update()
{
if (!photonView.IsMine || !controllable)
{
return;
}
if (shootingTimer > 0.0)
{
shootingTimer -= Time.deltaTime;
}
m_MovementInputValue = Input.GetAxis(m_MovementAxisName);
if (m_MovementInputValue == 0.0f)
{
m_MovementInputValue = joystick.Vertical;
}
m_TurnInputValue = Input.GetAxis(m_TurnAxisName);
if (m_TurnInputValue == 0.0f)
{
m_TurnInputValue = joystick.Horizontal;
}
Vector3 vector = new Vector3(
m_TurnInputValue,
rigidbody.velocity.y,
m_MovementInputValue
);
MovementVector = Quaternion.AngleAxis(60, Vector3.up) * vector;
EngineAudio();
}
public void FixedUpdate()
{
if (!photonView.IsMine || !controllable)
{
return;
}
Move();
Turn();
}
private void Move()
{
// Adjust the position of the tank based on the player's input.
// Vector3 movement = transform.forward * m_MovementInputValue * m_Speed * Time.deltaTime;
// rigidbody.MovePosition(rigidbody.position + movement);
Vector3 movement = MovementVector * m_Speed * Time.deltaTime;
rigidbody.MovePosition(rigidbody.position + movement);
}
private void Turn()
{
// Adjust the rotation of the tank based on the player's input.
// float turn = m_TurnInputValue * m_TurnSpeed * Time.deltaTime;
// Quaternion turnRotation = Quaternion.Euler(0f, turn, 0f);
// rigidbody.MoveRotation(rigidbody.rotation * turnRotation);
if (m_TurnInputValue != 0.0f || m_MovementInputValue != 0.0f)
{
rigidbody.rotation = Quaternion.LookRotation(MovementVector);
}
}
Check to make sure the component (such as a rigidbody) is attached to the right object (gameobject - parent vs. child objects). Also, the fact that it is constantly rotating is curious... it makes me think it has something to do with the Time.DeltaTime, but I see that's only being applied to your Vector3 movement, and not in your rotation function... Just curious, what happens when you remove the Time.DeltaTime part from your movement Vector3?

Camera Behaviour

This camera script is intended to rotate and look at the player while it is moving and snapping to the player slowly while it isn't. (Player passes over a Vector3 beforeMoving before it is starting movement). My issue is I want to "feed" the deltaPosition slowly so it isn't a sudden snap but rather a slow and smooth transition, also stop adding if arrived.
private void LateUpdate()
{
if (player.isMoving)
{
desiredPosition = player.beforeMoving + offset;
}
else
{
Vector3 deltaPosition = player.transform.position - player.beforeMoving;
desiredPosition += deltaPosition * Time.deltaTime;
}
Quaternion camTurnAngle =
Quaternion.AngleAxis(input * rotationSpeed, Vector3.up);
desiredPosition = camTurnAngle * desiredPosition;
transform.position = Vector3.Slerp(transform.position, desiredPosition, smoothFactor);
transform.LookAt(player.transform);
}
Edit: I thought I would share the final code.
private void LateUpdate()
{
Quaternion rotation = Quaternion.Euler(GetDegree(), input.x * rotationSpeed, 0f);
if (player.isMoving)
{
desiredPosition = player.beforeMoving + offset;
CalculatePanTime();
}
else if (!player.isMoving)
{
desiredPosition = player.transform.position + offset;
}
transform.position = Vector3.Slerp(transform.position, rotation * desiredPosition, GetSpeed());
transform.LookAt(player.transform);
}
private void CalculatePanTime()
{
stoppedTime = Time.time;
playerDelta = Vector3.Distance(player.transform.position, player.beforeMoving);
timeToPan = (playerDelta / snappingSpeed) * Time.deltaTime;
}
private float GetSpeed()
{
if (Time.time < stoppedTime + timeToPan)
{
controlsDisabled = true; return snappingSpeed;
}
else
{
controlsDisabled = false; return smoothSpeed;
}
}
You are telling us what you want the code to do, that is good. You are also posting the code you implemented to achieve your goal, that is also good. Can you also tell us what is not working as you want it to work as a result of that code?
From what I understand it is the "Vector3 deltaPosition = player.transform.position - player.beforeMoving;
desiredPosition += deltaPosition * Time.deltaTime; " that is not behaving as you expect it to
maybe try something like this:
private void LateUpdate()
{
// snap the rotation center slowly to the player's position if not moving, or player's position before he started moving
desiredPosition = Vector3.Lerp(desiredPosition, player.beforeMoving, 0.1f);
// rotate around rotation center
Quaternion camTurnAngle = Quaternion.AngleAxis(rotationSpeed * Time.time, Vector3.up);
desiredPosition += camTurnAngle * offset;
// set the position to rotate around rotation center, and look towards player
transform.position = Vector3.Lerp(transform.position, desiredPosition, smoothFactor);
transform.LookAt(player.transform);
}
the problem with your code is that you are overshooting. If you want to implement something that decides how fast the camera snaps, or how much time it should take to snap, try introducing a float player.timeStopMoving = Time.time that you can use to correctly compute the position correction while he is not moving.
if(player.isMoving)
{
desiredPosition = player.beforeMoving;
}
else
{
const float timeNeededToSnap = 2f;
// or float timeNeededToSnap = (player.transform.position - player.beforeMoving).magnitude; // (which you could compute only once in player script, when he stops moving, and then reuse the value instead of performing a ".magnitude" every frame)
if(Time.time < player.timeStopMoving + timeNeededToSnap)
{
desiredPosition = Vector3.Lerp(desiredPosition, player.transform.position, (Time.time - player.timeStopMoving) / timeNeededToSnap);
}
else
{
// an other problem here is: if the player starts moving AGAIN before your desiredPosition got the value, do you want desired position to glich to the new player.beforeMoving?...
desiredPosition = player.transform.position;
}
}
EDIT:
To make he lerp less linear, you can use:
if(Time.time < player.timeStopMoving + timeNeededToSnap)
{
var t = Mathf.Cos(Maths.Pi * 0.5f * (Time.time - player.timeStopMoving) / timeNeededToSnap); // as timeDelta/totalTime goes from 0->1, multiply it by Pi/2 and the Cos will also go from 0->1 but with a smoothing speed
desiredPosition = Vector3.Lerp(desiredPosition, player.transform.position, t);
}

How to stop rotation by LerpAngle after -90°

I have colliders where i can turn to the sides and i want camera to rotate as a player turn to the side. I'm using Mathf.LerpAngle for that, but when I press the key for turn to side, camera is rotating in loop. How can I make rotation stop?
The problem is that everytime I turn player should go -90 degrees to the left +90 to the right and there will be more turns so i can't use functions for setting rotation.
I was already trying to make it stop by that if statement with (lAngle > 90f)
float lAngle = Mathf.LerpAngle(minAngle, lMaxAngle, Time.deltaTime);
float rAngle = Mathf.LerpAngle(minAngle, rMaxAngle, Time.deltaTime);
Quaternion leftRotation = Quaternion.Euler(new Vector3 (0, lAngle, 0));
Quaternion rightRotation = Quaternion.Euler(new Vector3 (0, rAngle, 0));
transform.position = player.transform.position + offSet;
transform.LookAt (player.transform);
if (Input.GetKeyDown (KeyCode.LeftArrow) && GameObject.Find("Player").GetComponent<PlayerMovement>().turn) {
turnLeft = true;
} else if (Input.GetKeyDown (KeyCode.RightArrow) && GameObject.Find("Player").GetComponent<PlayerMovement>().turn) {
turnRight = true;
}
if(turnLeft) {
offSet = leftRotation * offSet;
transform.position = player.transform.position + offSet;
transform.LookAt (player.transform);
if (lAngle > 90f)
turnLeft = false;
}
if(turnRight) {
offSet = rightRotation * offSet;
transform.position = player.transform.position + offSet;
transform.LookAt (player.transform);
if (rAngle < -90f)
turnRight = false;
}
An alternative would be to use Quaternion.RotateTowards, as this allows rotations to negative values without the GameObject infinitely rotating (as it can't approach a negative value). Below you can see I'm storing the initial value of the GameObject's rotation, as a Vector3, before modifying this value whenever A or D keys are pressed. The rotation of the transform is then set to the result of Quaternion.RotateTowards.
public class FixedRotate : MonoBehaviour
{
[SerializeField]
private float m_rotationAngle;
[SerializeField]
private float m_rotationSpeed;
private Vector3 m_targetRotation;
public void Start()
{
m_targetRotation = transform.eulerAngles;
}
public void Update()
{
//Left
if(Input.GetKeyDown(KeyCode.A))
m_targetRotation.y -= m_rotationAngle;
//Right
if (Input.GetKeyDown(KeyCode.D))
m_targetRotation.y += m_rotationAngle;
transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.Euler(m_targetRotation), m_rotationSpeed);
}
}
The unfortunate thing about this approach is that you can't specify the amount of time a rotation takes, only the incremental value of which the object will rotate by. An alternative approach would be to use Mathf.Clamp to add Time.deltaTime / rotationTime to the existing y rotation of the GameObject and stop once it's reached +/- 90. Hope this helps.

Categories