How should I go about making a weapon system?

I’m creating a game and I need some help on creating a weapon system. I’ve never been experienced in creating weapons but now I’ve tried and over the past few months, 13 weapons have been created! Most of them worked but they had some flaws and about 1/4th of them had issues, but that’s not what I’ll be talking about on this post. I want to know how I can make a organized and easy to navigate weapon system.

My current ideas is to have all of the weapons stats, (damage, weight / affectsSpeed?, abilities, cooldown, etc…), passives, and abilities inside of one module, with the module also having functions for the weapon that the weapon can access through a server script.

Whenever I have an issue with my bombs, it may be caused by the base code but the issue with that is that if I have a problem with that base code then if I make a change to fix it then I need to change EVERY, SINGLE, BOMB that also has that base code and considering that me and my developing partner / friend have come up with 57 weapons and counting, and so it will be very hard to navigate and manage this.

There’s also other weapons that I will also need to manage like swords, slingshots, superballs, and launchers that I will have to navigate through which will make this harder.

This is the current hierarchy of children of the average bomb.

image

This is the current script for the average bomb (Classic Bomb).
local tool = script.Parent
local bombHandle = tool:WaitForChild("Handle")
local ActivateBombRemote = script.Parent:WaitForChild("ActivateBomb")

local tickSound = Instance.new("Sound")
tickSound.SoundId = "rbxasset://sounds\\clickfast.wav"
tickSound.Parent = tool

local explosionSound = Instance.new("Sound")
explosionSound.SoundId = "rbxasset://sounds\\Rocket shot.wav"
explosionSound.Parent = tool

local blastRegion3L = 10/2 -- The Length of the Blast (Dividing all of the dimensions by 2 so that the region3 is not twice its size after making its size relative to the position)
local blastRegion3W = 10/2 -- The Width of the Blast
local blastRegion3H = 10/2 -- The Maximum height of the region3

local baseDamage = 25

function explosionParticles(bomb)
	local explosionClone = bombHandle.Explosion:Clone()
	explosionClone.Parent = bomb
	explosionClone.Enabled = true

	return explosionClone
end

