Bullet Hell Projectiles

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.

1 Like

Have you considered using FastCast? It’s a pretty nice library for simulating bullet physics and hit detection.

Huh, I have not. It seems like a very helpful library, and I will check it out! I will reply again after checking it out

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?

Here is an example of what I mean:


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.

1 Like

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.

2 Likes

I see, thank you so much for the help! I will post a short snippet of my code later on for anybody else who wants to see how I implemented it

2 Likes

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.
image

1 Like

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:
image
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.
image

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