Erylies

The GunController class for an FPS gun system (Hidden Devs Application)

Aug 4th, 2025
125
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 13.94 KB | Source Code | 0 0
  1. --Constants
  2. local ReloadKey = Enum.KeyCode.R
  3. local LandDownTweenInfo = TweenInfo.new(0.1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
  4. local LandUpTweenInfo = TweenInfo.new(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
  5.  
  6. --Services
  7. local ReplicatedStorage = game:GetService("ReplicatedStorage")
  8. local ContentProvider = game:GetService("ContentProvider")
  9. local RunService = game:GetService("RunService")
  10. local UserInputService = game:GetService("UserInputService")
  11. local TweenService = game:GetService("TweenService")
  12. local SoundService = game:GetService("SoundService")
  13. local Debris = game:GetService("Debris")
  14.  
  15. --Modules
  16. local Modules = ReplicatedStorage.Modules
  17. local LocalPlayer = require(Modules.LocalPlayer)
  18.  
  19. --Remotes
  20. local Remotes = ReplicatedStorage.Remotes
  21. local GunRemote = Remotes.Gun
  22.  
  23. --Other
  24. local Camera = game.Workspace.CurrentCamera
  25. local ViewModelAudio = SoundService.ViewModel
  26.  
  27. --Functions
  28. local function Lerp(A, B, T)
  29.     -- This is just a basic lerping function, not much to talk about it
  30.     return A + (B - A) * T
  31. end
  32.  
  33. local function MultiplyCFrame(CurrentCFrame, Multiplicator)
  34.     -- Since CFrame.identity is basically CFrame.new(0, 0, 0), I lerped it to the current CFrame with the alpha being the multiplicator so it does a similar job as Vector3 * x
  35.     return CFrame.identity:Lerp(CurrentCFrame, Multiplicator)
  36. end
  37.  
  38. local function LoadAnimations(Model, Info)
  39.     -- This is a function to load animations on a model and bind Sounds, VFX, Functions to those animations' events
  40.     local Animations = Info.Animations and Info.Animations:GetChildren()
  41.     if not Animations then return end
  42.    
  43.     local Animator = Model.Humanoid and Model.Humanoid.Animator
  44.     if not Animator then return end
  45.  
  46.     local Tracks = {}
  47.  
  48.     ContentProvider:PreloadAsync(Animations)-- I preload the animations using the ContentProviderService's PreloadAsync method
  49.  
  50.     for _, Animation in Animations do
  51.         local Track = Animator:LoadAnimation(Animation)
  52.        
  53.         if Info.Sounds then
  54.             Track:GetMarkerReachedSignal("Sound"):Connect(function(SoundName)
  55.                 --This finds a sound named the same as the animationevent's parameter and plays it when the event is reached
  56.                 local AudioPlayer = Info.Sounds:FindFirstChild(SoundName)
  57.                 if not AudioPlayer then return end
  58.  
  59.                 AudioPlayer = AudioPlayer:Clone()
  60.  
  61.                 local Wire = Instance.new("Wire")-- To connect the audio to the output
  62.                 Wire.SourceInstance = AudioPlayer
  63.                 Wire.TargetInstance = ViewModelAudio
  64.                 Wire.Parent = AudioPlayer
  65.  
  66.                 AudioPlayer.Parent = ViewModelAudio
  67.  
  68.                 AudioPlayer:Play()
  69.                 Debris:AddItem(AudioPlayer, AudioPlayer.TimeLength + 1)--I use the AddItem method of the service "Debris" to make sure that the sound gets destroyed when it's done playing
  70.             end)
  71.         end
  72.        
  73.         if Info.VFX then
  74.             Track:GetMarkerReachedSignal("VFX"):Connect(function(VFXName)
  75.                 --This finds a ParticleEmitter named the same as the animationevent's parameter and emits it when the event is reached
  76.                 local ParticleEmitter = Info.VFX:FindFirstChild(VFXName)
  77.                 if not ParticleEmitter then return end
  78.  
  79.                 --This section parents the ParticleEmitter to a Part or an Attachment named the same as its "Parent" attribute
  80.                 local ParentName = ParticleEmitter:GetAttribute("Parent")
  81.                 local Parent = ParentName and Model:FindFirstChild(ParentName, true)
  82.                 if not Parent then return end
  83.  
  84.                 ParticleEmitter = ParticleEmitter:Clone()
  85.                 ParticleEmitter.Parent = Parent
  86.  
  87.                 ParticleEmitter:Emit(ParticleEmitter:GetAttribute("EmitCount") or 1)-- If the ParticleEmitter has an attribute called EmitCount then it emits it that many times
  88.                 Debris:AddItem(ParticleEmitter, ParticleEmitter.Lifetime.Max + 1)--I use the AddItem method of the service "Debris" to make sure that the ParticleEmitter gets destroyed when the particles disappear
  89.             end)
  90.         end
  91.        
  92.         if Info.FunctionFX then
  93.             Track:GetMarkerReachedSignal("FunctionFX"):Connect(function(FunctionFXName)
  94.                 --This finds a function module named the same as the animationevent's parameter and it runs the function when the event is reached
  95.                 local FunctionModule = Info.FunctionFX:FindFirstChild(FunctionFXName)
  96.                 if FunctionModule then require(FunctionModule)(Model) end
  97.             end)
  98.         end
  99.  
  100.         Tracks[Animation.Name] = Track
  101.     end
  102.  
  103.     return Tracks
  104. end
  105.  
  106. local GunController = {}
  107. GunController.__index = GunController
  108.  
  109. local LastCameraCFrame
  110. local LastRootPartCFrame
  111.  
  112. --This is the constructor function of the class "GunController"
  113. function GunController.new(GunTool)
  114.     local Info = GunRemote:InvokeServer("GetInfo", GunTool)--Requests the info of the gun from the server and doesn't run the rest of the code if the server couldn't find the information
  115.     if not Info then return end
  116.    
  117.     local ViewModel = Info.Assets.ViewModel:Clone()
  118.     ViewModel.Parent = ReplicatedStorage
  119.    
  120.     --This section applies the arm colors and the shirt to the ViewModel so it feels like the player is actually holding the gun
  121.     local HumanoidDescription = Instance.new("HumanoidDescription")
  122.     HumanoidDescription.Shirt = LocalPlayer.HumanoidDescription.Shirt
  123.     HumanoidDescription.RightArmColor = LocalPlayer.HumanoidDescription.RightArmColor
  124.     HumanoidDescription.LeftArmColor = LocalPlayer.HumanoidDescription.LeftArmColor
  125.    
  126.     local ViewModelHumanoid = ViewModel:FindFirstChildOfClass("Humanoid")
  127.     ViewModelHumanoid:ApplyDescription(HumanoidDescription)
  128.  
  129.     local Animations = LoadAnimations(ViewModel, Info)--Loads the animations and gets the AnimationTracks of them using the function
  130.    
  131.     --Defines some variables and sets the objects metatable to the GunController Class
  132.     local self = setmetatable(
  133.         {
  134.             Tool = GunTool,
  135.             Info = Info,
  136.             Animations = Animations,
  137.             Connections = {},
  138.             Ammo = Info.MaxAmmo,
  139.             ViewModel = ViewModel,
  140.             Idle = CFrame.identity,
  141.             Sway = CFrame.identity,
  142.             JumpOffset = CFrame.identity,
  143.             AimOffset = CFrame.identity,
  144.             LandingCFrame = Instance.new("CFrameValue"),
  145.             AimDampening = 1,
  146.             Bob = 0,
  147.             Stride = 0,
  148.             DefaultFOV = Camera.FieldOfView,
  149.             FieldOfView = Camera.FieldOfView
  150.         }, GunController)
  151.    
  152.     self:BindInputs()
  153.    
  154.     --This section makes it update the gun's CFrame constantly usind RunService, it also checks if the player is aiming (pressing down mouse button 2)
  155.     RunService:BindToRenderStep("ViewModel", Enum.RenderPriority.Camera.Value + 1, function(DeltaTime)
  156.         self.Aiming = self.Equipped and UserInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton2)
  157.         LocalPlayer.Humanoid.WalkSpeed = self.Aiming and Info.AimWalkSpeed or 16--Slows the player if aiming
  158.         self:Update(DeltaTime)
  159.     end)
  160.    
  161.     --This section is for making the gun "shake" when the player lands after falling
  162.     table.insert(self.Connections, LocalPlayer.Humanoid.StateChanged:Connect(function(_, State)
  163.         if State ~= Enum.HumanoidStateType.Landed then return end
  164.        
  165.         local Tween = TweenService:Create(self.LandingCFrame, LandDownTweenInfo, {Value = self.Info.LandingShakeOffset})
  166.         Tween:Play()
  167.  
  168.         task.delay(Tween.TweenInfo.Time, function()
  169.             local Tween = TweenService:Create(self.LandingCFrame, LandUpTweenInfo, {Value = CFrame.identity})
  170.             Tween:Play()
  171.         end)   
  172.     end))
  173.    
  174.     --This runs the Destroy method of the object if the player dies
  175.     LocalPlayer.Humanoid.Died:Once(function()
  176.         self:Destroy()
  177.     end)
  178.    
  179.     LastCameraCFrame = Camera.CFrame
  180.     LastRootPartCFrame = LocalPlayer.RootPart.CFrame
  181.    
  182.     return self
  183. end
  184.  
  185. function GunController:IsFree()
  186.     --This function is to check if the player can do an action. So for example the player cant shoot while reloading
  187.     return not self.Equipping and not self.Shooting and not self.Reloading
  188. end
  189.  
  190. function GunController:Shoot()
  191.     if not self:IsFree() then return end
  192.     if self.Ammo <= 0 then
  193.         self:Reload()
  194.         return
  195.     end
  196.     local Success = GunRemote:InvokeServer("Shoot", Camera.CFrame)--This is the server-sided part of the system
  197.     if not Success then return end--Doesn't continue with the client-sided part if the server doesn't give approval
  198.    
  199.     --Plays the animation and reduces the ammo by 1
  200.     self.Shooting = true
  201.    
  202.     self.Ammo -= 1
  203.     self:PlayAnimation("Shoot")
  204.    
  205.     self.Shooting = nil
  206. end
  207.  
  208. function GunController:Reload()
  209.     if self.Ammo >= self.Info.MaxAmmo or not self:IsFree() then return end--Doesn't let the player reload if the ammo is already full or if the gun is currently doing an action
  210.  
  211.     self.Reloading = true
  212.  
  213.     --Plays the animation and sets the ammo to max  
  214.     self:PlayAnimation("Reload")
  215.     self.Ammo = self.Info.MaxAmmo
  216.  
  217.     self.Reloading = nil
  218. end
  219.  
  220. function GunController:Equip()
  221.     if not self:IsFree() then
  222.         --This makes sure that the player can't equip the gun during unequipping
  223.         self.SkipUnequipped = true
  224.         self.Tool.Parent = LocalPlayer.Player.Backpack
  225.         return
  226.     end
  227.     UserInputService.MouseIconEnabled = false--Hides the mouse
  228.    
  229.     self.Equipping = true
  230.    
  231.     self.ViewModel.Parent = Camera
  232.     self:PlayAnimation("Equip")
  233.    
  234.     self.Equipping = nil
  235.     self.Equipped = true
  236. end
  237.  
  238. function GunController:Unequip()
  239.     if not self:IsFree() then
  240.         --This makes sure that the player can't unequip the gun during an action
  241.         self.SkipEquipped = true
  242.         self.Tool.Parent = LocalPlayer.Character
  243.         return
  244.     end
  245.    
  246.     self.Equipped = nil
  247.     self.Equipping = true
  248.    
  249.     self:PlayAnimation("Equip", nil, nil, -1)
  250.     self.ViewModel.Parent = ReplicatedStorage
  251.    
  252.     self.Equipping = nil
  253.    
  254.     UserInputService.MouseIconEnabled = true--Makes the mouse visible again
  255. end
  256.  
  257. function GunController:BindInputs()
  258.     --This function binds the inputs to actions (not fully mobile compatible yet)
  259.     table.insert(self.Connections, self.Tool.Equipped:Connect(function()
  260.         if self.SkipEquipped then self.SkipEquipped = nil return end
  261.        
  262.         self:Equip()
  263.     end))
  264.    
  265.     table.insert(self.Connections, self.Tool.Unequipped:Connect(function()
  266.         if self.SkipUnequipped then self.SkipUnequipped = nil return end
  267.        
  268.         self:Unequip()
  269.     end))
  270.    
  271.     table.insert(self.Connections, self.Tool.Activated:Connect(function()
  272.         self:Shoot()
  273.     end))
  274.    
  275.     table.insert(self.Connections, UserInputService.InputBegan:Connect(function(Input, GPE)
  276.         if GPE then return end--This makes sure that the player doesn't accidentally reload while typing
  277.        
  278.         if Input.KeyCode == ReloadKey then self:Reload() end
  279.     end))
  280. end
  281.  
  282. function GunController:Update(DeltaTime)
  283.     --This section calculates the movement between frames (camera and player/character)
  284.     local CameraCFrameDelta = LastCameraCFrame:ToObjectSpace(Camera.CFrame)
  285.     local RootPartCFrameDelta = LastRootPartCFrame:ToObjectSpace(LocalPlayer.RootPart.CFrame)
  286.  
  287.     local function GetLerpAlpha(Name)--I made this function to avoid too much repetition
  288.         return math.min(self.Info[Name.."Transition"] * DeltaTime, 1)
  289.     end
  290.    
  291.     local Bob = LocalPlayer.Humanoid.MoveDirection.Magnitude--This value is 1 or really close to 1 if the player is moving and 0 or really close to 0 if they aren't
  292.    
  293.     --Most of this part below is pretty much self explanatory but I will try my best to explain it further
  294.    
  295.     self.Stride = (self.Stride + DeltaTime * Bob * self.Info.BobSpeed) % (2 * math.pi)--Stride increases as the player walks and goes back to 0 when it reaches 2pi using the modulus operator(%)
  296.     self.Bob = Lerp(self.Bob, Bob, GetLerpAlpha("Bob"))
  297.    
  298.     --This is to make the gun sway towards the direction that the camera is turning. Its more powerful if the turn is faster
  299.     local Sway = MultiplyCFrame(CameraCFrameDelta.Rotation, self.Info.SwayIntensity)--Adjusts the intensity of the Sway depending on the custom value
  300.     self.Sway = self.Sway:Lerp(Sway, GetLerpAlpha("Sway"))
  301.    
  302.     --This is to make the gun swing if the player is falling or jumping
  303.     local JumpOffset = MultiplyCFrame(CFrame.new(0, RootPartCFrameDelta.Y, 0), self.Info.JumpIntensity)--Adjusts the intensity of the JumpOffset depending on the custom value
  304.     self.JumpOffset = self.JumpOffset:Lerp(JumpOffset, GetLerpAlpha("Jump"))
  305.    
  306.     --This is to make the player sort of get to the aiming perspective if aiming
  307.     local AimOffset = self.Aiming and self.Info.AimOffset or CFrame.identity
  308.     self.AimOffset = self.AimOffset:Lerp(AimOffset, GetLerpAlpha("Aim"))
  309.    
  310.     --This Narrows the FOV if aiming
  311.     local FieldOfView = self.Aiming and self.Info.AimFOV or self.DefaultFOV
  312.     self.FieldOfView = Lerp(self.FieldOfView, FieldOfView, GetLerpAlpha("Aim"))
  313.    
  314.     --This dampens the movement if the player is aiming so it's easier to aim
  315.     local AimDampening = self.Aiming and self.Info.AimDampening or 1
  316.     self.AimDampening = Lerp(self.AimDampening, AimDampening, GetLerpAlpha("Aim"))
  317.    
  318.     --I lerp all these values above so the movement feels smooth and doesn't feel "jittery"
  319.  
  320.     --Calculating the bob offset (idle "animation" too)
  321.     local BobX = math.sin(self.Stride) * self.Bob
  322.     local BobY = math.sin(2 * (tick() + self.Stride))
  323.     local BobOffset = Vector3.new(BobX, BobY, 0) * self.Info.BobIntensity
  324.     local BobCFrame = CFrame.new(BobOffset)
  325.  
  326.     local TotalOffset = BobCFrame * self.Idle * self.Sway * self.JumpOffset * self.LandingCFrame.Value--Calculates the final offset
  327.     local DampenedOffset = MultiplyCFrame(TotalOffset, self.AimDampening)--Applies the AimDampening
  328.    
  329.     self.ViewModel:PivotTo(Camera.CFrame * DampenedOffset * self.AimOffset)--Finally sets the viewmodels cframe
  330.     Camera.FieldOfView = self.FieldOfView--Sets the FOV
  331.    
  332.     --Self explanatory I think
  333.     LastCameraCFrame = Camera.CFrame
  334.     LastRootPartCFrame = LocalPlayer.RootPart.CFrame
  335. end
  336.  
  337. function GunController:PlayAnimation(AnimationName, ...)
  338.     --This function looks for the animation using the name and if it finds it, plays the animation and yields until the animation is done playing
  339.     local Animation = self.Animations[AnimationName]
  340.     if Animation then
  341.         Animation:Play(...)
  342.         Animation.Stopped:Wait()
  343.     end
  344. end
  345.  
  346. function GunController:Destroy()
  347.     --This function disconnects the connections and destroys the viewmodel
  348.     if not self.ViewModel or not self.ViewModel.Parent then return end
  349.    
  350.     RunService:UnbindFromRenderStep("ViewModel")
  351.     for _, Connection in self.Connections do
  352.         if Connection.Connected then--Doesn't disconnect again if already disconnected
  353.             Connection:Disconnect()
  354.         end
  355.     end
  356.     self.ViewModel:Destroy()
  357. end
  358.  
  359. return GunController
Advertisement
Add Comment
Please, Sign In to add comment