Network Efficient and Safe Practices with Projectiles

After a lot of research and practice done on my part after asking this question myself several months ago, I’ve decided to shed some light for everyone on some more efficient network practices when it comes to projectiles and their hitboxes. Games with lots of stuff going on shouldn’t have to have 150+ kb/s of network traffic all the time.

A few key things to note before diving deeper into this:

  1. There are lots of ways to go about doing these depending on your use-case, and 95% of the time, if it follows the rules I lay out here, you can apply this set of steps to make your code more network efficient.

  2. You should understand some basic practices of keeping VFX on the client and doing simple sanity checks on remotes.

  3. This is most optimally used for larger projectiles where raycasting is cumbersome or time-consuming to set up. Like a large fireball or a rocket. If your projectile is small enough to be handled by a raycast or two, like a bullet, FastCast might be for you. You can in fact still use FastCast in conjunction with this to handle wall/floor impacts, like if the rocket can hit players while in the air, but if it hits nobody it’ll explode when it hits something. I won’t be going over that in this, but I can personally confirm that it combines amazing with this.

Let’s begin!

The main reason you’d want to do this is to avoid needlessly replicating objects. Why have the projectile on the server if the projectile’s path is predictable anyways? Let’s make all the clients do the work and send way less data across the network. Rather than a whole model being replicated to everyone, we can condense it down into a single remote event.

Let’s lay out a couple ground rules that the projectile we’re making should adhere to in order for us to best implement this strategy.

  1. The projectile should have a path that is predictable. By this I mean it should follow a per-determined path. No homing projectiles! This is because we’ll need the server to be able to predict where the projectile is without being able to see it.

  2. The projectile’s properties must be the same within the server and the client. This includes starting point and speed. They need to be on the same page so the client can have the most responsive experience possible while also keeping it as fair as possible for everyone else.

When thinking of your projectiles with these rules in mind, identify some characteristics you want the projectile to follow. Do you want it to go to where the client clicks their mouse? Do you just want it to go in front of the player? How fast do you want it?

The Setup:

Remotes
We need 2 remotes, one for us to tell clients what to do, and another for the clients to respond back with what they’ve gathered. Since I’m going to make a fireball, I named the former “DoEffect” and I named the latter “Respond”.

Remotes

Scripts
Next, we need a LocalScript on the player so we can receive what the server wants us to do. To keep it simple, I’m going to just fire a string to tell the client what function to run, along with the parameters the fireball is going to follow.

So, I placed a LocalScript in StarterPlayerScripts, and put this in there.

-- in a LocalScript in StarterPlayerScripts

-- declare variables
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remotes = ReplicatedStorage.Remotes
local effectRemote = remotes.DoEffect
local respondRemote = remotes.Respond
local LocalPlayer = game.Players.LocalPlayer

local function fireball(params)
	
end

-- connect to the remote
effectRemote.OnClientEvent:Connect(function(effect, params)
	
	if effect == "Fireball" then
		fireball(params)
	end
	
end)

The parameters table is going to be an array. We could use a dictionary, but having string indexes increases network usage very quickly, and we don’t want that! This means that order does matter with what you put into the array when you send it to the client.

Next, let’s pass the client some stuff it needs to run the fireball.

First, we’re going to want to pass the character that’s using the fireball, then its spawning point and then its speed. Since this is a fireball that will be going straight, we don’t need direction, the CFrame we provide will give the orientation it needs. After all that, we want to send the time value the server knows the fireball spawned at. This is essentially an ID we’ll use later on.

Next, we want the fireball to move, so I’ll make a variable for RunService so we can make the fireball move every frame.

-- declare variables
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local remotes = ReplicatedStorage.Remotes
local effectRemote = remotes.DoEffect
local respondRemote = remotes.Respond
local LocalPlayer = game.Players.LocalPlayer

