Sections

1. Script Types Deep Comparison Critical

Roblox has three script types, each with a distinct execution context. Understanding where and how each runs is the foundation of everything else.

Script (Server)

LocalScript (Client)

ModuleScript (Shared)

Actor (Parallel Luau)

-- ModuleScript (ReplicatedStorage > SharedConfig)
return {
    GameName = "Combat Arena",
    MaxPlayers = 16,
    RespawnTime = 3,
    StartingGold = 100,
    WeaponData = {
        Sword = { Damage = 25, Range = 8, Cooldown = 0.8 },
        Gun = { Damage = 15, Range = 200, Cooldown = 0.3 },
    }
}

-- Server Script (ServerScriptService > GameManager)
local Config = require(game.ReplicatedStorage.SharedConfig)
print("Server loaded config:", Config.GameName)

-- LocalScript (StarterPlayer > StarterPlayerScripts > UIController)
local Config = require(game.ReplicatedStorage.SharedConfig)
print("Client loaded config:", Config.GameName)
Exercise: Create a ModuleScript in ReplicatedStorage with a table of enemy spawn data. Require it from both a Script in ServerScriptService and a LocalScript in StarterPlayerScripts. Print the data from each context. Observe that the same code runs in two different environments.

2. RemoteEvents: Fire & Forget Essential

RemoteEvent is the primary communication channel between server and client. It is a one-directional fire-and-forget message — no return value, no blocking. Always parent RemoteEvents to ReplicatedStorage so both sides can access them.

Methods

Example: Punch System

Player presses a key on their client → fires server → server validates, applies damage, broadcasts result to all clients.

-- RemoteEvent in ReplicatedStorage named "PunchEvent"

-- LocalScript (StarterCharacterScripts > PunchHandler)
local UserInputService = game:GetService("UserInputService")
local PunchEvent = game.ReplicatedStorage.PunchEvent
local Players = game:GetService("Players")
local player = Players.LocalPlayer

UserInputService.InputBegan:Connect(function(input, gameProcessed)
    if gameProcessed then return end
    if input.KeyCode == Enum.KeyCode.F then
        PunchEvent:FireServer()
    end
end)

-- Script (ServerScriptService > PunchHandler)
local PunchEvent = game.ReplicatedStorage.PunchEvent
local Players = game:GetService("Players")

PunchEvent.OnServerEvent:Connect(function(player)
    -- Server validates: is the player alive? Do they have a character?
    local character = player.Character
    if not character or not character:FindFirstChild("Humanoid") then return end

    -- Find nearest target within 12 studs
    local root = character:FindFirstChild("HumanoidRootPart")
    if not root then return end

    local closestPlayer, closestDist = nil, 12
    for _, otherPlayer in Players:GetPlayers() do
        if otherPlayer == player then continue end
        local otherChar = otherPlayer.Character
        if not otherChar then continue end
        local otherRoot = otherChar:FindFirstChild("HumanoidRootPart")
        if not otherRoot then continue end
        local dist = (root.Position - otherRoot.Position).Magnitude
        if dist < closestDist then
            closestDist = dist
            closestPlayer = otherPlayer
        end
    end

    if closestPlayer then
        local targetHumanoid = closestPlayer.Character.Humanoid
        targetHumanoid:TakeDamage(10)

        -- Broadcast punch animation to all clients
        PunchEvent:FireAllClients(player, closestPlayer)
    end
end)
Exercise: Set up a RemoteEvent in ReplicatedStorage. Create a LocalScript that fires it when the player presses "Q". On the server, listen with OnServerEvent and print the player's name. Verify both client and server logs appear.

3. RemoteFunctions: Request & Response Use Sparingly

RemoteFunction enables a request-response pattern. The caller blocks until the receiver returns a value. This is fundamentally different from RemoteEvent.

Methods

Risks & Pitfalls

-- RemoteFunction in ReplicatedStorage named "RequestDataFunction"

-- Server Script (ServerScriptService > DataProvider)
local RequestDataFunction = game.ReplicatedStorage.RequestDataFunction

RequestDataFunction.OnServerInvoke = function(player, dataType)
    -- Never trust the client's request fully, but dataType is a simple string
    if dataType == "inventory" then
        return GetPlayerInventory(player) -- returns table
    elseif dataType == "stats" then
        return GetPlayerStats(player)
    end
    return nil
