Making an FPS framework: 2020 edition

This is my first tutorial here!

Point out any errors in the code, please. I might have missed something.

I have made many past attempts at making pew pew guns on roblox in the past, and i thought i’d share some of the “basic” process for beginners here. Keep in mind none of this needs to be the way it is here, feel free to use your own methods.

This article covers the very basics on setting up a working gun, making it aim (and how to make it easy to add other things), and making it pew pew at other people. However, This article doesn’t cover projectile math (aka. actually damaging things) or server security! i will only mention how i did it myself.

1. You need to know these things to understand this tutorial properly!

Lua; how functions work, and how you can throw them between modules.
How modulescripts work,
Filtering enabled,
Rigging and how it works,
Download the old moon animation studio plugin to rig easier. Using plugins to rig for you won’t save you here. Everything should be explained, though, so don’t be discouraged.
Tables (arrays, storing functions and variables inside)
And other things i probably didn’t remember.

1.5 Custom velocity

For code to work properly (i use an older version of inputservice in velocity) you need the following module:

Credit: @ReturnedTrue

2. The spring module

It’s used pretty widely between games for all sorts of movement. Mostly sway. It’s a modulescript, which you can set up easily;

The module; (Prepared for old code style; uppercase the letters at will)
https://pastebin.com/WvPXPMbF

Credit: @x_o

Code usage example;

-- This is a localscript

-- set up some spring
local spring = require(game.ReplicatedStorage.spring)
local mySpring = spring.create()

-- random Random
local random = Random.new()

game:GetService("RunService").RenderStepped:Connect(function(deltaTime)
	
	-- update the spring based on deltaTime.
	local movement = mySpring:update(deltaTime)
	-- move the part in workspace based on the spring's movement. don't kill me for the code
	workspace.part.CFrame = CFrame.new(movement.x,5,0)
	
end)

while wait(random:NextNumber(.1,1)) do 
	-- shove the spring in a random direction with some sort of hammer. We like random directions, don't we?
	mySpring:shove(Vector3.new(random:NextNumber(-25,25),0,0)) 
end

3. Setting up the game

Firstly, we need some folders for general use:

Our magic gun code will be in modules>fps, and the thing that makes it magically do things will be inputController.

You may also get some custom modulescripts at this point. I use ReturnedTrue’s Velocity framework for inputs, leaderstats and code debugging and FastCast for projectiles (i wrote another module that uses fastcast and adds penetration, tracers, ricochets, and other magic things but it’s not public)

4. Setting up guns

Exciting, right? Get a gun model (I will use an ACS gun model cleared of scripts because i’m sharing the place)
image

We want to do a few things with it:

Add an offsets folder,
Add a settings module,
Add an animations folder,
Add a part named weaponRootPart, paste it in the position of the receiver, make sure the front face is going in the barrel’s direction and the up face is going up, and weld it to the receiver.
Rename one of the major parts (receiver, particularly) to receiver,
Add an attachment inside the receiver that we will use for muzzle flash, and put some cool particles inside of it,

You’ll see why we need both offsets and animations later. Here’s the final structure (don’t mind the stuff inside offsets just yet)

Finally, rig anything you might need for animations (chambering, pistol bolt, etc.) to the weaponRootPart!, weld the rest of the parts, make them not cast shadows (we’ll do it later again lol), and make sure everything is .Anchored = false, group it, name it, and throw it in ReplicatedStorage>weapons.

5. Making a viewmodel

BuT WheRe’s ThE ViewModEL? Less experienced scripters tend to merge the viewmodel with the gun while making it right now. This is stupid and dumb and i hate it for multiple reasons. A smarter approach is merging them together for animation when the gun is equipped. we will do this later, but for now let’s just make a viewmodel that can have that done in the first place.

Small note for blank Motor6d: local mt6d = Instance.new("Motor6D") mt6d.Parent = (viewmodel rootpart)

Make a part named rootPart, group it, and add blank motor6ds for both the arms (one named left, second named right)
Add a BLANK Motor6D, throw it in the rootpart, and set Part0 to the viewmodel rootpart. We will use it for connecting the gun with the viewmodel.
Finally, add an animation controller.

