§ How to · with Picoo

How to make a Roblox FPS (with AI)

By Sametcan Tasgiran, Founder & Developer·Published ·Updated

A Roblox FPS needs five wired-up systems: server-authoritative hit detection, ammo + reload, weapon switching, recoil pattern, and aim down sights. Describe your shooter and Picoo writes the scripts for it; the example below shows one way those systems split into files, including the FOV punch on fire and the ADS tween.

Example build: 14 files · 420 lines, three weapons (rifle, shotgun, pistol). The files and numbers you get follow your prompt.

Server-authoritative hits

In the example the client casts the ray and sends the hit position, and the server re-casts from the player's Head before it applies any damage. The client never sends a damage number.

Ammo + reload

Per-weapon magazine size, reserve ammo and reload time. AmmoService keeps ammo on the server, so a fire request with an empty magazine is rejected.

Weapon switching

1/2/3 keys swap weapons. Each weapon is a Tool with attributes (damage, mag, recoil) and a matching WeaponConfig entry.

Recoil pattern

Per-weapon recoil curve (vertical kick + horizontal randomness) applied to the camera, with an FOV punch on each shot for game feel.

ADS (aim down sights)

Right mouse aims: the FOV tweens in, the crosshair narrows and recoil drops.

HUD and hit markers

WeaponHUD shows ammo and reserve, Crosshair narrows on ADS, and HitMarker pops when the server confirms a hit.

Example output from a build like this

14 files in this example. Picoo writes this code for your prompt, so file names and sizes vary.

ServerScriptService/WeaponServer.lua

Hit re-cast + damage application

ServerScriptService/AmmoService.lua

Per-player ammo state, reload state machine

ReplicatedStorage/Modules/WeaponConfig.lua

Per-weapon damage, mag, recoil tables

ReplicatedStorage/Remotes/FireRemote

Fire request RemoteEvent

ReplicatedStorage/Remotes/ReloadRemote

Reload request RemoteEvent

StarterPlayer/StarterPlayerScripts/WeaponClient.lua

Input → ray cast → fire remote

StarterPlayer/StarterPlayerScripts/RecoilController.lua

Recoil curve + FOV punch

StarterPlayer/StarterPlayerScripts/ADSController.lua

FOV tween on right-mouse

StarterPack/Rifle (Tool)

Rifle Tool with attached attributes

StarterPack/Shotgun (Tool)

Shotgun Tool

StarterPack/Pistol (Tool)

Pistol Tool

StarterGui/WeaponHUD.lua

Ammo + reserve display, weapon icon

StarterGui/Crosshair.lua

Dynamic crosshair, narrows on ADS

StarterGui/HitMarker.lua

Server-fired hit marker pop-up

Sample output: ServerScriptService/WeaponServer.lua

--!strict
-- ServerScriptService/WeaponServer.lua  (example)
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local FireRemote = ReplicatedStorage:WaitForChild("Remotes"):WaitForChild("FireRemote")
local Config = require(ReplicatedStorage.Modules.WeaponConfig)

local lastFire: { [number]: number } = {}

FireRemote.OnServerEvent:Connect(function(player: Player, weaponName: string, hitPos: Vector3)
    local weapon = Config[weaponName]
    if not weapon or typeof(hitPos) ~= "Vector3" then return end
    local now = os.clock()
    if lastFire[player.UserId] and now - lastFire[player.UserId] < weapon.fireRate then return end
    lastFire[player.UserId] = now

    -- server re-cast from the shooter's own Head, never from a client origin
    local character = player.Character
    local head = character and character:FindFirstChild("Head")
    if not head or not head:IsA("BasePart") then return end
    local offset = hitPos - head.Position
    if offset.Magnitude < 0.01 then return end
    local params = RaycastParams.new()
    params.FilterType = Enum.RaycastFilterType.Exclude
    params.FilterDescendantsInstances = { character }
    local result = workspace:Raycast(head.Position, offset.Unit * weapon.range, params)
    local model = result and result.Instance:FindFirstAncestorOfClass("Model")
    local humanoid = model and model:FindFirstChildOfClass("Humanoid")
    if humanoid then humanoid:TakeDamage(weapon.damage) end
end)

Building a Roblox FPS

FPS is the genre with the most server-load and the most exploit pressure on Roblox. The two patterns that determine whether it survives launch: server-authoritative hit detection, and rate-limited fire events. The example below has both.

The hit detection pattern is hybrid: client casts the ray for instant visual feedback, server re-casts from the player's Head CFrame to validate damage. The client never gets to write a damage number — only a hint position the server can sanity-check. Standard hit-modifier exploits (set damage to 1000, fire through walls) do not work because the server re-casts from authoritative state.

The ammo system is server-side state. Per-player ammo + mag count lives in AmmoService — exploiters cannot empty-fire (server rejects the fire if mag is 0) or skip reload (server tracks reload duration). Reload is a state machine: idle → reloading (1.5 s in this example) → idle. During reload, fire requests are rejected.

Weapon switching is a Tool-based system. Each weapon is a Tool in StarterPack with attached attributes (damage, mag, fireRate, recoil). Switching is a Roblox-native equip/unequip — no custom switch logic needed. To add a weapon: drop a Tool in StarterPack, add an entry to WeaponConfig.lua with the same name. Picoo can also regenerate WeaponConfig with more entries via a follow-up prompt.

Recoil is a per-weapon curve. RecoilController applies vertical kick + horizontal randomness to the camera CFrame on every fire. FOV punch (5° here, recovering in 0.2 s) gives the fire feel without affecting hit detection. ADS multiplies recoil by 0.6 in this example — tunable per weapon.

See more on the Luau generator, the AI Luau script writer, the 3D model generator and the game builder, read how Picoo compares with ChatGPT, or browse the full blog.

Frequently asked

Is hit detection client-side or server-side?+

Server-side in this example. The client casts the ray for instant visual feedback (sparks, hit marker), and the server re-casts from the player's Head with workspace:Raycast to decide damage. No client-trusted hits.

Can I add more weapons?+

Yes. Add an entry to WeaponConfig.lua with damage, mag, fireRate, recoil — and drop a new Tool in StarterPack. Picoo can also regenerate the WeaponConfig with more entries.

How is recoil handled?+

Per-weapon recoil curve in WeaponConfig (vertical kick + horizontal random). RecoilController applies it on the camera CFrame. FOV punch is a tween on FieldOfView.

Does ADS reduce recoil?+

In this example, yes. ADS tweens FOV down (zoom in), narrows the crosshair, and applies a recoil multiplier of 0.6, set per weapon.

What about netcode for high-ping players?+

In this example the server uses the client's reported hit position only as an aim direction and re-casts from the player's Head on the server, so the damage decision never rests on a number the client sends. If your game needs lag compensation, ask for it in a follow-up prompt.

Related Picoo prompts