How to visualize a part's path?

I’m making a soccer ball and I’m wondering how to visualize a part’s path, like a trail of where it’s predicted to go? I’m using bodyVelocity and other bodyMovers

Maybe do this

Before shooting the ball, shoot another transparent ball with the same force and then visualize the path by placing parts on the path of the tranparent ball.

This is an amazing way of doing it BUT shooting the second ball with the same force as the first might not go the same way. I suggest shooting a transparent one like @OneXZero_DEV said, getting its positions as it moves, then make a second ball that isnt transparent that follows the first balls positions one by one. That way you can get the balls predicted positions. Another way would be to use pathfind but I doubt thatll help.

If the predicted path is a parabolic arc, i.e. if the ball is predicted to be only affected by gravity, you can visualize the path with just a single Beam. A Beam uses a cubic (3rd degree) bezier curve. Any parabolic arc can be represented as a quadratic (2nd degree) bezier curve and a bezier curve can be converted to a higher degree bezier curve that has the same shape.

So if you find the three control points of the quadratic bezier curve, you can convert them to the four control points of a cubic bezier curve. Given those control points, you can calculate the CFrames of Beam.Attachment0 and Beam.Attachment1, and the Beam properties CurveSize0 and CurveSize1.

You can then fine tune the beam with other properties such as number of Segments (the actual number of rendered segments is also affected by graphics quality, though) and the width properties.

As for calculating the quadratic bezier control points, a parabola is always in a single plane so finding the control points can first be moved into two dimensions and the results can later be converted to three dimensions.

We can decide that the first control point p0 in 2D is (0, 0) to simplify the calculations. The last control point p2 (curve end point) should be at a height below which the part most likely won’t go. In the case of a flat football field, this height choice is pretty clear. The point’s x coordinate can be calculated by solving the time from the desired y coordinate and calculating the x coordinate from that time.

The vector from the first control point of any degree bezier curve to its second control point is always parallel to the tangent of that curve at the first control point. Similarly, the vector from second last control point to last control point is always parallel to the tangent at the last control point. In the case of a quadratic bezier, the second control point and the second last control point are both the same point (p1, the only control point we are missing). This means that we can calculate p1 as an intersection of the two tangent lines. The velocity at p0 is parallel to the tangent at p0 and the velocity at p2 is parallel to the tangent at p2. So for both tangent lines, we know a point on the line and a vector parallel to the line. Any point Q on a line that goes through point A in the direction of a vector u can be represented as Q = A + us where the line parameter s is a real number. Thus, we get two equations:

p1 = p0 + v0 * s
p1 = p2 + v2 * t

We can then solve either s or t and use that to calculate p1. Edit: I should have clarified that the unknowns can’t be directly solved from these equations since these actually have four unknowns (s, t and both coordinates of p1). The right sides of these equations are actually combined into one equation and the resulting equation is split into separate equations for x and y coefficients. After this, we have two equations and two unknowns so we are able to solve the unknowns (we only need to calculate one of them in this situation).

-- combining into one equation (eliminating p1 from the equation)
--> one equation with two unknowns s and t
p0 + v0 * s = p2 + v2 * t

-- splitting into separate equations for x and y coefficients
--> two equations with two unknowns s and t
p0.x + v0.x * s = p2.x + v2.x * t
p0.y + v0.y * s = p2.y + v2.y * t

Additionally, in the code, I’ve written the formula for s i.e. p0V0LineParameterForIntersection with cross products. When you solve s from these equations, you just get a formula with sums of multiplications of two vector coefficients. However, if you know the cross product z coefficient formula, you can figure out how to rewrite these with Vector2:Cross() which gives the z coefficient of the cross product of two xy-plane vectors (the x and y coefficients of the cross product for xy-plane vectors are 0). I also further simplified the formula by removing one of the numerator cross products because since I decided that p0 in 2D is (0, 0), v2In2D:Cross(p0In2D) would always give 0.

Now we have every control point of the quadratic bezier in two dimensions. The next step is converting these to three dimensions. Then all that is left is converting these quadratic bezier control points to cubic bezier control points and changing the Beam properties and the CFrames of its attachments. Here’s the whole code. p0V0LineParameterForIntersection in the code is the same as s in the equations.

--!strict
local p0: Vector3 = Vector3.new(10, 6, 20)
local v0: Vector3 = Vector3.new(10, 200, 20)
local y2: number = 2

local beamWidth: number = 5

local yAcceleration: number = -workspace.Gravity

local attachment0: Attachment, attachment1: Attachment
local beam: Beam

local function getVectorFormat(numOfNumbers: number): string
	if numOfNumbers < 1 then
		return "()"
	end
	local numberFormat: string = "%.2f"
	local vectorFormat: string = "(" .. string.rep(numberFormat .. ", ", numOfNumbers - 1) .. numberFormat .. ")"
	return vectorFormat
end

local function getVector3String(vector: Vector3): string
	return string.format(getVectorFormat(3), vector.X, vector.Y, vector.Z)
