Mass projectile system?

So ive got a new game idea that im leaning into, but my main concern is how I will achieve the biggest aspect of the gameplay. Im not going to go too into details but its a game where each player will be equipped with a different type of projectile weapon which have different stats such as fire rate, recoil, and knockback. These projectiles are not instant, and have different travel times.

Now that ive shared context, Id like to create a performant system where the server can handle hundreds of projectiles at a time, each with their own hitbox and predetermined paths. My main concern is how I would make this performant. I am 100% sure the best way to handle projectiles mechanically would be bezier curves, as those are predetermined paths that i can set for each projectile, which allow me to do things like reflecting projectiles or slowing them down. But, if there is a better option here then do let me know.

As for hitboxes, a few open source modules come to mind, but I havent tried any in an environment where theres a massive amount of projectiles being handled at once, so if there is a hitbox system that is reliable when it comes to accuracy and performance on this scale, then please share.

While Bezier curves are a good method for knowing the direction of a projectile, they don’t always make sense. If you’re firing horizontally, it’d act how you’d expect, but if you ever ever fired in a declined angle, it would travel backwards in some fashion.

Projectile motion

Projectile motion is something that’s been well-documented and explored by coders, mathematicians, and physics enthusiasts; so something called SUVAT exists. It’s a collection of equations that allow us to determine properties of a projectile in motion. It’s velocity, it’s distance from the origin, it’s speed, etc.

This is if you want a system that allows for drop. If you’re looking for something that will always fire a projectile linearly, you simply leave out the portion of the equation that deals with drop.

Lots of projectiles
So with a basic understanding of SUVAT under your belt, you’ll want to experiment with getting initially a single projectile that works. Get it running on both your client, and on your server. Theres a couple of different approaches to handling something like this, but the most common way is that each client creates it’s own projectile, and tells the server when it fired, where it fired from, and the direction its traveling. The server then takes this data, and sends it to other clients, who then take that data and make a fake projectile. The original client is then in charge of handling all the collision data; where did it hit, when did it hit, what did it hit?

While we like to think of our players as being nice and trustworthy people, theres always a few bad apples. Hackers can now take these collisions and spoof them, by saying “This projectile has interacted with this object at this point.”. The server then gets the hit confirmation and trusts it and says A-OK. But remember those SUVAT equations we were talking about? The client isn’t the only place they can be checked. A really good method of checking if a collision actually happened is by seeing if the collision occurred along the path of the projectile. If the collision didn’t happen anywhere on it’s path, then it’s likely it never happened at all. If the collision did happen, but the projectile is saying it happened at 0.1 seconds, instead of the 12 seconds it should have taken, then it’s likely that the player is either lagging, or is hacking. This is where something called Server Authority and Sanity Checks come in.

Sanity Checks/Server Authority
This is your hack detection essentially. As all data is being passed through the server, you can analyze it. The main point of a Sanity Check is to say whether or not something actually makes sense. As with the previous mentions, these mean that you can look at the data being given VS the data you should have gotten. It’ll take some tweaking, and will require a bit of understanding for what you’re looking to check, but it will result in a system thats slightly more difficult to manipulate.

Once the server says that it’s a-ok, then it’ll prompt the clients to do whatever collision needed to have happened at any given point. And if the projectile is rejected? Just prompt each client to delete the bullet.

Performance
This is slightly more tricky. There are loads of different performance tips for this. Some people use parts that always exist, but thousands and thousands of units outside of the playing area, because moving a part vs creating a part is more performant. Phantom Forces uses (used? It’s been a while) a UI elements to create them, to give them a very unique appearance.

In terms of collisions, while Roblox’s in-built Raycasting is great, it can dip here and there. It’s often a good idea to break up the detection calls by a few frames, or limit the amount of projectile collision checks you’re doing every frame. Once you start getting projectiles over longer distances and faster, these start getting quite taxing. You could also use Parallel Luau to approach this, but I’ve not yet messed with that.

If you’re looking for something that’s physics-based, SUVAT is the way to go to determine how each projectile should start. If you’re looking for something thats code-based, SUVAT is still the way to go, but will be incredibly useful on both client/server.