function explode(bomb, user)
	for i = 1, 3 do task.wait(1)
		tickSound:Play()
	end

	task.wait(1)

	bomb.Anchored = true -- Anchores the bomb so that it doesn't go elsewhere due to the explosion.

	local explosionParticle = explosionParticles(bomb)
	local explosionSoundClone = explosionSound:Clone()
	explosionSoundClone.Parent = bomb
	explosionSoundClone:Play()

	bomb.Transparency = 1

	local region3 = Region3.new(
		bomb.Position - Vector3.new(blastRegion3L, blastRegion3H, blastRegion3W),
		bomb.Position + Vector3.new(blastRegion3L, blastRegion3H, blastRegion3W) -- Makes a square that is 5x5 long and wide, and 10 studs high (hence the 1 to 10)
	) -- Creates a region3 that destroys any parts in the explosions radius.

	local region3Visual = Instance.new("Part") -- The visualized part for the region3.
	region3Visual.Name = "blastRadiousVisual"
	region3Visual.CanCollide = false
	region3Visual.Size = region3.Size
	region3Visual.CFrame = region3.CFrame
	region3Visual.Anchored = true
	region3Visual.Transparency = .5
	region3Visual.Parent = workspace.Terrain
	region3Visual.BrickColor = BrickColor.new("Persimmon")

	local explosion = Instance.new("Explosion") -- The explosion instance
	explosion.Parent = bomb
	explosion.Position = bomb.Position
	explosion.BlastRadius = 0

	explosion.DestroyJointRadiusPercent = 0
	explosion.BlastPressure = 900000

	explosion.BlastRadius = blastRegion3L

	local overlapParams = OverlapParams.new()
	overlapParams.FilterDescendantsInstances = {tool, tool.Handle, region3Visual}

	local parts = workspace:GetPartBoundsInBox(region3.CFrame, region3.Size, overlapParams)

	explosion.Hit:Connect(function(Part)
		if Part.Name == "HumanoidRootPart" then
			local character = Part.Parent
			local player = game.Players:GetPlayerFromCharacter(character)

			if player and player.Team ~= user.Team then
				character:FindFirstChildOfClass("Humanoid"):TakeDamage(baseDamage)

				if character:FindFirstChildOfClass("Humanoid").Health == 0 then
					user.leaderstats.Kills.Value += 1
				end

			elseif player ~= user and player.Neutral == true then
				character:FindFirstChildOfClass("Humanoid"):TakeDamage(baseDamage)

				if character:FindFirstChildOfClass("Humanoid").Health == 0 then
					user.leaderstats.Kills.Value += 1
				end
			end
		end
	end)

	for i, v in pairs(parts) do
		if v.Name == "Brick" then
			v.Anchored = false

			v.CanTouch = false

			v.BrickColor = user.TeamColor

			for i, constraint in pairs(v:GetChildren()) do
				if constraint:IsA("Snap") or constraint:IsA("Weld") or constraint:IsA("WeldConstraint") then
					constraint:Destroy()
				end
			end

			task.delay(2, game.Destroy, v)
		end

		if v.Name == "GameBrick" then v.Name = "TaggedGameBrick" -- TaggedGameBricks are GameBricks in the game that have been effected by the bomb, while regular Bricks are just bricks you can destroy by default.
			v.Anchored = false

			v.CanTouch = false

			user.leaderstats.Bricks.Value += 1 -- The total bricks the player has broken throughout the round.
			user["T. Bricks"].Value += 1 -- The total bricks that the player has destroyed throughout there playtime
			user.leaderstats.Studs.Value += .1 -- Increases the players currency (Studs) by 0.1.
			v.BrickColor = user.TeamColor -- Changes the taggedBricks color to the local players team color to show that they have broken the part.

			task.delay(2, game.Destroy, v) -- later deletes the part.
		end
	end

	task.delay(3, game.Destroy, bomb)
	task.delay(3, game.Destroy, region3Visual)

	task.wait(2)

	explosionParticle.Enabled = false -- Disables the explosion particles.
end

ActivateBombRemote.OnServerEvent:Connect(function(user) -- This is the function that makes the bomb go off, which connects the ActivateBomb Remote Event to this script after it gets activated by the local script.
	local bombClone = bombHandle:Clone()
	bombClone.Parent = game.Workspace
	bombClone.CanCollide = true
	bombClone.CFrame = bombHandle.CFrame * CFrame.new(0, 2, 2)

	explode(bombClone, user)
end)

i

5 Likes

I’d recommend you create a module script with a weapon class so you don’t have to constantly modify stuff.

And yeah I did read the above.

local Weapon = setmetatable({}, {})
Weapon.__index = Weapon

function Weapon.new()
local self = setmetatable({__index = Weapon}, Weapon)
self.Damage = 1 -- base damage
return self
end
2 Likes

I have barely any knowledge on OOP or meta tables and judging from the code it seems like your using those two concepts, do you know any useful recourses that I can use to understand this code better?

2 Likes

Sorry for the kinda late response!

I’ve already referenced this post I’ve made once, and well; I think it’s a pretty simple explanation.

If it doesn’t help you then I can provide some other ones too!

Here: Metatables: Explain like I'm a toddler

Once you’ve grasped metatables, we can move into understanding that code I wrote:

3 Likes

This tutorial at the moment is ONLY focused on the OOP basics and inheritance

Quick note

local self = setmetatable({__index = Weapon}, Weapon)

Is the same thing as:

local self = setmetatable({}, Weapon)
self.__index = Weapon


After you’ve got a good understanding of my post, we can move into OOP.

Now what is OOP? OOP stands for Object Oriented Programming.

Why do people use OOP over “normal coding”? Well, OOP is a great way to organize code and overall make it look good!

Let’s start with a basic example:

local Class = setmetatable({}, {})
Class.__index = Class

function Class.new()
    local self = setmetatable({}, Class)
    self.__index = Class
    self.RandomValue = “shoot idk”
    return self
end

return Class

You might be wondering: “What is a class?”. Classes in simple terms is just a “blueprint” that helps create a framework for objects. Like a Weapon class would help create a Gun class or a Melee class.

Classes in Lua often create objects via a .new
method.

function Class.new()

Just creating a new ”.new” function. Nothing special cuz it’s just like any other function.

local self = setmetatable({}, Class)
self.__index = Class

Just setting the self variable to a child metatable (for which the metatable is the Class).

Then I’m just setting the __index function to the Class so that the object has access to ALL of the Class’ “stuff”.

Finally, I’m just returning the object so that I can set it to something that can be used later!


Now that we’re done with the basics, we can move to cool methods :sunglasses:

-- Weapon class:

local Weapon = setmetatable({}, {})
Weapon.__index = Weapon

function Weapon.new()
     local self = setmetatable({__index = Weapon}, Weapon)
     self.Type = nil
     self.Damage = 1 -- default
     return self
end

function Weapon:DealDamage()
    print(`{self.Type} just dealt {self.Damage} to something!`)
end

return Weapon

-- Gun class

local Weapon = require(path.to.module)
local Gun = setmetatable({}, {})
Gun.__index = Weapon

function Gun.new()
   local self = setmetatable(Weapon.new(), Gun)
   self.__index = Gun
   self.Type = “Gun”
   self.Damage = 10
return self
end

return Gun

You can probably see where this is going.

local self = setmetatable(Weapon.new(), Gun)
self.__index = Gun

Okay so here I’m just setting an object’s metatable returned from the Weapon.new function to the Gun class.

This new object has its __index method set to the Gun class. And the Gun class’ __index method is attached to the Weapon class which means that this new object also has access to the Weapon class’ methods too!

You might be asking yourself: “In the .new function you had to define self. But in the :Damage function you didn’t have to?”.

Well, if the function uses a colon : and not a period . then the self method is automatically whatever is calling it.

Now, what I mean by whatever calling it is that the Weapon class or an object from the Weapon class is using the :Damage method.

DISCLAIMER

The self variable can also be assigned so PLEASE keep that in mind!

For example:

-- Server Script:

local Gun = require(path.to.module)
local NewGun = Gun.new()

NewGun:DealDamage()
-- Output: Gun just dealt 10 damage to something!

How is the new gun object able to call :Damage() when it’s not explicitly being defined in the gun module? Well, remember how I set the new Weapon object’s __index method to the Weapon class (set in the Weapon.new() function)?

Well, the new gun module’s object is just the Weapon.new function which means that it has access to the Weapon classes’ functions!


Alright, hopefully that wasn’t too confusing. With all of this, we can finally piece all the scripts together like so:

-- Weapon class:

local Weapon = setmetatable({}, {})
Weapon.__index = Weapon

function Weapon.new()
local self = setmetatable({__index = Weapon}, Weapon)
self.Type = “Default Weapon”
self.Damage = 1
return self
end

function Weapon:DealDamage()
print(`{self.Type} just dealt {self.Damage} to something!`)
end

return Weapon

-- Gun class

local Gun = setmetatable({}, {})
Gun.__index = Weapon

function Gun.new()
   local self = setmetatable(Weapon.new(), Gun)
	self.Type = "Gun"
   self.Damage = 10
   return self
end

return Gun

-- Extra bonus Melee class

--local Weapon = require(path.to.module)
local Melee = setmetatable({}, {})
Melee.__index = Weapon

function Melee.new()
    local self = setmetatable(Weapon.new(), Melee)
    self.__index = Melee
    self.Type = "Melee"
    self.Damage = 1000
	return self
end

return Melee

-- Server script

--local Gun = require(path.to.module)
--local Melee = require(path.to.module)
--local Weapon = require(path.to.module)

local MyGun = Gun.new()
MyGun:DealDamage()
-- Output: Gun just dealt 10 damage to something!

local MyMelee = Melee.new()
MyMelee:DealDamage()
-- Output: Melee just dealt 1000 damage to something!

