Raycast Projectile Simplification

Hi. I was wondering if there was anything I could do to simplify this even more. Do I have anything unnecessary? What should I change/add?

I want the most simple way possible to fire and render a projectile. I’m not worried about running anything on the server yet. This is all done locally on the client.

local tool = script.Parent
local player = game.Players.LocalPlayer
local mouse = player:GetMouse()

local raycastParams = RaycastParams.new()
raycastParams.FilterDescendantsInstances = {}
raycastParams.FilterType = Enum.RaycastFilterType.Blacklist

function createProjectileObject()
	local bullet = Instance.new('BoxHandleAdornment')
	bullet.Color3 = Color3.new(1, 0.832349, 0.473213)
	bullet.Adornee = workspace
	bullet.Parent = workspace
	
	return bullet
end

local projectiles = {}

function step(dt)
	for index,projectile in projectiles do
		local timeLength = (tick()-projectile.startTime)
		projectile.currentPos = projectile.startPos + (projectile.direction*timeLength)
	
		local raycastResult = workspace:Raycast(projectile.lastPos,projectile.currentPos-projectile.lastPos,raycastParams)
		
		if raycastResult then
			projectile.hit = raycastResult.Instance
			projectile.currentPos = raycastResult.Position
		end	
		
		projectile.projectileObject.Size = Vector3.new(.1,.1,(projectile.lastPos-projectile.currentPos).Magnitude)
		projectile.projectileObject.CFrame = CFrame.new(projectile.currentPos,projectile.lastPos)*CFrame.new(0,0, -(projectile.lastPos-projectile.currentPos).Magnitude/2)
		
		if raycastResult then
			table.remove(projectiles,index)
		end
		
		projectile.lastPos = projectile.currentPos		
	end
end

game:GetService('RunService').RenderStepped:Connect(step)

function activated()
	local startPos = tool.Handle.CFrame.Position
	
	local muzzleVelocity = 200
	local direction = CFrame.new(tool.Handle.CFrame.Position,mouse.Hit.p)

	direction = direction.LookVector
	direction = direction * muzzleVelocity

	local projectile = {
		lastPos = startPos,
		startPos = startPos,
		startTime = tick(),
		projectileObject = createProjectileObject(),
		direction = direction,
		currentPos = Vector3.new(),

	}

	table.insert(projectiles,projectile)
end

tool.Activated:Connect(activated)

You’re creating a new Color3 instance on every function call. This is redundant, consider creating the color once, storing it as a variable, and reusing it instead.


You’re doing the math twice. Finding the magnitude is an expensive operation because it involves the use of square roots. You only need to calculate it once, store it, and then reuse it. This kind of optimization is called common subexpression elimination.


Since you’re using generalized iteration and not ipairs, it should be fine to have gaps inside the table, which means you can just set the index to nil instead of using table.remove, which can be expensive as it has to reorder the entire table.
Something like this:

if raycastResult then projectiles[index] = nil end

Small nitpick, there are vector constants! Use Vector3.zero instead of creating a new one.

1 Like

Much appreciation! I ripped this from some other raycasting tutorials. I’m concern about these lines and wonder if “timeLength” is necessary. Is it the most effective way of getting the direction? And in this example it doesn’t appear to take advantage of the delta time?

		local timeLength = (tick()-projectile.startTime)
		projectile.currentPos = projectile.startPos + (projectile.direction*timeLength)

Is this the change you suggested?

		local direction = (projectile.lastPos-projectile.currentPos).Magnitude
		projectile.projectileObject.Size = Vector3.new(.2,.2,direction)
		projectile.projectileObject.CFrame = CFrame.new(projectile.currentPos,projectile.lastPos)*CFrame.new(0,0, -direction/2)

Thank you so much!

Yes it is. And an additional note, it appears that there’s more room for improvement.

The latter part, CFrame.new(0,0, -direction/2), is simply just offsetting the bullet position to move it forward. But this can be done without creating a new CFrame, because the direction is already known.

If you remove the .Magnitude, this line will just find the displacement between the two vectors, which is the actual direction! We can just add this displacement to the new position before orientating it to look at lastPos.

