-- modernized 14/09/2020
-- Coolio module stuff
local handler = {}
local fpsMT = { __index = handler }
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")
local Debris = game:GetService("Debris")
local Players = game:GetService("Players")
ReplicatedStorage:WaitForChild("modules")
ReplicatedStorage.modules:WaitForChild("fastCastHandler")
ReplicatedStorage.modules:WaitForChild("spring")
local fastcastHandler = require(ReplicatedStorage.modules.fastCastHandler)
local spring = require(ReplicatedStorage.modules.spring)
-- Functions i like using and you will probably too.
-- Bobbing!
local function getBobbing(addition, speed, modifier)
return math.sin(tick() * addition * speed) * modifier
end
function handler.new(weapons)
local self = {}
self.loadedAnimations = {}
self.springs = {}
self.lerpValues = {}
self.ammo = {} -- per weapon
self.lerpValues.aim = Instance.new("NumberValue")
self.lerpValues.equip = Instance.new("NumberValue")
self.lerpValues.equip.Value = 1
self.springs.walkCycle = spring.create()
self.springs.sway = spring.create()
self.springs.fire = spring.create()
self.canFire = true
return setmetatable(self, fpsMT)
end
function handler:equip(wepName)
-- Explained how this works earlier. we can store variables too!
-- if the weapon is disabled, or equipped, remove it instead
if self.disabled then
return
end
if self.equipped then
self:remove()
end
if self.reloading then
return
end
-- get weapon from storage
local weapon = ReplicatedStorage.weapons:FindFirstChild(wepName) -- do not cloen
if not weapon then
return
end -- if the weapon exists, clone it, else, stop
weapon = weapon:Clone()
--[[
Make a viewmodel (easily accessible with weapon.viewmodel too!)
and throw everything in the weapon straight inside of it. This makes animation hierarchy work.
--]]
self.viewmodel = ReplicatedStorage.viewmodel:Clone()
for _, v in pairs(weapon:GetChildren()) do
v.Parent = self.viewmodel
if v:IsA("BasePart") then
v.CanCollide = false
v.CastShadow = false
end
end
-- Time for automatic rigging and some basic properties
self.camera = workspace.CurrentCamera
self.character = Players.LocalPlayer.Character
-- Throw the viewmodel under the map. It will go back to the camera the next render frame once we get to moving it.
self.viewmodel.rootPart.CFrame = CFrame.new(0, -100, 0)
-- We're making the gun bound to the viewmodel's rootpart, and making the arms move along with the viewmodel using hierarchy.
self.viewmodel.rootPart.weapon.Part1 = self.viewmodel.weaponRootPart
self.viewmodel.left.leftHand.Part0 = self.viewmodel.weaponRootPart
self.viewmodel.right.rightHand.Part0 = self.viewmodel.weaponRootPart
-- I legit forgot to do this in the first code revision.
self.viewmodel.Parent = workspace.Camera
self.settings = require(self.viewmodel.settings)
self.loadedAnimations.idle = self.viewmodel.AnimationController:LoadAnimation(
self.settings.animations.viewmodel.idle
)
self.loadedAnimations.reload = self.viewmodel.AnimationController:LoadAnimation(
self.settings.animations.viewmodel.reload
)
self.loadedAnimations.fire = self.viewmodel.AnimationController:LoadAnimation(
self.settings.animations.viewmodel.fire
)
self.loadedAnimations.idle:Play()
-- set ammo, either current or default filled
self.wepName = wepName
self.ammo[wepName] = self.ammo[wepName] or (self.settings.firing.magCapacity + 1)
local tweeningInformation = TweenInfo.new(0.5, Enum.EasingStyle.Quart, Enum.EasingDirection.Out)
TweenService:Create(self.lerpValues.equip, tweeningInformation, { Value = 0 }):Play()
--[[
Real life example:
self.loadedAnimations.idle = self.viewmodel.AnimationController:LoadAnimation(self.settings.anims.viewmodel.idle)
self.loadedAnimations.idle:Play()
self.tweenLerp("equip","In")
self.playSound("draw")
--]]
-- spawned because server requests are far from instant
task.spawn(function()
-- if server say no, then so does the client
local pass = ReplicatedStorage.weaponRemotes.equip:InvokeServer(wepName)
if not pass then
self:remove()
end
end)
self.curWeapon = wepName
self.equipped = true -- Yay! our gun is ready.
end
function handler:remove()
if self.reloading then
return
end
if self.firing then
self:fire(false)
end
if self.aiming then
self:aim(false)
end
local tweeningInformation = TweenInfo.new(0.6, Enum.EasingStyle.Quart, Enum.EasingDirection.Out)
TweenService:Create(self.lerpValues.equip, tweeningInformation, { Value = 1 }):Play()
self.equipped = false -- Nay! We can't do anything with the gun now.
self.disabled = true
self.curWeapon = nil
task.spawn(function()
-- cough
ReplicatedStorage.weaponRemotes.unequip:InvokeServer()
end)
task.wait(0.6) --task.wait until the tween finished so the gun lowers itself smoothly
if self.viewmodel then
self.viewmodel:Destroy()
self.viewmodel = nil
end
self.disabled = false
end
function handler:reload()
if self.firing then
self:fire(false)
end
if self.aiming then
self:aim(false)
end
if self.reloading then
return
end
self.reloading = true
self.ammo[self.wepName] = 0
self.loadedAnimations.reload:Play()
-- we can use keyframe reached here, i will use length instead. waiting for the animation to finish will yield infinitely
if not self.equipped then
return
end
task.wait(self.loadedAnimations.reload.Length)
self.ammo[self.wepName] = self.settings.firing.magCapacity
self.reloading = false
end
function handler:fire(tofire)
if self.reloading then
return
end
if self.disabled then
return
end
if not self.equipped then
return
end
if tofire and self.ammo[self.wepName] <= 0 then
self:reload()
return
end
if self.firing and tofire then
return
end
if not self.canFire and tofire then
return
end
-- this makes the loop stop running when set to false
self.firing = tofire
if not tofire then
return
end
-- while lmb held down do
local function fire()
if self.ammo[self.wepName] <= 0 then
return
end
-- It's better to replicate the change to other clients and play it there with the same code as here instead of using SoundService.RespectFilteringEnabled = false
local sound = self.viewmodel.receiver.pewpew:Clone()
sound.Parent = self.viewmodel.receiver
sound:Play()
-- replace? i've heard bad things about debris service
Debris:AddItem(sound, 5)
self.loadedAnimations.fire:Play()
self.ammo[self.wepName] = self.ammo[self.wepName] - 1
-- addition of deltatime here is a poor attempt at fixing the recoil being framerate based
-- this doesn't happen in my own game, dunno why
self.springs.fire:shove(Vector3.new(0.03, 0, 0) * self.deltaTime * 60)
task.delay(0.15, function()
self.springs.fire:shove(Vector3.new(-0.03, 0, 0) * self.deltaTime * 60)
end)
-- Muzzle flash
for _, v in pairs(self.viewmodel.receiver.barrel:GetChildren()) do
if v:IsA("ParticleEmitter") then
v:Emit(v.Rate)
end
end
-- origin, direction
-- barrel because realism, camera.CFrame because uh accuracy and arcadeying
-- make sure the barrel is facing where the gun fires
-- aaand make sure the gun is actually facing towards the cursor properly, players don't like offsets
local origin = self.viewmodel.receiver.barrel.WorldPosition
local direction = self.viewmodel.receiver.barrel.WorldCFrame
-- inconsistent :(
fastcastHandler:fire(origin, direction, self.settings)
task.wait(60 / self.settings.firing.rpm)
end
repeat
self.canFire = false
fire()
self.canFire = true
until self.ammo[self.wepName] <= 0 or not self.firing
if self.ammo[self.wepName] <= 0 then
self.firing = false
end
end
function handler:aim(toaim)
-- we'll be using this soon
-- We used it! ha!
-- add a TweenService variable at the top that references TweenService yourself, thanks
if self.disabled then
return
end
if not self.equipped then
return
end
self.aiming = toaim
UserInputService.MouseIconEnabled = not toaim --do this wherever you want
ReplicatedStorage.weaponRemotes.aim:FireServer(toaim)
-- This is an easy to make approach
if toaim then
-- customize speed at will.
local tweeningInformation = TweenInfo.new(1, Enum.EasingStyle.Quart, Enum.EasingDirection.Out)
TweenService:Create(self.lerpValues.aim, tweeningInformation, { Value = 1 }):Play()
else
local tweeningInformation = TweenInfo.new(0.5, Enum.EasingStyle.Quart, Enum.EasingDirection.Out)
TweenService:Create(self.lerpValues.aim, tweeningInformation, { Value = 0 }):Play()
end
end
function handler:update(deltaTime)
self.deltaTime = deltaTime
-- IF we have a gun right now. We're checking the viewmodel instead for "reasons".
if self.viewmodel then
-- for animations
-- breaks for some people? idk
local animatorCFrameDifference = self.lastReceiverRelativity
or CFrame.new() * self.viewmodel.camera.CFrame:ToObjectSpace(self.viewmodel.rootPart.CFrame):Inverse()
local x, y, z = animatorCFrameDifference:ToOrientation()
workspace.Camera.CFrame = workspace.Camera.CFrame * CFrame.Angles(x, y, z)
self.lastReceiverRelativity = self.viewmodel.camera.CFrame:ToObjectSpace(self.viewmodel.rootPart.CFrame)
-- get velocity for walkCycle
local velocity = self.character.HumanoidRootPart.Velocity
-- you can add priorities here! for example, equip offset for procedural equipping would be below aimOffset to overwrite it when removing the gun.
-- here, aim overwrites idle.
local idleOffset = self.viewmodel.offsets.idle.Value
local aimOffset = idleOffset:lerp(self.viewmodel.offsets.aim.Value, self.lerpValues.aim.Value)
local equipOffset = aimOffset:lerp(self.viewmodel.offsets.equip.Value, self.lerpValues.equip.Value)
-- it'll be final for a reason. You saw!
local finalOffset = equipOffset
-- Let's get some mouse movement!
local mouseDelta = UserInputService:GetMouseDelta()
if self.aiming then
mouseDelta *= 0.1
end
self.springs.sway:shove(Vector3.new(mouseDelta.X / 200, mouseDelta.Y / 200)) --not sure if this needs deltaTime filtering
-- speed can be dependent on a value changed when you're running, or standing still, or aiming, etc.
-- this makes the bobble faster.
local speed = 1
-- modifier can be dependent on a value changed when you're aiming, or standing still, etc.
-- this makes the bobble do more. or something.
local modifier = 0.1
if self.aiming then
modifier = 0.01
end
-- See? Bobbing! contruct a vector3 with getBobbing.
local movementSway = Vector3.new(
getBobbing(10, speed, modifier),
getBobbing(5, speed, modifier),
getBobbing(5, speed, modifier)
)
-- if velocity is 0, then so will the walk cycle
self.springs.walkCycle:shove((movementSway / 25) * deltaTime * 60 * velocity.Magnitude)
-- Sway! Yay!
local sway = self.springs.sway:update(deltaTime)
local walkCycle = self.springs.walkCycle:update(deltaTime)
local recoil = self.springs.fire:update(deltaTime)
-- RecoillllL!!!!!
self.camera.CFrame = self.camera.CFrame * CFrame.Angles(recoil.x, recoil.y, recoil.z)
--ToWorldSpace basically means rootpart.CFrame = camera CFrame but offset by xxx while taking rotation into account. I don't know. You'll see how it works soon enough.
self.viewmodel.rootPart.CFrame = self.camera.CFrame:ToWorldSpace(finalOffset)
self.viewmodel.rootPart.CFrame = self.viewmodel.rootPart.CFrame:ToWorldSpace(
CFrame.new(walkCycle.x / 4, walkCycle.y / 2, 0)
)
-- Rotate our rootpart based on sway
self.viewmodel.rootPart.CFrame = self.viewmodel.rootPart.CFrame * CFrame.Angles(0, -sway.x, sway.y)
self.viewmodel.rootPart.CFrame = self.viewmodel.rootPart.CFrame
* CFrame.Angles(0, walkCycle.y / 2, walkCycle.x / 5)
end
end
return handler
To embed this project on your website, copy the following code and paste it into your website's HTML: