How can I manually dictate how fast and high a tennis ball goes in my game?

Hi there, Im trying to make a tennis project for fun and im trying to figure out how can i control how fast and high a tennis ball goes in my game.

This is how it works in the game:

The player has manual control over where the ball goes, via the use of the target, the normal behiavior is that after he swings the racket and it hits the ball, the ball will fly or launch over the net and hit the ground on that target, then bounce. Similarly in a real tennis match.

So no matter what, when the ball touches the ground, it hits that target. No matter how high or fast it goes.

Here’s an example:

So I cooked up this code:

local RS = game:GetService("ReplicatedStorage")
local BMLEvent = RS.BallMachineLaunch
local SendBallEvent = RS.MoveBall
local CurrentBall = RS.ImportantAssets.TennisBall
local CBValue = RS.CurrentBallValue.Value
local upwardSpeed = 45    -- This will now represent the peak height of the bounce
local forwardSpeed = 2    -- This acts as a timescale; how fast the ball completes its bounce (tweaked for time)
local bounceFactor = 0.95 * 1.25 
local TrajectoryInturrupted = false
local CurrentClone = nil

-- Function to calculate the time it takes for the ball to reach the target
local function calculateFlightTime(targetPosition, startPosition)
	local horizontalDistance = (Vector3.new(targetPosition.X, 0, targetPosition.Z) - Vector3.new(startPosition.X, 0, startPosition.Z)).Magnitude
	return horizontalDistance / forwardSpeed  -- Time to reach the target controlled by forwardSpeed
end

-- Function to calculate the initial velocities (vx, vz, vy)
local function calculateInitialVelocity(startPosition, targetPosition, peakHeight, gravity, time)
	-- Get the horizontal direction to the target
	local direction = (targetPosition - startPosition).Unit

	-- Calculate the horizontal velocity components (X and Z)
	local vx = direction.X * (targetPosition - startPosition).Magnitude / time
	local vz = direction.Z * (targetPosition - startPosition).Magnitude / time

	-- Calculate the required vertical velocity (vy) based on the peak height
	local vy = (2 * peakHeight) / time

	return Vector3.new(vx, vy, vz)
end

local function SimulateBall(Part: BasePart, bball: BasePart, targetPosition: Vector3)
	local rs = game:GetService("RunService").Heartbeat
	local g = Vector3.new(0, -game.Workspace.Gravity, 0)  -- gravity vector
	local x0 = Part.Position  -- initial position of the ball

	-- Calculate the time to reach the target based on forwardSpeed
	local timeToTarget = calculateFlightTime(targetPosition, x0)

	-- Calculate the initial velocity (horizontal and vertical components)
	local v0 = calculateInitialVelocity(x0, targetPosition, upwardSpeed, g, timeToTarget)

	local nt = 0  -- time counter

	-- Clone the current ball and set its properties
	local c = CurrentBall:Clone()
	c.Parent = workspace
	c.CanCollide = false
	c.Anchored = true
	c.Transparency = 0

	CurrentClone = c
	TrajectoryInturrupted = false
	CBValue = CurrentClone
	local BounceSFX = c:WaitForChild("BounceSFX")

	-- Main loop for ball physics simulation
	while true do
		if not TrajectoryInturrupted then
			-- Calculate the new position based on time, velocity, and gravity
			-- The vertical position is updated by the parabolic motion formula
			local newPos = 0.5 * g * nt * nt + v0 * nt + x0
			c.CFrame = CFrame.new(newPos)

			-- Check if the ball has hit the ground (bounce)
			if newPos.Y <= 0.5 then
				-- Bounce by reversing and reducing the vertical speed
				v0 = Vector3.new(v0.X, -v0.Y * bounceFactor, v0.Z)
				nt = 0  -- reset time counter for the bounce
				x0 = Vector3.new(newPos.X, 0.5, newPos.Z)
				BounceSFX:Play()
			end

			nt = nt + rs:Wait()

			-- Break loop if the bounce height becomes too small (end simulation)
			if math.abs(v0.Y) < 0.1 then
				break
			end
		else
			c:Destroy()
		end
	end
end

-- Function to interrupt and reset the ball simulation
local function InterruptAndReset(Part, targetPosition)
	TrajectoryInturrupted = true

	if CurrentClone then
		CurrentClone:Destroy()
		CurrentClone = nil
	end

	-- Restart the simulation with the new target position
	SimulateBall(Part, CurrentBall, targetPosition)
end

BMLEvent.Event:Connect(function(Part, targetPosition)
	if CurrentBall ~= nil then
		InterruptAndReset(Part, targetPosition)
	end
end)

SendBallEvent.Event:Connect(function(hrp, targetPosition)
	if CurrentBall ~= nil then
		InterruptAndReset(hrp, targetPosition)
	end
end)

In this code, the Ball gets cloned, the projectile path is calculated and teh ball follows it, when it gets hit or after some time, the clone will get destroyed and a new ball is created with a new projectile path.

ForwardSpeed Dictates how fast the ball launches and bounces (think of it like Tween Timescale).

UpwardSpeed dictates the height (or originally, the maximum height) the ball attains during its bounce.

