Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 32

Player Control with FSMs

Recommended resources

Hello! Welcome to the next lesson in our course on Finite State Machines.

In our previous session, we successfully connected our game state FSM to core gameplay logic. We used the on callbacks for our state transitions to teleport players between the lobby and the map, and to reset their health when a round starts. This was a huge step in making our FSM the central orchestrator of the game flow.

However, our lobby still feels a bit chaotic. Players can run, jump, and behave as if they were in a round. Today, we'll address that by controlling what players are allowed to do based on the current game state.

Our learning outcome for this lesson is to use the game state FSM to manage player controls, such as disabling movement in the lobby. This is the final and crucial piece of our game state management puzzle, ensuring that the rules of each state are properly enforced.

How to Control Player Movement in Roblox

Before we can link player controls to our FSM, we first need to understand the tools Roblox gives us to manage them. The logic to disable a player's movement must run on the player's own machine, in a LocalScript. Our server-side FSM will tell the client when to do it, but the client does the actual work.

There are two primary methods to achieve this.

Method 1: Modifying Humanoid Properties

The Humanoid object inside a player's character controls many aspects of their physical behavior. By changing its properties, we can restrict movement.

Humanoid | Documentation - Roblox Creator Hub

The official Roblox documentation for the Humanoid object details several properties we can use. Let's look at the most relevant ones.

Please read the sections for the WalkSpeed, SetStateEnabled, and JumpPower/JumpHeight properties. Pay close attention to how WalkSpeed = 0 can stop movement, and how SetStateEnabled can be used to disable specific actions like jumping. This will give you a solid foundation for the different ways we can control a character.

From the documentation, you'll see we have a few options:

  • Humanoid.WalkSpeed = 0: This is a very direct way to stop a player from walking.
  • Humanoid.JumpPower = 0 or Humanoid.JumpHeight = 0: This prevents a player from jumping.
  • Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false): This is the recommended way to disable jumping, as it cleanly prevents the Humanoid from entering the "Jumping" state altogether.

While these work well, they require you to manage multiple properties. For a simpler, all-in-one solution, Roblox provides a more modern approach.

Method 2: The PlayerModule

Roblox includes a modular framework for character controls called PlayerModule. This is the standard system that handles player input for movement and the camera. It also provides a clean and simple API to enable or disable these controls completely.

Let's watch a video that demonstrates exactly how to use it.

Roblox - How to Stop/Disable Player Movement

This tutorial by The Letus Garden clearly explains how to access the PlayerModule and use its functions to disable and re-enable player controls.

Watch from 00:58 to 04:20. The video will show you how to find the PlayerModule in the Explorer during a playtest and how to write a LocalScript to get the Controls module and call its Disable() and Enable() methods. This is the exact technique we will use.

To summarize the video's key steps in a LocalScript:

  1. Get the local player's PlayerScripts.
  2. require() the PlayerModule.
  3. Call PlayerModule:GetControls().
  4. Use the returned controls object's :Disable() and :Enable() methods.

This approach is generally preferred because it's a single function call that handles everything related to character movement and camera control, making our code cleaner.


Integrating Control Logic with Our FSM

Now, let's apply this knowledge. Our FSM runs on the server, but player controls must be managed on the client. The communication link is the StateChangedEvent RemoteEvent we created in a previous lesson.

Our client-side LocalScript (the one that updates the UI text) will listen for state change broadcasts from the server. Based on the new state, it will either disable or enable the player's controls.

Let's modify your client-side script.

Step 1: Locate and Prepare Your Client Script

Find the LocalScript you created inside StarterGui that is responsible for listening to StateChangedEvent and updating the status text. We will add our new control logic here.

Step 2: Get the Controls Module

At the top of your script, add the necessary code to get a reference to the player's Controls module. Since this script is a LocalScript, it can easily access the LocalPlayer.

-- In your client-side LocalScript (e.g., inside StarterGui)

-- Get services and events
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local StarterGui = game:GetService("StarterGui")

local StateChangedEvent = ReplicatedStorage:WaitForChild("StateChangedEvent")
local GameState = require(ReplicatedStorage:WaitForChild("GameState"))

-- Get the local player and their controls
local localPlayer = Players.LocalPlayer
local playerScripts = localPlayer:WaitForChild("PlayerScripts")
local playerModule = require(playerScripts:WaitForChild("PlayerModule"))
local controls = playerModule:GetControls()

-- UI reference (you should already have this)
local statusLabel = -- Your path to the status label

Step 3: Enable/Disable Controls on State Change

Now, inside the StateChangedEvent.OnClientEvent function, we'll add logic to check the game state and call controls:Enable() or controls:Disable() accordingly. We want controls disabled during the Lobby, Countdown, and PostRound states, and enabled only during the InRound state.

-- ... (at the bottom of the same LocalScript)

StateChangedEvent.OnClientEvent:Connect(function(newState, data)
    -- This is our existing UI logic
    if newState == GameState.Countdown then
        statusLabel.Text = "Game starting in: " .. tostring(data.timeRemaining)
    elseif newState == GameState.InRound then
        statusLabel.Text = "Round in progress: " .. tostring(data.timeRemaining)
    -- ... (other UI states)
    else
        statusLabel.Text = tostring(newState)
    end
    
    -- NEW: Add player control logic
    if newState == GameState.InRound then
        -- The round is active, enable controls!
        controls:Enable()
    else
        -- We are in the lobby, countdown, or post-round. Disable controls.
        controls:Disable()
    end
end)

And that's it! When you test your game now, your character will be frozen in the lobby. You won't be able to move or jump. As soon as the FSM transitions to the InRound state, your controls will be automatically enabled, and you'll be able to play. When the round ends, you'll be teleported back to the lobby and your controls will once again be disabled.

This pattern powerfully demonstrates the role of the FSM as a central authority. The server dictates the state, and clients react to that state by changing UI, teleporting the player, and now, by enabling or disabling controls.


Conclusion

Congratulations! You have now fully integrated your game state FSM with all the essential mechanics of a round-based game. The FSM doesn't just track time; it dictates where players are, what they see on their screen, and what they are allowed to do.

Key Takeaways:

  • Player controls are managed on the client using a LocalScript.
  • The server-side FSM communicates the current game state to clients via RemoteEvents.
  • Clients listen for state changes and enable or disable controls accordingly.
  • The PlayerModule:GetControls() and its Enable()/Disable() methods provide a clean, high-level way to manage all character inputs.
  • By linking controls to the FSM, you ensure that game rules are consistently enforced for all players.

Next Lesson Preview:
Our game-loop FSM is now quite robust. However, as you build more complex systems (like our upcoming NPC AI), you might find that a simple, flat list of states can become hard to manage. What if an NPC can be Chasing and Attacking at the same time? In our next module, we'll begin exploring advanced FSM patterns, starting with Hierarchical Finite State Machines (HFSMs), which allow you to nest states within other states to manage complexity with elegance.