OOP Custom Tools (Fireballs, Guns, Swords, etc)

Hello, I’m looking for alternate ideas, or confirmation on if my current way of doing tools is correct

what I’m currently doing is creating a tool object (gun in my example) on the server and then telling the client to do the same

Equpping:
server tool Equipped > create tool object > fire client > create tool object

-- on server
local serverGun = ServerGun.new()
-- client
local clientGun = ClientGun.new()

Dequipping:
server tool destroyed > fire client > client tool destroyed

-- on server
serverGun:Destroy()
-- client
clientGun:Destroy()

working example I made while messing around with fastcast (isn’t the neatest code)

Server Gun
-- Services
local Players = game:GetService("Players")
-- Modules
local Quire = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").Quire)
local Weld = Quire("Weld")
local Maid = Quire("Maid")
local FastCast = Quire("FastCast")
local Network = Quire("Network")


local replicateProjectileEvent = Network("replicateProjectileRemoteEvent", "RemoteEvent")


local ServerGunModule = {}
ServerGunModule.__index = ServerGunModule

function ServerGunModule.new(player, character, humanoid, weaponInfo)
    local self = setmetatable({
        Player = player,
        Character = character,
        Model = weaponInfo.Model:Clone(),
        WeaponInfo = weaponInfo,
        Caster = FastCast.new(),
        Debounce = tick(),
        Maid = Maid.new()
    }, ServerGunModule)
    
    
    self:Weld()
    
    local connection = self.Caster.RayHit:Connect(function(hit, hitPos, norm, mat, bullet)
        if not hit then return end

        local enemyChar = hit.Parent
        local enemyHumanoid = enemyChar:FindFirstChild("Humanoid")

        if enemyHumanoid and enemyChar ~= character then
            enemyHumanoid.Health = enemyHumanoid.Health - weaponInfo.BulletDamage
        end
    end)

    self.Maid:GiveTask(function()
        connection:Disconnect()
    end)

    return self, self.Model
end

function ServerGunModule:Shoot() -- runs when client fires server to shoot
    if (tick() - self.Debounce) < self.WeaponInfo.Cooldown then return end
    self.Debounce = tick()
    

    local origin = self.Model.Barrel.Position
    local direction = self.Character.PrimaryPart.CFrame.LookVector * 999
    local velocity = self.WeaponInfo.BulletVelocity

    self.Caster:Fire(origin, direction, velocity)
    replicateProjectileEvent:FireAllClients(self.Player, origin, direction, velocity, self.WeaponInfo)
end

function ServerGunModule:Weld()
    local model = self.Model
    local modelPrimary = model.PrimaryPart
    local characterHand = self.Character.RightHand

    model.Parent = self.Character

    modelPrimary.CFrame = characterHand.CFrame

    Weld(modelPrimary, characterHand)
    self.Maid:GiveTask(model)
end

function ServerGunModule:Destroy()
    self.Maid:Destroy()
end



return ServerGunModule
Client Gun
-- Services
local UserInputService = game:GetService("UserInputService")

-- Modules
local Quire = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").Quire)
local Animations = Quire("Animations")
local ClientProjectileHandler = Quire("ClientProjectileHandler")
local Maid = Quire("Maid")
local Network = Quire("Network")

-- Remotes
local shootEvent = Network("ShootRemoteEvent", "RemoteEvent")



local ClientGunModule = {}
ClientGunModule.__index = ClientGunModule

function ClientGunModule.new(weaponInfo, character, humanoid, weaponModel)
    local self = setmetatable({
        Character = character,
        Humanoid = humanoid,
        Maid = Maid.new(),
        WeaponInfo = weaponInfo,
        Debounce = tick(),
        Model = weaponModel,
        Animations = {}
    }, ClientGunModule)
    
    self:HoldAnimation(true)
    
    self.Maid:GiveTask(
        UserInputService.InputBegan:Connect(function(input, proc)
            if input.UserInputType == Enum.UserInputType.MouseButton1 and not proc then
                self:Shoot()
            end
        end)
    )
    return self
end

function ClientGunModule:Shoot()
    if (tick() - self.Debounce) < self.WeaponInfo.Cooldown then return end
    self.Debounce = tick()
    
    shootEvent:FireServer()

    local origin = self.Model.Barrel.Position
    local direction = self.Character.PrimaryPart.CFrame.LookVector * 999
    local velocity = self.WeaponInfo.BulletVelocity
    
    local newBullet = self.WeaponInfo.Bullet:Clone()
    newBullet.Parent = workspace

    ClientProjectileHandler.Fire(origin, direction, velocity, newBullet)

end

function ClientGunModule:HoldAnimation(play)
    local holdAnim = Animations["Hold"]
    local isPlaying = holdAnim.IsPlaying

    if play and not isPlaying then
        holdAnim:Play()
    elseif not play and isPlaying then
        holdAnim:Stop()
    end
end

function ClientGunModule:Destroy()
    self.Maid:Destroy()
end

return ClientGunModule

I didn’t show 100% of the code (remotes etc) , but this is likely enough to understand what I’m doing

the only other idea I currently have is to have one local and one script and put it inside a character when they need to equip a tool

is there better alternatives to my method?

4 Likes

You dont need two seprate scripts; you can have 1 script. You can seperate the client and the server code by a simple if statment. the way to do this is simple:

local RunService = game:GetService("RunService")
local isServer = RunService:IsServer()
if isServer then
 -- cool server stuff
else
-- client stuff
end

this makes replication easy, esp since you don’t have to worry about linking separate portions with meta tables and have prone errors.
To make the tool work, you will have to have a ton of handlers to remove client replicated code or to add client replicated code when it is either picked up, dropped, or even has its ownership transferred to another player. I will also recommend making a remote system that autocratically updates remote tables. For the server, it is just a simple data collection; for the client, there is a handler required to listen for a child Added event to prevent more complexity.

4 Likes