6
\$\begingroup\$

Intro and code

I have the following function that calculates an object's "next" position and velocity (in 1 dimension) given a boundary [0, max].

public TwoVector<Double> getNextPositionAndVelocity(double position, double velocity, double max) {
    if (position + velocity < 0) {
        return TwoVector.<Double>builder()
                .x(Math.abs(position + velocity))
                .y(Math.abs(velocity))
                .build();
    }
    if (position + velocity > max) {
        return TwoVector.<Double>builder()
                .x(max - ((position + velocity) % max))
                .y(-Math.abs(velocity))
                .build();
    }
    return TwoVector.<Double>builder()
            .x(position + velocity)
            .y(velocity)
            .build();
}

Explanation

  • The first condition checks if adding the current velocity to the current position would cause the object to be beyond the "left" boundary. If that's the case, the next position should be equal to the amount the object would be beyond the left boundary added to the left boundary and the next velocity should point the object to the "right".
  • The second condition checks if adding the current velocity to the current position would cause the object to beyond the "right" boundary. If that's the case, the next position should be equal to the amount the object would be beyond the boundary subtracted from the boundary and the next velocity should point the object to the "left".
  • If neither condition is true, we can add the current velocity to the current position to get the next position without issue.

Question

Are there more straightforward functions to calculate each of these "next" values? Perhaps a function that "collapses" these branches?

\$\endgroup\$
  • \$\begingroup\$ Basically you're doing a 2D physics engine. Those are pretty well understood things and I think it would be a lot better to use already established concepts. Your ad hoc invention here seems like it would cause a lot of trouble trying to maintain it. Also look up Euler integration, it's typically used for physics simulations. \$\endgroup\$ – markspace Oct 9 at 18:45
  • 1
    \$\begingroup\$ Also for performance I don't like generics or your builder pattern. Physics simulations almost always have to sacrifice some type safety and code reuse in favor of raw speed. \$\endgroup\$ – markspace Oct 9 at 18:54
  • \$\begingroup\$ It's not clear to me what the result will be if your current position plus velocity is greater than, say, 4*max. \$\endgroup\$ – CiaPan Oct 9 at 20:13
  • \$\begingroup\$ Just as a coincidence I just happened to see this video in my YouTube list about physics engines. I haven't watched it in full but other Coding Train videos have been pretty good. youtube.com/watch?v=wB1pcXtEwIs \$\endgroup\$ – markspace Oct 9 at 20:32
3
\$\begingroup\$

Review

  • You have redundant code. Store position + velocity in a variable.
  • Use well-known variable names when the code is of a mathematical nature. Distance x Velocity v A clamped area [a,b].
  • It is not clear from your description what should happen if the distance is so far away from the boundaries, that when reflecting the movement you are still out of bounds, but on the other side.
  • .x(max - ((position + velocity) % max)) This does not correspond to your description of

    the next position should be equal to the amount the object would be beyond the boundary subtracted from the boundary

Proposed Solution

  • Use mathematical variable names.
  • Allow damped bouncing when the distance is too far out of bounds (Damped bouncing reflects the value x, alternating around the boundaries until it is clamped with the area [a,b]).
  • Make both 0 and max a variable, respectively a and b.
public TwoVector<Double> getNextPositionAndVelocity(double x, double v, double a, double b)
{
    assert a < b;
    double xn = x + v;

    // the loop allows for damped bouncing
    while (true) {
        if (xn < a) {
            xn = a - xn;
            v = Math.abs(v);
        }
        else if (xn > b) {
            xn = b - (xn - b);
            v = -Math.abs(v);
        }
        else {
            break;
        }
    }

    return TwoVector.<Double>builder().x(xn).y(v).build();
}
\$\endgroup\$
  • 1
    \$\begingroup\$ If performance is key, then (if I'm not totally mistaken) this should be calculatable without a loop. \$\endgroup\$ – RoToRa Oct 11 at 7:47
  • \$\begingroup\$ @RoToRa I think you might be right :) \$\endgroup\$ – dfhwze Oct 11 at 7:51
1
\$\begingroup\$

I assume "next" in getNextPositionAndVelocity means "after one unit of whatever temporal unit you use in the unit of the velocity", since the change of a position based on a velocity only makes sense in the context of a time span.

As for your code, you could simply model the path of the object by breaking it up into subpaths where the direction does not change, like this:

public double[] getNextPositionAndVelocity(double position, double velocity, double max) {
    double remainingDistance = Math.abs(velocity);
    double currentVelocity = velocity;
    double currentPosition = position;

    while (remainingDistance > 0.0) {
        double nextBoundary;
        if (currentVelocity < 0.0) {
            nextBoundary = 0.0;
        } else {
            assert currentVelocity > 0.0; //cannot be 0 or NaN, not if we're in the loop
            nextBoundary = max;
        }

        double maximumDistanceToTravelInCurrentDirection = Math.abs(nextBoundary - currentPosition);

        if (maximumDistanceToTravelInCurrentDirection <= remainingDistance) {
            currentPosition = nextBoundary;
            remainingDistance -= maximumDistanceToTravelInCurrentDirection;
            currentVelocity *= -1;
        } else {
            currentPosition += remainingDistance * Math.signum(currentVelocity);
            remainingDistance = 0;
        }
    }

    return new double[]{currentPosition, currentVelocity};
}

Admittedly, this is more code than your version, but then, you don't consider the possibility that the range is smaller than the velocity, and if this is the case, an object might bounce off an edge more than once.

The above code could be optimized by first calculating the velocity modulo twice the range, because after two range lenghts, the position and velocity of the object will be identical to its initial position and velocity.

Also, you should validate the arguments. This not only entails checking whether the arguments are valid with respect to each other (e.g. whether position lies within the permitted range), but also handling special cases like the infinities or Double.NaN. E.g., if you check whether position >= 0 && position <= max, then you automatically have NaN covered for position and max, because the comparison operators and the equality operator == always return false if one operand is NaN (even Double.NaN == Double.NaN returns false, which is why there's a method Double.isNaN(double)), but position might still be Double.POSITIVE_INFINITY if max is also Double.POSITIVE_INFINITY. Also, max should probably be greater than 0 (which would not be covered by the aforementioned condition either).

\$\endgroup\$
1
\$\begingroup\$
  1. x(max - ((position + velocity) % max))
    

    doesn't look quite right. % max does not help here much (see below) it is better replaced with - max, and thus the whole formula turns into 2 * max - (position + velocity).

  2. Do you assume large velocities are impossible, in other words, in the first alternative, can you guarantee that Math.abs(position + velocity) is always LE than max?

\$\endgroup\$

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.