1
\$\begingroup\$

I'm making a movement-based, first-person 3D game where the player should be able to gain a lot of horizontal speed over time with WASD, but also will stop due to friction very quickly the moment they release their movement key. The player's current horizontal movement is a velocity stored in a 2-dimensional vector.

Due to the requirement of needing to stop fast, I have a high amount of friction applied (the velocity is multiplied by 0.7.) However, in order to allow gaining high amounts of horizontal speed, the friction is much, much lower when the player is moving, 0.99.

I intend to separate the friction for forward/back from the friction for left/right, as this allows the player finer control over the current directions they're moving in.

Unfortunately, my code is broken, resulting in it working exactly as I intend only when you're facing in a cardinal direction — every other time, you have far too much friction. At this point, I realise that I'm not really sure what I'm doing, and my guesses aren't panning out. How do I correctly apply friction separately to the forwards/backwards and side directions?

I don't really want to include it in the question, but in case it's necessary, here's my current code — JavaScript, with a custom Vec2 class.
powXY is v1.x **= v2.x; v1.y **= v2.y. Everything else should be self-explanatory.

const direction = this.CAMERA.rotation.y;
const forwardBackVector = Vec2.fromDirection(direction).abs();
const leftRightVector = Vec2.fromDirection(direction + Math.PI / 2).abs();

let forwardBackInput = 0;
let leftRightInput = 0;

if (this.KEYBOARD.isDown("KeyE"))
    forwardBackInput++;
if (this.KEYBOARD.isDown("KeyD"))
    forwardBackInput--;

if (this.KEYBOARD.isDown("KeyS"))
    leftRightInput++;
if (this.KEYBOARD.isDown("KeyF"))
    leftRightInput--;

let forwardBackFriction = new Vec2(this.FRICTION).powXY(...forwardBackVector.xy);
let leftRightFriction = new Vec2(this.FRICTION).powXY(...leftRightVector.xy);

const acceleration = Vec2.ZERO;
if (forwardBackInput) {
    const normalisedVector = Vec2.fromDirection(direction + (forwardBackInput === 1 ? 0 : Math.PI));
    const dot = this.horizontalVelocity.dot(normalisedVector) * this.horizontalVelocity.length();
    acceleration.add(normalisedVector
        .multiply(dot < this.SPEED_HORIZONTAL_CAP ? this.SPEED_HORIZONTAL_FAST : this.SPEED_HORIZONTAL)
        .multiply(delta)
        .multiply(leftRightInput ? 0.5 : 1));

    forwardBackFriction = new Vec2(this.FRICTION_MOVE).powXY(...forwardBackVector.xy);
}

if (leftRightInput) {
    const normalisedVector = Vec2.fromDirection(direction + Math.PI / 2 * leftRightInput);
    const dot = this.horizontalVelocity.dot(normalisedVector) * this.horizontalVelocity.length();
    acceleration.add(normalisedVector.copy()
        .multiply(dot < this.SPEED_HORIZONTAL_CAP ? this.SPEED_HORIZONTAL_FAST : this.SPEED_HORIZONTAL)
        .multiply(delta)
        .multiply(forwardBackInput ? 0.5 : 1));

    leftRightFriction = new Vec2(this.FRICTION_MOVE).powXY(...leftRightVector.xy);
}

this.horizontalVelocity.add(acceleration);

this.CAMERA.position.x -= this.horizontalVelocity.y * delta;
this.CAMERA.position.z -= this.horizontalVelocity.x * delta;

