With this step-by-step tutorial, you’re going to learn how to create an exciting and action-packed Roblox FPS game complete with lobby, arena, scoreboard, and goal of getting 10 kills per round.
In addition, thanks to Roblox’s built-in multiplayer features, your game will be ready to be enjoyed by the millions of Roblox players in an instant – something becoming a necessity into today’s game industry!
If you’re ready to master building FPS games right in Roblox, let’s get started!

Project Files
You can download the complete project here. Unzip the file and double-click RobloxFPS to open it up in Roblox Studio.
Note that this tutorial won’t be going over the basics, so if this is your first time developing in Roblox, I recommend you follow this tutorial first to learn Roblox game making fundamentals.
Creating the Lobby
Let’s start by creating a new Roblox game, using the Baseplate template.
Then in the Toolbox window, let’s look for a warehouse model to act as our lobby. This is the one I’m using.

By default this warehouse doesn’t have collisions, so let’s go inside and add some parts to act as collider walls.

Cover all 4 sides of the inside.

We can then select all of them and…
- Set the Transparency to 1
- Enable Anchored
Let’s also drag in the SpawnLocation object as we want our players spawning inside of the lobby room.

Finally, we can create a folder in the workspace called Lobby, and put all of our lobby related objects in it.

Creating the Arena
Now let’s create our arena. For this, let’s go over to the Toolbox and look for a “compound”. These normally have walls surrounding it with buildings inside – perfect for an FPS arena. This is the one I’m using.

We can also change the Baseplate to match the compound.
- Set the Material to Sand
- Set the Color to Bright orange
- Delete the child Texture object

Next, let’s border up all the gaps so players can’t escape. Starting with the door, we can just rotate to close it.

We can do the same thing for the back entrance. Just copy and paste the doors there.

Now players should be blocked in and not able to get out.
Next up, are the spawn points. Go ahead and create a new SpawnLocation object inside of the Workspace and set it up how you like.

Go ahead and copy/paste a lot of them around the arena.

That’s our arena done! Let’s finish it off by creating a new folder called Arena and put all of the objects that relate to it, inside.

Teams
The way we’re going to have players spawn in the lobby at the start of the game and in the arena afterwards is going to be done through teams. Players can be assigned a team and spawn locations too. So when a player respawns, they will spawn at their team’s designated ones. Down in the Teams service, create 2 new Teams.
- Lobby, with a TeamColor of White.
- Playing, with a TeamColor of Lime green.
The team color is what will differentiate the 2 teams.

Now let’s select all of our arena spawn locations and…
- Disable Neutral
- Set the TeamColor to Lime green

Then for the single lobby spawn location…
- Disable Neutral
- Set the TeamColor to White

Game Loop
Now that we’ve got the lobby, arena and spawn locations setup, let’s create the game loop. Let’s start by creating a new script inside of the ServerScriptService service and rename it to GameManager.

Then, inside of the ReplicatedStorage service, we’re going to create a few value objects to hold data.
- Create a BoolValue and call it GameActive
- Create a StringValue and call it GameStatus
- Create an IntValue and call it KillsToWin
- Set the value to 10.

These are going to be used to hold data that we can change and refer to. Now let’s go over to the GameManager script and start with creating some variables.
local LobbyWaitTime = 5 local KillsToWin = game.ReplicatedStorage.KillsToWin local GameActive = game.ReplicatedStorage.GameActive local CurrentTime local GameStatus = game.ReplicatedStorage.GameStatus local LobbySpawn = workspace.Lobby.SpawnLocation local Players = game.Players local LobbyTeam = game.Teams.Lobby local PlayingTeam = game.Teams.Playing
The LobbyTimer function will be called when players return to the lobby to await the next game. It will countdown from LobbyWaitTime, change the GameStatus string value to display the countdown. Then when the countdown is complete, it will set GameActive to true.
local function LobbyTimer ()
CurrentTime = LobbyWaitTime
while GameActive.Value == false do
CurrentTime -= 1
GameStatus.Value = "Game starting in... " .. CurrentTime
if CurrentTime <= 0 then
GameActive.Value = true
end
wait(1)
end
endAfter this, we want to listen to when our GameActive BoolValue changes. When it does, we will either teleport players to the lobby or arena depending on if it’s true or false.
GameActive.Changed:Connect(function(value)
if value == true then
-- change all players team and respawn them
for i, player in pairs(Players:GetChildren()) do
player.Team = PlayingTeam
player:LoadCharacter()
end
GameStatus.Value = "First to ".. KillsToWin.Value .. " kills wins!"
elseif value == false then
-- change all players team and respawn them
for i, player in pairs(Players:GetChildren()) do
player.Team = LobbyTeam
player:LoadCharacter()
end
end
end)Now to get things started, let’s call the LobbyTimer function at the end of the script.
LobbyTimer()
If you press play, you should spawn in the lobby and after 5 seconds spawn in the arena.
Game Status GUI
We’ve got the GameStatus string value setup and changing, but we can’t see what it’s saying in-game. So let’s create some GUI text.
In the StarterGUI service, create a new ScreenGui and inside of that, create a TextLabel. Rename it to GameStatusText.