My issue with this is that, Is that if i tweak the forward speed or the upward speed a little bit, it overshoots the target position and misses it completely, or it will drastically slow down and (undershoot?) hit the ground too early.

Pls help.

You have to calculate a custom gravity that acts upon your object during freefall.

Equation:

A = (2 * InitialVelocity * Time) / (Time ^ 2)

how would I implement it?

sorry if the question sounds dumb.

cuz the only time related thing on this code is really just the nt which counts time.

Just asking but how familiar are you with physics?

If you want to give the ball some initial Y-Velocity then you cannot set the peak height. Lemme see what I can do here

Can i say around intermediate level?

Yea, the whole peak height idea came from the fact that in mario tennis, some swings cause the height of the ball to be really high.

Try this:

local g = Vector3.new(0, -((2 * upwardSpeed * timeToTarget) /timeToTarget ^2), 0)

When I did my physics calculations I worked with only the X and Y axis and I isolated them. This means I would calculate the Y and X positions individually and then put them together later. So working with three 3 vectors is kinda new but the same equations should apply to both

it works somewhat…?

hmm, it seems i need to think about this thing differently.

You see in mario tennis, if you charge up ur hit, the ball will or go over the net towards where you aim (aka the target) faster, however if you dont do so, it will be a lot slower, but it seems to always hit that aim/target height wise its not incredibly high. I dont know what, but im missing something,

another idea for this might be using Bezier curves depending on how familiar you are with them. you have the start and end position of the curve as well as the peak or vertex (aka the point at witch the ball is at its highest) then you would just use these to calculate the control point of the bezier curve. then allowing you to easily tween or lerp the tennis ball along the curve to simulate gravity. i havent tested this so i dont know how well it would work but you should be able to easily adjust the speed of the ball as well as how high up it goes if you do decide to use somthing like bezier curves


here is kinda what i mean. the first point is the start then the control and then the end.

Make the upward speed change depending on how hard you hit it. If you give the ball less Y-velocity then the ball won’t sharply curve up leading it to hit the net.

funnily enough, i started working on something similar.

The prototype I made:

-- Ball, control point, and target points for two bounces
local ball = script.Parent -- Assuming this is the tennis ball
local startPoint = Vector3.new(0, 10, 0) -- Adjust for your game

local targetPoint1 = Vector3.new(10, 5, 50) -- First target position
local targetPoint2 = Vector3.new(15, 5, 95) -- Second target position

local controlPointHeight1 = 15 -- Height of the first bounce
local controlPointHeight2 = 7 -- Height of the second bounce (lower than the first)

-- Time parameters
local duration1 = 0.015 -- Time for the ball to reach the first target
local duration2 = 0.05 -- Time for the ball to reach the second target (slower)
local steps = 15 -- Number of frames for each bounce

-- Bezier curve function: B(t) = (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2
local function bezierPoint(t, p0, p1, p2)
	local u = 1 - t
	local tt = t * t
	local uu = u * u
	return (uu * p0) + (2 * u * t * p1) + (tt * p2)
end

-- Function to move the ball along the Bezier curve
local function moveBallAlongCurve(startPos, controlPos, endPos, duration)
	for i = 0, steps do
		local t = i / steps -- Time progression
		local bezierPos = bezierPoint(t, startPos, controlPos, endPos) -- Calculate Bezier curve position

		-- Move the ball using CFrame to the calculated position
		ball.CFrame = CFrame.new(bezierPos)

		-- Wait for the next step (duration / steps gives time per step)
		wait(duration / steps)
	end
end

-- Function to launch the ball with two bounces
local function launchBall()
	-- Reset transparency to 0 (visible)
	ball.Transparency = 0

	-- First bounce: move towards targetPoint1 with the first control point height
	local controlPoint1 = Vector3.new((startPoint.X + targetPoint1.X) / 2, controlPointHeight1, (startPoint.Z + targetPoint1.Z) / 2)
	moveBallAlongCurve(startPoint, controlPoint1, targetPoint1, duration1)

	-- Second bounce: move towards targetPoint2 with the second control point height and slower speed
	local controlPoint2 = Vector3.new((targetPoint1.X + targetPoint2.X) / 2, controlPointHeight2, (targetPoint1.Z + targetPoint2.Z) / 2)
	moveBallAlongCurve(targetPoint1, controlPoint2, targetPoint2, duration2)

	-- After the second bounce, fade out the ball by setting transparency to 1
	ball.Transparency = 1
end

-- Launch the ball every 2.5 seconds
while task.wait(2.5) do
	launchBall()
end

Yeah if your going for something that looks a bit more realistic then i would say trying to calculate the the horizontal and vertical speed given the end, start, and peak would be your best bet as it would look closer to how an actually tenis ball might might behave. but if your looking for a solution thats less complicated i would say just using bezier curves is the best way as there are tons of open source bezier curve modules out there that do all the behind the scenes math stuff for you. the only problem is that the speed of the ball would be the same throughout the whole curve so it might look a little unrealistic. was there something wrong with this code or an error with it?

Yeah, This is the way. No overshooting problems or anything. the ball now has the intended behavior, TYSM.

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