local MyWeapon = Weapon.new()
MyWeapon:DealDamage()
-- Output: Weapon just dealt 1 damage to something!

And now everything is complete! Sorry if I made this a bit not beginner friendly as I was rushing to make this (although I still took 30+ minutes to finish this post).

Ima edit this post and make it easier to understand for future references.

Please correct me on some stuff if I’m wrong! I’m always tryna learn something!q

4 Likes

uhh
image

Not sure if I did anything wrong, but when I was trying to understand this I tried putting a script and a module script in SSS but it didn’t seem to work.

Script

local Weapon = require(script.Parent:WaitForChild("ModuleScript"))
local Gun = setmetatable({}, {})
Gun.__index = Weapon

Weapon.new()

function Gun.new()
	local self = setmetatable(Weapon.new(), Gun)
	self.__index = Gun
	self.Type = "Gun"
	self.Damage = 10
	return self
end

-- Extra bonus Melee class

local Weapon = require(script.Parent:WaitForChild("ModuleScript"))
local Melee = setmetatable({}, {})
Melee.__index = Weapon

function Melee.new()
	local self = setmetatable(Weapon.new(), Melee)
	self.__index = Melee
	self.Type = "Melee"
	self.Damage = 1000
	return self
end

-- Server script

local Gun = require(script.Parent:WaitForChild("ModuleScript"))
local Melee = require(script.Parent:WaitForChild("ModuleScript"))
local Weapon = require(script.Parent:WaitForChild("ModuleScript"))

Gun:Damage()
-- Output: Gun just dealt 10 damage to something!

Melee:Damage()
-- Output: Melee just dealt 1000 damage to something!

Weapon:Damage()
-- Output: Weapon just dealt 1 damage to something!

Module Script

local module = {}

local Weapon = setmetatable({}, {})
Weapon.__index = Weapon

function Weapon.new()
	local self = setmetatable({__index = Weapon}, Weapon)
	self.Type = nil
	self.Damage = 1 -- default
	return self
end

function Weapon:Damage()
	print(`{self.Type} just dealt {self.Damage} to something!`)
end


return module

2 Likes

The code doesn’t seem to work with.new and I get an error saying that the function is not a function of Gun

2 Likes

You aren’t returning the actual Weapon in your modulescript, just remove the 1st line and replace ‘return module’ with ‘return Weapon’

1 Like

Here’s what I use for inheritance:

local Weapon = require(PATH_TO_WEAPON)

local Gun = {}
Gun .__index = Gun 
setmetatable(Gun, Weapon) 

function Gun.new(params)
  local self = setmetatable(Weapon.new(params), Gun)

  return self
end
1 Like

Yeah that’s my fault, I was in a rush sorry! I’ll edit it to make it correct soon!

Also, I forgot to mention that I wrote this on mobile and I didn’t use separate code snippet separators soo yeah.

Added + fixed:

  • Forgot to create new objects (added to post)
  • Forgot to return the modules (cuz I didn’t write em in studio)
  • Fixed syntax… yep

When you see the comment that says
[name] class”,
that’s a separator between modules.

So the Weapon class and Melee class should be in DIFFERENT module scripts

And make sure to RETURN the Weapon class and get rid of the default module table.

If you do it correctly, it should work now as I’ve just tested the code and it works perfectly :+1:

1 Like

I forgot to ask this hours earlier, but what does this exactly do?

{__index = Weapon}

1 Like

It’s basically the same thing as this:

local self = setmetatable({}, Weapon)
self.__index = Weapon

I’m just setting the function inside the table to well, be lazy :smiley:

Now I’m kinda driving this in a different direction. Basically when you start to understand the code, I’ll show you how you COULD actually pair it with real models and instances:

-- example server script

local Gun = require(path.to.module)
local Melee = require(path.to.module)
local BuyGun = path.to.event
local GunModel = path.to.model
local MeleeModel = path.to.model

BuyGun.OnServerEvent:Connect(function(plr, weaponType)
   if weaponType == “Gun” then
      local NewGun = Gun.new()
      NewGun.Model = GunModel
      NewGun.Model.Parent = plr:WaitForChild(“Backpack”)
   elseif weaponType == “Melee” then
      local NewMelee = Melee.new()
      NewMelee.Model = MeleeModel
      NewMelee.Model.Parent = plr:WaitForChild(“Backpack”)
       end
   end
end)

Now this is just a simple little example of how it would be used.

Instead of doing that though, why not just have a list of every single class and weapon?

1 Like

You could, I’m just using a really basic example. Now I think I’m too lazy to continue writing so I guess you could give it a shot.

1 Like

Now that I think about it, in what other instances would you want to use OOP? For someone who doesn’t really have that much in depth knowledge in it, I’m not really sure what I would use for it other than this.

1 Like

Really anything.

Here’s a little example:

local Vector4 = setmetatable({}, {})
Vector4.__index = Vector4

function Vector4.new(x, y, z, a)
local self = setmetatable({
[“X”] = x, 
[“Y”] = y, 
[“Z”] = z, 
[“A”] = a}, Vector4)
self.__index = Vector4
return self
end

function Vector4:Get()
print(“Dunno”)
end

return Vector4
local House = setmetatable({}, {})
House.__index = House

function House.new()
local self = setmetatable({}, House)
self.__index = House
return self
end

function House:idk()
print(“still dunno”)
end

return House

Really you can build anything. According to another post, Roblox builds all of the LuaU classes similar to the example above with well, OOP.

I might even rewrite my games’ code just to make it easier to read.

Although I do prefer OOP composition, I still do use inheritance A LOT

I have a script in each of my bombs that include base parts of code for each bomb needs to actually explode. Do you think it would be smart for me to make a module including one explosion function that each bomb can use?

The only issue with me doing that though is that some bombs have extended code because they have more functionality compared to others, like how the classic bomb only has one bomb, but the cluster bomb explodes and then spawns 5 mini bombs that also explode but do half the damage and have half the radius of the cluster bomb.

local tool = script.Parent
local bombHandle = tool:WaitForChild("Handle")
local ActivateBombRemote = script.Parent:WaitForChild("ActivateBomb")

local tickSound = Instance.new("Sound")
tickSound.SoundId = "rbxasset://sounds\\clickfast.wav"
tickSound.Parent = tool

local explosionSound = Instance.new("Sound")
explosionSound.SoundId = "rbxasset://sounds\\Rocket shot.wav"
explosionSound.Parent = tool

local blastRegion3L = 10/2 -- The Length of the Blast (Dividing all of the dimensions by 2 so that the region3 is not twice its size after making its size relative to the position)
local blastRegion3W = 10/2 -- The Width of the Blast
local blastRegion3H = 10/2 -- The Maximum height of the region3

local baseDamage = 25

function explosionParticles(bomb)
	local explosionClone = bombHandle.Explosion:Clone()
	explosionClone.Parent = bomb
	explosionClone.Enabled = true

	return explosionClone
end

