Help me with my curve (football game)

i wanna achieve the same curve as Real Futbol 24, but i don’t seem to get it.
Server Script:

-- Remote event references
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local Workspace = game:GetService("Workspace")
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Players = game:GetService("Players")

-- Validate remote event paths
local eventsFolder = ReplicatedStorage:FindFirstChild("Folder") and ReplicatedStorage.Folder:FindFirstChild("Events")
if not eventsFolder or not eventsFolder:FindFirstChild("Shoot") then
	warn("[Shoot] Events folder or Shoot subfolder missing in ReplicatedStorage")
	return
end

local eventM1 = eventsFolder.Shoot:FindFirstChild("ShootM1")
if not eventM1 then
	warn("[Shoot] ShootM1 remote event missing")
	return
end

-- Module reference for vector visualization
local VectorViz
local success, err = pcall(function()
	VectorViz = require(ServerStorage:FindFirstChild("Modules"):FindFirstChild("ball"))
end)
if not success or not VectorViz then
	warn("[Shoot] Failed to require VectorViz module:", err)
	return
end

-- Require the kick velocity module
local KickVelocityModule = require(ReplicatedStorage.Folder.Modules:FindFirstChild("KickVelocityModule"))

-- Debounce table to prevent multiple triggers per player per event
local kickDebounce = {}

-- Utility: check if part is descendant of model
local function isDescendantOf(part, model)
	while part and part.Parent do
		if part.Parent == model then
			return true
		end
		part = part.Parent
	end
	return false
end

-- Utility: get all foot and lower leg parts of a character
local function getKickParts(character)
	local kickParts = {}
	local footNames = {["ShoeR"]=true, ["ShoeL"]=true, ["RightFoot"]=true, ["LeftFoot"]=true}
	local lowerLegNames = {["LowerLeg"]=true, ["RightLowerLeg"]=true, ["LeftLowerLeg"]=true, ["RightFoot"]=true, ["LeftFoot"]=true, ["ShoeR"]=true, ["ShoeL"]=true}
	for _, part in character:GetDescendants() do
		if part:IsA("BasePart") then
			if footNames[part.Name] or lowerLegNames[part.Name] then
				table.insert(kickParts, part)
			end
		end
	end
	return kickParts
end

-- Utility: check if part is a player's foot or lower leg
local function isPlayerFoot(part, character)
	if not part or not character then return false end
	local footNames = {["ShoeR"]=true, ["ShoeL"]=true, ["RightFoot"]=true, ["LeftFoot"]=true}
	if footNames[part.Name] and isDescendantOf(part, character) then
		return true
	end
	return false
end

-- Improved hitbox: check if hit part is close to any kick part (foot/lower leg)
local function isNearKickPart(hitPart, character, ball, maxDistance)
	if not hitPart or not character or not ball then return false end
	local kickParts = getKickParts(character)
	for _, kickPart in kickParts do
		if hitPart == kickPart then
			return true
		end
		-- If hit part is close enough to a kick part, allow
		local dist = (kickPart.Position - ball.Position).Magnitude
		if dist <= maxDistance then
			return true
		end
	end
	return false
end