Optionally, if you want coolio camera movement you can add a camera Part, rig it, and animate it later. I will add a small code sample to show how you’re “supposed” to script it.

Throw the viewmodel into replicatedstorage, or anywhere else you might want it. Just not workspace, it’s not prepared for such habitat without extra code sprinkling.

6. Setting up the controller to equip the weapon

We’ll make the gun equip itself using keys on the keyboard. I stole some code from my main game:

-- input controller

-- The fps module we're about to add
local weaponHandler = require(game.ReplicatedStorage.modules.fps)

--Custom input service. Do this how you want, i just couldn't ever remember how to use other services consistently.
local velocity = require(game.ReplicatedStorage.modules.Velocity):Init(true)
local inputs = velocity:GetService("InputService")

--Current weapon and translating binds from integer to enum. You don't need to understand that.
local curWeapon = nil
local enumBinds = {
	[1] = "One";
	[2] = "Two";
	[3] = "Three";
}

-- Server security. We don't need this right now, so i'll just comment it.
-- local weps,primaryAmmo,secondaryAmmo = game.ReplicatedStorage.weaponRemotes.New:InvokeServer()

--[[

	This means if you add a second weapon it'll automatically go under the bind 2,3, etc. 
	This is a bad thing, at least for later stages. The above commented line shows that we 
	should get the weapons from the server on-spawned. Here's how it looks in my game:
	
	local defaultWeapons = {
		[1] = "UMP45";
	--	[2] = "JESUS"; lol railgun
	}
--]]
local weps = game.ReplicatedStorage.weapons:GetChildren() 
local weapon = weaponHandler.new(weps)

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

-- equip code
for i,v in pairs(weps) do
	
	-- cooldown for spammy bois
	local working
	
	-- we will bind this per-weapon
	local function equip()
		
		-- if cooldown active, then don't execute the function. for less experienced scripters, this is just the equivalent of:
		 --[[
			
			local function brug()
				
				if working == false then
					
					-- do stuff
					
				end
				
			end
		
		 --]]
		
		if working then return end 
		
		working = true
		
		-- if the current equipped weapon is different from the one we want right now (also applies to the weapon being nil)
		if curWeapon ~= v then
			
			if weapon.equipped then
				weapon:remove()
			end
			weapon:equip(v.Name)
			curWeapon = v
			
		else
		-- if it's the same, just remove it
		
			spawn(function()
				weapon:remove()
			end)
			curWeapon = nil
			
		end
		
		working = false
	end
	
	-- This means you can have 3 different weapons at once.
	inputs.BindOnBegan(nil,enumBinds[i],equip,"Equip : "..i)
end

local function update(dt)
	weapon:update(dt)
end


-- marking the gun as unequippable
game.Players.LocalPlayer.Character:WaitForChild("Humanoid").Died:Connect(function() weapon:remove() weapon.disabled = true end)
game:GetService("RunService").RenderStepped:Connect(update)

This should be enough to have the gun nicely equip when we want it to. Now, let’s get to the real magic!

7. Making the guns magically equip

Now, we’re going to set up some code to make the input controller above not cry about things not existing. i hate him too.

Add a spring named spring to keep the FPS module from screaming about it too. Change name at will.

-- This is game.replicatedstorage.fps 

-- Coolio module stuff
local handler = {}
local fpsMT = {__index = handler}	

local replicatedStorage = game:GetService("ReplicatedStorage")
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
	
-- Cool for lerping numbers (useful literally everywhere)
local function lerpNumber(a, b, t)
	return a + (b - a) * t
end

function handler.new(weapons)
	local self = {}

	
	return setmetatable(self,fpsMT)
end

function handler:equip(wepName)
	-- we'll be using this soon
end

function handler:remove()
	
        -- shalt thou use this soon
	
end

function handler:aim(toaim)
	
	-- we'll be using this soon
	
end

function handler:update(deltaTime)

      -renderstepped, we'll be using this boi soon

end

--[[
	Used in my real game for framerate counting. I kept it in for fun.
	
	spawn(function()
		
		local fps = 0
		game:GetService("RunService").RenderStepped:Connect(function()
			fps = fps + 1
		end)
		
		while wait(1) do
			local gui = game.Players.LocalPlayer.PlayerGui:FindFirstChild("wepGui")
			if gui then
				gui.FPScount.Text = string.format("FPS: %s",fps)
			end
			fps = 0
		end
		
	end)
--]]
return handler

Alright. To recap, we’re making a new gun handler inside the control script:
local weaponHandler = require(game.ReplicatedStorage.modules.fps)
local weapon = weaponHandler.new(weps)

This means we can now do weapon:equip() or weapon:remove(). This is metatable stuff, i don’t understand it much but you should only know that you can add any new function that follows function:funcName() end and you will be able to do weapon:funcName() too.

we call .new() to prepare some stuff inside the module, like springs. Real life example;

Now, we will need to compile a viewmodel and kill it with no mercy based on input. This means putting stuff in :equip(weapon) and :remove().

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

	-- 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 i,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 = game.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
	
	--[[
		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")
		
	--]]
	
	self.equipped = true -- Yay! our gun is ready.
end

function handler:remove()
	
	-- Not much to see here yet. even the real life function for removing a weapon takes like 30 lines ***on the client***.
	self.viewmodel:Destroy()
       self.viewmodel = nil
	self.equipped = false -- Nay! your gun is gone.
	
end

Now, if the gun is equipped it obviously won’t move along with the camera. That’s sad. I’m sad.
Why not make it?

Here’s where a bigger part of future-proofing comes in. We will move our weapon around based on offsets. For now, though, let’s just move it to the camera every render-step.


function handler:update(deltaTime)

	-- IF we have a gun right now. We're checking the viewmodel instead for "reasons".
	if self.viewmodel then
		
		self.viewmodel.rootPart.CFrame = self.camera.CFrame
		
	end
end

Now, you might notice something’s not right. It’s very subtle though. The gun is implanted straight into your face.

We will fix this by adding an offset.

We do this by adding CFrame value into Offsets. name it idle or something.

Furthermore, let’s move the gun based on the input from it.

		-- it'll be final for a reason. You'll see!
		local finalOffset = self.viewmodel.offsets.idle.Value
		--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)

Let’s run the game. At this point, the command bar will surely be handy.

You might notice the gun is still inside your face. It won’t leave by itself, after all.
Let’s input some commands that will fix it!

workspace.Camera.viewmodel.offsets.idle.Value = CFrame.new(rightLeft,upDown,frontBack) * CFrame.Angles(rotUpdown,rotLeftright,rotRoll)
This is a command line job. replace the variables with numbers to get a nice looking offset, and eventually we will get something like this:

workspace.Camera.viewmodel.offsets.idle.Value = CFrame.new(0.7,-1.2,-1.1) * CFrame.Angles(0.005,math.pi / 2 + 0.005,0)

At this point, COPY THE IDLE OBJECT, stop the game, and throw it in the place of the old idle offset.
Now, let’s animate the arms since they look like they were removed from your body and the gun is floating using magic.

There’s not much to it, just run the game, copy the viewmodel, stop the game, and paste it in. It should animate nicely. Now, if we have animations, why would you need to use offsets? This is for aiming, and getting a better preview of how it will look ingame. Other offsets too. Notice how many there are in the video below.

Not getting backtracked, let’s make an animation. Don’t include idle movement, you can do that procedurally.

Set whatever priority your heart desires, and loop it.
Upload it, and throw an animation object into the gun’s animations folder with the appropiate ID.

Time to start using settings, although you can just do self.viewmodel.animations.idle.
Here’s how I do it:

Now, we can:
-add a self.loadedAnimations table inside .new()
image
(wow! i’m using 2 separate editors!)
-load the settings in the equip function
-load and throw the animation inside it

Basically:

--bla bla bla
	self.viewmodel.Parent = workspace.Camera
	
       --load settings. you might want to do this earlier depending on your needs
	self.settings = require(self.viewmodel.settings)
       --load animation from settings
	self.loadedAnimations.idle = self.viewmodel.AnimationController:LoadAnimation(self.settings.animations.viewmodel.idle)
	self.loadedAnimations.idle:Play(0) --no lerp time from default pos to prevent stupid looking arms for no longer than 0 frames	
	
	
	self.equipped = true -- Yay! our gun is ready.
--blablabla

The arms look dumb dumb, but we can change that anyway, so i don’t care.

8. Aiming and springs

Now, why not make it look pretty? Everyone likes pretty things.

To do this, we’re gonna make the gun sway around based on spring input, and add a walk cycle.
Let’s do some preparation and coolio math!

Here’s where fps unlockers break things. If the run cycle is rigged to run 60 times a second, it will have trouble running at 100, or even 300!

(no not mine)
To fix this top 10th shocking thing you need to know about, we will multiply the movement by deltaTime. this means, if the game is running slowly (30 frames a second), the movement will still be similiar to 140 FPS, or even more. Don’t skim out on this, It’s very important.

Inside the .new() function we create some springs:


        self.springs = {}
	self.springs.walkCycle = spring.create();
	self.springs.sway = spring.create()

Now, let’s do some update math too:


	-- IF we have a gun right now. We're checking the viewmodel instead for "reasons".
	if self.viewmodel then
		
		-- get velocity for walkCycle
		local velocity = self.character.HumanoidRootPart.Velocity
		
		-- it'll be final for a reason. You'll see!
		local finalOffset = self.viewmodel.offsets.idle.Value
		
		-- Let's get some mouse movement!
		local mouseDelta = game:GetService("UserInputService"):GetMouseDelta()
		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
		
		-- 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)
		
		--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 / 2,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,walkCycle.x)
	end

And now, we have sway. How magnificent.

9. Aiming (for real this time)

Now, you might have noticed nothing happens when we press MouseButton2. I need to congratulate you for your achievement in life, but more importantly explain how to do it.

What we’re going to do:
create a number value
make an aim offset
tween the value when aiming in/out
lerp offsets based on that value
check if you’re aiming and sway less if you are
remove the mouse when aiming

Alright. let’s make it look easy.
add another table inside .new() for easy access and add some values to it:

	local self = {}
	
	self.loadedAnimations = {}
	self.springs = {}
	self.lerpValues = {}
	
	self.lerpValues.aim = Instance.new("NumberValue")
	
	self.springs.walkCycle = spring.create();
	self.springs.sway = spring.create()
	
	return setmetatable(self,fpsMT)

Now, jump in the game and have another go at editing the idle value, this time to make it look as if you’re aiming. After you’re done, copy it and throw it into offsets as “aim”

Once you’re done, let’s do some more lerping:

		-- 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)
		
		-- it'll be final for a reason. You saw!
		local finalOffset = aimOffset

“Obviously”, the gun doesn’t aim just yet. To make it, let’s add some code:

inputs.BindOnBegan("MouseButton2",nil,function() weapon:aim(true) end,"AimPewPew")
inputs.BindOnEnded("MouseButton2",nil,function() weapon:aim(false) end,"AimPewPew")

This makes the function aim trigger when holding RMB. Obviously this input approach is straight up unclean and stupid and i hate it, but it’s simple. IRL code:


This does cool stuff. Don’t steal it please, though.
now, function handler:aim(toaim) will be used! Let’s edit it!

For the future: i wrote a simple function to make this much easier. self.tweenLerp("aim","In") is the function in question. This is because the in/out tween times are based on simple values we can grab autonomously instead of copy pasting code, and take my word for it: this process repeats itself many times. I’m not going to feature it in this tutorial; just know that it’s very possible indeed.
image

Here is our aiming code. Just basic tweening:


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
	-- 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)
		local properties = { Value = 1 }
		 tweenService:Create(self.lerpValues.aim,tweeningInformation,properties):Play()			
	else
		local tweeningInformation = TweenInfo.new(0.5, Enum.EasingStyle.Quart,Enum.EasingDirection.Out)
		local properties = { Value = 0 }
		tweenService:Create(self.lerpValues.aim,tweeningInformation,properties):Play()			
	end
	
end


Be proud of yourself. Now, let’s do the 2 remaining things.

Reducing movement is just a factor of reducing what shove does:

Removing the mouse is also just one function (use not toaim, i screwed up)

10. Pew Pewing

Get some quick muzzle flash ready inside the gun barrel. We need to make it look instant and punchy and stuff, and set Transparency to 1, then .Enabled = true. add a numberValue named transparency inside of the muzzle flash, and set it to the transparency it should have when firing.


image

Alright. let’s make it pew pew. You might also want a sound inside the receiver, since i heard guns make sounds.

set up a fire function the same as aim, just with a different name and bound to LMB.

We’re going to assume the weapon is automatic for now.
Add a table inside the settings named firing, and add a property called rpm, set it to 800 or something. whatever your gun spews bullets out at.

function handler:fire(tofire)
	
        -- wall of requirements :(
	if self.reloading then return end
	if self.disabled then return end
	if not self.equipped then 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()

		-- 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()
		
		game:GetService("Debris"):AddItem(sound,5)
		self.loadedAnimations.fire:Play()
		
		-- Muzzle flash. This is why we left it invisible and enabled.
		coroutine.wrap(function()		
			
			-- could be optimized a lot
			-- flash flashes inside the barrel, and smoke smokes for a short time
			
			for i,v in pairs(self.viewmodel.receiver.barrel:GetChildren()) do
				if v.Name == "flash" then
					v.Transparency = NumberSequence.new(v.transparency.Value)
				elseif v.Name == "smoke" then
					v.Enabled = true
				end
			end	
			
			wait()
			
			for i,v in pairs(self.viewmodel.receiver.barrel:GetChildren()) do
				if v.Name == "flash" then
					v.Transparency = NumberSequence.new(1)
				elseif v.Name == "smoke" then
					v.Enabled = false
				end
			end		
			
		end)()		
		
		wait(60/self.settings.firing.rpm)
	end
	
	repeat
		self.canFire = false
		fire()
		self.canFire = true
	until not self.firing

end

Oh nice! sound that you can’t hear, but it shoots nicely enough. I heard that these things tend to push into your hip a bit, and also heard that it’s called recoil, so we will do that right now.

I will make the most basic recoil and make it reset. Do whatever you want here. IRL example:

We’re going to add another spring in .new(), call it fire, make it rotate the camera whenever updated

		-- 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)

and shove to it when shooting

		self.springs.fire:shove(Vector3.new(0.03,0,0))
		spawn(function()
			wait(.15)
			self.springs.fire:shove(Vector3.new(-0.03,0,0))
		end)

The recoil looks terrible because it’s not randomized, but i’m not a very good game designer and i’m also lazy so have fun with it yourself.

11. More stuff (to the base)

These things will be covered with little detail.

Ammo is easily doable

12. Animation

Oh boy! you want to animate this thing in blender! You’re lucky that’s easy.
setup blender with Blender rig exporter/animation importer
use

workspace.Camera.CameraType = "Scriptable" workspace.Camera.CFrame = CFrame.new()

copy the make sure the viewmodel isn’t moving, copy it, drop it after the game has stopped,
make sure it’s still at 0,0,0 in the world,
Make all rootparts/hand markers/etc. transparency 0,
and export it. It should animate fine.

For animating the camera;

                --belongs in update loop
		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)

13. Other things

Projectiles can be easily handled with fastcast
ammo variables should be serversided
you should add plenty of settings
the aim lerp is easy to expand and can have a lot of other stuff added to it
Load animations along idle
You should add a dot in the middle of the screen for testing the aiming offset, It tends to be offset wrongly even if it looks correct
Rig things to the weaponRootPart, not the receiver. blender doesnt recognize things rigged to the receiver for some reason. this is why the test reload animation doesn’t have any mag movement
KeyframeReached is usable for sounds
You should have the server manage damage, ammo, equipped states etc.
I can make a part 2 if anyone cares about it

14. The Place File

It has all the stuff compiled in. The game is available here:

15. Part 2

