Make this gun system more OOP?

Hello,

This gun system is primary more functional programming than object oriented programming with the exception of a few functions stored within a module. Every gun automatically inherits the functions defined in the module script, and obviously this makes the system so much easier to patch bugs and build off of. But there are still many variables and functions that are being copied for every gun which is a big no-no.

I am new to OOP and ANY advice would really help. I want to make as many of these functions and variables object oriented as possible. I know there has to be a way to build this system entirely OO since every gun shares common characteristics like equip animations, firing sounds, muzzle flash, aimpart etc. I just don’t know how to go about building it OO.

Side note: If there is any way I can improve the formatting or syntax I would appreciate any suggestions.

-- Player
local Player = game:GetService("Players").LocalPlayer;
local PlayerGui = Player:WaitForChild("PlayerGui");
local PlayerData = Player:WaitForChild("PlayerData");
local Mouse = Player:GetMouse();
local Camera = workspace.CurrentCamera;
workspace:WaitForChild(Player.Name);

-- Character
local Character = Player.Character or Player.CharacterAdded:Wait();
local Humanoid = Character:WaitForChild("Humanoid");
local Torso = Character:WaitForChild("Torso");
local RightShoulderM6D = Torso:WaitForChild("Right Shoulder");
local LeftShoulderM6D = Torso:WaitForChild("Left Shoulder");
local HumanoidAnimator = Humanoid:WaitForChild("TestAnimator");

-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage");
local RunService = game:GetService("RunService");
local UserInputService = game:GetService("UserInputService");
local PhysicsService = game:GetService("PhysicsService");
local TweenService = game:GetService("TweenService");

-- Modules
local SpringModule = require(ReplicatedStorage:WaitForChild("Spring"));
local GameSettings = require(ReplicatedStorage:WaitForChild("GameSettings"));
local GunSettings = require(ReplicatedStorage:WaitForChild("GunSettings"));
local FunctionLibrary = require(ReplicatedStorage:WaitForChild("FunctionLibrary"))
local CameraControl = require(ReplicatedStorage:WaitForChild("CameraControl"));

-- Viewmodel 
local ViewModelStorage = ReplicatedStorage:WaitForChild(Player.Name.."ViewModelStorage");
local ViewModel = Camera:FindFirstChild("ViewModel") 
or ViewModelStorage:WaitForChild("ViewModel");
local ViewModelToolGrip = ViewModel:WaitForChild("Torso"):WaitForChild("ToolGrip");

-- Values changed in the viewmodel system for custom effects
local Offset = Character:WaitForChild("ViewModelController"):WaitForChild("Offset");
local MouseSwayIntensity = Character:WaitForChild("ViewModelController"):WaitForChild("MouseSwayIntensity");
local MovementSwayIntensity = Character:WaitForChild("ViewModelController"):WaitForChild("MovementSwayIntensity");

-- Items in RS
local GameAssets = ReplicatedStorage:WaitForChild("GameAssets");
local RemoteEvents = GameAssets:WaitForChild("RemoteEvents");
local DamageHumanoidEvent = RemoteEvents:WaitForChild("DamageHumanoid");
local CreateBloodEvent = RemoteEvents:WaitForChild("CreateBlood");


-- Gui elements
local MainGui = PlayerGui:WaitForChild("MainGui");
local Inventory = MainGui:WaitForChild("Inventory");
local Items = Inventory:WaitForChild("Items");
local MagHUD = MainGui:WaitForChild("MagHUD");
local ModeIndicator = MagHUD:WaitForChild("ModeIndicator");
local GunCursor = MainGui:WaitForChild("GunCursor");

-- Mobile controls
local MobileControls = PlayerGui:WaitForChild("MobileControls"):WaitForChild("GunControls");
local ShootButton = MobileControls:WaitForChild("Shoot");
local ReloadButton = MobileControls:WaitForChild("Reload");
local AimButton = MobileControls:WaitForChild("Aim");
local FireModeButton = MobileControls:WaitForChild("FireMode");
local UsingMobileControls = false;


