0
\$\begingroup\$

I have a 3d game, but for movement I use only two axes (X and Z) - X is vertical, Z is horizontal

Here's what I'm trying to achieve:

enter image description here

Player is the circle, InputDirection is Blue, NewInputDirection is Green. I'm struggling on getting the desired green direction.

Image 1 The player moves up (he's pushing onto the wall)

InputDirection: (-1f, 0f, 0f)

NewInputDirection: (0f, 0f, 0f) because we should stop him.

Image 2 The player moves towards the wall and the bottom. When he reaches the wall, his horizontal axis should be blocked - set to 0, so he slides)

InputDirection: (0.5f, 0f, -0.5f)

NewInputDirection: (0.5f, 0f, 0f)

Image 3 Same thing as above, but the wall is rotated.

Right now, my code looks like this:

RaycastHit hit;
bool collides = Physics.Raycast(this.playerRigidbody.position, inputDirection, out hit, 1f); // ray in front of the player

if(collides){ // if there's a collision
    // calculate the direction from the player to the collision contact point
    Vector3 collisionNormal = (hit.point - playerRigidbody.position).normalized;
    // idk...
}

I don't know how to calculate it. I've tried various ways. Should I also take the wall's rotation into account?

\$\endgroup\$
1
\$\begingroup\$

The simple answer here is vector projection. If n is a unit vector (length 1), then...

float projection = Vector3.Dot(n, someVector);`

is the signed length of the component of someVector that's parallel to n. We can subtract that component to leave only the perpendicular component:

Vector3 perpendicular = someVector - n * projection;

It looks like you have more going on here than just the vector math question, so I'll expand on that below.


You seem to be using this to let the player character move up to and along a wall, without penetrating through it.

For that purpose, I'd recommend using a spherecast or capsulecast instead. The reason is that the ray can squeeze into spaces that your player character couldn't, so if you just position yourself at the end of the ray, you might clip into an obstacle the ray barely missed to one side or the other.

RaycastHit hit;
bool collides = Physics.SphereCast(
                 playerRigidbody.position,
                 playerAvoidanceRadius,
                 inputDirection, 
                 out hit,
                 1f);

if(collides){ // if there's a collision
    // I've found that adding a slight extra margin here helps avoid unwanted intersections.
    float uninterruptedDistance = hit.distance - 0.001f;
    // Compute our blue arrow.
    Vector3 directTravel = inputDirection * uninterruptedDistance;

    // Subtract out the component of the direction that goes into the wall.
    // Leaving the direction that's parallel.
    float projection = Vector3.Dot(inputDirection, hit.normal);
    // Compute our green arrow direction.
    Vector3 slideDirection = (inputDirecton - hit.normal * projection).normalized;

    // If you want to move as far as you can along the blue, then start moving along the green...
    float remainingDistance = moveDistance - uninterruptedDistance;
    Vector3 slideTravel = slideDirection * remainingDistance;

    Vector3 totalTravel = directTravel + slideTravel;

    // Do what you want with it.
}

Note that we scanned for obstacles along only the blue arrow - so it's possible moving to the end of the green arrow will put us in collision with something we didn't scan for. So if you want to be really exacting here, you can recurse and fire a new ray/sphere/capsule cast along the new direction. I show an example of doing that in this answer.

\$\endgroup\$
  • \$\begingroup\$ Thank you for the answer, really helped me to understand what's going on. I'm just wondering, about the provided link (detecting other objects, eg. when you're standing in the corner between 2 walls). Wouldn't it be better to implement SphereCastAll in the above example, instead of shooting multiple raycasts as in the link? \$\endgroup\$ – Jacob Aug 21 at 16:10
  • \$\begingroup\$ The whole point of my question was that I don't want to "correct" the player's movement, but the direction he had chosen. In the function, I'm given an input direction and a speed. I need to use these two to construct a new direction vector which will not collide with the environment. Wouldn't it be better to just iterate through all RaycastHit's in the SphereCastAll and subtract the colliding direction of each of the collisions from the input direction? Theoretically, it should work. \$\endgroup\$ – Jacob Aug 21 at 16:41
  • \$\begingroup\$ No, SphereCastAll is redundant for that purpose. It keeps scanning past the first collision, finding all possible collisions along that line. But you want to stop at the first collision - any obstacles past that, your player never hits. After you pick a direction that avoids that collision, you're now looking at moving in a different direction. You have no guarantees about how far the object can move in this new direction unless you do a new query pointing in this direction. \$\endgroup\$ – DMGregory Aug 21 at 18:19

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.