local function fireball(params)
	-- properties of the fireball
	local character = params[1]
	local spawnPoint = params[2]
	local speed = params[3] or 45 -- add a default speed in case it's not provided, saves a few bytes if we don't send it
	local tickVal = params[4] -- i'll get to this later!
end

Now that we have everything we need to get the fireball going, let’s write the code to have it start moving!

I made a simple fireball, it contains a part with an attachment inside for the particles.

FireballExample

First, we’ll clone it on the client, and then place it at the position we provided the function!

Next, we’ll connect to RunService to make the fireball move! We’ll move the fireball straight by our speed multiplied by delta time so it’ll always stay the same speed even on slower/faster servers.

-- this is within our fireball function
local moveConnect
moveConnect = RunService.Heartbeat:Connect(function(DT)
		if fireballClone.Parent ~= nil then

			fireballClone.CFrame = fireballClone.CFrame * CFrame.new(0,0, -speed * DT) -- -Z goes straight	
			
		else
			-- if the fireball is gone, disconnect our connection!
			moveConnect:Disconnect()

		end
end)

Here’s what it looks like with our current code if we run this on the server:

local startTickVal = workspace:GetServerTimeNow()
game.ReplicatedStorage.Remotes.DoEffect:FireAllClients("Fireball", {game.Workspace.Player1, game.Workspace.Player1.HumanoidRootPart.CFrame, nil, startTickVal})


If you can’t view it, click here!

First, we passed in the function we’re running, which is “Fireball”, next we passed in the CFrame that the fireball will be spawning in the array, which is the HumanoidRootPart of my character! We don’t pass a speed value since we have a default one, saving us a few bytes, so we put nil where speed should be. Then we send the time value that will be returned to us later on.

Now that we have something that’s moving, let’s get on to the hitboxes!

Since this fireball is a circle, I want the hitbox to be relatively circular too! This means the spatial query method PartBoundsInRadius will do the trick.

I put this into a ModuleScript in ReplicatedStorage:

local PartBoundsInRadius = function(hitboxPosition, hitboxRadius, overlapParams)
	
	-- if a cframe was passed, just take the position out of it
	if typeof(hitboxPosition) == "CFrame" then
		hitboxPosition = hitboxPosition.Position
	end
	
	-- make a default overlap params if one is not passed in the argument
	local defOverlapParams = OverlapParams.new()
	defOverlapParams.FilterDescendantsInstances = {game.Workspace}
	defOverlapParams.FilterType = Enum.RaycastFilterType.Include
	overlapParams = overlapParams or defOverlapParams
	
	local hit
	hit = workspace:GetPartBoundsInRadius(hitboxPosition, hitboxRadius, overlapParams)
	
	-- now that we have all the parts the spatial query found, let's go through it and see if we can find anyone!
	
	local hitCharacters = {}
	
	for i, v in pairs(hit) do
		
		if v.Parent then
			v = v.Parent -- get the actual character model, since spatial query will return the parts in the model
		end
		
		-- if we've already accounted for their character or they don't have a humanoid, skip them!
		if table.find(hitCharacters, v) or not v:FindFirstChild("Humanoid") then continue end
		
        -- insert them into the array
		table.insert(hitCharacters, v)
		
	end
	
	return hitCharacters

end

return PartBoundsInRadius

This function lets us get characters within a spatial query so we can iterate through them in the function we’re going to handle our damaging in!

Let’s require this in the client, and check to see if we hit anybody on the client first.

So at the top of the script, I require the function and then use it within the heartbeat event so that it runs every time the fireball moves!

-- at the top of the script
local partsInRadius = require(ReplicatedStorage.PartBoundsInRadius)

-- inside the heartbeat connection, after we move the fireball
local hitChars = partsInRadius(fireballClone.CFrame, 3) -- we check the fireball's position in a radius of 3

Now, this is where all the magic comes together on the client!

First, we check to see if the player that’s using the fireball is the same as the LocalPlayer running the function, we do this through the GetPlayerFromCharacter method in PlayerService. We only want the person using the fireball to be checking the hitbox. If they are, then we can check the hitbox.