-- ShootM1 event handler
eventM1.OnServerEvent:Connect(function(player, shootPower)
	-- Validate player and character
	local character = player and player.Character
	if not character then return end

	local humanoid = character:FindFirstChildOfClass("Humanoid")
	local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
	if not (humanoid and humanoidRootPart) then return end

	-- Play kick animation
	local animM1 = ReplicatedStorage:FindFirstChild("Folder") and ReplicatedStorage.Folder:FindFirstChild("Animations")
	animM1 = animM1 and animM1:FindFirstChild("Shoot") and animM1.Shoot:FindFirstChild("M1")
	animM1 = animM1 and animM1:FindFirstChild("R") and animM1.R:FindFirstChild("M1")
	if animM1 then
		local track = humanoid.Animator:LoadAnimation(animM1)
		track:Play()
		track.Stopped:Wait()
	end

	-- Find the ball
	local ball = Workspace:FindFirstChild("Ball")
	if not ball then return end

	-- Set network owner to player for initial kick
	ball:SetNetworkOwner(player)

	-- Debounce per player per event
	if kickDebounce[player] then return end
	kickDebounce[player] = true

	-- Temporary .Touched connection
	local touchedConn
	local kicked = false

	touchedConn = ball.Touched:Connect(function(hit)
		if kicked then return end
		-- Only allow the player's own foot or lower leg, or if hit part is close to a kick part
		local hitboxDistance = 1.5 -- studs, tweak for realism
		if isPlayerFoot(hit, character) or isNearKickPart(hit, character, ball, hitboxDistance) then
			kicked = true
			-- Clamp shootPower to prevent exploits
			local minPower = 10
			local maxPower = 50
			local clampedPower = math.clamp(tonumber(shootPower) or minPower, minPower, maxPower)

			-- Calculate shoot direction: use player's look vector in XZ plane
			local look = humanoidRootPart.CFrame.LookVector
			local shootDir = Vector3.new(look.X, 0, look.Z)
			if shootDir.Magnitude > 0 then
				shootDir = shootDir.Unit
			else
				shootDir = Vector3.new(0, 0, 1)
			end

			-- Calculate curve force based on player's lateral position relative to the ball
			local playerToBall = ball.Position - humanoidRootPart.Position
			playerToBall = Vector3.new(playerToBall.X, 0, playerToBall.Z)
			local lateralOffset = 0
			if playerToBall.Magnitude > 0.01 then
				-- Perpendicular direction in XZ plane: (-z, 0, x)
				local perp = Vector3.new(-shootDir.Z, 0, shootDir.X)
				lateralOffset = perp.Unit:Dot(playerToBall.Unit)
				-- lateralOffset is positive if player is left of shootDir, negative if right
				-- Scale by distance for realism, clamp to [-1, 1]
				lateralOffset = math.clamp(lateralOffset * playerToBall.Magnitude / 3, -1, 1)
			end

			-- Only allow curve if player is extremely to the side of the ball
			local curveForce = 0
			local lateralThreshold = 0.4 -- slightly more extreme for realism
			local curveAmount = 0.55 -- finesse: reduce curve globally for more realistic finesse effect
			if math.abs(lateralOffset) > lateralThreshold then
				-- Sharply ramp up curve as player is further to the side
				local ramp = (math.abs(lateralOffset) - lateralThreshold) / (1 - lateralThreshold)
				curveForce = math.sign(lateralOffset) * ramp * curveAmount * (clampedPower / maxPower) * 1.5 -- reduced multiplier for realism
			end

			-- If power is low, do a ground curve (no vertical velocity)
			local groundCurveThreshold = 18
			local isGroundCurve = clampedPower <= groundCurveThreshold

			-- Apply shoot velocity (curve logic handled in module)
			KickVelocityModule.applyShootVelocity(ball, player, humanoidRootPart, clampedPower, VectorViz, shootDir, curveForce, curveAmount)

			-- Disconnect after kick
			if touchedConn then
				touchedConn:Disconnect()
			end
			kickDebounce[player] = nil
		end
	end)

	-- Timeout: disconnect after 0.5s if not triggered
	task.delay(0.5, function()
		if not kicked then
			if touchedConn then
				touchedConn:Disconnect()
			end
			kickDebounce[player] = nil
		end
	end)
end)

Module:

-- KickVelocityModule
-- Provides functions to apply kick and shoot velocities to a ball and visualize air resistance

local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Debris = game:GetService("Debris")

local module = {}

-- Power bounds for shooting (should match server)
local minPower = 10
local maxPower = 50
-- Calculate maxUpwardVelocity for 10 studs: v = sqrt(2 * g * h)
local gravity = workspace.Gravity or 196.2
local maxArcHeight = 10 -- studs
local maxUpwardVelocity = math.sqrt(2 * gravity * maxArcHeight) -- ≈ 19.8 for 10 studs

