How to Properly Implement a Weapon System with Inheritance

I recently ran into this issue where I needed to figure out the best way to implement my abstract Weapon class without creating too many dependencies (especially on the client side). Here’s what my current code looks like:

Server Controller
local class = script.Parent.Parent

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Assets = ReplicatedStorage.Assets
local Event = ReplicatedStorage.Event

local Utils = require(ReplicatedStorage.Module.Utils)

local idCounter = Utils.IdCounter.new()

local Controller = {}
Controller.__index = Controller

export type Weapon = typeof(setmetatable(
 {} :: {
  tool: Tool,
  
  cooldown: number,
  took: number,
  
  owner: Player,
 },
 {} :: typeof(Controller)
))

function Controller.Activate(self: Weapon, ...)
 print("Weapon has used") 
end

function Controller.Cooldown(self: Weapon)
 self.took = os.clock()
end

function Controller.CanActivate(self: Weapon): boolean
 return os.clock() - self.took > self.cooldown
end

function Controller.IsOwner(self: Weapon,
 player: Player): boolean
 
 return self.owner.UserId == player.UserId
end

local function Init(weapon: Weapon)
 local weaponId = idCounter:Add()
 
 class.ClientInit:FireClient(
  weapon.owner, weapon.tool,
  weaponId, weapon.cooldown
 )

 class.Activation.OnServerEvent:Connect(function(plr, id)
  if id ~= weaponId then return end
  if not weapon:IsOwner(plr) then return end
  
  if weapon:CanActivate() then weapon:Activate(); weapon:Cooldown() end
 end)
 
 local plrChar = weapon.owner.Character or weapon.owner.CharacterAdded:Wait()
 local playerHumanoid: Humanoid = plrChar.Humanoid
 
 playerHumanoid:EquipTool(weapon.tool)
end

Controller.new = function(name: string, 
 owner: Player, cooldown: number): Weapon
 
 local tool: Tool = Assets.Weapons[name]
 
 local self = {
  tool = tool,
  owner = owner,
  
  cooldown = cooldown,
  took = 0
 }
 
 Init(setmetatable(self, Controller))
 
 return self
end

return Controller
Client Controller
local class = script.Parent.Parent

local Controller = {}
Controller.__index = Controller

export type Weapon = typeof(setmetatable(
 {} :: {
  tool: Tool,
  cooldown: number,
  id: number,
 },
 {} :: typeof(Controller)
))


function Controller.BuildActivationData(self: Weapon): any...
 return 0
end

function Controller.Activate(self: Weapon): boolean
 class.Activation:FireServer(self.id, self:BuildActivationData())
end

Controller.new = function(tool: Tool,
 id: number, cooldown: number): Weapon
 
 local self = {
  tool = tool,
  cooldown = cooldown,
  id = id,
 }
 
 return setmetatable(self, Controller)
end

return Controller
Client Handler
local class = script.Parent.Parent

local Players = game:GetService("Players")
local LocalPlayer = Players.LocalPlayer

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Weapon = require(script.Parent.Controller)

local Handler = {}

Handler.Init = function()
 class.ClientInit.OnClientEvent:Connect(
  function(tool: Tool, id, cooldwon)
   local weapon = Weapon.new(tool, id, cooldwon)
   
   tool.Activated:Connect(function()
    weapon:Activate()
   end)
 end)
 
 Handler.Init = nil
end

return Handler

What I’m really aiming for is to be able to easily create child classes and override the BuildActivationData method without everything falling apart. You know how it goes - you start with what seems like a simple system, and before you know it, you’ve got dependency spaghetti!

I want to make it super straightforward to extend the base weapon functionality. Like, if I need to add a shotgun, or rocket launcher, I should just be able to create a new class that inherits from Weapon and only worry about the specific behavior that makes that weapon unique.

1 Like

Personally, I’d recommend using a hybrid approach between inheritance and composition or else going fully with composition. Inheritance works fine until your classes start to scale, and then it suddenly becomes hell, you change one thing in a subclass, and you end up dealing with ten errors and “compatibility” issues (not sure how to put it better, sorry). It’s just not that versatile.

Composition, on the other hand, solves most of the problems inheritance has. It’s extremely flexible, and when done properly it feels like coding on the softest cloud in heaven. You can mix and match many components to create multiple variants of the same mechanic and easily apply them to any weapon.

Of course, composition has its own issues too, but it’s definitely the approach I’d follow for a weapon system.

First of all, I’d suggest creating a “WeaponsFactory”: a module script that decides which components a tool/weapon should have based on its characteristics. For example, you could have:

  1. Firing components to handle fire modes (auto, burst, semi, laser-like),

  2. Bullet/shoot components for different projectile types (hitscan, physical projectiles that simulate physics whit math, maybe even homing),

  3. Reloading components for different reload systems (magazines, overheating, etc.).

  4. I’m not making you the work bro come on you can think in more components.

The factory would gather all the necessary components into a table and send it to your weapon constructor, which then handles inputs, activations, and so on.

If you plan to add other weapon types (like melee or grenades), then I’d recommend a hybrid approach:
Create a superclass (BaseWeapon) that handles basic inputs (activation/deactivation, equipping, etc.), and then make subclasses (Firearm, Melee, etc.) that inherit these core connections while adding their own behavior. The factory would then group components based on the weapon type, and everything would still work as described with your component system.

If you design your components well, you’ll see how easily they can be combined.

Need a firearm?: Hitscan component + magazine reload + automatic/semi/burst firing component.

Need a rocket launcher?: Projectile component + magazine reload + semi/burst firing component.

Need a flamethrower?: Projectile or hitscan component + laser-like firing + overheat component.

Need a shotgun?: Hitscan + semi + magazine reload.

I think you already understood the idea.

As for the server side, it really depends on your goals. Some games prioritize dynamism over security, but in most cases, some sanity checks based on the components and weapon type should be enough.

I hope this helps, I can tell you really care about keeping your code clean and organized, and that’s great. My best advice would be: don’t try to bury yourself chasing the “perfect” system as it doesn’t exist. That was my biggest mistake when I first started writing modular systems. Just focus on what you actually need and can maintain in the future.
And also, god I think I blessed composition and OOP here a lot I really hope @Yarik_superpro doesn’t find this soon :folded_hands:

2 Likes

I do not condone harassment in my adress

Speaking of your post, you made it extremely hard to read or interpret what could otherwise be described in 2-5 sentences at most.
All the games on Roblox must be made through ECS or pure functional programming, as this is the most compatible with language paradigms that are capable of having the most optimal speed possible and being scalable.
OOP, or inheritance, is absolutely alien and hostile to the optimization of Luau; the compiler does not attempt to optimize, and Luau directly advises you against using inheritance chains of any kind.

Sorry about that, I won’t do it again.
I’m not the best at writing, and English isn’t my native language. I got a bit off-topic, but this was one of my first replies on the DevForum and I tried to make it as detailed as possible.

I actually like reading your posts a lot. I know you base your arguments on bytecode and low-level stuff that feels like black magic to me :sweat_smile:.
Regarding ECS, I agree that it could work too — I’m just not as experienced with it as I am with OOP. I do understand that OOP isn’t a native paradigm in Luau and that metatables are kind of a workaround, but if it’s used only on the client and with a reasonable number of weapons, I don’t think it would cause any overhead.

edit: In the first reply I used chatgpt to improve syntaxis and now I noticed that he tagged you, I just was going to mention your name, sorry.

1 Like

can OOP fix your problem? OOP helped fix a similar problem i had and i fixed it by making a Weapon module then making modules for every gun. inherit functions from Weapon module and override them if needed. so that way the Weapon is the base class and guns are child classes

i do that for both client and server (server for functionality, client for visual stuff)