After that, we’ll declare a taggedCharacters table. We’ll use this table to keep track of who we’ve hit already with the fireball.

Finally, we’ll do some checks on the hitchars table so we can ensure that we don’t hit ourselves, nor do we hit the people we’ve already hit!

Here’s how our whole fireball function looks right now:

local function fireball(params)
	-- properties of the fireball
	local character = params[1]
	local spawnPoint = params[2]
	local speed = params[3] or 45 -- add a default speed in case it's not provided, saves a few bytes if we don't send it
	local tickVal = params[4] -- i'll get to this later!

	local fireballClone = ReplicatedStorage.Fireball:Clone()
	fireballClone.CFrame = spawnPoint
	fireballClone.Parent = game.Workspace
	game.Debris:AddItem(fireballClone, 4)
	
	local checkHitbox = false
	
	if LocalPlayer == game.Players:GetPlayerFromCharacter(character) then
		checkHitbox = true
	end
	
	local taggedCharacters = {}

	local moveConnect
	moveConnect = RunService.Heartbeat:Connect(function(DT)
		if fireballClone.Parent ~= nil then

			fireballClone.CFrame = fireballClone.CFrame * CFrame.new(0,0, -speed * DT) -- -Z goes straight
			
			if checkHitbox then
				local hitChars = partsInRadius(fireballClone.CFrame, 3)
				
				-- if there's nobody in the table, ignore the rest!
				if #hitChars <= 0 then return end
				
				-- we don't want to be hit by our own fireball, if we're the only ones hit, then ignore the rest!
				if #hitChars == 1 and table.find(hitChars, character) then
					return
				end
				
				-- we don't want the same people to be hit by the fireball multiple times, so let's check to see if anyone in the table has been
				-- hit by the fireball already, and if they have, then remove them
				-- we do this in a backwards order so we don't skip any indexes, since table.remove will automatically shift the table to fill
				-- the nil instances!
				for i = #hitChars, 1, -1 do
					if taggedCharacters[hitChars[i]] == true then
						table.remove(hitChars, i)
					end
				end
				
				-- if after everything is said and done we're the only ones left in the table, then ignore the rest!
				if #hitChars == 1 and table.find(hitChars, character) then
					return
				end
				
				-- tag everyone we send as true so we don't hit them again later!
				for i, v in pairs(hitChars) do
					taggedCharacters[v] = true
				end
				
				respondRemote:FireServer(hitChars, tickVal)
				
			end

			
			
			
		else
			-- if the fireball is gone, disconnect our connection!
			moveConnect:Disconnect()

		end
	end)


end

That’s a lot, but it’ll be worth it! Let’s head over to the server and handle the responses.

I’m going to make a function on the server that when ran will spawn the fireball and handle the responses back. I would not do it like this in a real game, I’d use ModuleScripts to hold these functions, as it makes it easier to call them.

Here’s how it looks on the server!

-- declare variables
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local remotes = ReplicatedStorage.Remotes
local effectRemote = remotes.DoEffect
local respondRemote = remotes.Respond

local function doFireball(player)
	local character = player.Character
    local spawnPoint = character.HumanoidRootPart.CFrame
	local taggedChars = {}

	-- set up our response before we tell all the clients what to do
	local startTickVal = workspace:GetServerTimeNow()
	respondRemote.OnServerEvent:Connect(function(playerResponded, hitChars, respondTickVal)
		
	end)
	
	effectRemote:FireAllClients("Fireball", {character, spawnPoint, nil, startTickVal})

end

Now, let’s finally add how we’re going to respond to the response from the player!

First, we’re going to check to see if the starting time value we took is the same as what was sent back, if it isn’t, then ignore it. This is how we differentiate fireballs from each other. If the player uses two of them, then those fireballs are going to have two different time values. Since workspace:GetServerTimeNow() is really precise, the odds two fireballs spawned by the same person will have the exact same time is really unlikely.