local EquipAnimation = Instance.new("Animation");
EquipAnimation.Parent = script.Parent;
EquipAnimation.AnimationId = "rbxassetid://12732854269"
EquipAnimation = HumanoidAnimator:LoadAnimation(EquipAnimation);
EquipAnimation:SetAttribute("ViewModelAnimation", true);
-- animations with attribute "ViewModelAnimation" will also be played on the viewmodel

local ReloadAnimation = Instance.new("Animation");
ReloadAnimation.Parent = script.Parent;
ReloadAnimation.AnimationId = "rbxassetid://12733162324";
ReloadAnimation = HumanoidAnimator:LoadAnimation(ReloadAnimation);
ReloadAnimation:SetAttribute("ViewModelAnimation", true);

local AimAnimation = Instance.new("Animation");
AimAnimation.Parent = script.Parent;
AimAnimation.AnimationId = "rbxassetid://12734776468";
AimAnimation = HumanoidAnimator:LoadAnimation(AimAnimation);
-- this animation does not play on viewmodel

local SprintAnimation = Instance.new("Animation");
SprintAnimation.Parent = script.Parent;
SprintAnimation.AnimationId = "rbxassetid://12745621182";
SprintAnimation = HumanoidAnimator:LoadAnimation(SprintAnimation);
SprintAnimation:SetAttribute("ViewModelAnimation", true)
SprintAnimation:SetAttribute("Sprint", true);


local SlotLink = script:WaitForChild("Data"):WaitForChild("SlotLink")
--SlotLink is an object value that stores a link to the current inventory slot where a magazine exists (if found)

-- Gun Objects
local Gun = script.Parent;
local Handle = Gun:WaitForChild("Handle");
local FireSound = Handle:WaitForChild("Fire");
local ReloadSound = Handle:WaitForChild("Reload");
local ToggleSound = Handle:WaitForChild("Toggle");
local MuzzleLight = Gun:WaitForChild("Muzzle"):WaitForChild("Light");
local MuzzleFlash = Gun:WaitForChild("Muzzle"):WaitForChild("Flash");
local CloneLight = nil;
local CloneFlash = nil;
local AimPart;
local GunClone;

-- Gun States
local GunEquipped = false
local FoundMagazine = false;
local Firing = false;
local Reloading = false;


-- Gun Settings
local AimingMouseSensitivity = UserInputService.MouseDeltaSensitivity / 2 -- reduce mouse movement sensitivity while aiming
local DefaultMouseSensitivity = UserInputService.MouseDeltaSensitivity;
local DefaultMouseIcon = UserInputService.MouseIcon;
local MouseIcon = "http://www.roblox.com/asset/?id=9947313248";
local LastFire = 0;
local ShootDelay = 1/12; -- rate of fire
local ReloadDelay = 3; -- amount of time it takes to reload
local HeadDamage = GunSettings.M16A2HeadDamage;
local BodyDamage = GunSettings.M61A2BodyDamage;
local MouseSway = 35; -- Amount of mouse sway effect the viewmodel will show. More sway makes the gun feel heavier
local MovementSway = 20; -- Amount of movement sway on viewmodel. More movement sway the heavier the gun feels
local Range = GunSettings.M16A2Range -- Range of gun 
local HipfireMaxSpread = 7; -- Maximum spread for hipfire. spread = math.random(-HipFireMaxSpread, HipeFireMaxSpread)
local MaxSpreadDivisor = 5;-- Divides random spread (direction = spread/math.random(1, MaxSpreadDivisor))
local AimingMaxSpread = 0; -- Amount of inaccuracy the gun has while aiming

local AppliedBulletSpread = Vector3.new();
local FireModes = {"Semi", "Burst", "Auto"}; -- firing modes gun can use
local FireModeIndex = 1; -- default firing mode for gun
local AimZoomIn = {FieldOfView = 55}; -- Aiming FOV
local AimZoomOut = {FieldOfView = 70}; -- Default FOV
local AimTweenInfo = TweenInfo.new(.35, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut);
local AimInTween = TweenService:Create(Camera, AimTweenInfo, AimZoomIn);
local AimOutTween = TweenService:Create(Camera, AimTweenInfo, AimZoomOut);

