Ricocheting while rotating around a point

local ProjectileHandler = {}

local function ReturnDecimal(range)
return Random.new():NextNumber(-range, range)
end

local function Lerp(a, b, t)
return a + (b - a) * t
end

local DamageEnemy = game.ReplicatedStorage.RemoteEvents.DamageEnemy

function ProjectileHandler.Shoot(Properties, Viewmodel)
if Properties.Type == “Projectile” then
local Angle = 0
local AngleDirectionToTravel = 1

	local RaycastParameters = RaycastParams.new()
	RaycastParameters.FilterType = Enum.RaycastFilterType.Exclude
	RaycastParameters.RespectCanCollide = true
	if Properties.IsPlayerShooting then
		RaycastParameters.FilterDescendantsInstances = {game.Players.LocalPlayer.Character, Viewmodel}
	end
	
	local Bullet = Instance.new("Part")
	Bullet:SetAttribute("TimesRicocheted", 0)
	Bullet:SetAttribute("EnemiesHit", 0)
	Bullet:SetAttribute("FollowsGravity", Properties.FollowsGravity)
	Bullet:SetAttribute("BulletVelocity", Vector3.new(Properties.Direction.X + ReturnDecimal(Properties.MaxInaccuracy), Properties.Direction.Y + ReturnDecimal(Properties.MaxInaccuracy), Properties.Direction.Z + ReturnDecimal(Properties.MaxInaccuracy)) * Properties.Speed)
	Bullet.Name = Properties.Name or "Bullet"
	Bullet.Size = Properties.Size or Vector3.new(1, 1, 1)
	Bullet.Color = Properties.Color or Color3.new(0, 0, 0)
	Bullet.Material = Properties.Material or Enum.Material.SmoothPlastic
	Bullet.CFrame = Properties.Origin
	Bullet.Parent = workspace
	Bullet.CollisionGroup = "Extra"
	game.CollectionService:AddTag(Properties.ExtraType)
	Bullet.CastShadow = false
	Bullet.AssemblyLinearVelocity = Bullet:GetAttribute("BulletVelocity")
	
	game.Debris:AddItem(Bullet, Properties.LifeTime)
	
	local Stepped = game:GetService("RunService").PreRender:Connect(function()
		local BulletVelocity = Bullet:GetAttribute("BulletVelocity")
		local CharacterCollisionRay = workspace:Blockcast(Bullet.CFrame, Bullet.Size, Bullet.AssemblyLinearVelocity.Unit * math.clamp(Properties.Speed / 25, 7, math.huge), RaycastParameters)
		
		if Bullet:GetAttribute("FollowsGravity") == false then
			Bullet.AssemblyLinearVelocity = BulletVelocity
		end
		Bullet.CFrame = CFrame.lookAt(Bullet.Position, Bullet.Position + Bullet.AssemblyLinearVelocity)

		for i, v in pairs(game.CollectionService:GetTagged("Magnet")) do
			if v:IsA("Part") and (v.Position - Bullet.Position).Magnitude < 10 then
				local SawCollisionRaycast = workspace:Raycast(Bullet.Position, Bullet.AssemblyLinearVelocity.Unit * 2, RaycastParameters)
				if Properties.MagneticType == "Swarm" then
					Bullet:SetAttribute("BulletVelocity", Lerp(BulletVelocity, (v.Position - Bullet.Position).Unit * Properties.Speed, 0.5))
				elseif Properties.MagneticType == "Circular" then
					Angle += (math.clamp(Properties.Speed / 900, 0, math.huge) * AngleDirectionToTravel)
					Angle = Angle % 360
					print(Angle)
					local NewX = v.Position.X + 8 * math.cos(Angle)
					local NewZ = v.Position.Z + 8 * math.sin(Angle)
					Bullet:SetAttribute("BulletVelocity", (Vector3.new(NewX, v.Position.Y, NewZ) - Bullet.Position).Unit * Properties.Speed / 2)
					
					if SawCollisionRaycast and SawCollisionRaycast.Instance then
						AngleDirectionToTravel *= -1
						print(SawCollisionRaycast.Instance.Name)
						wait()
					end
				end
			end
		end
		if CharacterCollisionRay and CharacterCollisionRay.Instance then
			if not CharacterCollisionRay.Instance.Parent:FindFirstChildOfClass("Humanoid") and Bullet:GetAttribute("TimesRicocheted") < Properties.MaxRicochets then
				Bullet:SetAttribute("TimesRicocheted", Bullet:GetAttribute("TimesRicocheted") + 1)
				Bullet.AssemblyLinearVelocity = Bullet.AssemblyLinearVelocity - (2 * Bullet.AssemblyLinearVelocity:Dot(CharacterCollisionRay.Normal) * CharacterCollisionRay.Normal)
				Bullet:SetAttribute("BulletVelocity", BulletVelocity - (2 * BulletVelocity:Dot(CharacterCollisionRay.Normal) * CharacterCollisionRay.Normal))
			else
				if Properties.IsPlayerShooting and CharacterCollisionRay.Instance.Parent == (game.Players.LocalPlayer.Character or Viewmodel) then return end
				
				if CharacterCollisionRay.Instance.Parent:FindFirstChild("Humanoid") and CharacterCollisionRay.Instance.Parent:FindFirstChild("Humanoid").Health > 0 then
					local HitHumanoid = CharacterCollisionRay.Instance.Parent:FindFirstChild("Humanoid")
					
					if Bullet:GetAttribute("EnemiesHit") and Bullet:GetAttribute("EnemiesHit") < Properties.MaxEnemiesToHit and not HitHumanoid:HasTag("BeenHit") then
						Bullet:SetAttribute("EnemiesHit", Bullet:GetAttribute("EnemiesHit") + 1)
						HitHumanoid:AddTag("BeenHit") 
						DamageEnemy:FireServer(HitHumanoid, Properties.Damage * ((Properties.LocationalDamage == true and CharacterCollisionRay.Instance:GetAttribute("WeakpointMultiplier") ~= nil) and CharacterCollisionRay.Instance:GetAttribute("WeakpointMultiplier") or 1))
						
						task.delay(0.75, function() HitHumanoid:RemoveTag("BeenHit") end)
						
						--Hit effects like explosions.

						for i = 1, Properties.ContinuousDamage.AmountOfLoops or 1 do
							Bullet.Anchored = true
							DamageEnemy:FireServer(HitHumanoid, Properties.ContinuousDamage.Damage * ((Properties.LocationalDamage == true and CharacterCollisionRay.Instance:GetAttribute("WeakpointMultiplier") ~= nil) and CharacterCollisionRay.Instance:GetAttribute("WeakpointMultiplier") or 1))
							task.wait(Properties.ContinuousDamage.WaitPerLoop)
						end
						Bullet.Anchored = false
						
						if Bullet:GetAttribute("FollowsGravity") == true and Properties.MaxEnemiesToHit > 1 and HitHumanoid.Health > 0 and Properties.ContinuousDamage.AmountOfLoops > 0 then
							Bullet.AssemblyLinearVelocity = Vector3.yAxis * Properties.Speed / 2
							Bullet:SetAttribute("TimesRicocheted", Properties.MaxRicochets)
						else Bullet:Destroy() end
						if Bullet:GetAttribute("EnemiesHit") >= Properties.MaxEnemiesToHit then Bullet:Destroy() end
					end
				elseif not (game.CollectionService:HasTag(CharacterCollisionRay.Instance, "Bullet") or CharacterCollisionRay.Instance.Name == "Bullet") then
					Bullet:Destroy()
				end
			end
		end
	end)
	
	Bullet.Destroying:Connect(function()
		Stepped:Disconnect()
	end)
	
	return Bullet
end

end

return ProjectileHandler

Basically i’m making a system which lets you create a bullet by sending a list of properties which the function uses. In properties, there are 3 ways bullets can interact with magnets. None: no interaction with magnets. Swarm: bullets swarm around the magnet until the magnet is destroyed and Circular: bullets spin around the magnet in a circle. what i want is for bullets to ricochet when colliding with walls and attracted by a magnet so they start rotating in the opposite direction, but i can never find the right way to detect collision. Ive tried raycasting and blockcasting, and even getting all objects tagged “Wall” and checking if the distance between the bullet and the object is less than 2, but no matter what i’ve tried it never works. is there something i’m missing?

For projectile collision detection your going to have to do something called Ray Marching,
Ray Marching Updating Object In Steps


This is basically casting multiple rays before the bullet can move to its next position, It does not use ROBLOX’s physics and is completely simulated on the client. Tools like FastCastRedux (Recommended) and Secure Cast use these methods,.

the only problem is of how we can replicate it to the client which can be done by sending a fire request to the server which fires all clients except you and are simulated on the clients and not the server for a smoother interpretation, though this can become very complex, but that’s why we have Secure Cast . unfortunately it does not compare to FastCast as it does not have many bullet customizations and is fairly just a frame work for people to work off of.

You can also look at this Post.

Here’s a preview of a simple bullet simulation using raymarching that I made for learning purposes, This could mean you’d have to revamp your system or implement FastCast.

function projectile:Update(delta:number)
	local gravity = projectile.Gravity
	local steps = projectile.Steps
	local quickness = self.Quickness or 1 -- Adjust physics speed with Quickness
	delta = (delta * quickness) / steps -- Adjust delta with steps and quickness

	local toRemove = {} -- Collect projectiles to remove after iteration

	for key, active_data in pairs(ActiveProjectiles) do
		-- Ensure required properties exist
		active_data.PathPoints = active_data.PathPoints or {}
		active_data.Velocity = active_data.Velocity or active_data.Object.CFrame.LookVector * active_data.Data.Properties.Speed

		-- Check lifespan or distance exceeded
		if isLifespanExceeded(active_data) or isDistanceExceeded(active_data) then
			--active_data.Data._hit:Fire(nil, nil, nil)
			table.insert(toRemove, key)
			continue
		end

		-- Multi-step simulation for accuracy
		for _ = 1, steps do
			-- Apply gravity to velocity
			active_data.Velocity = active_data.Velocity + gravity * delta
			
			local origin = active_data.Object.Position
			local direction = active_data.Velocity.Unit * (active_data.Velocity.Magnitude * delta)
			local removing = false

			-- Perform raycast
			local raycastResult = workspace:Raycast(origin, direction, RaycastParams.new {
				FilterType = Enum.RaycastFilterType.Exclude,
				FilterDescendantsInstances = active_data.Data.Properties.Ignore or {},
			})

			if raycastResult then
				local hit = raycastResult.Instance
				local position = raycastResult.Position
				local normal = raycastResult.Normal

				-- Handle collision with a humanoid
				if active_data.Data.Properties.Damage ~= 0 then
					local Min,Max = active_data.Data.Properties.Dropoff_Min, active_data.Data.Properties.Dropoff_Max
					
					local dropOff = math.clamp((active_data.Origin - position).Magnitude, Min, Max)
					dropOff = 1 - ( (dropOff - Min) / (Max - Min) )
					
					if dropOff > 0 then
						local humanoid = hit:FindFirstAncestorWhichIsA("Model") and hit:FindFirstAncestorWhichIsA("Model"):FindFirstChild("Humanoid")
						if humanoid then
							humanoid:TakeDamage(active_data.Data.Properties.Damage * dropOff)
						end
					end
				end

				active_data.Hit:Fire(hit, position, normal)
				table.insert(toRemove, key)
				
				removing = true
				break
			end

			-- Update position and path
			if not removing then
				local newPosition = origin + direction
				active_data.Object.CFrame = CFrame.new(newPosition, newPosition + active_data.Velocity.Unit)
				table.insert(active_data.PathPoints, newPosition)
			end
		end

		-- Draw the path if visualization is enabled
		if projectile.Visualization then
			Gizmo:DrawPath(active_data.PathPoints, false, 0.1)
		end
	end

	-- Remove projectiles after iteration to prevent table modification issues
	for _, key in ipairs(toRemove) do
		projectile:Remove(ActiveProjectiles[key])
		ActiveProjectiles[key] = nil
	end
end