This is not in the 1st part thanks to the devforum having issues with longer posts. Firefox crashed twice at the end of the previous one.
This part covers equipping, reloading, and firing the gun. It’s the last part.
1. Smooth value module
I wrote it myself. It’s useful for making movement more smooth with less code. There are a bunch of functions once you create a smoothValue object, mostly :set()
to set target, :get()
to get it and:update()
for the obvious updating.
The module: (Prepared for old code style again; uppercase the letters at will)
local smoothValue = require(game.ReplicatedStorage:WaitForChild("smoothValue"))
local value = smoothValue:create(0, 0, 10)
game:GetService("RunService").RenderStepped:Connect(function(dt)
local val = value:update(dt)
workspace.Part.Position = Vector3.new(val, 5, 0)
end)
while wait(2) do
value:set(5)
wait(2)
value:set(-5)
end
1.5 FastCast
Incredibly useful projectile module for creating bullets with bullet drop. Used later in this tutorial.
No examples, though. I don’t have any short ones.
Credit: @EtiTheSpirit
2. Another setup
The weapons need a Motor6D for a holster pose, equip pose, and we need a few more directories. Let’s add them:
And the weapon items:
Both new welds need to have Part1 set to the receiver. the Torso will be connect to them, so let’s adjust it to work that way.
weaponHold is for an equipped state and should ideally be somewhere near the right hand, connected to the right arm instead of the torso. backweld, though, is for when the gun is holstered. throw it on the back or something, your choice.
After you make a Dummy and try to edit welds using moon animation suite.
*they may, very often, be selected incorrectly. In that case, reparent weaponHold when trying to edit backweld, and put it back when you’re done, and vice versa with backweld.
I failed to realize the weapon size scale here. The gun is as tall as you are, so i had to resize it. This broke the animations, but i won’t bother with redoing them, just keep weapon scale in mind when adding new weapons.
For now, remove the part1 from both of the welds, and let’s start making it work using magic.
let’s make a Script inside ServerScriptService
and put some stuff inside it;
local remotes = game.ReplicatedStorage:WaitForChild("weaponRemotes")
local weapons = game.ReplicatedStorage:WaitForChild("weapons") -- table for keeping track of weapons
-- ayyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
local defaultWeapons = {
[1] = "m4a4"
}
-- feel free to separate this
-- this is the amount of ammo each gun gets spare
local magazineCount = 5
-- each time the player spawns, they get a new weapon slot:
remotes:WaitForChild("new").OnServerInvoke = function(player)
if not player.Character then return end
-- we create a new table for the player
players[player.UserId] = {}
local weaponTable = players[player.UserId]
-- some stuff for later
weaponTable.magData = {}
weaponTable.weapons = {}
-- add each available weapon
for index, weaponName in pairs(defaultWeapons) do
-- clone gun
local weapon = weapons[weaponName]:Clone()
local weaponSettings = require(weapon.settings)
-- index gun
weaponTable.weapons[weaponName] = { weapon = weapon; settings = weaponSettings }
-- save gun magazines
weaponTable.magData[index] = { current = weaponSettings.firing.magCapacity; spare = weaponSettings.firing.magCapacity * magazineCount }
-- holster goon
weapon.Parent = player.Character
weapon.receiver.backweld.Part0 = player.Character.Torso
end
-- we give the client the gun list
return defaultWeapons, weaponTable.magData
end
remotes:WaitForChild("equip").OnServerInvoke = function(player)
-- boi
end
remotes:WaitForChild("unequip").OnServerInvoke = function(player)
-- boi
end
-- for making a gun variable
game.Players.PlayerAdded:Connect(function(player)
-- this method of adding values to the player on-added is much better than pasting the same code all over again.
-- the gun variable is incredibly useful for keeping track of the gun inside other scripts, such as procedural animations w/ foot planting
local values = {
{ name = "gun"; value = nil; type = "ObjectValue" };
}
-- table good c+p bad
for _, v in pairs(values) do
local value = Instance.new(v.type)
value.Name = v.name
value.Value = v.value
value.Parent = player
end
end)
We also need to change up the client inputController code to get weapons from the server;
...
-- current weapon moved to gun module
local enumBinds = {
[1] = "One";
[2] = "Two";
[3] = "Three";
}
-- Server security. We need it this time around.
local weps, ammoData = game.ReplicatedStorage.weaponRemotes.new:InvokeServer()
local weapon = weaponHandler.new(weps)
-- let's just make it easier on me to not mention another edit
weapon.ammoData = ammoData
-- clearing viewmodels we could have kept in the camera because of script errors and stuff
local viewmodels = workspace.Camera:GetChildren()
for i,v in pairs(viewmodels) do
if v.Name == "viewmodel" then
v:Destroy()
end
end
...
… And also edit the equipping part.
-- if the current equipped weapon is different from the one we want right now (also applies to the weapon being nil)
-- v is a string now, so remove .Name
if weapon.curWeapon ~= v then
if weapon.equipped then
weapon:remove()
end
weapon:equip(v)
-- v is a string now, so remove .Name
else
-- if it's the same, just remove it
spawn(function()
weapon:remove()
end)
weapon.curWeapon = nil
end
working = false
Our gun now holsters! cool.
In case you get glued to a dummy you were adjusting the welds on, remove Part0 from the welds again.
3. Equipping and not Equipping
My game marks the gun as m4a4holstered
for “”“clarity”"". (aka. it’s terrible and i am ashamed of having written it) This is useless because the gun
variable inside each player was added later on.
What we need to do is essentially set an equipped weapon on the server, make sure it can be equipped (and refuse to equip it clientside if not), and unholster/holster accordingly.
remotes:WaitForChild("equip").OnServerInvoke = function(player, wepName)
if players[player.UserId].currentWeapon then return end
if not players[player.UserId].weapons[wepName] then return end
if not player.Character then return end
local weaponTable = players[player.UserId]
-- we mark the current gun
weaponTable.currentWeapon = weaponTable.weapons[wepName]
player.gun.Value = weaponTable.currentWeapon.weapon
-- unholster goon
weaponTable.currentWeapon.Parent = player.Character
weaponTable.currentWeapon.weapon.receiver.backweld.Part0 = nil
-- equip gun
weaponTable.currentWeapon.weapon.receiver.weaponHold.Part0 = player.Character["Right Arm"]
weaponTable.loadedAnimations.idle = player.Character.Humanoid:LoadAnimation(weaponTable.currentWeapon.settings.animations.player.idle)
weaponTable.loadedAnimations.idle:Play()
-- yes client u can equip gun
return true
end
-- reverse of equipping lol
remotes:WaitForChild("unequip").OnServerInvoke = function(player)
if not players[player.UserId].currentWeapon then return end
if not player.Character then return end
local weaponTable = players[player.UserId]
weaponTable.loadedAnimations.idle:Stop()
weaponTable.loadedAnimations = {}
-- holster gun
weaponTable.currentWeapon.Parent = player.Character
weaponTable.currentWeapon.weapon.receiver.backweld.Part0 = player.Character.Torso
-- un equip gun
weaponTable.currentWeapon.weapon.receiver.weaponHold.Part0 = nil
-- we mark the inexistence of the current gun
weaponTable.currentWeapon = nil
player.gun.Value = nil
--
return true
end
The client doesn’t actually care about the variable right now, though, so let’s fix that.
... blah blah code for equip above
--[[
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
spawn(function()
-- if server say no, then so does the client
local pass = game.ReplicatedStorage.weaponRemotes.equip:InvokeServer(wepName)
if not pass then print("god") self:remove() end
end)
self.curWeapon = wepName
self.equipped = true -- Yay! our gun is ready.
end
and also unequipping
function handler:remove()
if self.reloading then return end
local tweeningInformation = TweenInfo.new(0.6, Enum.EasingStyle.Quart,Enum.EasingDirection.Out)
local properties = { Value = 1 }
tweenService:Create(self.lerpValues.equip,tweeningInformation,properties):Play()
self.equipped = false -- Nay! We can't do anything with the gun now.
self.disabled = true
self.curWeapon = nil
spawn(function()
-- cough
game.ReplicatedStorage.weaponRemotes.unequip:InvokeServer()
end)
wait(0.6) --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
Cool, the gun equips/unequips. This change should be visible to others too.
But guess what, bullet spewer users typically don’t hold the gun like in the video. although i personally find this shocking, we need to fix it.
Let’s get an animation rig by ripping it out of the game! make sure you equip the gun during the copy paste
Cools. let’s animate it. Make an idle position, a hip_fire animation, an aim position, and an aim_fire animation. you may also add a running animation, and reload, although this won’t be covered right now.
Animation tips:
-
Back : Out
easing styles work well together with quick shooting animations. -
R6 limbs are typically shown as the lower limb, NOT a combination of both.
-
Use blender, unless you’re lazy, in which case just spend more time trying to make a procedural animation system like I did or use roblox. No, i won’t open source it.
Implementing them is simple. Upload them, throw them in as animation objects inside serverAnimations, and have the client send requests to do certain actions. We’ll use just idle for now.
Animation exporting settings are important! Poses need looping, idle “needs” a lower priority just to be safe, and keeping backups is unrelated, but important. i didn’t know what to write for the 3rd thing
For the sake of the tutorial I’m also keeping the animation files inside the animation objects.
Now, here’s an optional part; throw the animations inside settings. Same place as with the viewmodel. The advantage is that the script output doesn’t scream in red eye-hurting agony if you try to use an animation that doesn’t exist/isn’t implemented, and doing if self[path lol].revolverReload then
is easier than if self[path lol]:FindFirstChild("revolverReload") then
local data = {
animations = {
viewmodel = {
idle = root.animations.idle;
fire = root.animations.fire;
reload = root.animations.reload;
};
player = {
aim = root.serverAnimations.aim;
aimFire = root.serverAnimations.aimFire;
idle = root.serverAnimations.idle;
idleFire = root.serverAnimations.idleFire;
};
};
firing = {
rpm = 700;
magCapacity = 30;
}
}
return data
The animations are set-up now! let’s implement them.
a little communication there…
and over here… plot twist
Now that the server is nicely notified of the client actions, let’s write some more code.
we add a new table into the new
server function…
weaponTable.loadedAnimations = {}
and add small code samples for playing the animations.
-- equip function
weaponTable.currentWeapon.weapon.receiver.backweld.Part0 = nil
-- equip gun
weaponTable.currentWeapon.weapon.receiver.weaponHold.Part0 = player.Character["Right Arm"]
weaponTable.loadedAnimations.idle = player.Character.Humanoid:LoadAnimation(weaponTable.currentWeapon.settings.animations.player.idle)
weaponTable.loadedAnimations.idle:Play()
-- unequip function
local weaponTable = players[player.UserId]
weaponTable.loadedAnimations.idle:Stop()
weaponTable.loadedAnimations = {}
-- aiiiiimingggggggggggggg
remotes:WaitForChild("aim").OnServerEvent:Connect(function(player, toaim)
if not players[player.UserId].currentWeapon then return end
if not player.Character then return end
local weaponTable = players[player.UserId]
-- we mark this for firing animations
weaponTable.aiming = toaim
-- load the aim animation
if not weaponTable.loadedAnimations.aim then
weaponTable.loadedAnimations.aim = player.Character.Humanoid:LoadAnimation(weaponTable.currentWeapon.settings.animations.player.aim)
end
-- play or stop it
if toaim then
weaponTable.loadedAnimations.aim:Play()
else
weaponTable.loadedAnimations.aim:Stop()
end
end)
Now that’s what i call gun holding. There’s still one important part to cover here:
4. using FastCast
FastCast is a module made for projectiles while staying efficient, and easy to code. Relatively easy to code. Probably easy to code. I don’t know.
For now, let’s make our game FastCast ready. We’ll do this by making a separate module for FastCast; this is so replication is easier to manage. Obviously bullets can stay server-side only, but then they freeze and take time to create and it’s all messy and i hate it.
I’d also like to bring up server security; i won’t program any here, just give you this cool map for more elaborate use:
Let’s add the items in there:
(Gravity is equivalent of bullet drop, -100 to -500 work well)
(the Bullet is just a a neon cube, add tracers at will)
A cool thing about the “fastcasthandler” approach is that you can copy paste it between games easily
This uses the same projectile code as my main FPS game; just with certain parts removed, since I want to open-source it at some point.
Now, let’s get the fastcasthandler coded;
local fastcastHandler = {}
local replicatedStorage = game:GetService("ReplicatedStorage")
local players = game:GetService("Players")
-- babababa unknown require
local fastcast = require(replicatedStorage.modules.fastCastRedux)
local random = Random.new()
-- create a caster, basically the gun
local mainCaster = fastcast.new()
local bullets = {}
-- standard rayUpdated function; feel free to touch the code, just not the existing 2 lines
function rayUpdated(_, segmentOrigin, segmentDirection, length, bullet)
local BulletLength = bullet.Size.Z / 2 -- This is used to move the bullet to the right spot based on a CFrame offset
bullet.CFrame = CFrame.new(segmentOrigin, segmentOrigin + segmentDirection) * CFrame.new(0, 0, -(length - BulletLength))
end
-- Destroy the bullet, ask server to deal damage etc.
function rayHit(hitPart, hitPoint, normal, material, bullet)
bullet:Destroy()
end
-- The code pasted in is a bit old, excuse the variable inconsistencies like " , " spacing
-- this applies throughout all of the new tutorial lmao
function fastcastHandler:fire(origin, direction, properties, isReplicated, repCharacter)
local rawOrigin = origin
local rawDirection = direction
-- if the propertie aren't already required just require them
if type(properties) ~= "table" then
properties = require(properties)
end
local directionalCFrame = CFrame.new(Vector3.new(), direction.LookVector)
direction = (directionalCFrame * CFrame.fromOrientation(0, 0, random:NextNumber(0, math.pi * 2)) * CFrame.fromOrientation(0, 0, 0)).LookVector
local bullet = replicatedStorage.bullet:Clone()
bullet.CFrame = CFrame.new(origin, origin + direction)
bullet.Parent = workspace.fastCast
bullet.Size = Vector3.new(0.05, 0.05, properties.firing.velocity / 200)
-- useful with the server security i made, almost useless in this fps demo
local id = math.random(-100000,100000)
local idValue = Instance.new("NumberValue")
idValue.Name = "id"
idValue.Value = id
idValue.Parent = bullet
bullets[id] = {
properties = properties;
replicated = isReplicated;
}
if not isReplicated then
replicatedStorage.weaponRemotes.fire:FireServer(rawOrigin, rawDirection, id)
end
-- Custom list; blacklist humanoidrootparts too if your players can croiuch and prone
local customList = {}
customList[#customList+1] = repCharacter
customList[#customList+1] = workspace.Camera
-- fire the caster
mainCaster:FireWithBlacklist(origin, direction * properties.firing.range, properties.firing.velocity, customList, bullet, true, Vector3.new(0, replicatedStorage.bulletGravity.Value, 0))
end
mainCaster.RayHit:Connect(rayHit)
mainCaster.LengthChanged:Connect(rayUpdated)
return fastcastHandler
Along with changes to the settings;
firing = {
damage = 50;
headshot = 100;
rpm = 700;
magCapacity = 30;
velocity = 1500;
range = 5000;
}
and FPS module;
...
elseif v.Name == "smoke" then
v.Enabled = false
end
end
end)()
-- make sure to reference fastcastHandler at the top of the script
-- 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 variablys :(
fastcastHandler:fire(origin, direction, self.settings)
wait(60/self.settings.firing.rpm)
end
...
What a life achievement. The gun fires, with bullet drop and bullet velocity.
obviously, No one sees your gun firing. I blame schizophrenia. now that i failed to make a good joke, we need to make it replicate. the fastcastHandler already has some code for this, but we need to add to it:
-- Destroy the bullet, ask server to deal damage etc.
function rayHit(hitPart, hitPoint, normal, material, bullet)
if not hitPart then bullet:Destroy() return end
-- algorithm for finding humanoids
-- maybe not the most efficient? i'm not sure
-- doesn't work the best with accessories, headshots arent detected through hats
local humanoid = hitPart:FindFirstChild("Humanoid")
local curParent = hitPart
local headshot = false
repeat
-- if found head to shoot
if curParent.Name == "Head" then
headshot = true
end
curParent = curParent.Parent
humanoid = curParent:FindFirstChild("Humanoid")
until curParent == workspace or humanoid
if not humanoid then
-- hit particles and sounds and stuff
else
-- do NOT do this in a real game or i will come to your house for tea
replicatedStorage.weaponRemotes.hit:FireServer(humanoid, headshot)
end
end
-- pew
remotes:WaitForChild("fire").OnServerEvent:Connect(function(player, origin, direction)
local weaponTable = players[player.UserId]
if not weaponTable.currentWeapon then return end
if not player.Character then return end
-- don't do this without verification
-- we replicate the changes to other clients
remotes.fire:FireAllClients(player, origin, direction)
if weaponTable.aiming then
if not weaponTable.loadedAnimations.aimFire then
weaponTable.loadedAnimations.aimFire = player.Character.Humanoid:LoadAnimation(weaponTable.currentWeapon.settings.animations.player.aimFire)
end
weaponTable.loadedAnimations.aimFire:Play()
else
if not weaponTable.loadedAnimations.idleFire then
weaponTable.loadedAnimations.idleFire = player.Character.Humanoid:LoadAnimation(weaponTable.currentWeapon.settings.animations.player.idleFire)
end
weaponTable.loadedAnimations.idleFire:Play()
end
end)
-- player hit event
remotes:WaitForChild("hit").OnServerEvent:Connect(function(player, humanoid, headshot)
if not players[player.UserId].currentWeapon then return end
if not player.Character then return end
if headshot then
humanoid:TakeDamage(players[player.UserId].currentWeapon.settings.firing.damage)
else
humanoid:TakeDamage(players[player.UserId].currentWeapon.settings.firing.headshot)
end
end)
aaand we need a new localscript for replication
local replicatedStorage = game:GetService("ReplicatedStorage")
local tweenService = game:GetService("TweenService")
local players = game:GetService("Players")
local fastcastHandler = require(replicatedStorage.modules.fastCastHandler)
-- standard stuff above. just listen to the weapon fire replication and fire the gun again accordingly.
ReplicatedStorage.weaponRemotes.fire.OnClientEvent:Connect(function(player, origin, direction)
if player ~= Players.LocalPlayer then
-- varibles
local gun = player.gun.Value
local properties = gun.settings
-- replicated fire sound
local sound = gun.receiver.pewpew:Clone()
sound.Parent = gun.receiver
sound:Play()
-- muzzle flash for 3rd person
for _, v in pairs(gun.receiver.barrel:GetChildren()) do
if v:IsA("ParticleEmitter") then
v:Emit(v.Rate)
end
end
-- re-fire from the client
fastcastHandler:fire(origin, direction, properties, true)
end
end)
(old gif, new ones with animation or damage didn’t want to upload)
The gun now fires, deals damage, shows up to other clients, and is equip-able/holster-able.
Congratulations. Your life is complete. You may now make a fullfilling game. Good luck getting to the front page and beyond.
5. The place file (again)
The code differs from the tutorial to include the new task library and have better code formatting.
Thomks for reading.
Edit 1: bug fixes for the FPS framework
Edit 2: bug fixes, may be inconsistent with the main game
Edit 3: variable redefine fixed