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.
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)
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.
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?
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”.
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
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.