Oh and if you do code-based projectiles, try and make sure that all projectiles are moved using BulkMoveTo. It moves all objects that’re referenced within it at once, which is cheaper than updating individual object cframe properties.

Hope that’s useful, and do look into SUVAT! I made a projectile system that I’ve been running with for years and I didn’t realize how close to these equations I was!

1 Like

So if im understanding you correctly, SUVAT is basically a function library? Based off how you worded it I assume its not roblox specific but rather equations that can be used with any language. Could you share an example of how this could be useful with my use case? You said a collection of equations but that doesnt tell me much when it comes to handling projectiles.

And with bezier curves, the intention was to have a class based structure where each projectile object would have a property called “Path”, where I could control the speed of the projectile. The reasoning for this is because I plan on including special Abilities such as a mirror ability which lets you reflect projectiles based off their path, or a freeze ability which lets you stop them.

I appreciate the long and detailed explanation though

Yeah so SUVAT is a mathematical series of equations, basically.
S - Displacement (Distance)
U - Initial Velocity/Position
V - Final Velocity/Position
A - Acceleration
T - Time

Now, say we want to figure out how far a projectile is going to go if it doesn’t have any gravity affecting it, it’s a straight linear path with no acceleration.

The typical method I would use to find this in my projectile system (Bullets, no bullet drop or deceleration);
Current_Position = Original_Position + (Velocity * Time)
These variables allow me to do most of the sanity checks I’d need to do, as I know how much time has elapsed + the original position and the original direction of the bullet. It could also be written like this;
S_Current = S_Origin + (U * T)
That’s not an ‘official’ SUVAT equation, but it describes a projectiles path.

Have a look at this video which breaks down this topic;

Now thankfully, you largely don’t need to worry about breaking the initial velocity into its X, Y, Z components, because Roblox uses Vector’s to represent positions, velocity, rotation, etc. You’d only need to use them if you were attempting to determine having the projectile land at a certain point or reach a certain height.

In terms of reflecting/Freezing a bullet, using the original equation, you can just stop adding to the time variable. So, if a bullet freezes 5 seconds after launch and is frozen for another 5, S = S + U*T. All you have to do is stop adding to time (You’ll want to use Delta-time here, or the change in time from the start). Theres a whole host of things you can do with time, like reduce the amount of it per second.

Heres an early version of the projectile system I made using that equation, which lets me slow/stop projectiles and change their direction.

2 Likes

um yeah object pooling (someone made a script here but idk if its good) and parallel computing are gonna be your best friends for optimization

generally for optimizing performance, try and look for places where you can sacrifice memory to be able to do less calculating

1 Like

You call it “object pooling”, but yeah I already figured that spawning tons of parts would be an issue. Thankfully modules like ObjectCache and PartCache exist so that’s not much of an issue. What I am curious about is how parallel computing would help here. I have zero experience with it as its a pretty complicated thing to handle, so I don’t know exactly it would do for me in my use case.

I’m gonna have to experiment a bit more with this, but based off your explanations it doesn’t seem super complicated.

Though, if you could share like a full script for something like creating a projectile, that would help. To me it seems like your creating meshpart spheres and changing their velocity property once and then detect for collisions to change the velocity. Do correct me if I’m wrong though.

FastCast is an helpful free module

So I actually managed to use this and set up a basic system:

And so far, I see now that SUVAT is pretty much just basic physics. Super simple stuff.
Here’s some of the code:
Projectile Module on the server:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Types = require(ReplicatedStorage.GeneralTypes)

local Projectiles:{Types.Projectile} = {}
local ObjectCache = require(script.ObjectCache)

local cacheFolder = Instance.new("Folder")
cacheFolder.Name = "Cache"
cacheFolder.Parent = workspace
local TemplatePart = script.ProjectileTemplate
cache = ObjectCache.new(TemplatePart,100000,cacheFolder)

local ProjectileHandler = {}

local RS = game:GetService("RunService")

