3
\$\begingroup\$

Edit: This answer might also be helpful to those who come across this question.

I've been messing around with raycast collisions and slopes, but am having an issue when trying to get my collider to stay grounded and rotate to the angle of a slope. Everything works when I cast a ray from the exact center of my collider, but things mess up when I try to cast rays from another point along the horizontal center of the collider.

For example, let's say my character's position is (0,0), and the "center" ray has an origin at (0,0). This gives me the behavior I want, but if I try to cast a ray from (-0.5, 0) my character is no longer flush with the ground even though the center.y point is the same. Oddly enough, this discrepancy only manifests when the character is on a slope; if it's on a flat surface the behavior of both origin points is the same.

Here's the correct behavior using the center ray:

enter image description here

Here's a similar ray which is supposedly being cast from the center.y (the origin is Vector2(bounds.max.x, bounds.center.y) of my collider), but with incorrect behavior:

enter image description here

...but that same ray works fine on a flat surface:

enter image description here

Even though Unity's print() function tells me the origin point.y of both rays are the same, I suspect that this must not be true based on how the ray is being drawn by Debug.DrawRay(), and from simply testing things in the game view. My script is fairly simple:

using UnityEngine;

public class RayRotationTest : MonoBehaviour
{
    //Movement values to tweak in the inspector
    public float gravity = -20;
    public float speed = 5, jump = 5;
    public LayerMask collisionMask;

    private BoxCollider2D boxCollider;

    private Vector2 velocity;
    private bool below;

    //The different origin points of the ray
    private Vector2 center, centerRight, centerLeft;

    private void Awake()
    {
        boxCollider = GetComponent<BoxCollider2D>();
    }

    private void LateUpdate()
    {
        velocity.y += gravity * Time.deltaTime;
        MoveCharacter(velocity * Time.deltaTime);   
    }

    //Get the bounds of the box collider, and set the ray origin points accordingly
    private void UpdateRaycastOrigins()
    {
        Bounds bounds = boxCollider.bounds;

        center = new Vector2(bounds.center.x, bounds.center.y);
        centerLeft = new Vector2(bounds.min.x, bounds.center.y);
        centerRight = new Vector2(bounds.max.x, bounds.center.y);
    }

    private void MoveCharacter(Vector2 velocity)
    {
        UpdateRaycastOrigins();

        bool wasGrounded = below;
        below = false; //Reset state

        CollideVertically(ref velocity, wasGrounded);
        transform.Translate(velocity);
    }

    private void CollideVertically(ref Vector2 deltaMovement, bool wasGrounded)
    {
        float directionY = Mathf.Sign(deltaMovement.y);
        float rayLength = (wasGrounded) ? 5 : Mathf.Abs(deltaMovement.y) + boxCollider.size.y * .5f;

        Vector2 rayOrigin = (Mathf.Sign(deltaMovement.x) == -1) ? centerLeft : centerRight;
        rayOrigin.x += deltaMovement.x;

        //Unity says the y coordinates of both of these points are the same
        print(center);
        print(centerRight);

        RaycastHit2D hit = Physics2D.Raycast(rayOrigin, transform.up * directionY, rayLength, collisionMask);
        Debug.DrawRay(rayOrigin, transform.up * directionY * rayLength, Color.red);

        if (hit)
        {
            float collisionAngle = Vector2.Angle(hit.normal, Vector2.up);
            float currentRotation = transform.eulerAngles.z;
            transform.rotation = Quaternion.Euler(new Vector3(0, 0, collisionAngle));

            deltaMovement.y = (hit.distance - boxCollider.size.y * .5f) * directionY;
            velocity.y = 0; //This stops the character from accumulating gravity when grounded
            below = true;
        }
    }
}

Essentially, I'm casting a ray from the center of the collider down to the ground, making sure its direction matches the direction of the y axis when the collider is rotated; this is done by setting the ray direction to transform.up. If it hits something I set my change in movement this frame, deltaMovement.y, to be the distance away from the origin point of the ray, subtracting half of my box collider's size to make sure I'm flat on the ground. I don't understand why the formula deltaMovement.y = (hit.distance - boxCollider.size.y * .5f) * directionY works in all cases except for the case shown in the last image.

Surely I must be missing something having to do with the ray's origin point. I think I really just need to know how to change deltaMovement.y = (hit.distance - boxCollider.size.y *.5f) to fix this problem, but I'm not sure what to do.

\$\endgroup\$
  • \$\begingroup\$ It seems like you are creating a Character Controller. Did you have a look at the Standard Assets package on the Asset Store? I believe it contains samples that might be exactly what you need. \$\endgroup\$ – Eric Feb 6 at 9:15
  • \$\begingroup\$ Yes. I’ve seen some things, but I’m trying to learn how to program the controller logic myself. I actually have a system that works quite well for many different kinds of collisions (and, happily, doesn’t seem to break after a good amount of testing), but wanted to add the option to change rotation on slopes. \$\endgroup\$ – Ginger and Lavender Feb 6 at 13:53
2
\$\begingroup\$

bounds is an axis-aligned bounding box that looks like this:

axis-aligned bounding box in blue

Instead of using bounds (which are in world coordinates) to calculate your ray's origin, use the local position, Vector.right/left and an appropriate scalar value, and then transform to world coordinates:

var scalar = 0.5f;
center = transform.TransformPoint(transform.localPosition);
centerLeft = transform.TransformPoint(transform.localPosition + Vector2.left * scalar);
centerRight = transform.TransformPoint(transform.localPosition + Vector2.right * scalar);
\$\endgroup\$
  • \$\begingroup\$ I appreciate your reply. I'm wondering why I need to use transform.localPosition, though, since the Unity docs say it's the same as transform.position when the gameObject has no parent, which is the case for my character. \$\endgroup\$ – Ginger and Lavender Feb 5 at 19:16
  • \$\begingroup\$ That's correct! Since the Vector2.left and right are in local coordinates I figured it would be a little cleaner to add them to the transform.localPosition. If there is no parent you could substitute transform.localPosition for transform.position in the code above, and remove the first transform.TransformPoint. \$\endgroup\$ – Eric Feb 6 at 9:06
  • \$\begingroup\$ A different approach I sometimes use is to add 2 empty children to the game object and reference those from the script as public Transform LeftRayCastOrigin and RightRayCastOrigin. Saves some code and it's more easily tweaked. \$\endgroup\$ – Eric Feb 6 at 9:18
  • \$\begingroup\$ After testing your code and trying some things, it seems that your approach is mostly correct, but for this to work for me I had to discard the use of transform.localPosition, and instead use boxCollider.offset as the coordinates for the center of my box collider. I then used transform.TransformPoint() with your vector multiplication and got the result I wanted. \$\endgroup\$ – Ginger and Lavender Feb 6 at 13:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.