For the sake of simplicity I’m only using one caster on the server and one on the client for all projectiles, server is for damage and replicating to other clients, and client is for rendering your own projectiles instantly (before remote firing the server) and rendering projectiles sent by the server
ProjectileReplication_Client Module
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local Quire = require(ReplicatedStorage:WaitForChild("Shared").Quire)
local NetworkUtil = Quire("NetworkUtil")
local Caster = Quire("FastCast").new()
local Repr = Quire("Repr")
local projectileReplicationEvent = NetworkUtil:GetRemoteEvent("ProjectileReplicationEvent")
local projectileFolder = ReplicatedStorage:WaitForChild("Projectiles")
local workspaceProjectiles = workspace:WaitForChild("Projectiles")
-- black list setup and updating
local blacklist = {workspaceProjectiles}
local function onPlayerAdded(player)
local function onCharacterAdded(character)
table.insert(blacklist, character)
character.AncestryChanged:Connect(function(_, parent)
if parent then return end
table.remove(blacklist, table.find(blacklist, character))
end)
end
if player.Character then onCharacterAdded(player.Character) end
player.CharacterAdded:Connect(onCharacterAdded)
end
for _, player in ipairs(Players:GetPlayers()) do
onPlayerAdded(player)
end
Players.PlayerAdded:Connect(onPlayerAdded)
-- Projectiles
-- fireball
local fireBallProjectile = projectileFolder.FireballPart
local fireBallTweenTime = .2
local fireBallFadeTweenInfo = TweenInfo.new(fireBallTweenTime)
local fireBallTweenProperties = {Transparency = 1}
local ProjectileReplication_Client = {}
function ProjectileReplication_Client.fireProjectile(projectileData, origin, direction, velocity, _, _, ignoreWater, bulletAcceleration, canPierceFunction)
if projectileData.projectileType == "Fireball" then
local projectileCosmeticPart = fireBallProjectile:Clone()
projectileCosmeticPart.Parent = workspaceProjectiles
projectileData.fireBallFadeTween = TweenService:Create(projectileCosmeticPart, fireBallFadeTweenInfo, fireBallTweenProperties)
Caster:FireWithBlacklist(
projectileData,
origin,
direction,
velocity,
blacklist,
projectileCosmeticPart,
ignoreWater,
bulletAcceleration,
canPierceFunction
)
end
end
Caster.RayHit:Connect(function(projectileData, hitPart, hitPoint, normal, material, projectile)
if projectileData.projectileType == "Fireball" then
projectileData.fireBallFadeTween:Play()
wait(fireBallTweenTime)
end
projectile:Destroy()
end)
Caster.LengthChanged:Connect(function(projectileData, castOrigin, segmentOrigin, segmentDirection, length, projectile)
local baseCF = CFrame.new(segmentOrigin, segmentOrigin + segmentDirection)
local projectHalfSize = projectile.Size.Z/2
projectile.CFrame = baseCF * CFrame.new(0, 0, -(length - projectHalfSize))
end)
projectileReplicationEvent.OnClientEvent:Connect(ProjectileReplication_Client.fireProjectile)
return ProjectileReplication_Client
ProjectileReplication_Server Module
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Quire = require(ReplicatedStorage:WaitForChild("Shared").Quire)
local NetworkUtil = Quire("NetworkUtil")
local Caster = Quire("FastCast").new()
local projectileReplicationEvent = NetworkUtil:GetRemoteEvent("ProjectileReplicationEvent")
local workspaceProjectiles = workspace:WaitForChild("Projectiles")
-- black list setup and updating
local blacklist = {workspaceProjectiles}
local function onPlayerAdded(player)
local function onCharacterAdded(character)
table.insert(blacklist, character)
character.AncestryChanged:Connect(function(_, parent)
if parent then return end
table.remove(blacklist, table.find(blacklist, character))
end)
end
if player.Character then onCharacterAdded(player.Character) end
player.CharacterAdded:Connect(onCharacterAdded)
end
for _, player in ipairs(Players:GetPlayers()) do
onPlayerAdded(player)
end
Players.PlayerAdded:Connect(onPlayerAdded)
local ProjectileReplication_Server = {}
function ProjectileReplication_Server.fireProjectile(projectileData, origin, direction, velocity, _, projectileCosmeticPart, ignoreWater, bulletAcceleration, canPierceFunction)
Caster:FireWithBlacklist(
projectileData,
origin,
direction,
velocity,
blacklist,
projectileCosmeticPart,
ignoreWater,
bulletAcceleration,
canPierceFunction
)
for _, player in ipairs(Players:GetChildren()) do
if projectileData.player ~= player then
projectileReplicationEvent:FireClient(
player,
projectileData,
origin,
direction,
velocity,
projectileCosmeticPart,
nil,
ignoreWater,
bulletAcceleration,
canPierceFunction
)
end
end
end
Caster.RayHit:Connect(function(projectileData, hitPart, hitPoint, normal, material, projectile)
local humanoid = hitPart and hitPart.Parent:FindFirstChild("Humanoid")
if humanoid then
humanoid:TakeDamage(projectileData.damage)
end
end)
return ProjectileReplication_Server
Fireball_Client Tool Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Quire = _G.Quire
local ProjectileReplication_Client = Quire("ProjectileReplication_Client")
local ProjectileReplication = Quire("ProjectileReplication")
local tool = script.Parent
local handle = tool:WaitForChild("Handle")
local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
local character
local rightHand
local remotesFolder = tool.Remotes
local shootEvent = remotesFolder.ShootEvent
local statsFolder = tool.Stats
local stateFolder = tool.State
local SPEED = statsFolder.Speed.Value
local RANGE = statsFolder.Range.Value
local DAMAGE = statsFolder.Damage.Value
local PROJECTILE_TYPE = statsFolder.ProjectileType.Value
local FIREBALL_OFFSET = statsFolder.FireballOffset.Value
local GRAVITY = statsFolder.Gravity.Value
local COOLDOWN_TIME = statsFolder.CooldownTime.Value
local COOLDOWN_LOOP_WAIT = COOLDOWN_TIME * 1.2 -- leeway for network delay
local stateFolder = tool.State
local lastActivationTick = tick()
local looping = false
local function shoot()
local now = tick()
if now - lastActivationTick < COOLDOWN_TIME then return end
lastActivationTick = now
local origin = handle.Position + FIREBALL_OFFSET
local mouseDirection = (mouse.Hit.Position - origin).Unit
local direction = mouseDirection * RANGE
local velocity = mouseDirection * SPEED
local projectileCosmeticPart = nil
local ignoreWater = true
local bulletAcceleration = GRAVITY
local canPierceFunction = function(hitPart, hitPoint, normal, material)
return false
end
local projectileData = {
projectileType = PROJECTILE_TYPE,
player = player,
character = character,
}
ProjectileReplication_Client.fireProjectile(
projectileData,
origin,
direction,
velocity,
nil,
projectileCosmeticPart,
ignoreWater,
bulletAcceleration,
canPierceFunction
)
shootEvent:FireServer(mouseDirection)
end
local function shootLoop()
if looping then return end
looping = true
while looping do
shoot()
wait(COOLDOWN_LOOP_WAIT)
end
end
tool.Equipped:Connect(function()
character = tool.Parent
rightHand = character:WaitForChild("RightHand")
mouse.TargetFilter = character
end)
tool.Unequipped:Connect(function()
looping = false
end)
tool.Activated:Connect(shootLoop)
tool.Deactivated:Connect(function()
looping = false
end)
Fireball_Server Tool Script
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local Quire = _G.Quire
local NetworkUtil = Quire("NetworkUtil")
local ProjectileReplication_Server = Quire("ProjectileReplication_Server")
local projectileReplicationEvent = NetworkUtil:GetRemoteEvent("ProjectileReplicationEvent")
local tool = script.Parent
local handle = tool:WaitForChild("Handle")
local player = Players:FindFirstChild(tool.Parent.Name) or tool:FindFirstAncestorOfClass("Player")
local character = player.Character
local rightHand = character.RightHand
local remotesFolder = tool.Remotes
local shootEvent = remotesFolder.ShootEvent
local statsFolder = tool.Stats
local SPEED = statsFolder.Speed.Value
local RANGE = statsFolder.Range.Value
local DAMAGE = statsFolder.Damage.Value
local PROJECTILE_TYPE = statsFolder.ProjectileType.Value
local FIREBALL_OFFSET = statsFolder.FireballOffset.Value
local GRAVITY = statsFolder.Gravity.Value
local COOLDOWN_TIME = statsFolder.CooldownTime.Value
local stateFolder = tool.State
local lastActivationTick = tick()
local function shoot(mouseDirection)
local now = tick()
if now - lastActivationTick < COOLDOWN_TIME then return end
lastActivationTick = now
local origin = handle.Position + FIREBALL_OFFSET
local direction = mouseDirection * RANGE
local velocity = mouseDirection * SPEED
local projectileCosmeticPart = nil
local ignoreWater = true
local bulletAcceleration = GRAVITY
local canPierceFunction = function(hitPart, hitPoint, normal, material)
return false
end
local projectileData = {
projectileType = PROJECTILE_TYPE,
damage = DAMAGE,
player = player,
character = character,
}
ProjectileReplication_Server.fireProjectile(
projectileData,
origin,
direction,
velocity,
nil,
projectileCosmeticPart,
ignoreWater,
bulletAcceleration,
canPierceFunction
)
end
shootEvent.OnServerEvent:Connect(function(plr, direction)
if plr == player and assert(direction.Magnitude <= 1.1, "invalid direction") then
shoot(direction)
end
end)
It works, but is there any drawbacks when using only one caster for every projectile?, and I’m also having to pass an additional parameter (projectileData) to fast cast and passing it back through the events (RayHit, etc)
I haven’t had any performance issues yet, but I don’t have access to a large amount of testers to see if there will be