end

-- LocalScript (StarterPlayerScripts > DataRequester)
local RequestDataFunction = game.ReplicatedStorage.RequestDataFunction

-- WARNING: This blocks the client thread until the server responds
local success, result = pcall(function()
    return RequestDataFunction:InvokeServer("inventory")
end)

if success then
    print("Inventory received:", result)
else
    warn("Failed to fetch inventory:", result)
end
Exercise: Create a RemoteFunction that the client invokes to get their current server time (os.time()). On the server, return the time. Add a pcall wrapper on the client side. Then, do NOT set OnServerInvoke and observe the timeout error after ~30 seconds.

4. BindableEvents & BindableFunctions Internal Bus

BindableEvent and BindableFunction are for intra-context communication. They do not cross the network — server scripts use them to talk to other server scripts, and client scripts use them to talk to other client scripts.

Use Cases

-- BindableEvent in ReplicatedStorage named "ServerEventBus"
-- ModuleScript wrapper (ReplicatedStorage > EventBus)

local EventBus = {}
EventBus.__index = EventBus

function EventBus.new()
    local self = setmetatable({}, EventBus)
    self._bindable = Instance.new("BindableEvent")
    return self
end

function EventBus:Fire(...)
    self._bindable:Fire(...)
end

function EventBus:Connect(callback)
    return self._bindable.Event:Connect(callback)
end

return EventBus

-- Usage: CombatSystem.server.lua
local EventBus = require(game.ReplicatedStorage.EventBus)
local bus = EventBus.new()

game:GetService("ReplicatedStorage").CombatBus.Value = bus._bindable

-- Some other server system
bus:Connect(function(player, damage)
    print(player.Name .. " took " .. damage .. " damage")
end)

-- Fire from anywhere on the server
bus:Fire(player, 25)
Exercise: Create two server Scripts. Script A fires a BindableEvent every 5 seconds with a random number. Script B listens and prints the number. This decouples the producer from the consumer.

5. Security: Never Trust the Client Non-Negotiable

This is the single most important rule in Roblox development. The client is in the player's hands. Players can and will modify their client. Exploiters use tools like Synapse, Krnl, Script-Ware, and countless others to execute arbitrary Luau, fire remotes with forged arguments, teleport, noclip, and modify game state.

What Exploiters Can Do

Defense Strategies

-- INSECURE: Server trusts client-provided damage value
RemoteEvent.OnServerEvent:Connect(function(player, target, damageAmount)
    -- Exploiter can fire with damageAmount = 99999
    target.Humanoid:TakeDamage(damageAmount)
end)

-- SECURE: Server validates everything independently
local cooldowns = {}

RemoteEvent.OnServerEvent:Connect(function(player, targetPlayer)
    -- 1. Validate player has character
    local char = player.Character
    if not char then return end

    -- 2. Check cooldown
    local now = os.clock()
    if cooldowns[player] and now - cooldowns[player] < 1 then return end
    cooldowns[player] = now

    -- 3. Validate target exists and is within range
    local targetChar = targetPlayer.Character
    if not targetChar then return end

    local root = char:FindFirstChild("HumanoidRootPart")
    local targetRoot = targetChar:FindFirstChild("HumanoidRootPart")
    if not root or not targetRoot then return end

    local dist = (root.Position - targetRoot.Position).Magnitude
    if dist > 12 then return end -- Out of range, ignore

    -- 4. Server-authoritative damage calculation
    local baseDamage = 10
    local weapon = char:FindFirstChildOfClass("Tool")
    if weapon and weapon:FindFirstChild("Damage") then
        baseDamage = weapon.Damage.Value
    end

    targetChar.Humanoid:TakeDamage(baseDamage)
    print(`{player.Name} hit {targetPlayer.Name} for {baseDamage} damage`)
end)
Exercise: Create a RemoteEvent that "buys" an item. On the client, fire it with an item ID. On the server: validate that the item exists in a server-side config, check the player has enough gold (server-side), deduct gold, and grant the item. Never trust the client's gold amount or item validity.

6. Filtering & Replication Mandatory

FilteringEnabled has been mandatory since 2017. There is no way to disable it. This means clients cannot replicate changes to other clients directly. The server is the only authority that can replicate state changes.

What Replicates