local cPos, lPos = projectile.currentPos, projectile.lastPos
local disp = cPos - lPos --displacement
local dist = disp.Magnitude --distance
local obj = projectile.projectileObject --alias the object so it's easier to work with
obj.Size = Vector3.new(.2, .2, dist)
obj.CFrame = CFrame.lookAt(cPos + disp, lPos + disp) --we add the displacement to the position

I’m actually not sure if the displacement is going in the right direction, so when you test it if the bullet appears to go backward you can just flip the sign around and have it subtract the displacement instead.

CFrame.lookAt(cPos + disp, lPos + disp) --add
CFrame.lookAt(cPos - disp, lPos - disp) --subtract

Thank you so much for all the help. I feel bad asking so many questions so please don’t feel obligated to respond if you don’t want to. I really appreciate everything you’ve shown me already.

Just to be sure, is this how it’s intended to be setup? And sorry, I don’t see a comment regarding the dt?

function step(dt)
	for index,projectile in projectiles do
		local timeLength = (tick()-projectile.startTime)
		projectile.currentPos = projectile.startPos + (projectile.direction*timeLength)
		
		local cPos, lPos = projectile.currentPos, projectile.lastPos
		local disp = cPos - lPos
		local dist = disp.Magnitude
		local obj = projectile.projectileObject
		
		local raycastResult = workspace:Raycast(lPos,cPos-lPos,raycastParams)

		if raycastResult then
			projectile.hit = raycastResult.Instance
			projectile.currentPos = raycastResult.Position
		end	
		
		
		obj.Size = Vector3.new(.2, .2, dist)
		obj.CFrame = CFrame.lookAt(cPos - disp, lPos - disp)
		
		if raycastResult then
			projectiles[index] = nil
		end

		projectile.lastPos = cPos
	end
end

Some projectiles are stopping before making direct contact with the wall.

dt should be used in place of timeLength. dt is an accurate measurement of the time interval between the raycast steps. You multiply that to the projectile direction and its speed to find where it should be at the next raycast step.

Could you send me the place file so I can investigate it?

The projectile’s visuals probably haven’t gone to their most latest position besides the raycast.

obj.CFrame = CFrame.lookAt(cPos - disp, lPos - disp)

cPos looks like it hasn’t been updated to the raycast position if it hit, I think replacing cPos with projectile.currentPos will solve the visual “delay”.

Tachiyen Projectile Help.rbxl (33.4 KB)

Alright, I’ve made some changes, have a look.
Tachiyen Projectile Help.rbxl (33.5 KB)
ScreenShot_20221124125502

function step(dt)
	for index,projectile in projectiles do
		
		projectile.currentPos += projectile.direction*dt
		
		local cPos, lPos = projectile.currentPos, projectile.lastPos
		local disp = cPos - lPos
		local halfDisp = disp * .5
		local dist = disp.Magnitude
		local obj = projectile.projectileObject
		
		local raycastResult = workspace:Raycast(lPos, disp, raycastParams)	
		if raycastResult then
			projectile.hit = raycastResult.Instance
			projectiles[index] = nil
		end
		
		obj.Size = Vector3.new(.2, .2, dist)
		
		obj.CFrame = CFrame.lookAt(lPos, cPos) + halfDisp
		projectile.lastPos = projectile.currentPos
	end
end
1 Like

Thank you again! I have couple more questions.

What exactly is going on here? What should I look up on the wiki to write like this?

local projectiles: {[number]: projectile} = {}

Unfortunately a new issue is occurring where the bullets will stretch past the hit position…


That’s just Luau type annotations. The simplest explanation for it is that you’re telling the linter (the script analysis) exactly what to expect so that it can help you code and help you avoid mistakes. I’m using it to give the projectile objects autofill capabilities.
image

You can learn more here: Type checking - Luau

You can fix it by updating the currentPosition and everything else after the raycast hits.

		local raycastResult = workspace:Raycast(lPos, disp, raycastParams)	
		if raycastResult then
			projectile.hit = raycastResult.Instance
			projectiles[index] = nil
			
			--add this:
			cPos = raycastResult.Position
			disp = cPos - lPos
			halfDisp = disp * .5
			dist = disp.Magnitude
		end

ScreenShot_20221124131709

1 Like

Everything is working exactly as I wanted! Thank you so much! :grin:

I owe you!