Edit 1: idk
Edit 2: added credit for the spring module and velocity. Sorry if i pinged you guys accidentally.
Edit 3: part 2 now at your local thread
Edit 4: bug fixes for the code in the post. may be inconsistent with the main game
Edit 5: bug fixes

Thanks for reading
sai_2020-03-31_15-35-13

330 Likes

This was an amazing, well detailed, and well thought out tutorial and I’m sure that me and hundreds of other will benefit from this tutorial. Thank you blackshibe!

16 Likes

Wow, now that’s quite a tutorial.
About covered everything for me, I’ll definitely be using this a lot. Thank you for this.

And I appreciate you using Velocity!

6 Likes

Really nice tutorial, thanks for sharing! This is a lot more up to date compared to previous tutorials, and it’ll definitely help by acting as guidance for one of my current projects. Cheers! :+1:

sidenote: Also, the game you provided doesn’t seem to function as intended (can’t pick up the gun, use it, etc)

3 Likes

provide an error log. It’s all local, i don’t know why it wouldn’t work.

There is nothing of relevance in the log, so I’m unsure why it doesn’t work.

This image details what is happening (empty log and can’t use gun)

Image

EDIT: Issue solved by equipping by pressing 1 on keyboard.

2 Likes

Very detailed, well explained and useful. I know I along with many others who are new to making proper FPS frameworks will find this very helpful, props to you! :smiley:

1 Like

Very nice read!!! Thanks for sharing!

Very nice contribution! @EgoMoose made a similar tutorial a while back, though this one goes into good detail on some of the little things he missed.

Also, do you plan to make the game link Open Source so we can edit it? I wanted to make the guns replicate to the server, though the game is locked.

1 Like

I apparently didn’t uncopylock it. sorry.
Egomoose uses a completely different structure. This is more versatile, which is why i made the tutorial.
Edit: the place is uncopylocked now lmao

6 Likes

THANK YOU! for this great tutorial! this covers everything needed to get you started, I really appreciate the effort and time you’ve put into this. hopefully we see great FPS games sometime in the future.

btw, not sure if it’s only me, but pastebin is denying access for me, i was always looking for a good spring module and i think i might have found it, i just can’t access the code, would you mind posting it or sending it privately (if it’s too long)

This is only the framework for it, there isn’t any kind of server replication for this yet. Think of it as a challenge for you to script yourself :wink:.

Thank you for this! I love the insurgency style framework, and I’d love to see your plans for an Insurgency style game on ROBLOX.

It is indeed, but my firefox was suffering a slow and painful death so i had to cut the post short

1 Like

Quick tip for muzzle flash fx.

You can also use a function for particle fx called :Emit(n). It basically “emits” a certain number of particles depending on what n is (so if you said :Emit(1) then one particle effect would emit.) It also takes spread and stuff into account and still applies it randomly when you have a value greater than one for the n argument.

So why use :Emit()? It’s pretty much just doing what it says; emitting the particles. Again, it takes everything into account so stuff like aforementioned spread, lifetime, etc. This means you don’t have to do stuff like this:

muzzleFlash.Enabled = true
wait(0.1)
muzzleFlash.Enabled = false

and instead just shorten it down to one line without worrying about potential issues with the wait:

muzzleFlash:Emit(5)

In some cases, :Emit can also look more nicer than using a wait between two .Enabled statements. When tinkering around with it a bit, it looks quite nice!

I know this doesn’t really directly help the gun script itself, however I still think it can be beneficial for those who are really good at making particle effects :slightly_smiling_face:

5 Likes

As far as i’m aware the emitting isn’t instant, that’s why i just set transparency directly (it looks nicer)
Edit: still looks nicer to edit transparency but i started using Emit everywhere else to save on coroutine usage

1 Like

LOL “btw you have to press 1 to equip the gun”

I liked the game just for that.

Also, you can press R and ADS while reloading. Might want to fix that.

2 Likes

ADS while reloading is allowed intentionally

1 Like

Realistically, it’s semi-doable for a seasoned shooter, though it would likely require them to move their eyes down to view the pouch/magazine they intend to insert.

But the offsets make the camera all wobbly, relative to the gun. If this (ADS while reloading) is truly intentional, could you make it less wobbly?