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: