How to make reliable collision detection

I am trying to anchor a bullet when it hits something to see how reliable the collision detection is.

the issue is that it’s not as reliable as I hoped. anchoring the bullet way before or way later than it should.

roblox’s touched event isn’t reliable at all, so I’m using rays in order to detect collisions, drawing a ray right in front of the bullet, I have tried this with look vector and attachments in front of the bullet both getting the same exact results.

current code:

local connection
bullet = script.Parent

connection = game:GetService("RunService").Heartbeat:Connect(function()
	local ray = Ray.new(bullet.Position, bullet.RayAttachment.WorldPosition)
	local hitPart, hitPos = workspace:FindPartOnRay(ray)

	if hitPart and hitPart.CanCollide and hitPart ~= bullet then
		print(hitPart)	
		bullet.Anchored = true
		connection:Disconnect()
	end
end)

code with LookVector ray

local connection
bullet = script.Parent

connection = game:GetService("RunService").Heartbeat:Connect(function()
	local ray = Ray.new(bullet.Position, bullet.CFrame.LookVector+Vector3.new(0,0,-2))
	local hitPart, hitPos = workspace:FindPartOnRay(ray)

	if hitPart and hitPart.CanCollide and hitPart ~= bullet then
		print(hitPart)	
		bullet.Anchored = true
		connection:Disconnect()
	end
end)

both with the same results

attachment:
image
I have tried putting the attachment further and even making attachments (each with its
own ray) around the bullet to form a sphere-like hitbox since when the collision of the bullet is detected too late it would flip around a lot.

here you will see the bullets detecting either too late and flipping around or too early.

2 Likes

I’m fairly certain there’s no way to do it with the physics system, there’s just no way to get the exact position of the hit. You can however use raycasts to see if the bullet is about to hit something, and then when the Touched event fires you can simple move the bullet to be at the last detected hit location.

Another approach is to entirely ignore the physics system and instead do your own physics simulation to get the bullet to move in a ballistic trajectory, and do raycasts along the way to get an exact hit location and have full control over the movement that way.

I see, I’ve got a little inspired by the Wii tank game and tiny tanks on roblox, where collisions with bullets work just fine. I’m trying to achieve just that but have no clue how, but tiny tanks made it some how possible.

1 Like

It’s definitely possible to get good bullet mechanics, but IMO that’s just not something the physics system can do well. Here’s a super simple simulation to get you started:

local bullets = {}

function newBullet(p0: Vector3, v0: Vector3)
    local bullet = {Position = p0, Velocity = v0, Model = game.ReplicatedStorage.Bullet:Clone()}
    updateBullet(bullet, 0)
    table.insert(bullets, bullet)
end

function destroyBullet(bullet)

end

function updateBullet(bullet, dt)
    bullet.Velocity += Vector3.new(0, -game.Workspace.Gravity, 0) * dt    
    
    local raycastResult = game.Workspace:Raycast(bullet.Position, bullet.Velocity * dt, bulletRaycastParams)
    if raycastResult then
         bullet.Position = raycastResult.Position
    else
         bullet.Position += bullet.Velocity * dt
    end
    
    bullet.Model:SetPrimaryPartCFrame(CFrame.new(bullet.Position, bullet.Position + bullet.Velocity))
   
    if raycastResult then
        bulletHit(bullet, raycastResult)
    end
end

function bulletHit(bullet, raycastResult)
    --Deal damage, make bullet hole, whatever else
    destroyBullet(bullet)
end

game:GetService("RunService").Stepped:Connect(function(_, dt)
    for _, bullet in ipairs(bullets) do
        updateBullet(bullet, dt)
    end
end)

This is meant to run all on the server, but you might want to not have any visuals handled on the server, like the actual bullet model, and instead send a signal to all players that a bullet has been fired with whatever parameters, and then recreate a purely visual bullet on each client that gets simulated every RenderStepped instead, to make the bullet appear to move more smoothly.

1 Like

that’s a possible solution, only RS.Stepped is only client-side, not that I couldn’t change that to heartbeat instead. but for me it doesn’t change much else from what i have now:

local connection
bullet = script.Parent

