Hello! I am creating bullet-hell-like attacks, and I am not sure what I should use for moving and detecting collision for the bullets. The bullets will be travelling in a straight line unaffected by physics.
I am currently planning to move the bullet’s CFrame with RunService, and use a .Touched event to detect collision, but please tell me if there are more accurate and/or performant ways to do projectiles.
I managed to get it working. However, I want the bullets to have an actual hitbox instead of being a ray. Is there any way to do that with the API, and if there isn’t should I just use FastCast to move the bullets?
In that case, since your bullets don’t actually use physics, you could relatively easily implement your own version of raycast hit detection using shapecasts. A shapecast is basically a raycast just that you’re checking for collisions in a radius defined by a given shape. Using raycasts or shapecasts is better than relying on .Touched events as .Touched wont trigger if a quickly moving bullets body passes through an object entirely from one frame to the next.
Thank you for the response! In that case, if I am using Shapecast, what would the most performant method of moving the projectile be? Would simply updating the CFrame with RunService be performant?
I’m pretty sure using .RenderStepped in addition to updating your bullets CFrame directly should work fine performance wise. The only other way to move your bullets I can come up with is using BodyMovers but I’m not quite sure if that would really result in performance gains. If you decide to go with the CFrame route, it’s better to only have a single .RenderStepped event and to create a pool of bullets that all get updated in the same function call. Regarding hit detection you can probably get away with only shapecasting every few frames to reduce the number of casts per second. Shapecasts are relatively computationally expensive so it’s good to optimise their usage.
Here is a simplified version of my code, I am using a client-sided script for better accuracy and less strain on the server:
Example script
local RUN_SERVICE = game:GetService("RunService")
local REP_STORAGE = game:GetService("ReplicatedStorage")
local bulletSize = 1
local bullet = REP_STORAGE.Bullet:Clone()
bullet.Parent = workspace
local velocity = Vector3.new(0,0,-15)
local duration = 5
local timeElapsed = 0
local frameCycle = 0
local framesPerCast = 5
local prevPos = bullet.Position
local function CheckForCollision()
local currentPos = bullet.Position
--Check for intersecting parts
local intersectCheck = workspace:GetPartBoundsInRadius(bullet.Position, bulletSize)
if(#intersectCheck > 0) then
prevPos = currentPos
return intersectCheck[1]
end
--Spherecast
local sphereCast = workspace:Spherecast(currentPos, bulletSize, prevPos - currentPos)
prevPos = currentPos
if(sphereCast) then
return sphereCast.Instance
end
end
RUN_SERVICE.Heartbeat:Connect(function(dT)
if(frameCycle == 0) then
local check = CheckForCollision()
if(check) then
--A part was hit
end
end
if(timeElapsed > duration) then
--Bullet's lifetime has finished
end
bullet.CFrame *= CFrame.new(velocity * dT)
frameCycle = (frameCycle + 1) % framesPerCast
timeElapsed += dT
end)
Extra notes:
Sorry if the script is messy, I just made a quick prototype to understand how I could implement it
You can change out GetPartBoundsInRadius for GetPartBoundsInBox or GetPartsInPart
You can change out ShapeCast for SphereCast or BlockCast
The script is a server script, so if you can use a client sided script with RenderStepped to be more accurate
You may also want to change the interval between each ShapeCast to seconds rather than frames, so that running on high or low FPS would not affect the accuracy as much
I would recommend creating a class for the bullets, so that creating new bullets would not be as clunky
Is the issue of shapecasts not detecting parts initially intersecting a problem? I’ve been trying to make some sort of projectile system that relies on shapecasts, but I’ve had some issues with projectiles not detecting hits.
Edit:
After looking through your code, I’m noticing you have shapecasts implemented in a way that is different from what the comment says. Right now, it is shooting a shapecast from the part and in the opposite direction of the last frame.
Oh! Thank you for noticing that! Yeah, that’s a mistake. Just flip the previousPos and currentPos in the ShapeCast to fix it
You might also want to put the framceCycle += 1 line after the if statement checking it, so that a ShapeCast is done on the very first frame
Edit: I will probably try to make a ModuleScript to make it more streamlined, and post it in a couple of days, so please tel me if there are any more issues in the initial script
Here’s I meant by the ShapeCast not checking initially intersecting parts:
The gray part will not be checked since it’s intersecting the original shape. (the documentation doesn’t say anything about it, but you can refer to SphereCasts for info on how they function.)
When the projectiles are fast, this won’t be a problem since the checks are long enough for it to not matter. When projectiles are slower/bigger, you might run into some issues. I noticed that you implemented functionality for changing acceleration. If a projectile were to slow down, the previousPos and currentPos would be too close for the projectile to detect collisions.
This will shoot the shape cast backwards, which means that the projectile can’t detect collisions from the front.
Oh, you’re right, I misunderstood what you meant. I will try to see if I could work on a fix for the already intersecting parts and slow projectile problem
My solution is to use a :GetPartBoundsInBox before doing the ShapeCast. Here’s how it looks like:
if(frameCycle == 0) then
local currentCFrame = newBullet.CFrame
local intersectCheck = workspace:GetPartBoundsInBox(currentCFrame, BULLET_SIZE, OVERLAP_PARAMS)
if(#intersectCheck > 0) then --If there is a part intersecting the intial position
partHit = intersectCheck[1]
Cleanup()
else --No part intersects the initial position
local currentPos = currentCFrame.Position
local castResult = workspace:Shapecast(newBullet, currentPos - previousPos, params.CastParams)
if(castResult and castResult.Instance) then
partHit = castResult.Instance
Cleanup()
end
previousPos = currentPos
end
end
You can also change it out for a GetPartBoundsInRadius or GetPartsInPart