How would I render enemy FastCast bullets on the client?

Hello, recently i’ve been experiencing severe issues with rendering a large quantity of bullets (hundred or fifty a second) in my AI script, as its been causing a severe amount of lag.

I am using FastCast and PartCache to optimize everything, yet it still lags.

This is a direct continuation from my previous post which discusses this issue further, and i’ve been recommended to ‘render the bullets on the client not the server’, but I have no idea how to do this, here is the script that i’ve done so far on a local script in replicatedstorage:

-- local script
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local Remotes = ReplicatedStorage.Events
local Event = Remotes[script.Name]
local FastCast = require(ReplicatedStorage.FastCastRedux)
local PartCache = require(ReplicatedStorage.PartCache)

ReplicatedStorage.Events.FastCastClient.OnClientEvent:Connect(function(NPC)
	
	local Caster = FastCast.new()

	local behaviour = Caster.newBehavior()
	local raycastParams = RaycastParams.new()
	raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
	raycastParams.FilterDescendantsInstances = {NPC,workspace.Projectiles,workspace.OreSpawningArea}


	local bulletTemplate = Instance.new('Part')
	bulletTemplate.Size = Vector3.new(0.2,0.2,12)
	bulletTemplate.Material = Enum.Material.Neon
	bulletTemplate.Color = Color3.fromRGB(255, 225, 0)
	bulletTemplate.CanCollide = false

	local CachedBullet = PartCache.new(bulletTemplate,31,workspace.Projectiles)

	behaviour.RaycastParams = raycastParams
	behaviour.MaxDistance = 1000
	behaviour.AutoIgnoreContainer = false
	behaviour.CosmeticBulletContainer = workspace.Projectiles
	behaviour.CosmeticBulletProvider = CachedBullet

	Caster.LengthChanged:Connect(function(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)

	Caster.RayHit:Connect(function(cast,result,velocity,bullet)

		local enemyHum = result.Instance:FindFirstAncestorWhichIsA('Model'):FindFirstChild('Humanoid')
		if enemyHum then
			enemyHum:TakeDamage(5)
		end

		CachedBullet:ReturnPart(bullet)
	end)
	
	local function Fire(firepointAttachment,direction,velocity)
		Caster:Fire(firepointAttachment.WorldPosition,direction,velocity,behaviour)
	end
	
	-- I have no idea what to put here, I need to be able to somehow do the Fire() function from the server
-- should I look into things like remote functions or something?
	
end)
-- module/server script
		if NPC:FindFirstChild('Gun') then
			
			ReplicatedStorage.Events.FastCastClient:FireAllClients(NPC)
			
			NPCProperties.Fire = Fire -- I need the Fire function from the client to be here, but I don't know how to achieve that
		end
full script, I don't know if it will be necessary but just incase
local Players = game:GetService('Players')
local RunService = game:GetService('RunService')
local PathfindingService = game:GetService('PathfindingService')
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local CollectionService = game:GetService('CollectionService')
local RaycastHitbox = require(ReplicatedStorage.RaycastHitboxV4)
local maxDistance = math.huge
local Anims = script.Parent.R6Anims
local FastCast = require(ReplicatedStorage.FastCastRedux)
local PartCache = require(ReplicatedStorage.PartCache)
local Debris = game:GetService('Debris')

local AI = {}
AI.__index = AI

function AI.new(NPC,WalkAnim,AttackType)
	CollectionService:AddTag(NPC,NPC.Name)
	CollectionService:GetInstanceAddedSignal(NPC.Name):Connect(function(NPC)
		task.wait(5)
		local Team = NPC.Parent.Name
		local NPCProperties = setmetatable({},AI)
		local hum = NPC:WaitForChild('Humanoid')
		local humrp = NPC.HumanoidRootPart
		local WalkAnim = hum:LoadAnimation(WalkAnim)
		local RunOnce = false
		local AttackDB = false
		local ReloadDB = false
		local NPCHitbox = nil
		local idleAnim
		
		if NPC:FindFirstChild('Gun') then
			
			ReplicatedStorage.Events.FastCastClient:FireAllClients(NPC)
			
			NPCProperties.Fire = Fire -- I need the Fire function from the client to be here, but I don't know how to achieve that
		end
		
		-- makes sure that the loadanimation has an animation object
		if NPC:FindFirstChild('idleAnim') then
			idleAnim = hum:LoadAnimation(NPC.idleAnim)
			NPCProperties.IdleAnim = idleAnim
			idleAnim:Play()
		end
		
		NPC.PrimaryPart:SetNetworkOwner(nil)
		
		NPCProperties.NPC = NPC
		NPCProperties.Humanoid = hum
		NPCProperties.humrp = humrp
		NPCProperties.hum = hum
		NPCProperties.NPCHitbox = NPCHitbox
		NPCProperties.AttackDB = AttackDB
		NPCProperties.RunOnce = RunOnce
		NPCProperties.Team = Team
		NPCProperties.WalkAnim = WalkAnim
		NPCProperties.ReloadDB = ReloadDB
		
		hum.Died:Connect(function()
			NPC:BreakJoints()
			Debris:AddItem(NPC,5)
		end)
		
		--Pathfinding
		local CoreOfFactory = workspace.CentreOfFactory

		local function findTarget()
			local team
			local MaxDistance
			if Team == 'Allies' then
				team = workspace.Enemies:GetChildren()
				maxDistance = 200
			else
				team = workspace.Allies:GetChildren()
				maxDistance = math.huge
			end
			local nearestTarget

			for _,enemy in pairs(team) do
				if enemy then
					if enemy.Humanoid.Health > 0 then
						local target = enemy
						local distance = (humrp.Position - target.HumanoidRootPart.Position).Magnitude

						if distance < maxDistance then
							nearestTarget = target
							MaxDistance = distance
						end
					end
				end
			end
			return nearestTarget
		end

		local function agro()
			WalkAnim:Play()
			repeat task.wait(0.05)
				local target = findTarget()
				
				if target and target.HumanoidRootPart then
					local pos1 = humrp.Position - Vector3.new(0,humrp.Position.Y,0)
					local pos2 = target.HumanoidRootPart.Position - Vector3.new(0,target.HumanoidRootPart.Position.Y,0)
					local distance = (pos1 - pos2).Magnitude
					local whenToStop = 4
					
					if AttackType == 'Gun' then
						whenToStop = 100
					end

					if distance <= whenToStop then
						WalkAnim:Stop()
							NPCProperties.target = target
							AI[AttackType](NPCProperties)
					else
						local targetVel = target.PrimaryPart.Velocity
						local leadBy = 7
						local lead
						
						
						if targetVel.Magnitude < 0.1 then
							lead = Vector3.new(0, 0, 0)
						else
							lead = targetVel.Unit * leadBy
						end
						hum:MoveTo(target.PrimaryPart.Position + lead)
					end
				end
			until target == nil or target.Humanoid.Health == 0 or (humrp.Position - target.HumanoidRootPart.Position).Magnitude > maxDistance
		end
		
		local function pathfindAgro()
			
			local path = PathfindingService:CreatePath()
			
			local target = findTarget()

			local success, errorMessage = pcall(function()
				path:ComputeAsync(humrp.Position, target.HumanoidRootPart.Position)
			end)
			
			if idleAnim then
				idleAnim:Play()
			end
			
			WalkAnim:Play()
			if success and path.Status == Enum.PathStatus.Success then
				local waypoints = path:GetWaypoints()
				for _,waypoint in pairs(waypoints) do

					if waypoint.Action == Enum.PathWaypointAction.Jump then
						hum.Jump = true
					end

					if target and (humrp.Position - target.HumanoidRootPart.Position).Magnitude < math.huge then
						AI[AttackType](NPCProperties)
					end

					hum:MoveTo(waypoint.Position)
				end
			end
		end
		
		NPCProperties.pathfindAgro = pathfindAgro

		local function walkToCore()
			if Team == 'Enemies' then
				local path = PathfindingService:CreatePath()

				local success, errorMessage = pcall(function()
					path:ComputeAsync(humrp.Position, CoreOfFactory.Position)
				end)
				WalkAnim:Play()
				if success and path.Status == Enum.PathStatus.Success then
					local waypoints = path:GetWaypoints()
					for _,waypoint in pairs(waypoints) do

						if waypoint.Action == Enum.PathWaypointAction.Jump then
							hum:ChangeState(Enum.HumanoidStateType.Jumping)
						end

						if findTarget() and (humrp.Position - findTarget().HumanoidRootPart.Position).Magnitude < maxDistance then
							WalkAnim:Play()
							agro()
						end

						hum:MoveTo(waypoint.Position)
						hum.MoveToFinished:Wait()
					end
				end
				WalkAnim:Stop()
			else
				if findTarget() and (humrp.Position - findTarget().HumanoidRootPart.Position).Magnitude < maxDistance then
					WalkAnim:Play()
					agro()
				else
					WalkAnim:Stop()
				end
			end
		end

		walkToCore()
	end)
end

function AI.Punch(self)
	if self.AttackDB == false then
		self.AttackDB = true
		self.WalkAnim:Stop()
		self.NPC:SetPrimaryPartCFrame(CFrame.new(self.NPC.HumanoidRootPart.Position,Vector3.new(self.target.HumanoidRootPart.Position.X,self.NPC.HumanoidRootPart.Position.Y,self.target.HumanoidRootPart.Position.Z)))
		local AttackAnim = self.hum:LoadAnimation(Anims.Attack)
		local Damage = 15
		local delay = 1
		
		if self.RunOnce == false then
			self.RunOnce = true
			self.NPCHitbox = RaycastHitbox.new(self.NPC['Right Arm'])
			self.NPCHitbox:SetPoints(self.NPC['Right Arm'],{Vector3.new(0,0,1),Vector3.new(-1,1,3),Vector3.new(-3.5,1,3)})
		end

		AttackAnim:Play()
		self.NPCHitbox:HitStart()

		self.NPCHitbox.OnHit:Connect(function(hit,humanoid)
			if humanoid.Parent.Name ~= self.NPC.Name and not humanoid:FindFirstAncestor(self.Team) then
				humanoid:TakeDamage(Damage)
			end
		end)
		coroutine.wrap(function()
			AttackAnim.Stopped:Wait()
			self.NPCHitbox:HitStop()
			task.wait(delay)
			self.AttackDB = false
			self.WalkAnim:Play()
		end)()
	end
end

function AI.Sword(self)
	if self.AttackDB == false then
		self.AttackDB = true
		self.IdleAnim:Stop()
		self.WalkAnim:Stop()
		self.NPC:SetPrimaryPartCFrame(CFrame.new(self.NPC.HumanoidRootPart.Position,Vector3.new(self.target.HumanoidRootPart.Position.X,self.NPC.HumanoidRootPart.Position.Y,self.target.HumanoidRootPart.Position.Z)))
		local AttackAnim = self.hum:LoadAnimation(Anims.Sword)
		local Damage = 15
		local delay = 1

		if self.RunOnce == false then
			self.RunOnce = true
			self.NPCHitbox = RaycastHitbox.new(self.NPC['Right Arm'])
			self.NPCHitbox:SetPoints(self.NPC['Right Arm'],{Vector3.new(0,-1,-1),Vector3.new(0,-2,-2),Vector3.new(0,-1,-3),Vector3.new(0,-2,-4),
				Vector3.new(0,-1,1),Vector3.new(0,-2,0),Vector3.new(0,-1,1)})
		end

		AttackAnim:Play()
		self.NPCHitbox:HitStart()

		self.NPCHitbox.OnHit:Connect(function(hit,humanoid)
			if humanoid.Parent.Name ~= self.NPC.Name and not humanoid:FindFirstAncestor(self.Team) then
				humanoid:TakeDamage(Damage)
			end
		end)
		coroutine.wrap(function()
			AttackAnim.Stopped:Wait()
			self.IdleAnim:Play()
			self.WalkAnim:Play()
			self.NPCHitbox:HitStop()
			task.wait(delay)
			self.AttackDB = false
		end)()
	end
end

function AI.Gun(self)
	if self.AttackDB == false then
		self.AttackDB = true
		self.IdleAnim:Stop()
		self.WalkAnim:Stop()
		self.NPC:SetPrimaryPartCFrame(CFrame.new(self.NPC.HumanoidRootPart.Position,Vector3.new(self.target.HumanoidRootPart.Position.X,self.NPC.HumanoidRootPart.Position.Y,self.target.HumanoidRootPart.Position.Z)))
		local AttackAnim = self.hum:LoadAnimation(self.NPC.attackAnim)
		local ReloadAnim = self.hum:LoadAnimation(self.NPC.reloadAnim)
		local Damage = self.NPC.Gun.Damage.Value
		local fireRate = self.NPC.Gun.fireRate.Value
		local MagSize = self.NPC.Gun.MagSize
		local FullMagSize = self.NPC.Gun.FullMagSize.Value
		local firePoint = self.NPC.Gun.FirePoint
		local attackSound = self.NPC.Gun:WaitForChild('Handle').attackSound
		local reloadSound = self.NPC.Gun:WaitForChild('Handle').reloadSound

		local direction = self.target.HumanoidRootPart.Position - firePoint.Position
		local raycastParams = RaycastParams.new()
		local result = workspace:Raycast(firePoint.Position,direction,raycastParams)

		if result and result.Instance:FindFirstAncestor(self.target.Name) then
			if MagSize.Value > 0 then
				MagSize.Value -= 1
				AttackAnim:Play()
				attackSound:Play()
				AttackAnim:AdjustSpeed(2)
				
				self.Fire(firePoint.Attachment,direction,750)
				
			else
				if self.ReloadDB == false then
					self.ReloadDB = true
					reloadSound:Play()
					ReloadAnim:Play()
					coroutine.wrap(function()
						ReloadAnim.Stopped:Wait()
						self.IdleAnim:Play()
					end)()
					reloadSound.Ended:Wait()
					MagSize.Value = FullMagSize
					self.ReloadDB = false
				end
			end
		else
			self.pathfindAgro()
		end

		coroutine.wrap(function()
			self.IdleAnim:Play()
			task.wait(fireRate)
			self.AttackDB = false
		end)()
		end
end


return AI

please help

1 Like

sorry for sounding desperate and impatient, but I really need this solved, I just need to fire the local function Fire from a serverscript, please help.

1 Like

Sorry for anyone who tried to help, this is happening too often, I need to stop posting a dev forum about everything I do, I eventually figured out how to do this, but in turn it created much more lag and I don’t know what to do, see this here:

Again, I deeply apologize for the inconvenience, I shall try to minimize the amount of dev forums I post, thanks again and please check out the attached post, i’ve been struggling with this for months (not an exaggeration, like 2 months) without avail.

2 Likes

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