-- Player States
local Sprinting = PlayerData:WaitForChild("Sprinting");
local IsAiming = PlayerData:WaitForChild("IsAiming");
local LockMouseCenter = PlayerData:WaitForChild("LockMouseCenter");
local LockThirdPerson = PlayerData:WaitForChild("LockThirdPerson");
local InFirstPerson;
local Aiming = false;

local AimSpring = SpringModule.new(0)
AimSpring.Damper = 1
AimSpring.Speed = 20;
-- the faster the speed the faster the gun aims down sights

local Recoil = SpringModule.new(Vector3.new())
Recoil.Speed = 100
-- the lower the recoil speed, the more the gun kicks

local CamRecoil = SpringModule.new(0)
CamRecoil.Speed = 20;
CamRecoil.Damper = 0.8
-- the lower the speed, the more camera recoil there is
-- the lower the damper, the more bounce there is

local PreviousCamTransform = CFrame.new();

local function UpdateMagHUD()
	ModeIndicator.Text = FireModes[FireModeIndex];
	MagHUD.Visible = true;
	if SlotLink.Value ~= nil and SlotLink.Value.Data.BulletsHeld.Value > 1 then
		MagHUD.Text = SlotLink.Value.Data.BulletsHeld.Value.."/"..SlotLink.Value.Data.MaxBulletsHeld.Value;
	else
		MagHUD.Text = "Empty";
	end
end

local function EquipGun()
	UpdateMagHUD();
	GunCursor.Visible = true;
	UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter;
	if not FoundMagazine then
		CheckForMagazineInInventory();
	end
	local OnMobileDevice = FunctionLibrary.CheckIfMobileDevice();
	if OnMobileDevice then
		UsingMobileControls = true
		MobileControls.Visible = true
	end
	ModeIndicator.Text = FireModes[FireModeIndex];
	Character["Right Arm"]:WaitForChild("RightGrip"):Destroy();
	ViewModel.Parent = Camera;
	GunClone = Gun:Clone();
	GunClone.Parent = ViewModel;
	local GunCloneMuzzle = GunClone.Muzzle;
	-- used for creating flash effects on viewmodel 
	CloneLight = GunCloneMuzzle.Light;
	CloneFlash = GunCloneMuzzle.Flash;
	ViewModelToolGrip.Part1 = GunClone.BodyAttach
	AimPart = GunClone.AimPart;
	local M6D = Character.Torso:WaitForChild("BodyAttachMotor");
	M6D.Part1 = Gun.BodyAttach;
	UserInputService.MouseIconEnabled = false;
	MovementSwayIntensity.Value = MovementSway;
	MouseSwayIntensity.Value = MouseSway;
	EquipAnimation:Play();
	EquipAnimation:GetMarkerReachedSignal("AnimationPause"):Connect(function()
		EquipAnimation:AdjustSpeed(0);
		print("Animation frozen.")
	end)
	InFirstPerson = FunctionLibrary.CheckIfInFirstPerson();
	if InFirstPerson then
		ModifyTransparency(ViewModel, 0)
		ModifyTransparency(Gun, 1)
	else
		ModifyTransparency(ViewModel, 1)
		ModifyTransparency(Gun, 0);
	end
	for _, Object in ipairs(GunClone:GetChildren()) do
		if Object.ClassName =="Part" 
		or Object.ClassName == "MeshPart" 
		then
		-- don't destroy the object
		else
		Object:Destroy();
		end
	end
end
Gun.Equipped:Connect(EquipGun)

local function UnequipGun()
	AimOut();
	MagHUD.Visible = false;
	ViewModel.Parent = ViewModelStorage;
	EquipAnimation:Stop();
	SprintAnimation:Stop();
    GunClone:Destroy();
	GunCursor.Visible = false;
	
end
Gun.Unequipped:Connect(UnequipGun);

function CheckForMagazineInInventory()
	local PlayerHasMagazine, Slot = FunctionLibrary.CheckForMagazineInInventory(SlotLink, script.Data);
	if PlayerHasMagazine == true then
		FoundMagazine = true
		SlotLink.Value = Slot
		UpdateMagHUD()
	else
		FoundMagazine = false
		SlotLink.Value = nil;
		UpdateMagHUD()
	end
end