function explode(bomb, user)
	for i = 1, 3 do task.wait(1)
		tickSound:Play()
	end

	task.wait(1)

	bomb.Anchored = true -- Anchores the bomb so that it doesn't go elsewhere due to the explosion.

	local explosionParticle = explosionParticles(bomb)
	local explosionSoundClone = explosionSound:Clone()
	explosionSoundClone.Parent = bomb
	explosionSoundClone:Play()

	bomb.Transparency = 1

	local region3 = Region3.new(
		bomb.Position - Vector3.new(blastRegion3L, blastRegion3H, blastRegion3W),
		bomb.Position + Vector3.new(blastRegion3L, blastRegion3H, blastRegion3W) -- Makes a square that is 5x5 long and wide, and 10 studs high (hence the 1 to 10)
	) -- Creates a region3 that destroys any parts in the explosions radius.

	local region3Visual = Instance.new("Part") -- The visualized part for the region3.
	region3Visual.Name = "blastRadiousVisual"
	region3Visual.CanCollide = false
	region3Visual.Size = region3.Size
	region3Visual.CFrame = region3.CFrame
	region3Visual.Anchored = true
	region3Visual.Transparency = .5
	region3Visual.Parent = workspace.Terrain
	region3Visual.BrickColor = BrickColor.new("Persimmon")

	local explosion = Instance.new("Explosion") -- The explosion instance
	explosion.Parent = bomb
	explosion.Position = bomb.Position
	explosion.BlastRadius = 0

	explosion.DestroyJointRadiusPercent = 0
	explosion.BlastPressure = 900000

	explosion.BlastRadius = blastRegion3L

	local overlapParams = OverlapParams.new()
	overlapParams.FilterDescendantsInstances = {tool, tool.Handle, region3Visual}

	local parts = workspace:GetPartBoundsInBox(region3.CFrame, region3.Size, overlapParams)

	explosion.Hit:Connect(function(Part)
		if Part.Name == "HumanoidRootPart" then
			local character = Part.Parent
			local player = game.Players:GetPlayerFromCharacter(character)

			if player and player.Team ~= user.Team then
				character:FindFirstChildOfClass("Humanoid"):TakeDamage(baseDamage)

				if character:FindFirstChildOfClass("Humanoid").Health == 0 then
					user.leaderstats.Kills.Value += 1
				end

			elseif player ~= user and player.Neutral == true then
				character:FindFirstChildOfClass("Humanoid"):TakeDamage(baseDamage)

				if character:FindFirstChildOfClass("Humanoid").Health == 0 then
					user.leaderstats.Kills.Value += 1
				end
			end
		end
	end)

	for i, v in pairs(parts) do
		if v.Name == "Brick" then
			v.Anchored = false

			v.CanTouch = false

			v.BrickColor = user.TeamColor

			for i, constraint in pairs(v:GetChildren()) do
				if constraint:IsA("Snap") or constraint:IsA("Weld") or constraint:IsA("WeldConstraint") then
					constraint:Destroy()
				end
			end

			task.delay(2, game.Destroy, v)
		end

		if v.Name == "GameBrick" then v.Name = "TaggedGameBrick" -- TaggedGameBricks are GameBricks in the game that have been effected by the bomb, while regular Bricks are just bricks you can destroy by default.
			v.Anchored = false

			v.CanTouch = false

			user.leaderstats.Bricks.Value += 1 -- The total bricks the player has broken throughout the round.
			user["T. Bricks"].Value += 1 -- The total bricks that the player has destroyed throughout there playtime
			user.leaderstats.Studs.Value += .1 -- Increases the players currency (Studs) by 0.1.
			v.BrickColor = user.TeamColor -- Changes the taggedBricks color to the local players team color to show that they have broken the part.

			task.delay(2, game.Destroy, v) -- later deletes the part.
		end
	end

	task.delay(3, game.Destroy, bomb)
	task.delay(3, game.Destroy, region3Visual)

	task.wait(2)

	explosionParticle.Enabled = false -- Disables the explosion particles.
end

ActivateBombRemote.OnServerEvent:Connect(function(user) -- This is the function that makes the bomb go off, which connects the ActivateBomb Remote Event to this script after it gets activated by the local script.
	local bombClone = bombHandle:Clone()
	bombClone.Parent = game.Workspace
	bombClone.CanCollide = true
	bombClone.CFrame = bombHandle.CFrame * CFrame.new(0, 2, 2)

	explode(bombClone, user)
end)

Yes I think you could include those functions in a Bomb class.

You can always pass a parameter to the .new function like this:

Bomb.new(bombType)

Then you could check what the type is and change stuff like that.

local Bomb = setmetatable({}, {})
Bomb.__index = Bomb

function Bomb.new(bombType, model)
local self = setmetatable({__index = Bomb}, Bomb)
self.BombType = bombType
self.Model = model
return self
end

function Bomb:Explode()
if self.Type == “Super Bomb” then
self.Model:GO_KABOOM(“!!!!!”)
-- obviously add your code
end
end

return Bomb