Let’s now move the text over to the top-center of the screen.
- Set the BackgroundTransparency to 1
- Set the TextColor3 to white
- Set the TextSize to 40

Then under the ScreenGui object, we need to create a new LocalScript. This is a script that runs only on the local player’s computer.

This script is just going to have two variables. One for the text object and the other for the GameActive value. Then we’ll change the text whenever the value of GameStatus has been changed.
local Text = script.Parent.GameStatusText
local GameStatus = game.ReplicatedStorage.GameStatus
GameStatus.Changed:Connect(function(value)
Text.Text = GameStatus.Value
end)Now if you press play, you should see the text counting down and when in the arena, displaying the kills to get.
First-Person Mode
Right now, you’ll notice that the player is still third-person. To fix this, select the StarterPlayer.
- Set the CameraMode to LockFirstPerson
- Set the CameraMaxZoomDistance to 0.5

Pressing play, the camera should now be locked in first-person mode.
Creating the Gun
With our player setup, let’s create our gun. Over in the Toolbox, search for Handgun and we want to find the Working Handgun model. Here’s a link to the asset. Drag that into the game and open it up, we’re going to modify it a bit.
Delete all parts except for…
- The base Handgun tool.
- The Handle and its sounds.

Now we can create a local script for the gun called GunScript.

Before we write code, let’s go over to the ReplicatedStorage service and create a new RemoteEvent. A remote event can be used to communicate between the client and server. In our case, when the player shoots another player, we’ll be telling that to the server so they can damage them.

Now back to our gun script. Let’s start with some variables.
-- Parts
local weapon = script.Parent
local camera = workspace.CurrentCamera
-- Sound Effects
local shootSFX = weapon.Handle["FireSound"]
local reloadSFX = weapon.Handle["Reload"]
-- Replicated Storage
local replicatedStorage = game:GetService("ReplicatedStorage")
local damagePlayerEvent = replicatedStorage:WaitForChild("DamagePlayer")
-- Damage
local Damage = 25
-- Ammo
local CurMagAmmo = 12
local MagSize = 12
local ReserveAmmo = 48
local CanShoot = trueNext, we can create the Shoot function which gets called when we press the left mouse button.
function Shoot (mouse)
-- play shoot sfx
shootSFX:Play()
local target = mouse.Target
local humanoid = nil
CurMagAmmo -= 1
-- get the target HUMANOID
if target.Parent:FindFirstChild("Humanoid") then
humanoid = target.Parent:FindFirstChild("Humanoid")
elseif target.Parent.Parent:FindFirstChild("Humanoid") then
humanoid = target.Parent.Parent:FindFirstChild("Humanoid")
end
-- if we hit a player, damage them
if humanoid then
local hitPlayer = game.Players:GetPlayerFromCharacter(humanoid.Parent)
damagePlayerEvent:FireServer(hitPlayer, Damage)
end
endThe Reload function gets called when we need to reload our gun. Here’s what it does:
- Disable the ability to shoot.
- Play the reload sound and wait for it to finish.
- Enable the ability to shoot.
- Subtract from the reserve ammo.
- Set the current mag ammo.
function Reload ()
CanShoot = false
reloadSFX:Play()
wait(reloadSFX.TimeLength)
CanShoot = true
local leftOverAmmo = CurMagAmmo
if ReserveAmmo < MagSize then
CurMagAmmo = ReserveAmmo
else
CurMagAmmo = MagSize
end
ReserveAmmo -= MagSize - leftOverAmmo
if ReserveAmmo < 0 then
ReserveAmmo = 0
end
endAlright, now how do we connect these functions to actual interactions? When we equip the gun, we’ll attach a new event that gets called when we press the left mouse button. When that happens, we’ll either shoot, reload or do nothing.
weapon.Equipped:Connect(function (mouse)
mouse.Button1Down:Connect(function()
if CanShoot == true then
if CurMagAmmo > 0 then
Shoot(mouse)
elseif ReserveAmmo > 0 then
Reload()
end
end
end)
end)Now let’s test it out. But how do we give the player their gun? Well let’s drag it out of the Workspace and into the StarterPack. This is where we can give the players things when they join the game.