if startTickVal ~= respondTickVal then return end

Next, we’ll check to see if the person that responded back is the person the connection is looking for. We only want to listen to the player that actually used the fireball, so we’ll check to see if they’re the correct player.

if player ~= playerResponded then return end

Alright, now that we’ve got a response that is valid, let’s go through the characters they returned and see who we can hit.

Here’s the code I have for that:

local hitboxConnect
	hitboxConnect = respondRemote.OnServerEvent:Connect(function(playerResponded, hitChars, respondTickVal)
		if startTickVal ~= respondTickVal then return end
		if player ~= playerResponded then return end

		local respondedTick = workspace:GetServerTimeNow()

		for i, v in pairs(hitChars) do
			
			
			-- if we somehow sent our character, then skip us
			if v == character then continue end
			
			-- if we've already hit this person, then skip them
			if taggedChars[v] then continue end

			-- get the possible position of the fireball on the server, since the server can't see it!
			-- this is why projectiles must be predictable for this to work!
			local positionCFrame = spawnPoint * CFrame.new(0, 0, -45 * (respondedTick - startTickVal))
			-- make sure no exploiters hitbox extend too far
			if (v.HumanoidRootPart.Position - positionCFrame.Position).magnitude > 7 then return end
			
			taggedChars[v] = true
			
			v.Humanoid:TakeDamage(10)

		end
		
	end)

This is where the magic on the server happens! Since we have a predictable path the projectile is following, we can guess where the projectile is with how long it took for a response to be received. We subtract the time the server received a response by the time the server started the projectile.

Once we get the position of the fireball, we can do a magnitude check to if they’re within a good range to get hit by the fireball. This helps with latency since the character’s positions the server sees is different. We give them some leeway by making the range bigger than what the hitbox actually was. We also don’t want exploiters sending characters that are way too far away from the fireball, hitting them in the process! Make sure not to set this too big though!

After that, we now have a character that is within range and hasn’t been hit yet, so we insert them into the taggedChars table so we can’t hit them again, then we make them take damage!

Here’s how it looks ingame:

If you can’t view it, click here!

Here’s all the code in both scripts:

Server Script:

-- declare variables
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local remotes = ReplicatedStorage.Remotes
local effectRemote = remotes.DoEffect
local respondRemote = remotes.Respond

local function doFireball(player)
	local startTickVal = workspace:GetServerTimeNow()
	
	local character = player.Character
	-- set up our response before we tell all the clients what to do
	
	local spawnPoint = character.HumanoidRootPart.CFrame
	
	local taggedChars = {}
	
	local hitboxConnect
	hitboxConnect = respondRemote.OnServerEvent:Connect(function(playerResponded, hitChars, respondTickVal)
		if startTickVal ~= respondTickVal then return end
		if player ~= playerResponded then return end

		local respondedTick = workspace:GetServerTimeNow()

		for i, v in pairs(hitChars) do
			
			
			-- if we somehow sent our character, then skip us
			if v == character then continue end
			
			-- if we've already hit this person, then skip them
			if taggedChars[v] then continue end

			-- get the possible position of the fireball on the server, since the server can't see it!
			-- this is why projectiles must be predictable for this to work!
			local positionCFrame = spawnPoint * CFrame.new(0, 0, -45 * (respondedTick - startTickVal))
			-- make sure no exploiters hitbox extend too far
			if (v.HumanoidRootPart.Position - positionCFrame.Position).magnitude > 7 then return end
			
			taggedChars[v] = true
			
			v.Humanoid:TakeDamage(10)

		end
		
	end)
	
	effectRemote:FireAllClients("Fireball", {character, spawnPoint, nil, startTickVal})
	
	-- we know the fireball is going to be there for 4 seconds, since we told debris service to add it for that long
	task.delay(4, function()
		hitboxConnect:Disconnect()
	end)
	
end


-- this is to just fire the fireball, in a real game you'd have a skill handler that players would call on to use skills!
while true do
	
	for i, v in pairs(game.Players:GetChildren()) do
		local suc, res = pcall(function()
			doFireball(v)
		end)
		
		if not suc then
			print(res)
		end
	end
	
	task.wait(1)
end

Local Script:

-- declare variables
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local remotes = ReplicatedStorage.Remotes
local effectRemote = remotes.DoEffect
local respondRemote = remotes.Respond
local LocalPlayer = game.Players.LocalPlayer

local partsInRadius = require(ReplicatedStorage.PartBoundsInRadius)

local function fireball(params)
	-- properties of the fireball
	local character = params[1]
	local spawnPoint = params[2]
	local speed = params[3] or 45 -- add a default speed in case it's not provided, saves a few bytes if we don't send it
	local tickVal = params[4] -- i'll get to this later!

	local fireballClone = ReplicatedStorage.Fireball:Clone()
	fireballClone.CFrame = spawnPoint
	fireballClone.Parent = game.Workspace
	game.Debris:AddItem(fireballClone, 4)
	
	local checkHitbox = false
	
	if LocalPlayer == game.Players:GetPlayerFromCharacter(character) then
		checkHitbox = true
	end
	
	local taggedCharacters = {}

	local moveConnect
	moveConnect = RunService.Heartbeat:Connect(function(DT)
		if fireballClone.Parent ~= nil then

			fireballClone.CFrame = fireballClone.CFrame * CFrame.new(0,0, -speed * DT) -- -Z goes straight
			
			if checkHitbox then
				local hitChars = partsInRadius(fireballClone.CFrame, 3)
				
				-- if there's nobody in the table, ignore the rest!
				if #hitChars <= 0 then return end
				
				-- we don't want to be hit by our own fireball, if we're the only ones hit, then ignore the rest!
				if #hitChars == 1 and table.find(hitChars, character) then
					return
				end
				
				-- we don't want the same people to be hit by the fireball multiple times, so let's check to see if anyone in the table has been
				-- hit by the fireball already, and if they have, then remove them
				-- we do this in a backwards order so we don't skip any indexes, since table.remove will automatically shift the table to fill
				-- the nil instances!
				for i = #hitChars, 1, -1 do
					if taggedCharacters[hitChars[i]] == true then
						table.remove(hitChars, i)
					end
				end
				
				-- if after everything is said and done we're the only ones left in the table, then ignore the rest!
				if #hitChars == 1 and table.find(hitChars, character) then
					return
				end
				
				-- tag everyone we send as true so we don't hit them again later!
				for i, v in pairs(hitChars) do
					taggedCharacters[v] = true
				end
				
				respondRemote:FireServer(hitChars, tickVal)
				
			end	
			
		else

			-- if the fireball is gone, disconnect our connection!
			moveConnect:Disconnect()

		end
	end)


end

-- connect to the remote
effectRemote.OnClientEvent:Connect(function(effect, params)

	if effect == "Fireball" then
		fireball(params)
	end

end)

That’s it! You now know the process of keeping network usage low while also making projectiles responsive and safe. This way of thinking can be applied to a variety of things, like maybe an arrow or spikes that circle around you.

There are some downsides to this system!

  1. This system assumes that clients instantly receive a response from the server and vice-versa. This is not the case, as it takes time for the remote to fire to the client and for them to fire it back. This can lead to missing shots if the client takes too long to receive and send the event. This can be solved by finding out the length of time it takes for a remote to be received by the client and the length of time it takes for the server to receive the remote from the client, and then subtracting this total travel time between both sides from the time it took to receive the event in the hitbox connection when figuring out where the projectile is. This is normally not an issue as the leeway we provide usually handles this travel time for us, but it can likely become an issue with faster projectiles.