for (const friction of [forwardBackFriction, leftRightFriction]) {
    this.horizontalVelocity.x *= friction.x ** delta;
    this.horizontalVelocity.y *= friction.y ** delta;
}
```
\$\endgroup\$
3
  • \$\begingroup\$ Another way to attack this, rather than using selective friction, is using selective traction — with a higher stopping traction. I show an example of that kind of approach here. \$\endgroup\$
    – DMGregory
    Nov 19 '21 at 12:53
  • \$\begingroup\$ @DMGregory unfortunately I don’t think that’s gonna work for my case, because I’ll need to change friction in the air as well, switching to low friction when movement isn’t even a thing. Selective traction would be a good secondary tool, at least. For now I’m gonna need to just figure out how to do friction though \$\endgroup\$ Nov 20 '21 at 1:10
  • \$\begingroup\$ I'd use a different physics model in mid air. The physics of an airborne biped are very different than those of a biped with their feet on the ground. 😉 \$\endgroup\$
    – DMGregory
    Nov 20 '21 at 1:16
1
\$\begingroup\$

This is actually quite simple to do with a dot product to project the velocity onto a unit vector, to get the component of the velocity pointing in that direction:

function getComponentInDirection(vector, unitDirection) {
    // I'm not sure how your vector library works, so I'll be conservative 
    // and assume we need an explicit copy when we want to avoid modifying the argument.
    const component = unitDirection.copy();

    // Scale this vector to the length of the projection of the input onto this direction.
    // (Incidentally, this dot product is your speed along the given direction)
    component.multiply(unitDirection.dot(vector)); 
    return component;
}

With that in hand, you can split your velocity into two parts, scale each one separately, then add them back together again.

function applyDirectionalFriction(velocity, forwardDirection, 
                                  forwardFriction, sidewaysFriction, delta)
{
    // Get the component facing forward/backward (assuming forwardDirection has length 1)
    const forwardVelocity = getComponentInDirection(velocity, forwardDirection);

    // Whatever remains after subtracting the forward velocity must be sideways.
    const sidewaysVelocity = (velocity.clone()).subtract(forwardVelocity);

    // Apply the respective friction values to each.
    forwardVelocity.multiply(forwardFriction ** delta);
    sidewaysVelocity.multiply(sidewaysFriction ** delta);

    // Re-combine into a finished velocity.
    return fowardVelocity.add(sidewaysVelocity);
}
\$\endgroup\$
1
\$\begingroup\$

I knew that I could get it to work with velocity on the x or y axes, but I had no idea how to fix it when your direction wasn't on those axes. So I had the idea to just rotate the velocity to be on the x/y axes, apply the friction, then rotate back. It feels a little overcomplicated calculation-wise, but it works, and it's comprehensible, so it seems like the way to go.

Here's my updated code:

const direction = this.CAMERA.rotation.y;

let forwardBackInput = 0;
let leftRightInput = 0;

if (this.KEYBOARD.isDown("KeyE"))
    forwardBackInput++;
if (this.KEYBOARD.isDown("KeyD"))
    forwardBackInput--;

if (this.KEYBOARD.isDown("KeyS"))
    leftRightInput++;
if (this.KEYBOARD.isDown("KeyF"))
    leftRightInput--;

let forwardBackFriction = this.FRICTION;
let leftRightFriction = this.FRICTION;

const acceleration = Vec2.ZERO;
if (forwardBackInput) {
    const normalisedVector = Vec2.fromDirection(direction + (forwardBackInput === 1 ? 0 : Math.PI));
    const dot = this.horizontalVelocity.dot(normalisedVector) * this.horizontalVelocity.length();
    acceleration.add(normalisedVector
        .multiply(dot < this.SPEED_HORIZONTAL_CAP ? this.SPEED_HORIZONTAL_FAST : this.SPEED_HORIZONTAL)
        .multiply(delta)
        .multiply(leftRightInput ? 0.5 : 1));

    forwardBackFriction = this.FRICTION_MOVE;
}

if (leftRightInput) {
    const normalisedVector = Vec2.fromDirection(direction + Math.PI / 2 * leftRightInput);
    const dot = this.horizontalVelocity.dot(normalisedVector) * this.horizontalVelocity.length();
    acceleration.add(normalisedVector.copy()
        .multiply(dot < this.SPEED_HORIZONTAL_CAP ? this.SPEED_HORIZONTAL_FAST : this.SPEED_HORIZONTAL)
        .multiply(delta)
        .multiply(forwardBackInput ? 0.5 : 1));

    leftRightFriction = this.FRICTION_MOVE;
}

this.horizontalVelocity.add(acceleration);

this.CAMERA.position.x -= this.horizontalVelocity.y * delta;
this.CAMERA.position.z -= this.horizontalVelocity.x * delta;

this.horizontalVelocity.rotate(-direction);
this.horizontalVelocity.x *= forwardBackFriction ** delta;
this.horizontalVelocity.y *= leftRightFriction ** delta;
this.horizontalVelocity.rotate(direction);
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .