Projectile Hit Detection?

Hey, so I’ve been working on a projectile system and I’ve been having trouble with hit detection. I don’t want to use Region3 or .Touched since they’re either exploitable/ non-performant. I’ve stuck with Raycasts as they’re fairly inexpensive in short distances (afaik), one thing is that I have no clue on how to properly detect the object detected by this method.

Current code to get a detected object:


local function Check(C, Arrow)
	-- C is Character
	if Arrow and Arrow:IsDescendantOf(workspace) then
		local LinearVelocity = Arrow:FindFirstChild("LinearVelocity")
		local ArrowOrigin = Arrow.Position
		local ArrowCFrame = Arrow.CFrame
		local LookVector = ArrowCFrame.LookVector
		local ArrowDirection = LookVector*2
		--
		local Params = RaycastParams.new()
		Params.FilterType = Enum.RaycastFilterType.Blacklist
		Params.FilterDescendantsInstances = {C, Arrow}
		local Raycast = workspace:Raycast(ArrowOrigin, ArrowDirection, Params)

		if Raycast then
			return Raycast
		else
			task.wait()
			LinearVelocity.VectorVelocity = LinearVelocity.VectorVelocity - Vector3.new(0,.4,0)
			Check(C, Arrow)
		end
	end
end
1 Like

I think you could try using ClientCast for it. As it also uses Raycasting and is more reliable rather than the Touched Events & Region3.

1 Like

I’ve looked at all these modules that are a wrapper for a Melee raycasting system, and I can just say that using them in this method wouldn’t be a very good idea at all, as I’m cloning a new Arrow everytime, instead of just one Melee itself. I really don’t think that would be any faster than using Region3 at that point unless it manages to compute much, much faster.

Then i assume you could try using Workspace:GetPartsInPart(), That’s the best thing i’ve found so far.

You should use run service. Every frame put a raycast between the projectile position of the previous frame and the current position. If you detect something then do whatever the projectile is supposed to do. If not then let it keep flying.

If the projectile is quite big you may need to put attachments on it and track all the attachments

Yes, that is what I am currently doing, as task.wait() has comparable speeds to RunService.Heartbeat:Wait(), It’s raycasting, but I don’t know the math on how to get it to raycast properly and detect. Also, it’s fairly small and isn’t very large.

I’m considering using this method, but I want to try using Raycasts instead as it’ll be a bit quicker to detect as the Arrow won’t have to be inside of the actual thing detected.

Then you can try using it plus what @lightningstrike30451 also said. Create a Connection and use RunService.Heartbeat to get the Parts, Put them inside a temporary table to avoid possible damage repetitions. Then you can just pretty much do whatever you want with the Parts, Aftermath, Clear the table and Connection.

1 Like

So, Positions are vectors. Set the origin of the raycast to the previous position, and then the direction to the current position - previous position. Vector subtraction dictates that this creates a vector that goes from previous to current(when you position it at the previous position)
vector

1 Like

I tried doing this, and it didn’t seem to change anything, do I have to .Unit*2 it or is there something I’m missing?

local function Check(C, Arrow, ArrowDirection)
	-- C is Character
	if Arrow and Arrow:IsDescendantOf(workspace) then
		local LinearVelocity = Arrow:FindFirstChild("LinearVelocity")
		local ArrowOrigin = Arrow.Position
		--
		local Params = RaycastParams.new()
		Params.FilterType = Enum.RaycastFilterType.Blacklist
		Params.FilterDescendantsInstances = {C, Arrow}
		if not ArrowDirection then
			ArrowDirection = ArrowOrigin
			ArrowDirection = (ArrowOrigin-ArrowDirection)
		else
			ArrowDirection = (ArrowOrigin-ArrowDirection)
		end
		local Raycast = workspace:Raycast(ArrowOrigin, ArrowDirection, Params)

		if Raycast then
			return Raycast
		else
			task.wait()
			LinearVelocity.VectorVelocity = LinearVelocity.VectorVelocity - Vector3.new(0,.4,0)
			Check(C, Arrow, ArrowDirection)
		end
	end
end

Edit: I messed up, hold on lol

If you’re asking about how to do the raycasts reliably, here’s a method I commonly use:

  1. Keep the arrow anchored, with CanCollide, CanTouch, and CanQuery set to false. We won’t use the arrow’s velocity to change its position, and instead do things through code.
  2. Once the arrow is fired, we spawn it into the world with a velocity. This velocity will not be modified by the default physics engine because the the arrow is anchored.
  3. Every physics step, we do a raycast to check and see if something’s been hit. If not, wait for the next physics step.

Here’s a code sample of how you would do something like this. Please let me know about any potential questions you have!

local RunService = game:GetService("RunService")
local arrowTemplate: BasePart = script.Arrow

local function applyPhysics(velocity: Vector3, deltaTime: number): (Vector3)
	local newVelocity: Vector3 = velocity * (1 - 0.05 * deltaTime)
	-- the above line applies drag to the object
	-- drag is represented by '0.05'. The arrow will lose 5% of its speed every second
	-- feel free to change/remove the drag!
	newVelocity += Vector3.new(0, -workspace.Gravity * deltaTime, 0)
	-- the above line will apply gravity to the object, similar to the physics engine
	return newVelocity
end

local function spawnArrow(character: Model, position: Vector3, velocity: Vector3): (RaycastResult?)
	local arrow: BasePart = arrowTemplate:Clone()
	arrow.CFrame = CFrame.new(position, position + velocity)  -- the second parameter makes the arrow face the direction it's going
	arrow.Velocity = velocity -- we will change this value manually

	-- these are the RaycastParams for the raycast
	local params: RaycastParams = RaycastParams.new()
	params.FilterType = Enum.RaycastFilterType.Blacklist
	params.FilterDescendantsInstances = {character, arrow}

	-- loop while the arrow isn't in the void.
	-- add extra parameters if you'd like!

	local deltaTime: number = 0.017 -- the first step value
	-- deltaTime will also be set by RunService.Stepped, but we want to cast right now instead of waiting for the next physics step

	while arrow.Position.Y > workspace.FallenPartsDestroyHeight do
		local result: RaycastResult = workspace:Raycast(arrow.Position, arrow.Velocity * deltaTime)
		-- multiplying the velocity by 'deltaTime' returns the distance travelled by the arrow
		-- in 'deltaTime' amount of time (seconds)

		if result then
			-- we hit something!
			-- return the RaycastResult
			-- if you'd like to know what you hit, do 'result.Instance'
			return result
		else
			-- we didn't hit something
			-- lets move the arrow, and modify its velocity as well
			-- the following order is important!
			arrow.Position += arrow.Velocity * deltaTime
			arrow.Velocity = applyPhysics(arrow.Velocity, deltaTime)
			-- ...and a lazy way of rotating the arrow
			arrow.CFrame = CFrame.new(arrow.Position, arrow.Position + arrow.Velocity)
			-- finally, we go back to the start of the loop and wait for the next physics step
			deltaTime = RunService.Stepped:Wait()
		end
	end

	-- the 'while' loop exited without hitting something
	return nil
end

There are more efficient ways of doing this, but the above code sample is easier to understand. If you’d like the efficient option, I can send you the Lua file I’ve written a few weeks ago.

7 Likes

You should most definitely use raycasts.
I wouldn’t recommend ClientCast for this as it isn’t tailored for ranged hitboxes like bullets, you should still use a simple raycast operation.
Some tips would be to:

  1. Create a RaycastParams object when the bullet is created
  2. In a RunService loop, calculate the bullets next position, and raycast from the bullets current position towards the next.
  3. If there was something hit, stop the bullet and deal damage.
  4. If nothing was hit, move the bullet towards that new position.

EDIT: the answer is in the post above, but to clarify as to why Region3 is worse: it’s much more expensive, it’s axis-locked (ie, no rotation support), and it does not offer any additional data such as surface normal.

5 Likes