Writing an FPS framework, part 2 (2020)

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:

image

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
image
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.
    image

  • 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.
image

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

288 Likes

Thank you so much for this! I’m having a bit of a problem where whenever I pull out the gun the following happens: https://gyazo.com/d0d645a790b91344347c959cd6ff4dde

if I were to fire the viewmodel and gun would go the normal place, so I’m not sure if it’s just an issue with the animation or something since it’s not throwing any errors either! ^^ I’ve already exported all the animations included in the place file so I’m not quite sure what the issue is. Either way, thank you so much for making this amazing framework!

15 Likes

This is a known bug. I don’t know what’s causing it, other than animations being potentially broken. Try importing the sequence for idle, making sure it’s looped, and exporting it again.

4 Likes

You have to actually upload the animations yourself. You can’t use other peoples anims by id unless you are in a group with them. The fix is uploading the attached anims that are inside/under the animations, the motor6ds, light blue cube.

5 Likes

I know! ^^ That’s already been done which is why I was confused.

4 Likes

I’ll try that! Will everything still work fine with the new Moon Animation Suite? I’m not familiar with it’s new updated version

3 Likes

God knows what that monstrosity is. Ask me in PMs for the older version’s backup.

4 Likes

Hey, are you planning on making a fully functional open source fps framework out of this as there are alot of unused stuff inside the scripts that say “for later use” ?

2 Likes

It’s pretty much the base for a fully working framework. I took it as my job to teach people how to make it work properly (some attempts at this are just sad) and let them figure their own way out from there like i did with egomoose’s tutorial.

5 Likes

Hm. Alright, is there any way that I can contact you privately? Discord would be a great use for DMs. If not, RBXL or Devforum PMs will be fine.

@LOLGuy2587
If you want to contact me just use PMs on the devforum.

@BetterCallBrenda
Second, the idle animation itself was wrong. I found this out with another person on discord. sorry for that. I updated the place with a new animation, it should work now

1 Like

I have a small issue here, how would i go about making tools that arent guns? Shovels? Medkits? Repair Tools?

Would it just be another modulescript tied to the clicks?

Use different, probably specific functions instead of :fire() and use more or less the same setup for rigging them.

But the firing functions are the same for all weapons, so i’d have to have all of the code for every tool/wep in one script?

1 Like

I’d offload it into settings and do some magic to fire the custom event instead of the default one if it exists. just be wary of exploiters.

1 Like

Why be wary of exploiters? Wouldnt you have a server side check for ammo too?

1 Like

“”“Editing”“”" a function in a module once is pretty easy to do with any lua exector afaik. the ammo check is “partially” implemented, the ammo is stored in the table with the ‘new’ server event already, it just wasn’t implemented fully.

1 Like

Great tutorial! If I use this in a game of mine should I credit you in the desc?

1 Like

Knowledge is knowledge! no credit needed.

4 Likes

If I use this weapon in Roblox Studio, it will fail animation.


Any way to resolve this?

3 Likes