Spherecast Being Inconsistent?

Hello DevForum Community,

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.

Thank you in advance!

3 Likes

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

3 Likes

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.

1 Like

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.

hey! I’ve fixed your problem, at

local predictedPos = spherecastOrigin + velocity * dt + Vector3.yAxis * 1.1

as you can see, I added a yAxis and made it 1.1 studs, which in turn shouldn’t affect gameplay alot. It now doesn’t phase through.