end

local function getVector2String(vector: Vector2): string
	return string.format(getVectorFormat(2), vector.X, vector.Y)
end

local function createBeamAndAttachments(): ()
	beam = Instance.new("Beam")
	beam.Width0, beam.Width1 = beamWidth, beamWidth
	beam.Parent = workspace
	
	attachment0, attachment1 = Instance.new("Attachment"), Instance.new("Attachment")
	attachment0.Parent, attachment1.Parent = workspace.Terrain, workspace.Terrain
	
	beam.Attachment0, beam.Attachment1 = attachment0, attachment1
	
	beam.Parent = workspace
end

local function fitBeamToCubicBezier(p0: Vector3, p1: Vector3, p2: Vector3, p3: Vector3): ()
	local p0ToP1: Vector3, p2ToP3: Vector3 = p1 - p0, p3 - p2
	local attachment0RightVector: Vector3 = p0ToP1.Unit
	local attachment1RightVector: Vector3 = p2ToP3.Unit
	local attachmentUpVector: Vector3 = attachment0RightVector:Cross(Vector3.yAxis)
	attachment0.CFrame = CFrame.fromMatrix(p0, attachment0RightVector, attachmentUpVector)
	attachment1.CFrame = CFrame.fromMatrix(p3, attachment1RightVector, attachmentUpVector)
	beam.CurveSize0, beam.CurveSize1 = p0ToP1.Magnitude, p2ToP3.Magnitude
end

local function getCubicBezierControlPointsFromQuadraticBezierControllPoints(p0: Vector3, p1: Vector3, p2: Vector3): (Vector3, Vector3, Vector3, Vector3)
	return p0, 1/3 * (p0 + 2 * p1), 1/3 * (2 * p1 + p2), p2
end

local function fitBeamToQuadraticBezier(p0: Vector3, p1: Vector3, p2: Vector3)
	local cubicP0: Vector3, cubicP1: Vector3, cubicP2: Vector3, cubicP3: Vector3 = getCubicBezierControlPointsFromQuadraticBezierControllPoints(p0, p1, p2)
	fitBeamToCubicBezier(cubicP0, cubicP1, cubicP2, cubicP3)
end

local function updateCurve(p0: Vector3, v0: Vector3, y2: number): ()
	local v0Horizontal: Vector3 = v0 - v0.Y * Vector3.yAxis
	
	--== 2D calculations ==--
	-- X and Z of v0 define the 2D positive x-axis direction. The 2D positive y axis direction is the same as the 3D y axis direction.
	-- The horizontal acceleration is 0. I decided p0 in 2D is (0, 0).
	local v0In2D: Vector2 = Vector2.new(v0Horizontal.Magnitude, v0.Y)
	local y2In2D: number = y2 - p0.Y
	
	local p2Time: number = -(math.sqrt(v0.Y^2 + 2 * yAcceleration * y2In2D) + v0.Y) / yAcceleration
	local p2In2D: Vector2 = Vector2.new(v0.X * p2Time, y2In2D)
	local v2In2D: Vector2 = v0In2D + Vector2.new(0, yAcceleration * p2Time)
	
	-- Choosing p0 to be (0, 0) simplified the line intersection formula to this.
	local p0V0LineParameterForIntersection: number = p2In2D:Cross(v2In2D) / v0In2D:Cross(v2In2D)
	-- Again, p0 in 2D is (0, 0), and is thus omitted from this calculation.
	local p1In2D: Vector2 = v0In2D * p0V0LineParameterForIntersection
	
	--== 3D calculations ==--
	-- Vector3.yAxis is equal to the Unit Vector of the vertical component of v0 assuming that the that v0.Y > 0.
	local v0HorUnit: Vector3 = v0Horizontal.Unit
	local p1: Vector3 = p0 + p1In2D.X * v0HorUnit + p1In2D.Y * Vector3.yAxis
	local p2: Vector3 = p0 + p2In2D.X * v0HorUnit + p2In2D.Y * Vector3.yAxis
	
	fitBeamToQuadraticBezier(p0, p1, p2)
	
	print(`p2Time: {p2Time}`)
	print(`p1: {getVector2String(p1In2D)}; p2: {getVector2String(p2In2D)}`)
	print(`p0: {getVector3String(p0)}, p1: {getVector3String(p1)}, p2: {getVector3String(p2)}`)
end

createBeamAndAttachments()
updateCurve(p0, v0, y2)

For an explanation about the parabolic motion formulas and an alternative approach that calculates the cubic bezier control points without first calculating the quadratic ones, see this tutorial by EgoMoose:

Also, if the ball is supposed to move in the kind of arc I described, then you should probably use

ballPart:ApplyImpulse(ballPart.AssemblyMass * (v0 - ballPart.AssemblyLinearVelocity))

instead of a BodyVelocity. Regardless, the old BodyMovers are deprecated. LinearVelocity is a replacement for BodyVelocity.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.