function ProjectileHandler:Activate()
	local function updateProjectiles()
		
		local CFrameList:{CFrame} = {}
		local PartList:{Part} = {}
		
		for _,Projectile in Projectiles do
			table.insert(PartList,Projectile.Instance)			
			local T = workspace:GetServerTimeNow() - Projectile.StartTime
			
			local CalculatedCFrame = CFrame.lookAt(Projectile.StartPosition + (Projectile.Velocty * T),Projectile.Instance.Position + Projectile.Velocty)

			table.insert(CFrameList,CalculatedCFrame)
		end
		
		workspace:BulkMoveTo(PartList,CFrameList)
	end
	
	RS.Heartbeat:Connect(updateProjectiles)
end

function ProjectileHandler:CreateProjectile(Player:Types.Player,Radius:number,Speed:number,Velocity:Vector3,StartPosition:Vector3):Types.Projectile
	local Part = cache:GetPart()
	Part.Anchored = true
	Part.CanCollide = false
	Part.Parent = workspace.Projectiles
	
	local Projectile:Types.Projectile = {
		Instance = Part,
		Radius = Radius,
		Speed = Speed,
		StartTime = workspace:GetServerTimeNow(),
		Velocty = Velocity,
		StartPosition = StartPosition
	}
	table.insert(Projectiles,Projectile)
	table.insert(Player.Projectiles,Projectile)
	return Projectile	
end





return ProjectileHandler

Projectile Fire:

local ProjectileFire= {}

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Types = require(ReplicatedStorage.GeneralTypes)
local SharedModules = ReplicatedStorage.SharedModules
local ProjectileHandler = require(SharedModules.ProjectileHandler)
local Utilities = ReplicatedStorage.Utilities
local General = require(Utilities.General)

function ProjectileFire:Fire(Player:Types.Player)
	local MousePosition = General:GetMousePosition(Player.Player)
	local Projectile = ProjectileHandler:CreateProjectile(Player,5,5,CFrame.lookAt(Player.Character.HumanoidRootPart.Position,MousePosition).LookVector * 60,Player.Character.Head.Position)
end

return ProjectileFire

Any thoughts or improvements I could make?

Also, I’m trying to add a speed variable S, but when I change this variable the speed changes but it skips ahead instead of smoothly increasing.

As you can see in this video, after 2 seconds, the speed value increases to 3, but it ends up skipping ahead instead of a smooth transition. And you can’t see it in the video but if I change the speed value to zero, it returns to the original position instead of stopping in place.

Here’s the code for that:

			local T = workspace:GetServerTimeNow() - Projectile.StartTime			
			local S = Projectile.Speed
			
			if T > 2 then
				S = 3		
			end
			
			local CalculatedCFrame = CFrame.lookAt(Projectile.StartPosition + (Projectile.Velocty * S * T),Projectile.Instance.Position + Projectile.Velocty)```

Update: I figured it out:

You can update the position of a projectile every frame using this equation:

Position = SP + (V0 * S * DeltaT)
--SP = Starting Position (Vector3)
--V0 = Starting Velocity (Vector3)
--S = Your speed multiplier (Number)
--DeltaT = Time interval (Number)

And this works really well. For finding DeltaT I’m just finding the difference in time between the last frame and now, which I’m pretty sure is constant as I’m using RunService.Heartbeat which I believe has the same amount of time between each frame. Anyways though I’m pretty sure you could set DeltaT to just some really small number without any calculations and it would still work.

And with the speed multiplier I can control the speed of the projectile at any point in time, allowing me to speed it up, slow it down, or even momentarily freeze them like in this video:

Now I just need to figure out hitboxes. I’m considering either FastCast or MuchachoHitbox

1 Like

Not certain if I’m reading it right, but while creating the parts on the server is useful, the server only needs the data itself. There’s no issue with doing both, so long as the server can handle it. Usually, whatever is controlling the movement of the projectile also handles the collision checks. Because they’re bullets and can be hundreds if not thousands at any given point in a game, this usually means that the server handling all of these individually becomes an issue.

This might be different with things like parallel luau, but I’ve yet to mess with it too much. I think one of the Phantom Forces Devs posted a video about using parallel Luau to have the server handle players and bullets, but I couldn’t tell you how to approach it.

It’s been really cool seeing how quickly you went from messing with it to finding + solving problems. Keep it up :slight_smile:

1 Like