local function FireGun()
	if (Gun.Parent ~= Character) 
	then return
		else
		if FoundMagazine == true 
			and SlotLink.Value.Data.BulletsHeld.Value > 0 
			and not Reloading then
			-- make sure gun does not fire faster than ShootDelay
			local CurrentFire = tick()
			if CurrentFire - LastFire >= ShootDelay 	
			then  
				-- fire gun
				if InFirstPerson then
					FunctionLibrary.MuzzleFlashEffect(CloneFlash, CloneLight);
					else
					FunctionLibrary.MuzzleFlashEffect(MuzzleFlash, MuzzleLight);
				end
				LastFire = CurrentFire;
				Recoil.Velocity = Vector3.new(0, 0, 12)
				CamRecoil.Velocity = 100
				FireSound:Play();
				SlotLink.Value.Data.BulletsHeld.Value -= 1;
				SlotLink.Value.MagazineDisplay.Text = SlotLink.Value.Data.BulletsHeld.Value.."/"..SlotLink.Value.Data.MaxBulletsHeld.Value;
				UpdateMagHUD();
				if Aiming then
					-- generate random bullet spread
					AppliedBulletSpread = Vector3.new(math.random(-AimingMaxSpread, AimingMaxSpread), math.random(-AimingMaxSpread, AimingMaxSpread), math.random(-AimingMaxSpread, AimingMaxSpread))/math.random(MaxSpreadDivisor);
				else
					AppliedBulletSpread = Vector3.new(math.random(-HipfireMaxSpread, HipfireMaxSpread),math.random(-HipfireMaxSpread, HipfireMaxSpread), math.random(-HipfireMaxSpread, HipfireMaxSpread))/math.random(1, MaxSpreadDivisor);
				end
				local RaycastResult, SubjectHumanoid = FunctionLibrary.FireGunRaycast(Gun, SlotLink.Value, AppliedBulletSpread, Range);
				if RaycastResult then
					local Damage = nil;
					print("Ray hit "..RaycastResult.Instance.Name)
					if SubjectHumanoid then
						print("Do damage");
						if RaycastResult.Instance.Name == "Head" then
							Damage = HeadDamage;
							print("Hit head")
						else
							Damage = BodyDamage;
							print("Hit body")
						end
						SubjectHumanoid:TakeDamage(Damage);
						DamageHumanoidEvent:FireServer(SubjectHumanoid, Damage)
						CreateBloodEvent:FireServer(RaycastResult.Instance);
					else
						print("Creat effect for".. RaycastResult.Instance.Name);
						FunctionLibrary.CreateBulletHitEffect(RaycastResult);
					end
				end
			end
		else print("Attempting reload.."); -- if there is not a magazine, then try to reload
			if not Reloading then
			ReloadGun();
			end
		end
	end
end

function ReloadGun()
	CheckForMagazineInInventory();
	if FoundMagazine then
	Reloading = true
	ReloadAnimation:Play();
	ReloadSound:Play();
	ReloadSound:Play();
	MagHUD.Text = "Reloading..";
	wait(ReloadDelay);
	Reloading = false;
	print("Reloaded");
	UpdateMagHUD();
	end
end

function AimIn()
	print("Aim in")
	Aiming = true;
	AimAnimation:Play();
	AimInTween:Play();
	Humanoid.WalkSpeed = GameSettings.AimingWalkSpeed;
	GunCursor.Visible = false;
	AimAnimation:GetMarkerReachedSignal("AnimationPause"):Connect(function()
		AimAnimation:AdjustSpeed(0);
	end)
end

function AimOut()
	print("Aim out")
	Aiming = false; -- stop aiming gun
	AimOutTween:Play();
	AimAnimation:Stop();
	Humanoid.WalkSpeed = GameSettings.WalkSpeed;
	GunCursor.Visible = true;
	UserInputService.MouseDeltaSensitivity = DefaultMouseSensitivity;

end

function ModifyTransparency(Descendants, Transparency)
	for _, Part in ipairs(Descendants:GetDescendants()) do
		if Part.ClassName == "Part" 
			or Part.ClassName == "MeshPart" 
		then
			Part.LocalTransparencyModifier = Transparency;
		end
	end
end