local travelTime = timeForClientToGet + timeForServerToGet
local positionCFrame = spawnPoint * CFrame.new(0, 0, -45 * ((respondedTick - startTickVal) - travelTime))
  1. Since we compare the server’s character positions rather than what the client sees, this can lead to missing shots as well, this can also be solved by keeping track of character positions over time and storing it somewhere. This essentially takes “snapshots” of everybody’s positions over time. If the client is 0.4 seconds behind the server and the server knows this, the server can look through all the hit characters, get their position 0.4 seconds ago, and then they’d have the position of the character as the client saw it, leading to smoother results. If anybody has an article or thread for this type of system, let me know and I can add it!

Here’s some other great tutorials that I recommend you to read through to get an idea of how to keep things network efficient.

Network Practices by Pyseph
Network Optimization by Hexcede

Constructive critique is welcome! Cheers!

35 Likes

Moving as much as possible onto the client and preventing needless replication is highly beneficial, especially as you scale for dozens of players per server. Good resource!

I’d like to mention that you should use workspace:GetServerTimeNow(), which was specifically added for this reason - it allows you to get a synced timestamp between the client and server, with a 1 ms (!) margin of error. time() is a lot less precise for such usecases.

9 Likes

Good resource!
Quick tip, when checking the distance to the hit characters on the server you can use the players latency to calculate a theoretical max distance instead of using a constant value.

(v.HumanoidRootPart.Position - positionCFrame.Position).magnitude > (5 + v.Humanoid.WalkSpeed * Latency) 
— Add the 5 to account for character size

You can get latency using Player:GetNetworkPing() (not recommended) or by passing GetServerTimeNow() as a parameter to the respond event.

hitboxConnect = respondRemote.OnServerEvent:Connect(function(playerResponded, hitChars, respondTickVal, clientTime)
local Latency = (workspace:GetServerTimeNow() - ClientTime)
if Latency > 1 then return end — Limit latency to 1 second
— The rest of the code
end)

Excuse the lack of formatting, I wrote this on my phone

8 Likes

Very good! However someone has already covered this.
https://devforum.roblox.com/t/making-a-combat-game-with-ranged-weapons-fastcast-may-be-the-module-for-you/133474

2 Likes

FastCast is mainly for smaller projectiles like bullets, while this should be used for larger stuff along with custom hitboxes, just use a different spatial query method like GetPartsInPart. You can however use FastCast in conjunction with this. In a fighting game I’m making, I use FastCast to detect wall impacts on the server, while also keeping the projectile VFX and hitbox detection on the client.

I actually did initially use FastCast alone, and just handled the VFX on the client, but I ended up scrapping it because it was too cumbersome to add a bunch of raycasts for large objects, and it was too precise (funnily enough) to rely on for hitboxes for my tastes.

3 Likes

Sorry to bump this topic, but I was using this method for a projectile and came into an issue for myself.
How would I synchronize the clean up of the fireball?

I tried this: When a hit occurred around the beginning of the code, I fire to all clients telling them to clean up using something like Debris or something but I keep running into a synchronization issue. I have this fireball in a tool, so when I spam click on the tool, it cleans up prematurely.

1 Like

What I’ve done is add a time value to the name of the projectile.

So for instance, I’ll get the time when the move is used and store it into a variable, then I’ll use that for all my synchronization for that specific use of the move.

So I would do something like this:

-- server code
local moveTick = workspace:GetServerTimeNow()

-- assume you have a remote called ClientEffect for effects
remotes.ClientEffect:FireAllClients("Fireball", character, moveTick)

local exploded = false

-- handle raycasting code here if you do wall impacts

task.delay(4, function()
     if not exploded then
          exploded = true
          remotes.ClientEffect:FireAllClients("FireballExplode", character, moveTick)
          -- end fireball raycasts here
     end
end)

--////client code

--// fireball code

--clone fireball
fireball.Parent = workspace.Projectiles
fireball.Name = "Fireball"..character.Name..moveTick

-- do the rest of your fireball code

--// explosion code
local fireball = workspace.Projectiles:FindFirstChild("Fireball"..character.Name..moveTick)

