What is the best way to create ball and shot control for a football game?

Hi guys, I’m creating a soccer game inspired by Super League Soccer. I love that game and would like to recreate the ball control and kicking mechanics!

I have two questions:

  • The first is about the best way to control the ball when the player has it so that it has the correct rotation, based on the direction the player is moving. I’m doing this with a 6D motor inside the ball, but I’m not sure if it’s the right way.

  • And the second is about the kicking, I want to have predictability about the ball’s trajectory (on the client). I even tried to create a Bézier curve that shows the trajectory before the kick, but when I try to move it around the server with a heartbeat it behaves very strangely :frowning:

I’m not looking for ready-made scripts, I really want to understand how these games create such fluid experiences within Roblox. I want to learn the best practices for this type of real-time update for all players.

Another test I did was using Body Velocity, but that way I can’t predict what the ball’s trajectory will be. Here is a video of the current state of gameplay using the Bézier curve to move the ball.

This code creates the trajectory using Beam and fires the event to the server with: initial, control and final positions.

function onShoot(actionName, inputState)
	if actionName ~= InputActionsEnum.OFFENSIVE.SHOOT then return end

	if inputState == Enum.UserInputState.Begin then
		holdDuration = 0
		height = 0

		createBeam()

		heartbeatConnection = RunService.Heartbeat:Connect(function(delta)
			holdDuration = holdDuration + delta

			controlPoint = (ballHolder.Position + shotAim.Position) / 2

			if holdDuration >= MAX_HOLD_DURATION.Value then return end

			local direction = humanoidRootPart.CFrame.LookVector

			shotAim.Position = shotAim.Position + direction * delta * SHOOT_POWER.Value

			controlPoint = (ballHolder.Position + shotAim.Position) / 2

			height = math.clamp((controlPoint - ballHolder.Position).Magnitude, 0, 10)

			if not beam then return end

			beam.CurveSize0 = height
			beam.CurveSize1 = -height
		end)
	elseif inputState == Enum.UserInputState.End then
		if not heartbeatConnection then return end
		heartbeatConnection:Disconnect()
		doKick(true)
	end
end

On the server, I process this event and move the ball

function onKicked(
	playerWhoKicked,
	startPosition,
	controlPosition,
	endPosition,
	percentage,
	isKickToGoal
)
	if ball == nil then return end

	player = nil
	humanoidRootPart = nil

	if isKickToGoal then
		MatchStatsServiceInstance:addShootToPlayer(playerWhoKicked)
	end

	local points = createCurve(startPosition, controlPosition, endPosition)

	ballDisabled = true
	ball.Motor.Part1 = nil
	ball:SetNetworkOwner(nil)

	local bodyPosition = Instance.new("BodyPosition")
	bodyPosition.Position = startPosition
	bodyPosition.MaxForce = Vector3.new(math.huge, math.huge, math.huge)
	bodyPosition.P = 50000
	bodyPosition.Parent = ball

	local currentStep = 0

	local heartbeatConnection
	heartbeatConnection = RunService.Heartbeat:Connect(function(delta)
		if currentStep < #points and ball then
			local nextPoint = points[currentStep + 1]

			local direction = (nextPoint - ball.Position).Unit
			local distanceToNextPoint = (nextPoint - ball.Position).Magnitude

			local movement = direction * 500 * delta
			bodyPosition.Position = ball.Position + movement

			if distanceToNextPoint > 0 then
				local lookAtPosition = ball.Position + direction
				ball.CFrame = CFrame.lookAt(ball.Position, lookAtPosition)
			end

			if distanceToNextPoint < 1 then
				currentStep = currentStep + 1
			end
		else
			heartbeatConnection:Disconnect()
			bodyPosition:Destroy()
			ballDisabled = false
		end
	end)

	local args = {
		eventType = BallEventTypesEnum.LOST
	}

	BallEvent:FireClient(playerWhoKicked, args)
end