--[[
    applyKickVelocity(ball, player, humanoidRootPart, horizontalVelocity, verticalVelocity, VectorViz)
    - ball: MeshPart (the soccer ball)
    - player: Player (the player kicking)
    - humanoidRootPart: Part (the player's HumanoidRootPart)
    - horizontalVelocity: number (velocity along look vector)
    - verticalVelocity: number (velocity upwards)
    - VectorViz: Module (visualization module)
--]]
function module.applyKickVelocity(ball, player, humanoidRootPart, horizontalVelocity, verticalVelocity, VectorViz)
	if not (ball and player and humanoidRootPart and VectorViz) then return end
	assert(typeof(ball) == "Instance" and ball:IsA("MeshPart"), "Start: Ball isn't a meshpart")

	-- Unanchor and set network owner to server for physics
	ball:SetNetworkOwner(nil)
	if ball.Anchored then
		ball.Anchored = false
	end

	-- Calculate and apply velocity
	local vel = humanoidRootPart.CFrame.LookVector * horizontalVelocity + Vector3.new(0, verticalVelocity, 0)
	ball.AssemblyLinearVelocity += vel

	-- Visualize air resistance
	local vizKey = "AirResistanceKick" .. player.UserId
	VectorViz:CreateVisualiser(vizKey, ball.Position, Vector3.zero, {
		Colour = Color3.new(0, 0, 255),
		Width = 0.1,
		Scale = 1.5
	})

	local connectionKick
	connectionKick = RunService.Stepped:Connect(function()
		if not ball or not ball.Parent then
			if connectionKick then connectionKick:Disconnect() end
			return
		end
		local AirResistance = -ball.AssemblyLinearVelocity * math.exp(ball.AssemblyLinearVelocity.Magnitude ^ 2 / 2750)
		VectorViz:UpdateBeam(vizKey, ball.Position, AirResistance)
	end)

	task.wait(0.21)

	if connectionKick then connectionKick:Disconnect() end
	VectorViz:DestroyVisualiser(vizKey)
end

--[[
    applyShootVelocity(ball, player, humanoidRootPart, shootPower, VectorViz, shootDirection, curveForce, curveAmount)
    - ball: MeshPart (the soccer ball)
    - player: Player (the player shooting)
    - humanoidRootPart: Part (the player's HumanoidRootPart)
    - shootPower: number (forward velocity magnitude)
    - VectorViz: Module (visualization module)
    - shootDirection: Vector3 (unit vector for shoot direction in XZ plane)
    - curveForce: number (amount of curve, positive = left, negative = right)
    - curveAmount: number (multiplier for curve effect)
--]]
function module.applyShootVelocity(ball, player, humanoidRootPart, shootPower, VectorViz, shootDirection, curveForce, curveAmount)
	if not (ball and player and humanoidRootPart and VectorViz) then return end
	assert(typeof(ball) == "Instance" and ball:IsA("MeshPart"), "Start: Ball isn't a meshpart")

	-- Unanchor and set network owner to server for physics
	ball:SetNetworkOwner(nil)
	if ball.Anchored then
		ball.Anchored = false
	end

	-- Use provided shootDirection (unit vector in XZ plane) or fallback to look vector
	local forwardDir
	if shootDirection and shootDirection.Magnitude > 0.01 then
		forwardDir = Vector3.new(shootDirection.X, 0, shootDirection.Z)
		if forwardDir.Magnitude > 0 then
			forwardDir = forwardDir.Unit
		else
			forwardDir = Vector3.new(humanoidRootPart.CFrame.LookVector.X, 0, humanoidRootPart.CFrame.LookVector.Z).Unit
		end
	else
		forwardDir = Vector3.new(humanoidRootPart.CFrame.LookVector.X, 0, humanoidRootPart.CFrame.LookVector.Z).Unit
	end

	-- Calculate upward velocity: 0 at minPower, maxUpwardVelocity at maxPower (linear for direct mapping to height)
	local clampedPower = math.clamp(shootPower, minPower, maxPower)
	local powerAlpha = (clampedPower - minPower) / (maxPower - minPower)
	local verticalVelocity = powerAlpha * maxUpwardVelocity

	-- Calculate curve vector in XZ plane (perpendicular to forwardDir)
	local curve = curveForce or 0
	local curveMultiplier = curveAmount or 1

	-- Decrease forward force by multiplying by a factor (e.g., 0.65)
	local forwardForceScale = 0.65
	local velocity = forwardDir * (clampedPower * forwardForceScale) + Vector3.new(0, verticalVelocity, 0)
	ball.AssemblyLinearVelocity += velocity

	-- Visualize the curve (trajectory) and air resistance
	local vizKey = "AirResistanceShoot" .. player.UserId
	VectorViz:CreateVisualiser(vizKey, ball.Position, Vector3.zero, {
		Colour = Color3.new(1, 0.5, 0), -- Orange for shoot
		Width = 0.13,
		Scale = 2
	})

	-- Apply Magnus effect using BodyAngularVelocity and BodyForce for a short duration
	if math.abs(curve) > 0.01 then
		local curveDuration = 0.52 -- slightly longer for smooth finesse effect

		-- Calculate spin axis (Y axis for left/right curve)
		local spinAxis = Vector3.new(0, -math.sign(curve), 0) -- negative for right, positive for left

		-- Angular velocity magnitude: scale with curve and power and curveAmount
		local angularSpeed = math.abs(curve) * 6.5 * curveMultiplier + 2.5 -- reduced for realism

		local bav = Instance.new("BodyAngularVelocity")
		bav.AngularVelocity = spinAxis * angularSpeed
		bav.MaxTorque = Vector3.new(0, 1e5, 0)
		bav.P = 1e4
		bav.Parent = ball

		-- Magnus force: F = S * (v x w)
		local magnusConstant = 0.08 * curveMultiplier -- reduced for realism
		local v = ball.AssemblyLinearVelocity
		local w = bav.AngularVelocity
		local magnusForce = magnusConstant * v:Cross(w)

		local bf = Instance.new("BodyForce")
		bf.Force = magnusForce
		bf.Parent = ball

		Debris:AddItem(bav, curveDuration)
		Debris:AddItem(bf, curveDuration)
	end

	local connectionShoot
	connectionShoot = RunService.Stepped:Connect(function()
		if not ball or not ball.Parent then
			if connectionShoot then connectionShoot:Disconnect() end
			return
		end
		local AirResistance = -ball.AssemblyLinearVelocity * math.exp(ball.AssemblyLinearVelocity.Magnitude ^ 2 / 2750)
		VectorViz:UpdateBeam(vizKey, ball.Position, AirResistance)
	end)

	task.wait(0.28)

	if connectionShoot then connectionShoot:Disconnect() end
	VectorViz:DestroyVisualiser(vizKey)
end

return module

In my game:

Real Futbol 24:

3 Likes
-- RealisticBallPhysics
-- Handles real-time, realistic soccer ball physics (enhanced realism)

local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

local ball = workspace.Ball

local connections = {}

-- Physics parameters (tweak for realism)
local AIR_RESISTANCE_COEFF = 0.01 -- Linear drag coefficient (base)
local AIR_RESISTANCE_QUAD = 0.003 -- Quadratic drag coefficient (increases with speed)
local MIN_VELOCITY = 0.5 -- Threshold to stop the ball
local BOUNCE_DAMPING = 0.82 -- How much velocity is kept after bounce (0-1, higher = more bounce)
local FRICTION = 0.98 -- Friction applied when ball is on ground (horizontal only)
local GROUND_CHECK_DIST = 0.8 -- Distance to check for ground contact

-- Optional: Spin/curve parameters
local SPIN_DAMPING = 0.98
local MAGNUS_COEFF = 0.15 -- Magnus effect strength (curve)
local ROLLING_FRICTION = 0.96 -- Rolling friction when on ground
local SPIN_TRANSFER = 0.12 -- How much spin is transferred to linear velocity on ground

-- Deformation parameters
local DEFORMATION_DURATION = 0.18 -- seconds to squash or restore
local MIN_DEFORMATION = 0.88 -- minimum Y scale (squash)
local MAX_DEFORMATION = 1.16 -- maximum XZ scale (stretch)
local RESTORE_SPEED = 8 -- how fast to restore scale

-- Micro-bounce/jitter parameters
local MICRO_BOUNCE_CHANCE = 0.18 -- Chance per bounce to add a micro-bounce
local MICRO_BOUNCE_STRENGTH = 1.5 -- Max Y velocity added for micro-bounce

-- Sleep/wake system
local SLEEP_VELOCITY = 0.12 -- Below this, ball can sleep
local SLEEP_TIME = 1.2 -- Seconds below threshold before sleeping
local WAKE_TOUCH_FORCE = 2 -- If force > this, wake up

-- Sound parameters
local BOUNCE_SOUND_ID = "rbxassetid://9118828562" -- Example bounce sound
local BOUNCE_VOLUME = 0.5

-- State for deformation
local deforming = false
local deformPhase = nil -- "squash" or "restore"
local deformTarget = Vector3.new(1, 1, 1)
local deformStart = Vector3.new(1, 1, 1)
local deformTime = 0

-- Store the original size of the ball for proper scaling
local originalSize = ball.Size

-- Sleep state
local sleepTimer = 0
local isSleeping = false

-- Sound setup
local bounceSound = Instance.new("Sound")
bounceSound.SoundId = "rbxassetid://9125510147"
bounceSound.Volume = BOUNCE_VOLUME
bounceSound.Name = "BounceSound"
bounceSound.Parent = ball

-- Utility: Raycast down to check if ball is on ground
local function isOnGround()
    local rayOrigin = ball.Position
    local rayDirection = Vector3.new(0, -GROUND_CHECK_DIST, 0)
    local params = RaycastParams.new()
    params.FilterDescendantsInstances = {ball}
    params.FilterType = Enum.RaycastFilterType.Exclude
    local result = workspace:Raycast(rayOrigin, rayDirection, params)
    return result ~= nil
end

-- Helper: Check if any player is currently using a tool (UsingTool == true)
local function anyPlayerUsingTool()
    local players = Players:GetPlayers()
    for i, player in players do
        local character = player.Character
        if character then
            local usingTool = character:FindFirstChild("UsingTool")
            if usingTool and usingTool:IsA("BoolValue") then
                if usingTool.Value == true then
                    return true
                end
            end
            -- Also check if any Tool is equipped in the character
            for j, child in character:GetChildren() do
                if child:IsA("Tool") then
                    return true
                end
            end
        end
    end
    return false
end

-- Realistic rolling friction (applies only when on ground)
local function applyRollingFriction()
    if isOnGround() then
        local velocity = ball.AssemblyLinearVelocity
        -- Only apply to XZ plane (horizontal movement)
        ball.AssemblyLinearVelocity = Vector3.new(
            velocity.X * ROLLING_FRICTION,
            velocity.Y,
            velocity.Z * ROLLING_FRICTION
        )
    end
end

-- Magnus effect (curve ball due to spin)
local function applyMagnusEffect(dt)
    local velocity = ball.AssemblyLinearVelocity
    local spin = ball.AssemblyAngularVelocity
    -- Magnus force is proportional to cross(spin, velocity)
    local magnus = spin:Cross(velocity) * MAGNUS_COEFF * dt
    ball.AssemblyLinearVelocity = velocity + magnus
end

-- Ball deformation (visual squash/stretch on impact)
local function applyDeformationOnImpact(impactVelocity)
    -- Only deform if impact is significant
    if math.abs(impactVelocity.Y) > 1 then
        -- Squash Y, stretch XZ based on impact velocity (clamped)
        local impactStrength = math.clamp(math.abs(impactVelocity.Y) / 40, 0, 1)
        local squashY = 1 - (1 - MIN_DEFORMATION) * impactStrength
        local stretchXZ = 1 + (MAX_DEFORMATION - 1) * impactStrength
        deformStart = Vector3.new(1, 1, 1)
        deformTarget = Vector3.new(stretchXZ, squashY, stretchXZ)
        deformTime = 0
        deforming = true
        deformPhase = "squash"
        -- Do NOT instantly set ball.Size; let the Heartbeat loop handle smooth squash
    end
end

-- Ball sound effects (kick, bounce)
local function playBallSound(eventType, velocity)
    if eventType == "bounce" then
        bounceSound.Volume = BOUNCE_VOLUME * math.clamp((velocity and velocity.Magnitude or 1) / 30, 0.15, 1)
        bounceSound.PlaybackSpeed = 0.95 + math.random() * 0.15
        bounceSound:Play()
    end
end

-- Sleep/wake logic
local function trySleep(dt)
    local v = ball.AssemblyLinearVelocity.Magnitude
    local w = ball.AssemblyAngularVelocity.Magnitude
    if v < SLEEP_VELOCITY and w < SLEEP_VELOCITY then
        sleepTimer = sleepTimer + dt
        if sleepTimer > SLEEP_TIME and not isSleeping then
            isSleeping = true
            ball.AssemblyLinearVelocity = Vector3.new()
            ball.AssemblyAngularVelocity = Vector3.new()
        end
    else
        sleepTimer = 0
        if isSleeping then
            isSleeping = false
        end
    end
end

local function wakeUp(force)
    if isSleeping and (force and force > WAKE_TOUCH_FORCE or not force) then
        isSleeping = false
        sleepTimer = 0
    end
end

-- Micro-bounce/jitter for realism
local function maybeMicroBounce()
    if math.random() < MICRO_BOUNCE_CHANCE then
        local microY = (math.random() * 2 - 1) * MICRO_BOUNCE_STRENGTH
        ball.AssemblyLinearVelocity = ball.AssemblyLinearVelocity + Vector3.new(0, microY, 0)
    end
end

-- Spin transfer: some angular velocity is converted to linear velocity on ground
local function transferSpinToLinear()
    local spin = ball.AssemblyAngularVelocity
    local up = Vector3.new(0, 1, 0)
    local spinAxis = spin:Dot(up)
    if math.abs(spinAxis) > 0.1 then
        -- Only transfer if spinning around vertical axis
        local transfer = Vector3.new(-spin.Z, 0, spin.X) * SPIN_TRANSFER
        ball.AssemblyLinearVelocity = ball.AssemblyLinearVelocity + transfer
        -- Dampen spin a bit more
        ball.AssemblyAngularVelocity = ball.AssemblyAngularVelocity * 0.95
    end
end

-- Remove bounce logic from Touched event (handled in Heartbeat instead)
--[[
ball.Touched:Connect(function(hit)
    if not hit:IsA("BasePart") then return end
    local velocity = ball.AssemblyLinearVelocity
    -- Only bounce if moving downward fast enough
    if velocity.Y < -1 then
        -- Bounce: invert and dampen Y, apply friction to XZ
        ball.AssemblyLinearVelocity = Vector3.new(
            velocity.X * FRICTION,
            math.abs(velocity.Y) * BOUNCE_DAMPING,
            velocity.Z * FRICTION
        )
        applyDeformationOnImpact(velocity)
        playBallSound("bounce")
    elseif velocity.Magnitude > 0.1 then
        -- Only apply friction to X and Z, not Y
        ball.AssemblyLinearVelocity = Vector3.new(
            velocity.X * FRICTION,
            velocity.Y,
            velocity.Z * FRICTION
        )
    end
end)
]]

-- Track ground contact for bounce logic
local wasOnGround = false

-- Touch event for waking up the ball
table.insert(connections, ball.Touched:Connect(function(hit)
	if not hit:IsA("BasePart") then return end
	local force = (ball.AssemblyLinearVelocity - Vector3.new()).Magnitude
	wakeUp(force)
end))

table.insert(connections, RunService.Heartbeat:Connect(function(dt)
    if not ball or not ball.Parent then return end

    -- Sleep/wake logic
    trySleep(dt)
    if isSleeping then
        return
    end

    -- Air resistance (drag): Fd = -kv - kv^2
    local velocity = ball.AssemblyLinearVelocity
    local drag = -velocity * AIR_RESISTANCE_COEFF - velocity.Unit * (velocity.Magnitude^2) * AIR_RESISTANCE_QUAD
    if velocity.Magnitude > 0.01 then
        ball.AssemblyLinearVelocity = velocity + drag * dt
    end

    -- Dampen spin over time
    ball.AssemblyAngularVelocity = ball.AssemblyAngularVelocity * SPIN_DAMPING

    -- Apply rolling friction if on ground
    applyRollingFriction()

    -- Apply Magnus effect (curve) if ball is spinning and moving
    if ball.AssemblyAngularVelocity.Magnitude > 0.1 and ball.AssemblyLinearVelocity.Magnitude > 0.1 then
        applyMagnusEffect(dt)
    end

    -- Bounce logic: detect ground impact and bounce
    local onGround = isOnGround()
    local velocityNow = ball.AssemblyLinearVelocity
    if onGround and not wasOnGround then
        -- Just hit the ground this frame
        if velocityNow.Y < -1 then
            -- Bounce: invert and dampen Y, apply friction to XZ
            ball.AssemblyLinearVelocity = Vector3.new(
                velocityNow.X * FRICTION,
                math.abs(velocityNow.Y) * BOUNCE_DAMPING,
                velocityNow.Z * FRICTION
            )
            applyDeformationOnImpact(velocityNow)
            playBallSound("bounce", velocityNow)
            maybeMicroBounce()
            transferSpinToLinear()
        elseif velocityNow.Magnitude > 0.1 then
            -- Only apply friction to X and Z, not Y
            ball.AssemblyLinearVelocity = Vector3.new(
                velocityNow.X * FRICTION,
                velocityNow.Y,
                velocityNow.Z * FRICTION
            )
        end
    end
    wasOnGround = onGround

    -- Deformation restore logic (SMOOTH squash and restore)
    if deforming then
        deformTime = deformTime + dt
        if deformPhase == "squash" then
            local alpha = math.clamp(deformTime / DEFORMATION_DURATION, 0, 1)
            -- Lerp from deformStart (1,1,1) to deformTarget (squash/stretch)
            local currentScale = Vector3.new(
                deformStart.X + (deformTarget.X - deformStart.X) * alpha,
                deformStart.Y + (deformTarget.Y - deformStart.Y) * alpha,
                deformStart.Z + (deformTarget.Z - deformStart.Z) * alpha
            )
            ball.Size = Vector3.new(
                originalSize.X * currentScale.X,
                originalSize.Y * currentScale.Y,
                originalSize.Z * currentScale.Z
            )
            if alpha >= 1 then
                -- Start restoring to normal
                deformPhase = "restore"
                deformStart = deformTarget
                deformTarget = Vector3.new(1, 1, 1)
                deformTime = 0
            end
        elseif deformPhase == "restore" then
            local alpha = math.clamp(deformTime * RESTORE_SPEED / DEFORMATION_DURATION, 0, 1)
            -- Lerp from deformStart (squash/stretch) to (1,1,1)
            local currentScale = Vector3.new(
                deformStart.X + (deformTarget.X - deformStart.X) * alpha,
                deformStart.Y + (deformTarget.Y - deformStart.Y) * alpha,
                deformStart.Z + (deformTarget.Z - deformStart.Z) * alpha
            )
            ball.Size = Vector3.new(
                originalSize.X * currentScale.X,
                originalSize.Y * currentScale.Y,
                originalSize.Z * currentScale.Z
            )
            if alpha >= 1 then
                -- Fully restored
                ball.Size = originalSize
                deforming = false
                deformPhase = nil
            end
        end
    end
end))

supposedly the ball physics.

1 Like

wow, wow holy gatekeeping man…

2 Likes

Hey man, I don’t mean to be rude, but your code isn’t that simple. And just like you don’t know, there are many other people who don’t know either.

But putting that aside, what seems to be happening is that the ball doesn’t curve as sharply as expected. One thing you could try is adjusting the MAGNUS_COEFF , since that apparently defines how much the ball is going to curve.

If that doesn’t work, try adjusting some constants, since when you transfer real-life constants to Roblox, you have to take into account that the values might need to be changed.

Oh okay, i’ll try doing that then, thanks

Okay, I tried it, curve is great but how do i make it curve without losing accuracy in the shot, maybe you will understand if u watch this:

Hey! Something that Real Futbol 24 might be using are Bezier Curves. To implement these you would need to somehow first calculate where the ball will land, and from there get a point towards where the player is facing. Then, have an array of 3 points: The ball’s initial position, the mid-point (which will cause the ball to curve), and the landing position. Having those, you can create a function that given a parameter t with values ranging from 0 to 1, will return the position in which the ball should be. You might need some math knowledge to understand how this works.

Something you could also try is using LineForces, one of which would go towards the landing point. Maybe at first make a force aiming towards the point where the player is looking, and then gradually disable this force and make the one towards the point be the only one present. I haven’t tried this but it might work! Let me know :slight_smile:

1 Like