-- destroy the fireball however you want

Basically I name the client-sided fireball to the name of the character that used it as well as the time value of that move. Then since I know that’s what it’ll be named, when it’s time for the fireball to explode I give the client the same values as before so it can find the right fireball to handle.

Not sure about the cleaning up prematurely issue without some code to see. Make sure that each subsequent use of the tool doesn’t affect the previous use.

2 Likes

ahh okay, cheers. I fixed the issue using the unique identifiers. I just had to specify which spells to destroy, thanks!

Hello i just wanted to add that changing the speed from 45 to a greater number like 60, will have a tendency of not registering hits and completely pass through some npcs without dealing damage.

Another issue ive faced is when i use it on an npc thats rotated sideways, it also doesnt register them and passes right through.

If you have any solutions please let me know!

Make sure you’re giving the result sent back from the client enough leeway! How much leeway are you giving it? Print out the result of the part where you check the distance between what the server calculated as the position of the projectile to the NPC. See if it’s greater than the leeway you’re giving it and consider increasing your leeway.

Faster projectiles can be an issue due to the travel-time the packet takes to reach the server/client. That added extra bit of time can add distance to the projectile on the server and make the server think the projectile is farther than it really was on the client!

That 2nd issue sounds strange, as the projectile should be registering if it touches any BasePart. I can’t exactly point it out without some code or an example place, but try figuring out where it’s not registering. Is it not registering on the client? Is the server getting the result but not allowing the hit to occur?

1 Like

Wow you were completely right lol. I forgot to change the distance on the server script. Sorry for the confusion! Thank you

For the second error i am still unable to find a solution. It appears to damage up close to the target, but when applying some distance it passes straight through them. Heres a video example. It could just be my rigs somehow, but im not too sure tbh.

https://gyazo.com/5ed9f780ac30bd5279f7c2d5630a1310
https://gyazo.com/4d04a1045938d2c0bf5605d8f031fb2e

Glad to know that helped.

I would try and figure out where it’s going wrong, that is a weird issue that I’ve never had, I even tried replicating it by increasing the speed of my fireball in the test place that’s shown and I have 0 issues with speeds of 100+, as long as I give it a little extra leeway.

If it’s losing it on the client, you might be filtering it out somehow before sending it off. Also try printing out the results of the PartsInRadius function or whatever you’re using to check if somehow the function/module isn’t picking up the NPC.

If it’s recieved on the server, then try and figure out where the server is dropping the result, and modify from there.

What’s weird is the fact that at the end there it is hitting that further away NPC, so I’d like to guess that it’s just being filtered out somewhere.

1 Like

I think i found the source. It prints the npc out in the GetPartsBoundInRadius script, and on the client and server script. But when it gets to the magnitude script:

if (v.HumanoidRootPart.Position - positionCFrame.Position).magnitude > 7 then print("MAGNITUDE") return end -- added print to test results

it prints back magnitude. Testing it out a bit, it turns out i just had to increase the magnitude value. Still weird it happened though, but im glad i found a fix. Thanks again

edit: still encounter it sometimes. very strange. ill try to look into it some more

1 Like

Okay i figured out it’s registering the magnitude between the projectile and the NPCs as a lot larger than it should be. For instance, i have the radius of the attack set to 10, because thats the approximate size of the particle. But even when the projectile is right next to the enemy, it will print out saying the magnitude is above that radius.
https://gyazo.com/773bb60ce6461528bf972055ded9f8d9

I assume this is due to the server approximating where the projectile will be, and getting it wrong. Whats a good way i can get around this to more accurately determine where the projectile could be? Thank you.

You could try to visualize where it’s hitting by making parts on the client and server at where they see it, that’d give a better idea maybe of what’s going on.

Also, the system assumes that packet travel is instant, when in reality it isn’t. You can try and subtract the overall travel time (server → client → server) from the time it took to respond. That would likely give a more accurate result.