Fastcast hitting more times than it should

Hi, I am currently making a tower defense game. I decided to use FastCast to solve an issue with smooth projectiles, it worked for the most part, it just made this problem.

The FastCast seems to hit more times than it should.

output:

08:18:20.893  Hit  -  Server - 1:44
  08:18:22.292    -  Server - 1:25
  08:18:22.342   ▶ Hit (x2)  -  Server - 1:44
  08:18:23.809    -  Server - 1:25
  08:18:23.842   ▶ Hit (x3)  -  Server - 1:44
  08:18:25.326    -  Server - 1:25
  08:18:25.392   ▶ Hit (x4)  -  Server - 1:44
  08:18:26.842    -  Server
  08:18:27.008   ▶ Hit (x5)  -  Server

Here is a video of the problem.

robloxapp-20230219-0821265.wmv (1.9 MB)

Basically, every time the tower fires and hits something, it seems to hit an additional time the more the tower fires.

I have used tutorials to get here, specifically
Gnome Code’s Tower Defense tutorial,
Suphi’s projectile physics video,
EgoMoose’s Projectile motion devforum post
and BRicey’s Vid on fast cast.

The code will have some form of the four sources above.

Here is the code for the tower. (Not every part of it is relevant, This is also mostly from Gnome Code’s Tutorial)

local RS = game:GetService("ReplicatedStorage")
local SS = game:GetService("ServerStorage")
local PS = game:GetService("PhysicsService")
local PFS = game:GetService("PathfindingService")

local Tower = {}
local events = RS.Events
local funcs = RS.Functions
local BEvents = SS.BindE
local SellF = funcs.SellorMoveDefender
local TowerSpawnE = BEvents.SpawnTower
local TowerAnimE = events.AnimateTower
local ChangeFocusE = events.ChangeFocus
local APVE = events.ChangeAPValue
local enemies = game.Workspace.Enemies
local NullOE = BEvents.NullOwner
local map = workspace.Woods


local MovesLibrary = require(script.Moves)
local FastC = require(script.Parent.FastCastRedux)
local ArmoryList = {}




function findtarget(Summoned, Range)

	if Summoned and Range then
		local lockedmode = Summoned:FindFirstChild("Core"):WaitForChild("Focus").Value
		local MainTarget = nil
		local distancecheck = nil
		local distancecheck2 = nil
		local Rantable = {}
		for i, Target in ipairs(enemies:GetChildren()) do
			
			local distance = (Summoned.HumanoidRootPart.Position - Target.HumanoidRootPart.Position).Magnitude
			if lockedmode == "High Priority" then
				if distance <= Range then
					if Target:WaitForChild("Core"):WaitForChild("Priority").Value > (distancecheck or 0) then
						distancecheck = Target:WaitForChild("Core"):WaitForChild("Priority").Value
						distancecheck2 = (Target.HumanoidRootPart.Position - map:WaitForChild("Main Nodes"):WaitForChild(tostring(Target:WaitForChild("Core"):WaitForChild("Priority").Value)).PriCheck.Position).Magnitude
						MainTarget = Target
					elseif Target:WaitForChild("Core"):WaitForChild("Priority").Value == (distancecheck or 0) then
						if distancecheck2 == nil or (Target.HumanoidRootPart.Position - map:WaitForChild("Main Nodes"):WaitForChild(tostring(Target:WaitForChild("Core"):WaitForChild("Priority").Value)).PriCheck.Position).Magnitude < distancecheck2 then
							distancecheck = Target:WaitForChild("Core"):WaitForChild("Priority").Value
							distancecheck2 = (Target.HumanoidRootPart.Position - map:WaitForChild("Main Nodes"):WaitForChild(tostring(Target:WaitForChild("Core"):WaitForChild("Priority").Value)).PriCheck.Position).Magnitude
							MainTarget = Target
						end
					end
				end
			elseif lockedmode == "Low Priority" then
				if distance <= Range then
					print(Target:WaitForChild("Core"):WaitForChild("Priority").Value, (Target.HumanoidRootPart.Position - map:WaitForChild("Main Nodes"):WaitForChild(tostring(Target:WaitForChild("Core"):WaitForChild("Priority").Value)).PriCheck.Position).Magnitude)
					if Target:WaitForChild("Core"):WaitForChild("Priority").Value < (distancecheck or math.huge) then
						distancecheck = Target:WaitForChild("Core"):WaitForChild("Priority").Value
						distancecheck2 = (Target.HumanoidRootPart.Position - map:WaitForChild("Main Nodes"):WaitForChild(tostring(Target:WaitForChild("Core"):WaitForChild("Priority").Value)).PriCheck.Position).Magnitude
						MainTarget = Target
					elseif Target:WaitForChild("Core"):WaitForChild("Priority").Value == (distancecheck or math.huge) then
						if distancecheck2 == nil or (Target.HumanoidRootPart.Position - map:WaitForChild("Main Nodes"):WaitForChild(tostring(Target:WaitForChild("Core"):WaitForChild("Priority").Value)).PriCheck.Position).Magnitude > distancecheck2 then
							distancecheck = Target:WaitForChild("Core"):WaitForChild("Priority").Value
							distancecheck2 = (Target.HumanoidRootPart.Position - map:WaitForChild("Main Nodes"):WaitForChild(tostring(Target:WaitForChild("Core"):WaitForChild("Priority").Value)).PriCheck.Position).Magnitude
							MainTarget = Target
							print("Registered")
						end
					end
				end
			elseif lockedmode == "Closest" then
				if distance <= Range then
					if  distancecheck == nil or distancecheck >= distance then
						distancecheck = distance
						MainTarget = Target
					end
				end
			elseif lockedmode == "Farthest" then
				if distance <= Range then
					if  distancecheck == nil or distancecheck <= distance then
						distancecheck = distance
						MainTarget = Target
					end
				end
			elseif lockedmode == "High Hp" then
				if distance <= Range then
					if  distancecheck == nil or distancecheck <= Target.Humanoid.Health then
						distancecheck = Target.Humanoid.Health
						MainTarget = Target
					end
				end
			elseif lockedmode == "Low Hp" then
				if distance <= Range then
					if  distancecheck == nil or distancecheck >= Target.Humanoid.Health then
						distancecheck = Target.Humanoid.Health
						MainTarget = Target
					end
				end
			elseif lockedmode == "Random" then
				if distance <= Range then
					table.insert(Rantable, Target)
					MainTarget = Rantable[math.random(1,#Rantable)]
				end
			end
		end
		return MainTarget
	else
		warn("Summoned", Summoned, "| Or Range " ,Range, "Not Found!")
	end

end



function Tower.Attack (Summoned, LastTarget, Player, TowerCaster, castBehavior)
	local core = Summoned:FindFirstChild("Core")
	local BaseStats = core:FindFirstChild("BaseStats")
	if core and BaseStats then
		local target = findtarget(Summoned, BaseStats.Range.Value )
		if target then
			if LastTarget == nil and BaseStats:FindFirstChild("AimTime") then
				local TCFrame = CFrame.lookAt(Summoned.HumanoidRootPart.Position, target.HumanoidRootPart.Position)
				TowerAnimE:FireAllClients(Summoned, "Aiming")
				for i = 1, 10 do
					Summoned.HumanoidRootPart.CFrame = TCFrame
					task.wait(BaseStats.AimTime.Value/10)
				end
				
			else
				if LastTarget ~= nil then
					TowerAnimE:FireAllClients(Summoned, "Attack")
					local TCFrame = CFrame.lookAt(Summoned.HumanoidRootPart.Position, target.HumanoidRootPart.Position)
					Summoned.HumanoidRootPart.CFrame = TCFrame
					
					local pool = Summoned:FindFirstChild("Abilities"):GetChildren()
					if pool and pool ~= {} then
						local ranpool = math.random(1, #pool)
						local failsafe = nil
						
						for i = 1, #pool do
							if pool[i].Value == ranpool then
								failsafe = "Found"
								MovesLibrary.CheckMove(pool[i].Name, Summoned, target, TowerCaster, FastC, castBehavior)
							end
						end
						if failsafe == nil then
							warn("No Move Found For", ranpool, ". A list for all abilities", pool)
						end
					end
					
					
					wait(BaseStats.Firerate.Value )
				end
			end
			
		else
			if LastTarget == nil then
			else
				TowerAnimE:FireAllClients(Summoned, "Idle")
			end

			wait(0.05)
		end
		Tower.Attack (Summoned, target, Player, TowerCaster, castBehavior)
	end

end

function Tower.Selocate(player, model)
	if model then
		if model:FindFirstChild("Core"):FindFirstChild("Owner").Value == player.Name then
			if model:FindFirstChild("Core"):WaitForChild("Type").Value == "Tower" then

				player.Money.Value += model.Core.Cost.Value + math.floor(model.Core.MoneySpent.Value/1.1)
				player.Scrap.Value +=math.floor(model.Core.ScrapSpent.Value/math.random(1, 2))
				model:Destroy()
				return true
			elseif model.FindFirstChild("Core"):WaitForChild("Type").Value == "Defender" then
				return false
			elseif model.FindFirstChild("Core"):WaitForChild("Type").Value == "Hero" or model.FindFirstChild("Core"):WaitForChild("Type").Value == "General" then
				player.PlayerGui.PlayerController.MoveSpecialT.Value = true
				return false

			else
				warn("Type Not Found", model.FindFirstChild("Core"):WaitForChild("Type").Value == "Hero")
				return false
			end
		elseif model:FindFirstChild("Owner").Value == "" then
			local CheckClick = NullOE:Fire(player, model, "Record")
			if CheckClick == true then
				model.FindFirstChild("Core"):WaitForChild("Vote").Value += 1
				if model.FindFirstChild("Core"):WaitForChild("Vote").Value >= #game.Players:GetChildren() - 1 then

					for i = 1, #game.Players:GetChildren() do
						player.Money.Value += math.floor((model.Core.Cost.Value + math.floor(model.Core.MoneySpent.Value/1.1)) / #game.Players:GetChildren())
						player.Scrap.Value +=math.floor((math.random(math.floor(model.Core.ScrapSpent.Value), math.floor(model.Core.ScrapSpent.Value/2))) / #game.Players:GetChildren())
						model:Destroy()
						return true
					end
				else
					return false
				end
			else
				model.FindFirstChild("Core"):WaitForChild("Vote").Value -= 1
			end

		else
			warn("YOU ARE NOT THE OWNER")
			return false
		end
	else
		warn("No Model Found")
		return false
	end
end


function Tower.Spawn(Player,Name, TCframe)
	local Towercheck = RS.Towers:FindFirstChild(Name)
	if Towercheck then
		local Summoned = Towercheck:Clone()
		Summoned.Parent = workspace.Towers
		Summoned.HumanoidRootPart:SetNetworkOwner(nil)
		Summoned.HumanoidRootPart.CFrame = TCframe


		local ownvalue = Instance.new("StringValue")
		ownvalue.Name = "Owner"
		ownvalue.Value = Player.Name
		ownvalue.Parent = Summoned:WaitForChild("Core")

		local Tavalue = Instance.new("StringValue")
		Tavalue.Name = "Focus"
		Tavalue.Value = "High Priority"
		Tavalue.Parent = Summoned:WaitForChild("Core")

		local CFrame = Instance.new("CFrameValue")
		CFrame = Vector3.new(math.huge, math.huge, math.huge)
		CFrame = Summoned.HumanoidRootPart.CFrame

		local TowerCaster = nil
		local castParams = nil
		local castBehavior = nil
		if Summoned.Core:WaitForChild("PrimaryProjectile").Value then
			print("Accepted")
			TowerCaster = FastC.new()
			castParams = RaycastParams.new()
			castParams.FilterType = Enum.RaycastFilterType.Whitelist
			castParams.IgnoreWater = true

			local g = Vector3.new(0, -workspace.Gravity, 0)

			castBehavior = FastC.newBehavior()
			castBehavior.RaycastParams = castParams
			castBehavior.Acceleration =  g -- -workspace.Gravity
			castBehavior.AutoIgnoreContainer = false
			castBehavior.CosmeticBulletContainer = game.Workspace.Projectiles
			castBehavior.CosmeticBulletTemplate = Summoned.Core.PrimaryProjectile.Value
			castParams.FilterDescendantsInstances = {map.Baseplate.Value, game.Workspace.Enemies}
			
		end
		
		for i, object in pairs (Summoned:GetDescendants()) do
			if object:IsA("BasePart") then
				PS:SetPartCollisionGroup(object, "Tower")
			end
		end

		coroutine.wrap(Tower.Attack)(Summoned, nil, Player, TowerCaster, castBehavior)
		else
		warn("TowerNotFound", Name)
	end
end


TowerSpawnE.Event:Connect(Tower.Spawn)

APVE.OnServerEvent:Connect(function(p, Command, Variable, Type)
	if Type == "Tower" then
		if Variable and Command then
			Variable.Value = Command
		else
			warn("Variable or command not found", Variable, Command)
		end
	end
end)

SellF.OnServerInvoke = Tower.Selocate

ChangeFocusE.OnServerEvent:Connect(function(p, tower)
	local Core = tower:WaitForChild("Core")
	if Core:WaitForChild("Owner").Value == p.Name or Core:WaitForChild("Owner").Value == "" then
		if Core:WaitForChild("Focus").Value == "High Priority" then
			Core:WaitForChild("Focus").Value = "Low Priority"
		elseif Core:WaitForChild("Focus").Value == "Low Priority" then
			Core:WaitForChild("Focus").Value = "Closest" 
		elseif Core:WaitForChild("Focus").Value == "Closest" then
			Core:WaitForChild("Focus").Value = "Farthest"
		elseif Core:WaitForChild("Focus").Value == "Farthest" then
			Core:WaitForChild("Focus").Value = "High Hp"
		elseif Core:WaitForChild("Focus").Value == "High Hp" then
			Core:WaitForChild("Focus").Value = "Low Hp"
		elseif Core:WaitForChild("Focus").Value == "Low Hp" then
			Core:WaitForChild("Focus").Value = "Random"
		elseif Core:WaitForChild("Focus").Value == "Random" then
			Core:WaitForChild("Focus").Value = "High Priority"
		end
		events.UpdateStats:FireClient(p)
	end
end)
return Tower

--target.Humanoid:TakeDamage((BaseStats.Attack.Value + Changes.Attack.Value) or 1)
--[[
					if target.Humanoid.Health <= 0 then
						for i, Playertab in pairs(game.Players:GetChildren()) do
							Playertab.Money.Value +=  target.Humanoid.MaxHealth/2
							Playertab.Scrap.Value +=  math.round(math.random(target.Humanoid.MaxHealth/10, target.Humanoid.MaxHealth/5))
						end
						Player.Money.Value +=  target.Humanoid.MaxHealth
						Player.Scrap.Value +=  math.round(math.random(target.Humanoid.MaxHealth/10, target.Humanoid.MaxHealth/5))
						Player.Power.Value +=  math.random(0,1)
					end
					--]]

As for how a move is chosen, it goes in a series of modules.

Module finder module script.

local Library = {}

function Library.CheckMove(MoveName,Summoned,Target, TowerCaster, FastC, castBehavior)
	local lastcheck = nil
	local modulegroup = script:GetChildren()
	if lastcheck == nil then
		for i = 1, #modulegroup do
			local movechecker = require(modulegroup[i])
			if MoveName and Summoned and Target then
				movechecker.FindMove(MoveName, Summoned, Target, TowerCaster, FastC, castBehavior)
			else
				warn("No move name, Summoned mob, or Target found", MoveName, Summoned, Target)
			end

			if movechecker ~= {} then
				lastcheck = "found"
			end
		end
	end
	
	
end


return Library

And it goes to this module script which controls the moves. (Some of it was from BRicey’s tutorial and EgoMoose’s Projectile Post)

local SS = game:GetService("ServerStorage")
local RS = game:GetService("ReplicatedStorage")
local DS = game:GetService("Debris")

local map = workspace.Woods
local castertable = {}

local Movepool = {}



local events = RS.Events
local MobAnimE = events.AnimateMob





local Movepool = {}




function Movepool.FindMove( Movename, Summoned, Target, TowerCaster, FastC, castBehavior)
print("")

	if Movename == "NormalShot" then
		Movepool = "Found"

		if TowerCaster == nil or castBehavior == nil then
			warn(Summoned, "DOES NOT HAVE A CASTER",TowerCaster, "OR BEHAVIOR,",castBehavior," PLEASE ADD ONE OR CHANGE THE ABILITY")
		end

		local function onLengthChanged(cast, lastPoint, direction, length, velocity, bullet)
			if bullet then 
				local bulletLength = bullet.Size.Z/2
				local offset = CFrame.new(0, 0, -(length - bulletLength))
				bullet.CFrame = CFrame.lookAt(lastPoint, lastPoint + direction):ToWorldSpace(offset)
			end
		end


		local function onRayHit(cast, result, velocity, bullet)
			print("Hit")
			local hit = result.Instance

			local character = hit:FindFirstAncestorWhichIsA("Model")
			if character and character:FindFirstChild("Humanoid") then

				character.Humanoid:TakeDamage((Summoned:FindFirstChild("Core"):FindFirstChild("BaseStats"):FindFirstChild("Attack") or game.Workspace.Surrogates.SoloSurrogate).Value - (Summoned:FindFirstChild("Core"):FindFirstChild("Changes"):FindFirstChild("Attack") or game.Workspace.Surrogates.NullSurrogate).Value)
			end
			game:GetService("Debris"):AddItem(bullet, 1)
		end
		local function fire()
			castBehavior.Acceleration = Vector3.new(0,-workspace.Gravity, 0)
			castBehavior.AutoIgnoreContainer = false
			castBehavior.CosmeticBulletContainer = game.Workspace.Projectiles
			castBehavior.CosmeticBulletTemplate = Summoned.Core.PrimaryProjectile.Value

			local pos1 = (Summoned:FindFirstChild("ProjSpawn") or Summoned.HumanoidRootPart).Position
			local pos2 = Target.HumanoidRootPart.Position
			local dir = pos2 - pos1
			local dirA = (pos2 - pos1).Unit
			local duration = dir.Magnitude / (Summoned.Core.BaseStats.ProjectileSpeed.Value or 50)

			pos2 = Target.HumanoidRootPart.Position + Target.HumanoidRootPart.AssemblyLinearVelocity * duration + Vector3.new(math.random(-3,3),math.random(-3,3),math.random(-3,3))
			dir = (pos2 - pos1).Unit

			
			
			TowerCaster:Fire(pos1, dirA, (Target.HumanoidRootPart.Position - pos1 -0.5*Vector3.new(0,-workspace.Gravity, 0)*duration*duration)/duration , castBehavior)

		end

		fire()
		
		TowerCaster.LengthChanged:Connect(onLengthChanged)
		TowerCaster.RayHit:Connect(onRayHit)


		--[[
		local pos1 = (Summoned:FindFirstChild("ProjSpawn") or Summoned.HumanoidRootPart).Position
		local pos2 = Target.HumanoidRootPart.Position

		local dir = pos2 - pos1
		local duration = dir.Magnitude / (Summoned.Core.BaseStats.ProjectileSpeed.Value or 50)

		pos2 = Target.HumanoidRootPart.Position + Target.HumanoidRootPart.AssemblyLinearVelocity * duration
		dir = pos2 - pos1
		local force = dir / duration + Vector3.new(0, game.Workspace.Gravity/2 * duration, 0)
		local clone = Summoned.Core:WaitForChild("PrimaryProjectile"):Clone()
		clone.Parent = game.Workspace.Projectiles
		clone.WeldConstraint:Destroy()
		clone.Position = pos1
		clone:ApplyImpulse(force * clone.AssemblyMass)
		clone:SetNetworkOwner(nil)
		--]]


	elseif Movename == "ArchingShot" then -- The stuff below is not as relevant as I plan to make one without gravity acceleration and move the current one I am working on to here.
		Movepool = "Found"

		MobAnimE:FireAllClients(Summoned, "Attack")
		wait(0.5 / Summoned:FindFirstChild("Animations"):FindFirstChild("Attack"):FindFirstChild("PlaySpeed").Value or 0.5)
		Target:TakeDamage(Summoned:Waitforchild("Core"):WaitForChild("BaseStats"):WaitForChild("Attack").Value)
	else
	end
end
return Movepool

That was a lot I put in so let me restate the problem.

The problem is NOT the system itself I only put it there in case the problem is linked to the system, The problem is that the Ray Hit Function is hitting more times than it should overtime

I have tried to make a debounce system, I even compared it to an edited version of BRicey’s script,

local tool = script.Parent
local fireEvent = tool.FireEvent
local FastCast = require(tool.FastCastRedux)
local firePoint = tool.Handle

local bulletsFolder = workspace:FindFirstChild("BulletFolder") or Instance.new("Folder", workspace)
bulletsFolder.Name = "BulletFolder"

local bulletTemplate = Instance.new("Part")
bulletTemplate.Anchored = true
bulletTemplate.CanCollide = false
bulletTemplate.Shape = "Ball"
bulletTemplate.Size = Vector3.new(0.5, 0.5, 0.5)
bulletTemplate.Material = Enum.Material.Neon

local function test(caster, castParams)
	print(caster, castParams)
end
--FastCast.VisualizeCasts = true

local caster = FastCast.new()

local castParams = RaycastParams.new()
castParams.FilterType = Enum.RaycastFilterType.Blacklist
castParams.IgnoreWater = true

local g = Vector3.new(0, -workspace.Gravity, 0)

local castBehavior = FastCast.newBehavior()
castBehavior.RaycastParams = castParams
castBehavior.Acceleration =  g -- -workspace.Gravity
castBehavior.AutoIgnoreContainer = false
castBehavior.CosmeticBulletContainer = bulletsFolder
castBehavior.CosmeticBulletTemplate = bulletTemplate



test(caster, castParams)

local function onEquipped()
	castParams.FilterDescendantsInstances = {tool.Parent, bulletsFolder}
end

local function onLengthChanged(cast, lastPoint, direction, length, velocity, bullet)
	if bullet then 
		local bulletLength = bullet.Size.Z/2
		local offset = CFrame.new(0, 0, -(length - bulletLength))
		bullet.CFrame = CFrame.lookAt(lastPoint, lastPoint + direction):ToWorldSpace(offset)
	end
end

local function onRayHit(cast, result, velocity, bullet)
	print("Triggered")
	local hit = result.Instance
	
	local character = hit:FindFirstAncestorWhichIsA("Model")
	if character and character:FindFirstChild("Humanoid") then
		character.Humanoid:TakeDamage(50)
	end
	
	game:GetService("Debris"):AddItem(bullet, 2)
	
end

local function fire(player, mousePosition)
	local origin = firePoint.Position
	local direction = (mousePosition - origin).Unit
	local tim = (game.Workspace.Target.Position - tool.Handle.Position).Magnitude/100
	caster:Fire(origin, direction, (game.Workspace.Target.Position - tool.Handle.Position -0.5*g*tim*tim)/tim , castBehavior)
end

fireEvent.OnServerEvent:Connect(fire)

tool.Equipped:Connect(onEquipped)

caster.LengthChanged:Connect(onLengthChanged)
caster.RayHit:Connect(onRayHit)

and it worked fine for that one but not for this one.
I also checked the projectiles fired and only one seemed to fire. The problem seems to be the Hit function itself.

1 Like

That is a lot of code that I will not read.

From my knowledge fastcast uses an RayHit event and these connections can stack unless disconnected. Most issues I have seen in the past is due to people creating multiple connections which you havent mentioned yet.

Make sure to only have one ray hit connection per caster per tower.

The critical part is the line below and how many times it ran in your function per tower.

3 Likes

ok, Thanks for letting me know.

Its solved now thank you!

All I had to do was move both functions (ray hit and length changed) to the tower script.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.