If you press play, you can test it out. After shooting 12 times, you should hear the reload sound.
Weapon Gui
In order to see our current ammo, we’ll need to create a gui. In the StarterGui, create a new ScreenGui object called AmmoGui and inside of that create a new TextLabel called AmmoText.

Position and setup the ammo text to look like this:

Now we’re not going to keep the gui inside of StarterGui. So let’s drag it over to ReplicatedStorage. This is because we need to clone and destroy it.

Back in the GunScript, let’s create two new variables.
local PlayerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local AmmoText = nilPlayerGui is the container which holds all player gui at runtime. AmmoText is what we’ll assign when we create the gui.
Just below the variables, let’s create the UpdateAmmoText function. This will simply update the text to display the current ammo.
function UpdateAmmoText ()
if AmmoText ~= nil then
AmmoText.Text = CurMagAmmo .. "/" .. ReserveAmmo
end
endDown where we listen to the Equipped event, let’s add in code to create, assign and update the ammo text.
weapon.Equipped:Connect(function (mouse)
-- NEW CODE STARTS HERE
local guiClone = game.ReplicatedStorage.AmmoGui:Clone()
guiClone.Parent = PlayerGui
AmmoText = guiClone.AmmoText
UpdateAmmoText()
-- NEW CODE ENDS HERE
mouse.Button1Down:Connect(function()
if CanShoot == true then
if CurMagAmmo > 0 then
Shoot(mouse)
elseif ReserveAmmo > 0 then
Reload()
end
end
end)
end)Then we need to listen to when the weapon is unequipped and destroy the gui.
weapon.Unequipped:Connect(function()
PlayerGui.AmmoGUI:Destroy()
end)Finally, at the end of both the Shoot and Reload functions, we need to call the UpdateAmmoText function to update the text.
UpdateAmmoText()
Now you can press play and test it out!
Here’s the gun script as of this point in the tutorial:
-- Parts
local weapon = script.Parent
local camera = workspace.CurrentCamera
-- Sound Effects
local shootSFX = weapon.Handle["FireSound"]
local reloadSFX = weapon.Handle["Reload"]
-- Replicated Storage
local replicatedStorage = game:GetService("ReplicatedStorage")
local damagePlayerEvent = replicatedStorage:WaitForChild("DamagePlayer")
-- Damage
local Damage = 25
-- Ammo
local CurMagAmmo = 12
local MagSize = 12
local ReserveAmmo = 48
local CanShoot = true
local PlayerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local AmmoText = nil
function UpdateAmmoText ()
if AmmoText ~= nil then
AmmoText.Text = CurMagAmmo .. "/" .. ReserveAmmo
end
end
function Shoot (mouse)
-- play shoot sfx
shootSFX:Play()
local target = mouse.Target
local humanoid = nil
CurMagAmmo -= 1
-- get the target HUMANOID
if target.Parent:FindFirstChild("Humanoid") then
humanoid = target.Parent:FindFirstChild("Humanoid")
elseif target.Parent.Parent:FindFirstChild("Humanoid") then
humanoid = target.Parent.Parent:FindFirstChild("Humanoid")
end
-- if we hit a player, damage them
if humanoid then
local hitPlayer = game.Players:GetPlayerFromCharacter(humanoid.Parent)
damagePlayerEvent:FireServer(hitPlayer, Damage)
end
UpdateAmmoText()
end
function Reload ()
CanShoot = false
reloadSFX:Play()
wait(reloadSFX.TimeLength)
CanShoot = true
local leftOverAmmo = CurMagAmmo
if ReserveAmmo < MagSize then
CurMagAmmo = ReserveAmmo
else
CurMagAmmo = MagSize
end
ReserveAmmo -= MagSize - leftOverAmmo
if ReserveAmmo < 0 then
ReserveAmmo = 0
end
UpdateAmmoText()
end
weapon.Equipped:Connect(function (mouse)
local guiClone = game.ReplicatedStorage.AmmoGui:Clone()
guiClone.Parent = PlayerGui
AmmoText = guiClone.AmmoText
UpdateAmmoText()
mouse.Button1Down:Connect(function()
if CanShoot == true then
if CurMagAmmo > 0 then
Shoot(mouse)
elseif ReserveAmmo > 0 then
Reload()
end
end
end)
end)
weapon.Unequipped:Connect(function()
PlayerGui.AmmoGUI:Destroy()
end)Leaderstats
Let’s now create the leaderboard to display kills, deaths, and wins.
In the ServerScriptService, create a new script called Leaderboard.
We also need to create what’s known as a BindableEvent inside of ReplicatedStorage. Call it WinGame.

A bindable event is like a remote event, but instead of being client-server or server-client, a bindable event is just server-server.
Now in our Leaderboard script, let’s begin with some variables.
local GameManager = game.ServerScriptService.GameManager local KillsToWin = game.ReplicatedStorage.KillsToWin local WinGameEvent = game.ReplicatedStorage.WinGame
Then when a new player is added to the game, we can listen for that.
-- create leaderstats when a new player is added game.Players.PlayerAdded:Connect(function(player) end)
Inside of this event, let’s create our leaderstats folder and make it a child of our new player.
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = playerThen as a child of this leaderstats folder, we need to create values for the kills, deaths, wins and attacker (the player who is currently attacking us).
local kills = Instance.new("IntValue")
kills.Name = "Kills"
kills.Value = 0
kills.Parent = leaderstats
local deaths = Instance.new("IntValue")
deaths.Name = "Deaths"
deaths.Value = 0
deaths.Parent = leaderstats
local wins = Instance.new("IntValue")
wins.Name = "Wins"
wins.Value = 0
wins.Parent = leaderstats
local attacker = Instance.new("ObjectValue")
attacker.Name = "Attacker"
attacker.Value = nil
attacker.Parent = playerNext, we need a way of changing these values when a player is killed. Add this code just under where we created our values but still inside of the player added event.
-- change leaderstats when a player dies
player.CharacterAdded:Connect(function(character)
character:WaitForChild("Humanoid").Died:Connect(function()
player.leaderstats.Deaths.Value += 1
-- increase the killer's kills
if player.Attacker.Value then
local killer = player.Attacker.Value
killer.leaderstats.Kills.Value += 1
-- if the killer has enough kills, they win!
if killer.leaderstats.Kills.Value == KillsToWin.Value then
WinGameEvent:Fire(player.Attacker.Value)
end
end
end)
end)What are we doing here?
- When a player dies, increase their Deaths value by 1.
- Increase their Attacker kills by 1.
- If the Attacker has the KillsToWin call the win game event with the attacker as the parameter value.
Now if we press play, you should see the leaderboard at the top-right.
Damaging Players
We’ve already got the DamagePlayer remote event setup and calling in the gun script. Now we need to create a new script inside of ServerScriptService called PlayerDamageManager.
First, let’s get our variables. The replicated storage and damage event.
local replicatedStorage = game:GetService("ReplicatedStorage")
local damagePlayerEvent = replicatedStorage:WaitForChild("DamagePlayer")
Then we can listen to when the event has been called on the server. It will send over three values. The attacker, hit player and damage.
damagePlayerEvent.OnServerEvent:Connect(function(attacker, hitPlayer, damage)
hitPlayer.Attacker.Value = attacker
hitPlayer.Character.Humanoid:TakeDamage(damage)
end)We can test this out with multiple players by going to the Test tab, setting the player number and clicking Start.

You should be able to shoot and kill other players. You can always increase the lobby wait time in the GameManager if the clients aren’t all in the arena.
Winning the Game
Let’s go over to the GameManager script. Here, let’s first set the player’s initial team to Lobby once they join the game.
-- called when a player joins the game
Players.PlayerAdded:Connect(function(player)
player.Team = LobbyTeam
end)
Next, we need to create a variable to reference the win game remote event.
local WinGameEvent = game.ReplicatedStorage.WinGame
Now let’s also listen to the WinGame remote event. This gets called once a player reaches a certain amount of kills.
WinGameEvent.Event:Connect(function(winner)
-- increase the winner's wins stat
winner.leaderstats.Wins.Value += 1
GameActive.Value = false
end)This will increase the winner’s Wins and set GameActive to false. This will teleport players back to the lobby.
Conclusion
Congratulations on completing the tutorial! We just finished creating a Roblox FPS game from scratch – and learned quite a lot while doing so. We covered setting up a lobby and arena, how to make an FPS camera in Roblox, and even made a multiplayer-friendly leaderboard so every player can see where they stand.
From here, though, you can expand upon this game and skills in a lot of ways. For this project, you could strive to add new mechanics such as health packs, ammo packs, different weapons, etc. Or, if you want to try your hand at something, you can try making a first-person survival game. The sky is the limit with the systems set up here and they can easily be used for other games and genres!
Thank you very much for following along, and I wish you the best of luck with your future Roblox games.




