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.