I am attempting to make an 8 ball game but am having issues having outcomes be the same between server and client. Right now, I have custom elastic collisions which works well on the client as you can see in the client versions of the balls (black/white). However, when the server balls are fired (red), they have different outcomes. Both of the simulations are running the exact same collision code math as well as the same sphere properties.
Server Code
local objects = workspace.ServerBalls:GetChildren()
local CollisionUtils = require(script.CollisionUtils)
game:GetService("RunService").Heartbeat:Connect(function()
for i=1, #objects-1 do
local part1 = objects[i]
local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Whitelist
for j=i+1, #objects do
local part2 = objects[j]
overlapParams.FilterDescendantsInstances = {part2}
if #workspace:GetPartsInPart(part1, overlapParams) == 1 then
local part1Mass = part1.Mass
local part2Mass = part2.Mass
local part1Velocity = Vector2.new(part1.AssemblyLinearVelocity.X, part1.AssemblyLinearVelocity.Z)
local part2Velocity = Vector2.new(part2.AssemblyLinearVelocity.X, part2.AssemblyLinearVelocity.Z)
local part1Position = Vector2.new(part1.Position.X, part1.Position.Z)
local part2Position = Vector2.new(part2.Position.X, part2.Position.Z)
local Velocity1, Velocity2 = CollisionUtils.calculateElasticCollisionVelocities(part1Velocity, part2Velocity, part1Position, part2Position, part1Mass, part2Mass)
local midpointX = (part1Position.X + part2Position.X) / 2
local midpointY = (part1Position.Y + part2Position.Y) / 2
local dist = math.sqrt((part1Position.X - part2Position.X)^2 + (part1Position.Y - part2Position.Y)^2)
local part1PositionX = midpointX + (part1.Size.X / 2) * (part1Position.X - part2Position.X) / dist
local part1PositionY = midpointY + (part1.Size.Y / 2) * (part1Position.Y - part2Position.Y) / dist
local part2PositionX = midpointX + (part2.Size.X / 2) * (part2Position.X - part1Position.X) / dist
local part2PositionY = midpointY + (part2.Size.Y / 2) * (part2Position.Y - part1Position.Y) / dist
part1.AssemblyLinearVelocity = Vector3.new(Velocity1.X, 0, Velocity1.Y)
part2.AssemblyLinearVelocity = Vector3.new(Velocity2.X, 0, Velocity2.Y)
part1.Position = Vector3.new(part1PositionX, part1.Position.Y, part1PositionY)
part2.Position = Vector3.new(part2PositionX, part2.Position.Y, part2PositionY)
end
end
end
end)
game.ReplicatedStorage.FireShot.OnServerEvent:Connect(function(player)
workspace.ServerBalls.CueBall:ApplyImpulse(Vector3.new(234138.36802486575,0,0))
end)
while true do
task.wait()
if game.Players:FindFirstChild("jakebball11") then
for _,v in workspace.ClientBalls:GetChildren() do
v:SetNetworkOwner(game.Players.jakebball11)
end
end
end
Client Code
local objects = workspace.ClientBalls:GetChildren()
local CollisionUtils = require(script.CollisionUtils)
game:GetService("RunService").Heartbeat:Connect(function()
for i=1, #objects-1 do
local part1 = objects[i]
local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Whitelist
for j=i+1, #objects do
local part2 = objects[j]
overlapParams.FilterDescendantsInstances = {part2}
if #workspace:GetPartsInPart(part1, overlapParams) == 1 then
local part1Mass = part1.Mass
local part2Mass = part2.Mass
local part1Velocity = Vector2.new(part1.AssemblyLinearVelocity.X, part1.AssemblyLinearVelocity.Z)
local part2Velocity = Vector2.new(part2.AssemblyLinearVelocity.X, part2.AssemblyLinearVelocity.Z)
local part1Position = Vector2.new(part1.Position.X, part1.Position.Z)
local part2Position = Vector2.new(part2.Position.X, part2.Position.Z)
local Velocity1, Velocity2 = CollisionUtils.calculateElasticCollisionVelocities(part1Velocity, part2Velocity, part1Position, part2Position, part1Mass, part2Mass)
local midpointX = (part1Position.X + part2Position.X) / 2
local midpointY = (part1Position.Y + part2Position.Y) / 2
local dist = math.sqrt((part1Position.X - part2Position.X)^2 + (part1Position.Y - part2Position.Y)^2)
local part1PositionX = midpointX + (part1.Size.X / 2) * (part1Position.X - part2Position.X) / dist
local part1PositionY = midpointY + (part1.Size.Y / 2) * (part1Position.Y - part2Position.Y) / dist
local part2PositionX = midpointX + (part2.Size.X / 2) * (part2Position.X - part1Position.X) / dist
local part2PositionY = midpointY + (part2.Size.Y / 2) * (part2Position.Y - part1Position.Y) / dist
part1.AssemblyLinearVelocity = Vector3.new(Velocity1.X, 0, Velocity1.Y)
part2.AssemblyLinearVelocity = Vector3.new(Velocity2.X, 0, Velocity2.Y)
part1.Position = Vector3.new(part1PositionX, part1.Position.Y, part1PositionY)
part2.Position = Vector3.new(part2PositionX, part2.Position.Y, part2PositionY)
end
end
end
end)
task.wait(4)
workspace.ClientBalls.CueBall:ApplyImpulse(Vector3.new(234138.36802486575,0,0))
task.wait(3)
game.ReplicatedStorage.FireShot:FireServer()
Collision Utils
local CollisionUtils = {}
function CollisionUtils.calculateElasticCollisionVelocities(v1, v2, pos1, pos2, mass1, mass2)
local numerator1 = (v1 - v2):Dot(pos1 - pos2)
local numerator2 = (v2 - v1):Dot(pos2 - pos1)
local denominator1 = (pos1 - pos2).Magnitude^2
local denominator2 = (pos2 - pos1).Magnitude^2
local newV1 = v1 - (2 * mass2 / (mass1 + mass2)) * (numerator1 / denominator1) * (pos1 - pos2)
local newV2 = v2 - (2 * mass1 / (mass1 + mass2)) * (numerator2 / denominator2) * (pos2 - pos1)
return newV1, newV2
end
return CollisionUtils