How do I calculate a new position from current and target position for intuitive non-instant dragging?

I have a simple 2D game where I can move an object using the mouse. I've been improving on the 'feel' of the movement gradually, however it still doesn't behave as you'd expect if the object were to be moved by human hands in a real-world scenario. My improvements were as follows:

New position = mouse position Sharp unrealistic movement. No max speed.

Cap the maximum speed Nicer. Still stops abruptly. Doesn't accelerate smoothly

Add 'inertia', recording last change in position and applying it to subsequent moves

Vastly improved, however...

• Object is dragged as if on elastic, slow to accelerate, very slow to stop.
• Once moving, object oscillates or orbits around a stationary mouse position.

I'm looking for suggestions on how to make the movement more intuitive:

• Moving an object from A to B a human would rarely overshoot
• A user shouldn't need to correct against inertia to stop an object
• I'd like to be able to adjust the weight of the object so that heavier things are harder to get moving

My current code is as follows:

float maxInertiaChange = 0.1;
Vector2 lastPosition = Vector2.negativeInfinity;
Vector2 inertia = Vector2.zero;

private Vector2 InterpretPosition(Vector2 mousePosition) {
Vector2 newPosition;
if (lastPosition.x != float.NegativeInfinity) {
newPosition = lastPosition + (inertia * Time.deltaTime);
newPosition = GetPointTowardsTargetWithMaxDistance(newPosition, mousePosition, maxInertiaChange);
inertia = (newPosition - lastPosition) / Time.deltaTime;
} else {
newPosition = mousePosition;
}
lastPosition = newPosition;
return newPosition;
}

private static Vector2 GetPointTowardsTargetWithMaxDistance(Vector2 startVector, Vector2 targetVector, float maxDistance) {
float lerpScale = maxDistance / Vector2.Distance(startVector, targetVector);
return Vector2.Lerp(startVector, targetVector, lerpScale);
}


Something I've used before is to plan the movement as a Bézier curve.

The first control point on the curve is the current position of the object. The first tangent is proportionate to the current velocity of the object. (This gives you first-derivative continuity so the motion is smooth) The final control point is the position of the mouse, and the final tangent is zero (the object should slow to a rest as it arrives).

The advantage of planning the trajectory as a spline this way is it lets you explicitly control the total duration the motion should take (eg. if you want the object to always catch up and stop within half a second of the mouse stopping/releasing).

When the mouse moves, you plan a new Bézier curve, choosing a duration (eg. proportionate to the distance of the object from the mouse, up to some max), and then setting:

P_0 = draggedObject.position;
P_1 = P_0 + dreaggedObject.velocity * duration / 3f;
P_2 = P_3 = mousePosition;


Then at each timestep after this, you can get the object's position & velocity by evaluating the cubic Bézier's parametric equation at t = Clamp01(timeSinceMouseMove/duration);

• Thanks, this sounds like a solid suggestion. I'll give it a try today and report back – Arachin Aug 29 '18 at 11:11
• This works like a charm. For others looking to follow this example I recommend skimming this: catlikecoding.com/unity/tutorials/curves-and-splines . You can update the spline on the fly as follows: P0 = current position, P1 = current velocity (see link, you can get this using the first derivative), P2 = P3 = new target position – Arachin Aug 29 '18 at 19:20
• One trick to watch out for: if your duration is short compared to your distance/speed, you can get a "bounce" effect where the object has to accelerate up to a high speed to reach the target in time, overshooting the target then doubling-back right at the end, which can look a bit distracting. Padding out the duration or damping the speed on release can smooth this out and avoid the bounce. – DMGregory Aug 29 '18 at 19:29