What Does NOT Replicate

-- Server Script: Creating a part replicates to all clients
local part = Instance.new("Part")
part.Size = Vector3.new(4, 1, 4)
part.BrickColor = BrickColor.new("Bright red")
part.Position = Vector3.new(0, 10, 0)
part.Anchored = true
part.Parent = workspace

-- Setting an Attribute that replicates to all clients
part:SetAttribute("Health", 100)
part:SetAttribute("IsDestroyable", true)

-- Client LocalScript: Reading the replicated part
local part = workspace:WaitForChild("Part")
print(part.BrickColor.Name) -- "Bright red"
print(part:GetAttribute("Health")) -- 100

-- Client can read, but setting these will NOT replicate to other clients
part.BrickColor = BrickColor.new("Bright blue") -- Only this client sees blue
Exercise: Create a server Script that spawns a part with a random color every 3 seconds. Verify on a second client (or two Studio windows) that both see the same parts and colors. Then add a LocalScript that tries to delete a part — observe it does not delete on the server or other clients.

7. Character & Player Setup Essential

Every player who joins has a Player object created by the server. The character model is loaded into the Workspace. Understanding the character lifecycle is key to building any multiplayer game.

Key Events & Objects

-- Server Script (ServerScriptService > PlayerSetup)
local Players = game:GetService("Players")

local function OnPlayerAdded(player)
    print(`{player.Name} joined the game (UserId: {player.UserId})`)

    -- Wait for character to load
    player.CharacterAdded:Connect(function(character)
        print(`{player.Name}'s character loaded`)

        local humanoid = character:WaitForChild("Humanoid")
        local rootPart = character:WaitForChild("HumanoidRootPart")

        -- Teleport to spawn location
        local spawnLocation = workspace:FindFirstChild("SpawnLocation")
        if spawnLocation then
            rootPart.Position = spawnLocation.Position + Vector3.new(0, 3, 0)
        end

        -- Set up player data / leaderstats
        SetupLeaderstats(player)

        -- Equip starter tools
        local starterTools = game.ServerStorage:FindFirstChild("StarterTools")
        if starterTools then
            for _, tool in starterTools:GetChildren() do
                tool:Clone().Parent = player.Backpack
            end
        end
    end)

    -- Handle character removal (death)
    player.CharacterRemoving:Connect(function(character)
        print(`{player.Name}'s character being removed`)
    end)

    -- Handle player leaving
    player.AncestryChanged:Connect(function(child, parent)
        if not parent then
            print(`{player.Name} left the game`)
            -- Save data here
            SavePlayerData(player)
        end
    end)
end

-- Connect for existing and future players
for _, player in Players:GetPlayers() do
    OnPlayerAdded(player)
end
Players.PlayerAdded:Connect(OnPlayerAdded)
Exercise: Set up a server Script that prints "X joined" when any player joins. Wire up CharacterAdded to print the character's name. Then add a LocalScript in StarterCharacterScripts that prints "Character spawned" — observe it runs each time the character loads, including respawn.

8. The Leaderstats Pattern Standard

leaderstats is a Roblox convention for a folder that automatically displays in the game's leaderboard (Tab menu). Create a Folder named "leaderstats" inside the player, with IntValue, StringValue, or NumberValue children. Their names become column headers, and their values display to all players in the lobby.

Rules

-- Server Script (ServerScriptService > LeaderstatsManager)
local Players = game:GetService("Players")

local function SetupLeaderstats(player)
    -- Remove existing if any (safety)
    local existing = player:FindFirstChild("leaderstats")
    if existing then existing:Destroy() end

    local leaderstats = Instance.new("Folder")
    leaderstats.Name = "leaderstats"
    leaderstats.Parent = player

    local gold = Instance.new("IntValue")
    gold.Name = "Gold"
    gold.Value = 100
    gold.Parent = leaderstats

    local level = Instance.new("IntValue")
    level.Name = "Level"
    level.Value = 1
    level.Parent = leaderstats

    local kills = Instance.new("IntValue")
    kills.Name = "Kills"
    kills.Value = 0
    kills.Parent = leaderstats

    local status = Instance.new("StringValue")
    status.Name = "Status"
    status.Value = "Alive"
    status.Parent = leaderstats
end

-- Add gold (server-authoritative)
local function AddGold(player, amount)
    local leaderstats = player:FindFirstChild("leaderstats")
    if not leaderstats then return end
    local gold = leaderstats:FindFirstChild("Gold")
    if gold then
        gold.Value += amount -- Replicates automatically
    end
end

Players.PlayerAdded:Connect(SetupLeaderstats)
Exercise: Implement the leaderstats setup on PlayerAdded. Add a RemoteEvent that the client can fire to "earn gold." On the server, add 10 gold to the player's leaderstats each time. Observe the leaderboard in-game (press Tab).

9. Tools & Backpack Core

Tool is a Roblox object that players can equip from their Backpack. Tools handle equipping, unequipping, and activation automatically. Every Tool has a Handle (a Part) used for physics and raycasting.

Key Tool Mechanics

-- LocalScript inside a Tool (StarterPack > Sword > LocalScript)
local tool = script.Parent
local RemoteEvent = game.ReplicatedStorage.PunchEvent -- weapon hit event
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local cooldown = false

tool.Activated:Connect(function()
    if cooldown then return end
    cooldown = true

    -- Fire server to process hit
    RemoteEvent:FireServer(tool)

    -- Play swing animation locally (client prediction)
    local humanoid = player.Character.Humanoid
    local animator = humanoid:FindFirstChildOfClass("Animator")
    if animator and tool:FindFirstChild("SwingAnim") then
        local animTrack = animator:LoadAnimation(tool.SwingAnim)
        animTrack:Play()
    end

    task.wait(1) -- Swing cooldown
    cooldown = false
end)

-- Server Script (ServerScriptService > CombatHandler)
local RemoteEvent = game.ReplicatedStorage.PunchEvent

RemoteEvent.OnServerEvent:Connect(function(player, tool)
    local character = player.Character
    if not character then return end

    -- Validate tool is actually equipped by this player
    if tool.Parent ~= character then return end

    local handle = tool:FindFirstChild("Handle")
    local damageValue = tool:FindFirstChild("Damage")
    if not handle or not damageValue then return end

    local damage = damageValue.Value
    local range = tool:FindFirstChild("Range") and tool.Range.Value or 8

    -- Raycast from handle forward
    local origin = handle.Position
    local direction = handle.CFrame.LookVector * range
    local rayResult = workspace:Raycast(origin, direction)

    if rayResult and rayResult.Instance then
        local hitChar = rayResult.Instance:FindFirstAncestorOfClass("Model")
        if hitChar and hitChar:FindFirstChild("Humanoid") then
            local targetHumanoid = hitChar.Humanoid
            targetHumanoid:TakeDamage(damage)

            -- Broadcast hit effect
            RemoteEvent:FireAllClients(player, hitChar, damage)
        end
    end
end)
Exercise: Create a Tool in StarterPack with a Handle (Part). Add a LocalScript that prints "Activated" on click. Add a Damage IntValue and a Range NumberValue. Wire up the server-side hit detection.

10. Weapons & Hit Detection Combat

Weapon hit detection must be server-authoritative. The client can predict the shot for responsiveness, but the server must validate and apply damage. Two main approaches: melee (raycasting from tool) and ranged (raycasting from camera).

Melee Hit Detection

Ranged Hit Detection (Gun)

-- Server Script (ServerScriptService > GunHandler)
local ShootEvent = game.ReplicatedStorage.ShootEvent
local Players = game:GetService("Players")

ShootEvent.OnServerEvent:Connect(function(player, origin, direction)
    local character = player.Character
    if not character then return end

    -- Validate max range (200 studs for a gun)
    if direction.Magnitude > 200 then
        direction = direction.Unit * 200
    end

    -- Filter: ignore the shooter's character
    local params = RaycastParams.new()
    params.FilterType = Enum.RaycastFilterType.Blacklist
    params.FilterDescendantsInstances = { character }

    local result = workspace:Raycast(origin, direction, params)

    if result then
        local hitPart = result.Instance
        local hitChar = hitPart:FindFirstAncestorOfClass("Model")

        if hitChar then
            local humanoid = hitChar:FindFirstChild("Humanoid")
            if humanoid then
                // Check if hitChar belongs to a player
                for _, otherPlayer in Players:GetPlayers() do
                    if otherPlayer.Character == hitChar and otherPlayer ~= player then
                        local weapon = character:FindFirstChildOfClass("Tool")
                        local damage = 15
                        if weapon and weapon:FindFirstChild("Damage") then
                            damage = weapon.Damage.Value
                        end

                        -- Headshot multiplier
                        if hitPart.Name == "Head" then
                            damage *= 2
                        end

                        humanoid:TakeDamage(damage)
                        ShootEvent:FireAllClients(player, hitChar, result.Position, damage)
                        break
                    end
                end
            end
        end
    end
end)

