I’m working on a volleyball-like game in Roblox where precise ball physics is crucial. I’ve implemented a custom physics system for a ball using Spherecast for collision detection. The goal is to have complete control over ball physics for smooth client-side replication.
What I want to achieve:
Create a robust ball physics system with accurate and consistent collision detection.
The issue:
Randomly, the Spherecast fails to detect collisions with the ground, causing the ball to phase through it momentarily before correcting its position. This issue is sporadic and can occur on any collision. Here’s a gif showing the problem.
Solutions Tried:
I’ve experimented with different Spherecast parameters, but the inconsistency persists. I suspect the issue might be related to the Heartbeat event’s timing in collision checks.
Here’s the relevant code snippet:
function BallRender:Step(dt : number)
self._velocity += self._gravity * dt
local radius = self._ball.Size.Y / 2
local spherecastOrigin = self._ball.Position
local spherecastRadius = radius
local spherecastDirection = self._velocity * dt
local spherecastParams = RaycastParams.new()
spherecastParams.FilterType = Enum.RaycastFilterType.Exclude
spherecastParams.FilterDescendantsInstances = {self._ball}
local spherecastResult = workspace:Spherecast(spherecastOrigin, spherecastRadius, spherecastDirection, spherecastParams)
if spherecastResult then
local hitNormal = spherecastResult.Normal
local hitPoint = spherecastResult.Position
self._ball.Position = hitPoint + hitNormal * radius
local normalVelocityComponent = self._velocity:Dot(hitNormal) * hitNormal
local tangentVelocityComponent = self._velocity - normalVelocityComponent
normalVelocityComponent *= -self._elasticity
tangentVelocityComponent *= self._friction
self._velocity = normalVelocityComponent + tangentVelocityComponent
self._angularVelocity = Vector3.new(tangentVelocityComponent.Z, 0, -tangentVelocityComponent.X) * 0.1
self._ball.RotVelocity = self._angularVelocity
else
self._ball.Position += self._velocity * dt
self._ball.RotVelocity = self._angularVelocity
end
end
Has anyone experienced similar issues or have suggestions on how to improve Spherecast’s reliability for this purpose? I’m looking for insights or alternative approaches that might help in achieving smoother and more consistent physics for the ball.
If I’m correct I think this is an issue with shapecasts, as I was having issues using them for a custom character controller where sometimes no collision would be detected at certain angles. Also make sure that your raycast goes towards where the ball will be next frame as that will account for frame drops where sometimes no collision can be detected. You could also use print statements to see when collisions aren’t and are being detected
Thanks for your input! I’ve tried implementing your suggestion of predicting the ball’s movement for the Spherecast, accounting for potential frame drops. Despite this, I still encounter random instances where collisions (even with walls, perfect ground, or slopes) are missed. It seems the issue isn’t just with the Spherecast direction but also with the inconsistency of collision detection itself. Sometimes it registers correctly; other times, it completely misses without any apparent reason. I’ve also incorporated print statements for debugging, but they only confirm the erratic nature of the collision detection. I suspect it might be related to running these calculations within the Heartbeat event, but I’m uncertain about the best way to optimize this. Any further advice on making collision detection more consistent, especially under the constraints of Heartbeat, would be highly valuable
The way shapecasting is implemented (based on raycasting), the ray won’t detect a part if it starts inside of it. With shapecasts, that means the ball won’t detect an obstacle it intersects with, even if it’s in the field of detection.
I’ve recently drawn a small diagram about the blockcasting nuances in another thread.
Place a block or a sphere precisely flat on the baseplate and shapecast downwards. Then lift it a fracion of a stud upwards, try again, and see the results. In the former case you shouldn’t hit anything.
I infer spheres are even slightly more difficult because, of course, no sphere model is a true perfect globe, but rather a polygonal model/mesh.
Given that the ball position is not physics based, what if you try including GetPartsInPart(), perhaps supplementing what you have? Or maybe you could base your calculations entirely on GetPartsInPart() with the query radius slightly bigger than that of the ball.
Thanks for your suggestions regarding collision detection. I attempted both methods you proposed but faced challenges in achieving satisfactory results. With GetPartsInPart , I struggled to accurately determine collision normals and hit points. Originally, my goal was to create a smooth and responsive ball physics system for a competitive game. I wanted to avoid the limitations of Roblox’s network ownership and physics system, as outlined in my previous post here. However, simulating my own physics is proving to be quite complex and may be overkill. I’m re-evaluating my approach and would appreciate any further insights or alternatives that might simplify this process !
Messed around with your original code and got it working. I think this is definitely a problem with shape cast’s and ray casts not detecting for certain directions as whenever I had my ball bounce off a slope once it started to slow down it would eventually just phase through because no ray cast was being detected even though my version of the code made sure that the ray cast or the ball cannot phase through. If your curious here’s the code I used:
local RunService = game:GetService("RunService")
local ball = script.Parent
local baseplate = workspace.BasePlate
local velocity = Vector3.zero
local gravity = Vector3.new(0, -10, 0)
local skinWidth = .015
local friction = .9
local elasticity = .7
function Step(dt : number)
velocity += gravity * dt
local radius = ball.Size.Y / 2
local spherecastOrigin = ball.Position
local spherecastRadius = radius
local spherecastDir = velocity.Unit
local spherecastDist = velocity.Magnitude + skinWidth
local predictedPos = spherecastOrigin + velocity * dt
local spherecastParams = RaycastParams.new()
spherecastParams.FilterType = Enum.RaycastFilterType.Exclude
spherecastParams.FilterDescendantsInstances = {ball}
local spherecastResult = workspace:Spherecast(spherecastOrigin, spherecastRadius - skinWidth, spherecastDir * spherecastDist, spherecastParams)
--[[if spherecastResult then
print(spherecastResult.Distance - skinWidth, (predictedPos - spherecastOrigin).Magnitude)
end]]
local pointBall = Vector3.new(ball.Position.X, ball.Position.Y - radius, ball.Position.Z)
local pointPart = Vector3.new(ball.Position.X, baseplate.Position.Y + baseplate.Size.Y/2, ball.Position.Z)
local collisionDistance = (pointBall - pointPart).Y
if collisionDistance < 0 then
print("phased through")
end
if spherecastResult then
print(tick())
local rayDistance = spherecastResult.Distance - skinWidth
local distanceToFuture = (predictedPos - spherecastOrigin).Magnitude
if distanceToFuture >= rayDistance then
local hitNormal = spherecastResult.Normal
local hitPoint = spherecastResult.Position
ball.Position = hitPoint + hitNormal * radius
local normalVelocityComponent = velocity:Dot(hitNormal) * hitNormal
local tangentVelocityComponent = velocity - normalVelocityComponent
normalVelocityComponent *= -elasticity
tangentVelocityComponent *= friction
velocity = normalVelocityComponent + tangentVelocityComponent
--self._angularVelocity = Vector3.new(tangentVelocityComponent.Z, 0, -tangentVelocityComponent.X) * 0.1
--self._ball.RotVelocity = self._angularVelocity
else
ball.Position += velocity * dt
end
else
ball.Position += velocity * dt
--self._ball.RotVelocity = self._angularVelocity
end
local part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.CanQuery = false
part.CanTouch = false
part.Shape = Enum.PartType.Ball
part.Size = Vector3.new(radius, radius, radius)*2
part.Transparency = .5
part.BrickColor = BrickColor.Blue()
part.Position = ball.Position + velocity * dt
part.Parent = workspace
task.delay(.01, function()
part:Destroy()
end)
end
RunService.Heartbeat:Connect(Step)
Hello, Thanks for you time on the subject, what you seems it’s pretty interresting however i tried your code and i was still able to get some “phased” issue as you can see here: https://gyazo.com/66c94ad1ec726beff8d3d3aeeac3e94f However even if the physics was working perfectly i kinda wonder if this is the ways for what i was looking for orginally, as i mentioned in my previous anwer or in this post here. I would really like other point of view on the subject cause i just would like a nice and smooth ball/soccer experience for all players point of view but from what i’ve been trying it’s really hard to achieve.
I’m more interested in this issue as a whole because this is a problem with shape casting and I’ve experimented with custom physics in the past. I actually got no phasing by instead using get parts in bounds towards the future position finding the closest point on the part hit and raycast towards it to get the normal and handle collision.I can’t send the code now as I’m on vacation but for advice on syncing the ball I would heavily recommend this GDC talk on how rocket league does it: https://youtu.be/ueEmiDM94IE?si=2mIdjKqtzZiISIsT. You will still have minor desyncs but that’s prone to happen and using the new unreliable remote event could help you achieve something better than what default roblox does.