connection = game:GetService("RunService").Heartbeat:Connect(function()
	local ray = Ray.new(bullet.Position, bullet.RayAttachment.WorldPosition)
	local hitPart, hitPos = workspace:FindPartOnRay(ray)
	
	local ray2 = Ray.new(bullet.Position, bullet.RayAttachment2.WorldPosition)
	local hitPart2, hitPos2 = workspace:FindPartOnRay(ray2)
	
	local ray3 = Ray.new(bullet.Position, bullet.RayAttachment3.WorldPosition)
	local hitPart3, hitPos3 = workspace:FindPartOnRay(ray3)
	
	local ray4 = Ray.new(bullet.Position, bullet.RayAttachment4.WorldPosition)
	local hitPart4, hitPos4 = workspace:FindPartOnRay(ray4)
	
	local ray5 = Ray.new(bullet.Position, bullet.RayAttachment5.WorldPosition)
	local hitPart5, hitPos5 = workspace:FindPartOnRay(ray5)
	
	if hitPart and hitPart.CanCollide and hitPart ~= bullet then
		hit(hitPart)
	end
	
	if hitPart2 and hitPart2.CanCollide and hitPart2 ~= bullet then
		hit(hitPart2)
	end
	
	if hitPart3 and hitPart3.CanCollide and hitPart3 ~= bullet then
		hit(hitPart3)
	end
	
	if hitPart4 and hitPart4.CanCollide and hitPart4 ~= bullet then
		hit(hitPart4)
	end
	
	if hitPart5 and hitPart5.CanCollide and hitPart5 ~= bullet then
		hit(hitPart5)
	end
end)

function hit(hitPart)
	print(hitPart)	
	bullet.Anchored = true
	connection:Disconnect()
end

image
image

No, both HeartBeat and Stepped work on both the server and client. RenderStepped is client only.

Here’s a screenshot of how it behaves once you implement the logic for destroying a bullet:

function destroyBullet(bullet)
	for i, _bullet in ipairs(bullets) do
		if _bullet == bullet then
			table.remove(bullets, i)
			break
		end
	end
end

The red transparent parts are just a trail to show the different positions it ends up at. As you cansee, the bullet ends up perfectly embedded in the part it hit, without “bouncing back” before the Touched event has time to fire so that your script can react by setting Anchored to true. Since we have full-ish control over how the bullet moves, we can ensure that it doesn’t bounce before hitting. It seems to work every time:

Here's the full script to get that to work:
local TagS = game:GetService("CollectionService")

local bullets = {}
bulletRaycastParams = RaycastParams.new()
bulletRaycastParams.FilterType = Enum.RaycastFilterType.Blacklist

function newBullet(p0: Vector3, v0: Vector3)
	local bullet = {Position = p0, Velocity = v0, Model = game.ReplicatedStorage.Bullet:Clone()}
	bullet.Model:SetPrimaryPartCFrame(CFrame.new(bullet.Position, bullet.Position + bullet.Velocity))
	bullet.Model.Parent = game.Workspace
	table.insert(bullets, bullet)
end

function destroyBullet(bullet)
	for i, _bullet in ipairs(bullets) do
		if _bullet == bullet then
			table.remove(bullets, i)
			break
		end
	end
end

function updateBullet(bullet, dt)	
	local ghostBullet = bullet.Model:Clone()
	ghostBullet.PrimaryPart.Transparency = 0.4
	ghostBullet.Parent = game.Workspace
	
	bullet.Velocity += Vector3.new(0, -game.Workspace.Gravity, 0) * dt    

	local raycastResult = game.Workspace:Raycast(bullet.Position, bullet.Velocity * dt, bulletRaycastParams)
	if raycastResult then
		bullet.Position = raycastResult.Position
	else
		bullet.Position += bullet.Velocity * dt
	end
	
	bullet.Model:SetPrimaryPartCFrame(CFrame.new(bullet.Position, bullet.Position + bullet.Velocity))

	if raycastResult then
		bulletHit(bullet, raycastResult)
	end
end

function bulletHit(bullet, raycastResult)
	print(raycastResult.Instance)
	--Deal damage, make bullet hole, whatever else
	bullet.Model.PrimaryPart.Color = Color3.fromHSV(.5, 1, 1)
	destroyBullet(bullet)
end

game:GetService("RunService").Stepped:Connect(function(_, dt)
	for _, bullet in ipairs(bullets) do
		updateBullet(bullet, dt)
	end
end)

local gun = game.Workspace.Gun
local muzzleSpeed = 3000
while wait(0.75) do
	newBullet(
		gun.Position,
		(gun.CFrame * CFrame.Angles(
			math.random(-100,100)/1000,
			math.random(-100,100)/1000,
			0
		)).LookVector * muzzleSpeed
	)
	bulletRaycastParams.FilterDescendantsInstances = TagS:GetTagged("BulletIgnore")
end
5 Likes