-- LocalScript (StarterPack > Gun > GunClient)
local tool = script.Parent
local ShootEvent = game.ReplicatedStorage.ShootEvent
local camera = workspace.CurrentCamera
local cooldown = false

tool.Activated:Connect(function()
    if cooldown then return end
    cooldown = true

    local origin = camera.CFrame.Position
    local direction = camera.CFrame.LookVector * 200

    -- Client-side prediction: play muzzle flash
    local handle = tool:FindFirstChild("Handle")
    if handle then
        -- Spawn a bullet trail (local only)
        local trail = Instance.new("Part")
        trail.Size = Vector3.new(0.2, 0.2, 0.2)
        trail.BrickColor = BrickColor.new("Bright yellow")
        trail.CFrame = CFrame.new(handle.Position, origin + direction)
        trail.Anchored = true
        trail.CanCollide = false
        trail.Parent = workspace
        task.delay(0.1, function() trail:Destroy() end)
    end

    // Fire server for authoritative hit detection
    ShootEvent:FireServer(origin, direction)

    task.wait(0.3) -- Fire rate
    cooldown = false
end)
Exercise: Build a "Ray Gun" tool. The client fires the server with camera origin and direction. The server raycasts and applies damage to any player hit. Add a headshot multiplier (2x). Test head vs body damage in Studio with two players.

11. Network Optimization Performance

Poorly optimized networking destroys player experience. Laggy remotes, excessive traffic, and unresponsive controls are the fastest way to lose players. Roblox has rate limits on remote events (~200–300 calls/second per client before throttling).

Strategies

-- Server-side rate limiter (ServerScriptService > RateLimiter)
local rateLimits = {}
local RATE_LIMIT_WINDOW = 1 -- seconds
local MAX_CALLS_PER_WINDOW = 5

local function CheckRateLimit(player)
    local now = os.clock()
    local playerLimits = rateLimits[player]

    if not playerLimits then
        rateLimits[player] = { count = 1, windowStart = now }
        return true
    end

    if now - playerLimits.windowStart > RATE_LIMIT_WINDOW then
        -- Reset window
        playerLimits.count = 1
        playerLimits.windowStart = now
        return true
    end

    if playerLimits.count >= MAX_CALLS_PER_WINDOW then
        warn(`{player.Name} rate limited`)
        return false
    end

    playerLimits.count += 1
    return true
end

-- Batch update example: damage all enemies at once
local pendingDamageBatch = {}

local function QueueDamage(target, amount)
    local key = target
    pendingDamageBatch[key] = (pendingDamageBatch[key] or 0) + amount
end

// Flush batch every 100ms
local BatchEvent = game.ReplicatedStorage.BatchEvent

while true do
    task.wait(0.1)
    for target, totalDamage in pendingDamageBatch do
        if target and target.Parent then
            local humanoid = target:FindFirstChild("Humanoid")
            if humanoid then
                humanoid:TakeDamage(totalDamage)
            end
        end
    end
    table.clear(pendingDamageBatch)

    -- Send batch event instead of many individual events
    BatchEvent:FireAllClients(damageBatch)
end

-- Attribute-based health update (no remote needed)
-- Server sets attribute, client reads it for UI
player.Character.Humanoid:SetAttribute("DisplayHealth", currentHealth)
-- Client LocalScript reads attribute changes for health bar
Exercise: Implement a rate limiter that allows a maximum of 3 remote calls per second per player. Wire it to your punch/gun event. If the player exceeds the limit, print a warning and ignore the event.

12. Actor Model & Parallel Luau Advanced

The Actor container enables running Luau scripts in parallel threads. This is Roblox's solution for CPU-bound work that would otherwise block the main game thread. However, it is limited and often misunderstood.

How Actors Work

When NOT to Use Actors

-- Actor container setup (ServerScriptService > PathfindingActor)
-- Create an Actor in ServerStorage, put a Script inside it