local function UpdateSystem()
	if Gun.Parent == Character 
		and GunClone 
		and ViewModel
	then
		GunCursor.Position = UDim2.new(0, Mouse.X, 0, Mouse.Y);
		InFirstPerson = FunctionLibrary.CheckIfInFirstPerson();
		if InFirstPerson == true and Character.Torso.LocalTransparencyModifier ~= 1 
		and Gun.Handle and Gun.Handle.LocalTransparencyModifier ~= 1
		then
		ModifyTransparency(ViewModel, 0);
		ModifyTransparency(Gun, 1);
		print("show viewmodel")
		elseif InFirstPerson == false and Character.Torso.LocalTransparencyModifier ~= 0 
			and Gun.Handle.LocalTransparencyModifier ~= 0 
		then
		ModifyTransparency(ViewModel, 1);
		ModifyTransparency(Gun, 0)
		print("hide viewmodel");
		AimOut();
	end
	local CamTransform = CFrame.fromEulerAnglesXYZ(math.rad(CamRecoil.Position), 0, 0)
	Camera.CFrame = Camera.CFrame * (CamTransform * PreviousCamTransform:Lerp(CFrame.new(), 0.02):Inverse())
	PreviousCamTransform = CamTransform
	local HandleTransform = ViewModel:GetPrimaryPartCFrame():ToObjectSpace(GunClone.Handle.CFrame)
	local OriginalTransform = HandleTransform * CFrame.new(Recoil.Position) * HandleTransform:Inverse()
	local AimTransform = AimPart.CFrame:ToObjectSpace(GunClone.Handle.CFrame) * CFrame.new(Recoil.Position) * HandleTransform:Inverse()
	Offset.Value = OriginalTransform:Lerp(AimTransform, AimSpring.Position)
		if Aiming then
			AimSpring.Target = 1
		    UserInputService.MouseDeltaSensitivity = AimingMouseSensitivity;
			IsAiming.Value = true;
		else
			AimSpring.Target = 0
			UserInputService.MouseDeltaSensitivity = DefaultMouseSensitivity;
			IsAiming.Value = false;
		end
		if Sprinting.Value == true then
			if not SprintAnimation.IsPlaying then
			SprintAnimation:Play();
			SprintAnimation:GetMarkerReachedSignal("AnimationPause"):Connect(function()
				SprintAnimation:AdjustSpeed(0);
				end)
			end
		else
			SprintAnimation:Stop();
		end
	end
end
RunService.RenderStepped:Connect(UpdateSystem)

function CheckFireMode()
	if FireModes[FireModeIndex] == "Semi" then
		FireGun()
	end
	if FireModes[FireModeIndex] == "Burst" then
		for x = 1, 3 do
			wait(ShootDelay)
			FireGun()
		end
	end
	if FireModes[FireModeIndex] == "Auto" then
		Firing = true
		while Firing do
			wait(ShootDelay)
			FireGun();
		end
	end
end

local function ToggleFireMode()
	ToggleSound:Play();
	if FireModes[FireModeIndex + 1] ~= nil then
		FireModeIndex += 1 
	else
		FireModeIndex = 1;
	end
	print(FireModes[FireModeIndex]);
	ModeIndicator.Text = FireModes[FireModeIndex]
end

UserInputService.InputBegan:Connect(function(Input, GameProcessed)
	-- detect all relevant user input and procede with corresponding actions
	if Input.UserInputType == Enum.UserInputType.MouseButton1
	and not GameProcessed and not Reloading
	then
		Firing = true;
		CheckFireMode();
	end
	if Input.UserInputType == Enum.UserInputType.MouseButton2 
		and Sprinting.Value == false
		and not GameProcessed 
		and Character.Head.LocalTransparencyModifier > 0.6 then -- aim gun
		AimIn()
	end
	if Input.KeyCode == Enum.KeyCode.R --reload gun
		and not GameProcessed and Gun.Parent == Character then
		ReloadGun();
	end
	if Input.KeyCode == Enum.KeyCode.T --toggle fire mode
		and not GameProcessed and Gun.Parent == Character then
		ToggleFireMode()
	end
end)

UserInputService.InputEnded:Connect(function(Input, GameProcessed)
	if Input.UserInputType == Enum.UserInputType.MouseButton1 then
		Firing = false; -- stop firing gun
	end
	if Input.UserInputType == Enum.UserInputType.MouseButton2 
		and Character.Head.LocalTransparencyModifier > 0.6 then
		AimOut();
	end
end)

ShootButton.MouseButton1Down:Connect(function()
	Firing = true
	CheckFireMode();
end)
ShootButton.MouseButton1Up:Connect(function()
	Firing = false
end)
ReloadButton.MouseButton1Click:Connect(function()
	ReloadGun();
end)
FireModeButton.MouseButton1Click:Connect(function()
	ToggleFireMode();
end)
AimButton.MouseButton1Click:Connect(function()
	if not Aiming
		and Sprinting.Value == false
		and Character.Head.LocalTransparencyModifier > 0.6 then -- aim gun
		AimIn()
	else
	if Aiming  then -- aim gun
		AimOut()
		end
	end
end)
2 Likes

This should be your best friend.

You could have a GunHandler or Gun object and put methods like “ToggleFireMode” in it.

4 Likes

Ok heres a good tip

Follow OOP tutorials like the one another user suggested following

Make a client sided version of the OOP object and create a separae module for the server sided OOP version, this makes it easier to replicate stuff from the server over to the client.

1 Like

Effective object oriented programming stems from effective object oriented design.

Object oriented design is a feasible paradigm because our world is made of objects, making OOP conceptually intuitive for programmers. Because of this, my best advice for you is to forget about the code for now. Think in terms of the objects that would realistically be involved in a gun system and what potential inheritances or connections are necessary in order to implement the desired system. After that, think about what you would want those objects to be able to store and do. While it sounds redundant, to implement OOP, you have to think in terms of objects. Here’s an example.

Let’s say I wanted to make an animal farm for an animal farm tycoon game. Here is a list of objects that immediately come to mind.

  • Farm → represents the entire farm
  • Boundary → represents a constraining area of space
  • Pig → a farm animal to be on the farm
  • Cow → a farm animal to be on the farm
  • Barn → the living quarters of the farmer

Now that I have some objects, I think about some of the commonalities between different objects, and what objects “use” other objects.

  • The farm should be broken down into various boundaries. (The farm object “uses” the boundary object)
  • The barn should be encapsulated by a boundary, representing a boundary exclusively for the farm. (The boundary and the barn should use each other and be able to interact)
  • The pig and cow objects are both animals and will have similar functionality. I can use a superclass (Animal) to define common functionality to avoid code repetition.

It’s time to think about the properties/fields and methods that each object might have!

Farm:

  • width
  • length
  • :AssignToPlayer(Player)
  • :Reset()

Boundary:

  • width
  • length
  • type

Animal:

  • :Walk()
  • :Eat()

Pig:

  • :WalkToWheat()

Cow:

  • :ChewGrass()

Barn:

  • width
  • length
  • boundary

Obviously, if I were really creating this system, I would design many more fields and methods. If an algorithm is intricate, I may even conceptualize it via pseudocode. In practice, I would also design a UML class diagram to visually represent my blueprint.

Finally, I’d also think about handling error/exceptional cases. What are all of the different errors that could go wrong in each of my methods? I like to explicitly list the potential issues out and state how I will handle them. How you handle errors should be dictated by how robust or correct your system is intended to be (those are two opposite things!) For game development in Roblox, though, you should strive to prioritize robustness over correctness.

Now that you have your design conceptually laid out, you can finally get to programming! If you’ve created a well-planned, detailed design, then programming it should yield minimal challenges.

If the technical details of programming OOP in Lua is your concern, definitely check out the post that @SevenDevelopment posted in the above reply. That’s the same post I used to learn how to implement OOP functionality in Lua.

If you’ve programmed OOP in other languages before, such as Java, note that Lua is not actually an OOP language. Lua does not support a default class definition system and lacks major principles of OOP, such as abstraction and encapsulation (which can be replicated using metatables, but isn’t ideal). Lua can be used to replicate OOP functionality while not actually being a true OOP language.

Hopefully this helps, if you have any specific questions regarding OOP or your gun system I’d be happy to help.

5 Likes