-- Script inside the Actor (Actor > PathfinderScript)
local PathfindingService = game:GetService("PathfindingService")
local bindable = script.Parent:FindFirstChildOfClass("BindableEvent")

local function ComputePath(startPos, endPos)
    local path = PathfindingService:CreatePath({
        AgentRadius = 2,
        AgentHeight = 5,
        AgentCanJump = true,
    })
    local success, waypoints = pcall(function()
        path:ComputeAsync(startPos, endPos)
        return path:GetWaypoints()
    end)
    return success and waypoints or nil
end

-- Listen for messages from the main thread
while true do
    local _, startPos, endPos = bindable.Event:Wait()
    local result = ComputePath(startPos, endPos)
    // Send result back
    bindable:Fire(result)
end

-- Main thread Script (ServerScriptService > EnemyAI)
local actor = game.ServerStorage.PathfindingActor:Clone()
actor.Parent = workspace

-- Create BindableEvent for communication
local channel = Instance.new("BindableEvent")
channel.Parent = actor

// Fire work to the actor (non-blocking)
channel:Fire(startPos, endPos)

-- Listen for result
channel.Event:Connect(function(result)
    if result then
        print("Path computed with", #result, "waypoints")
    end
end)
Exercise: Create an Actor in ServerStorage with a Script that computes random math (e.g., generate 1000 random numbers and sort them). From a main script, send a request to the Actor and print the result. Measure the time difference vs doing it on the main thread.

13. Custom Character Rigging Expert

Roblox supports two default rigs: R6 (6-body-part blocky) and R15 (15-body-part articulated). For custom NPCs or non-humanoid characters, you build your own rig with Motor6D joints and AnimationController.

R15 Rig Structure

-- Server Script: Create a custom NPC with a simple rig
local function CreateCustomNPC(position)
    local model = Instance.new("Model")
    model.Name = "CustomNPC"
    model.Parent = workspace

    -- Create body parts
    local root = Instance.new("Part")
    root.Name = "HumanoidRootPart"
    root.Size = Vector3.new(3, 1, 1)
    root.Position = position
    root.Anchored = false
    root.CanCollide = true
    root.Parent = model

    local torso = Instance.new("Part")
    torso.Name = "Torso"
    torso.Size = Vector3.new(2, 2, 1)
    torso.BrickColor = BrickColor.new("Bright red")
    torso.Parent = model

    local head = Instance.new("Part")
    head.Name = "Head"
    head.Size = Vector3.new(1.5, 1.5, 1.5)
    head.Shape = Enum.PartType.Ball
    head.BrickColor = BrickColor.new("Bright yellow")
    head.Parent = model

    -- Connect with Motor6D joints
    local rootJoint = Instance.new("Motor6D")
    rootJoint.Name = "RootJoint"
    rootJoint.Part0 = root
    rootJoint.Part1 = torso
    rootJoint.C0 = CFrame.new(0, 0, 0)
    rootJoint.C1 = CFrame.new(0, 1, 0)
    rootJoint.Parent = root

    local neckJoint = Instance.new("Motor6D")
    neckJoint.Name = "Neck"
    neckJoint.Part0 = torso
    neckJoint.Part1 = head
    neckJoint.C0 = CFrame.new(0, 1, 0)
    neckJoint.C1 = CFrame.new(0, -0.75, 0)
    neckJoint.Parent = torso

    -- Add Humanoid (for health, movement, etc.)
    local humanoid = Instance.new("Humanoid")
    humanoid.HipHeight = 0
    humanoid.MaxHealth = 100
    humanoid.Health = 100
    humanoid.Parent = model

    return model
end

-- Alternative: AnimationController for non-humanoid rigs
-- For creatures / mechs that don't use Humanoid

local function CreateMechRig(position)
    local model = Instance.new("Model")
    model.Name = "Mech"
    model.Parent = workspace

    local body = Instance.new("Part")
    body.Name = "Body"
    body.Size = Vector3.new(4, 3, 2)
    body.Position = position
    body.Anchored = false
    body.Parent = model

    local animController = Instance.new("AnimationController")
    animController.Parent = model

    local animator = Instance.new("Animator")
    animator.Parent = animController

    local animation = Instance.new("Animation")
    animation.AnimationId = "rbxassetid://1234567890" -- Replace with real ID
    animation.Parent = model

    // Play animation on the non-humanoid rig
    local track = animator:LoadAnimation(animation)
    track:Play()

    return model
end
Exercise: Create a custom NPC rig with at least 5 body parts connected by Motor6D joints. Add a Humanoid. Spawn it in Workspace and verify it appears with proper joints. Experiment with setting Motor6D.Transform to pose the rig.

14. Common Networking Mistakes Warning

These mistakes are the most common causes of exploitable games, performance issues, and hard-to-debug bugs. Every solo developer makes them at some point. Learn to recognize them.

Mistake 1: Trusting Client Data

The cardinal sin. Never use client-provided values for damage amounts, item IDs, currency amounts, or permission checks without server-side validation. Always re-derive or validate on the server.

// WRONG: Client provides target position, server teleports
TeleportEvent.OnServerEvent:Connect(function(player, targetPos)
    player.Character.HumanoidRootPart.Position = targetPos -- Exploiter teleports anywhere
end)

// RIGHT: Server validates teleport distance from last known position
TeleportEvent.OnServerEvent:Connect(function(player, targetPos)
    local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
    if not root then return end
    local dist = (root.Position - targetPos).Magnitude
    if dist > 50 then return end -- Too far, reject
    root.Position = targetPos
end)

Mistake 2: Overusing RemoteFunctions

Every InvokeServer blocks the client thread. If the server takes time (e.g., DataStore read), the client freezes. Use RemoteEvent + callback pattern instead.

Mistake 3: Spamming FireAllClients

Broadcasting updates for every minor change wastes bandwidth. If only one player should see a change (e.g., their personal UI), use FireClient. Batch updates for group changes.

Mistake 4: Not Handling Network Lag

Players on high ping (200–500ms) will experience delayed responses. Never assume a RemoteEvent listener fires instantly. Use timestamps server-side, not client-provided timestamps.

// WRONG: Using client time for cooldowns
CooldownEvent.OnServerEvent:Connect(function(player, clientTime)
    if clientTime - lastUsed[player] > 1 then -- Client can lie about time
        -- process
    end
end)

// RIGHT: Server tracks time independently
CooldownEvent.OnServerEvent:Connect(function(player)
    local now = os.clock()
    if now - lastUsed[player] > 1 then
        lastUsed[player] = now
        -- process
    end
end)

Mistake 5: Forgetting to Clean Up Event Connections

Every :Connect() creates a reference. If you connect to CharacterAdded without disconnecting old connections when a character respawns, you get connection leaks. This causes memory bloat and duplicate event fires. Use RBXScriptSignal:Once() for one-shot listeners, or store and disconnect connections.

// WRONG: New connection every respawn, old ones persist
Players.PlayerAdded:Connect(function(player)
    player.CharacterAdded:Connect(function(character)
        print("Character loaded") -- Runs multiple times per respawn!
    end)
end)

// RIGHT: Disconnect old connection before creating new one
Players.PlayerAdded:Connect(function(player)
    local connection = nil -- scope for cleanup
    connection = player.CharacterAdded:Connect(function(character)
        print("Character loaded (clean connection)")
        // If needed, disconnect and reconnect
    end)

    // Disconnect when player leaves
    player.AncestryChanged:Connect(function(_, parent)
        if not parent and connection then
            connection:Disconnect()
        end
    end)
end)

// Alternative: use a table of connections per player
local playerConnections = {}

Players.PlayerAdded:Connect(function(player)
    local connections = {}

    table.insert(connections, player.CharacterAdded:Connect(function(character)
        print(`{player.Name} respawned`)
    end))

    table.insert(connections, player.CharacterRemoving:Connect(function(character)
        print(`{player.Name} removed`)
    end))

    playerConnections[player] = connections
end)

Players.PlayerRemoving:Connect(function(player)
    // Clean up all connections for this player
    local connections = playerConnections[player]
    if connections then
        for _, conn in connections do
            conn:Disconnect()
        end
        playerConnections[player] = nil
    end
end)
Exercise: Review every RemoteEvent and RemoteFunction in a game you've built (or build a small test). Check: (1) Is every argument validated server-side? (2) Are RemoteFunctions used only where strictly necessary? (3) Are event connections cleaned up when a player leaves? Fix any violations.