This commit is contained in:
Joe 2026-02-02 12:08:47 -06:00
parent a9b4f8e83b
commit e6efc04756
8494 changed files with 495610 additions and 0 deletions

View file

@ -0,0 +1,670 @@
---
title: Cleanup and reset
description: Create a battle royale experience in Roblox Studio. Finish coding the scripts.
next: /education/battle-royale-series/finishing-the-project
prev: /education/battle-royale-series/ending-matches
---
Time to code the last phase of the game: cleanup and reset. Code in this phase ensures that the game loops to intermission and that future matches start the same for each player.
## Update the GUI
Before doing cleanup and reset, inform players how the game ended using the DisplayManager to show the appropriate status.
### Get the winner's name
Start by getting the winning player's name if there was one. Previously, the code checked if the size of the active players table was down to 1. To get the remaining player's name, return the name at the first index of that table.
1. In PlayerManager, start a new module function named `getWinnerName()`.
```lua
function PlayerManager.getWinnerName()
end
-- Events
Players.PlayerAdded:Connect(onPlayerJoin)
```
2. Add an if statement that runs if something exists in `activePlayers[1]`. Although the table count was checked before, the player might have disconnected or left the game.
```lua
function PlayerManager.getWinnerName()
local winningPlayer = activePlayers[1]
if winningPlayer then
end
end
```
3. In the if statement:
- Return the player's name.
- For the else, return an error string.
```lua
function PlayerManager.getWinnerName()
local winningPlayer = activePlayers[1]
if winningPlayer then
return winningPlayer.Name
else
return "Error: No winning player found"
end
end
```
### Get the end status
Use a module function to take information from the correct end state, whether the timer ends or one player is left. Then, send that state variable to DisplayManager to update the status GUI with the appropriate message.
1. In **MatchManager**, code a new module function named `getEndStatus()` with a parameter named `endState`. To store the message that will be sent, add an empty variable named `statusToReturn`.
```lua
function MatchManager.prepareGame()
playerManager.sendPlayersToMatch()
matchStart:Fire()
end
function MatchManager.getEndStatus(endState)
local statusToReturn
end
matchStart.Event:Connect(startTimer)
matchEnd.Event:Connect(stopTimer)
return MatchManager
```
2. Set the value of `statusToReturn` using if and elseif statements. Check for the end state variables: `FoundWinner` and `TimerUp`. For error checking, include an else at the end.
```lua
function MatchManager.getEndStatus(endState)
local statusToReturn
if endState == gameSettings.endStates.FoundWinner then
elseif endState == gameSettings.endStates.TimerUp then
else
end
end
```
3. Add the following for each condition:
`FoundWinner`
- A variable for the winner using `playerManager.getWinnerName()`.
- Update `statusToReturn` with a string announcing the winner.
`TimerUp`
- Update `statusToReturn` with a string announcing time ran out.
`Else`
- Update `statusToReturn` with an error message in case there are issues with getting the end game message.
```lua
function MatchManager.getEndStatus(endState)
local statusToReturn
if endState == gameSettings.endStates.FoundWinner then
local winnerName = playerManager.getWinnerName()
statusToReturn = "Winner is : " .. winnerName
elseif endState == gameSettings.endStates.TimerUp then
statusToReturn = "Time ran out!"
else
statusToReturn = "Error found"
end
end
```
4. Send back the message by typing `return statusToReturn`.
```lua
function MatchManager.getEndStatus(endState)
local statusToReturn
if endState == gameSettings.endStates.FoundWinner then
local winnerName = playerManager.getWinnerName()
statusToReturn = "Winner is : " .. winnerName
elseif endState == gameSettings.endStates.TimerUp then
statusToReturn = "Time ran out!"
else
statusToReturn = "Error found"
end
return statusToReturn
end
```
### Display and test
Get the updated announcement in GameManager and display it to the players using the DisplayManager.
1. Open **GameManager**. In the while true do loop, **delete** the last print statement. Then, create a variable named endStatus. Set it equal to calling `matchManager.getEndStatus(endState)`.
```lua
while true do
displayManager.updateStatus("Waiting for Players")
repeat
task.wait(gameSettings.intermissionDuration)
until #Players:GetPlayers() >= gameSettings.minimumPlayers
displayManager.updateStatus("Get ready!")
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
local endState = matchEnd.Event:Wait()
local endStatus = matchManager.getEndStatus(endState)
end
```
2. To display the returned message in the GUI label, call `displayManager.updateStatus()` and pass in `endStatus`.
```lua
local endStatus = matchManager.getEndStatus(endState)
displayManager.updateStatus(endStatus)
```
3. So the game pauses to let players see the message, add a wait using `transitionTime`.
```lua
local endStatus = matchManager.getEndStatus(endState)
displayManager.updateStatus(endStatus)
task.wait(gameSettings.transitionTime)
```
4. Start a **test server** and check that players see the following messages for the time up and winning player conditions.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/education/battle-royale-series/arena_7_showDifferentGUI_timeOut.jpg" />
<figcaption>Time is up condition</figcaption>
</figure>
<figure>
<img src="../../assets/education/battle-royale-series/arena_7_showDifferentGUI_foundWinner.jpg" />
<figcaption>Winning player condition</figcaption>
</figure>
</GridContainer>
### Troubleshooting tips
At this point, you're unable to see the messages, try one of the following below.
- If your end status message is "Error Found", none of the conditions were successful. Check the code in `MatchManager.getEndStatus()` against the code samples.
- If the end status doesn't display, check that `task.wait(gameSettings.transitionTime)` is after sending the message to displayManager.
## Start new matches
Before starting a new match, there will be a brief transition. This gives players time to see the end status and makes being teleported to the lobby feel less sudden.
At the end of the transition, remaining players will be removed from the arena, and all of the code will reset. This ensures that players will start the next match with a clean version of the game.
### Handle transitions
When players move into the transition state, remove their weapons.
1. In PlayerManager, find the local functions. Copy and paste the highlighted code for `removePlayerWeapon()` below. The code will remove an individual player's weapon if it's actively equipped or in the player's backpack.
```lua
local function removePlayerWeapon(whichPlayer)
-- Check to see if a player exist in case they disconnected or left.
if whichPlayer then
local character = whichPlayer.Character
-- If the player has it currently on their character
local weapon = character:FindFirstChild("Weapon")
if weapon then
weapon:Destroy()
end
-- If the player has the weapon in their backpack
local backpackWeapon = whichPlayer.Backpack:FindFirstChild("Weapon")
if backpackWeapon then
backpackWeapon:Destroy()
end
else
print("No player to remove weapon")
end
end
```
2. Start a new module function named `removeAllWeapons()`.
```lua
function PlayerManager.removeAllWeapons()
end
-- Events
Players.PlayerAdded:Connect(onPlayerJoin)
return PlayerManager
```
3. In that function, use a for loop to go through the active players table. In the loop, call `removePlayerWeapon()` and pass in the player found.
```lua
function PlayerManager.removeAllWeapons()
for playerKey, whichPlayer in activePlayers do
removePlayerWeapon(whichPlayer)
end
end
```
### Clean between matches
Cleanup will be its own function in MatchManager. For now, cleanup will just use that previously created function to remove player weapons. As you expand the game, more can be added, such as functions to reset a map that changed during a match.
1. Open MatchManager. Add a new module function named `cleanupMatch()`. In that function, call `playerManager.removeAllWeapons()`.
```lua
function MatchManager.cleanupMatch()
playerManager.removeAllWeapons()
end
matchStart.Event:Connect(startTimer)
matchEnd.Event:Connect(stopTimer)
return MatchManager
```
2. Next, call the cleanup function. Open **GameManager** and find the while true do loop. So players have weapons removed during the ending intermission, call `matchManager.cleanupMatch()` before the last `Library.task.wait()`.
```lua
while true do
displayManager.updateStatus("Waiting for Players")
repeat
task.wait(gameSettings.intermissionDuration)
until #Players:GetPlayers() >= gameSettings.minimumPlayers
displayManager.updateStatus("Get ready!")
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
local endState = matchEnd.Event:Wait()
local endStatus = matchManager.getEndStatus(endState)
displayManager.updateStatus(endStatus)
matchManager.cleanupMatch()
task.wait(gameSettings.transitionTime)
end
```
3. Start a test server and run a match. Wait for the timer to run out and confirm that the player's weapon is removed during the end game intermission.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/education/battle-royale-series/arena_7_showPlayerWeapons_withWeapon.jpg" />
<figcaption>During a match</figcaption>
</figure>
<figure>
<img src="../../assets/education/battle-royale-series/arena_7_showPlayerWeapons_withoutWeapon.jpg" />
<figcaption>After the match</figcaption>
</figure>
</GridContainer>
### Reset matches
You may have noticed a few other things in the game, like players still being in the arena after a match ends. With the match cleaned up, next reset the game. This includes sending players in the arena back into the lobby and clearing the active players table. With a reset in place, a game loop can run indefinitely.
First, start a function to send players back to the lobby.
1. In PlayerManager:
- Create a module function named `resetPlayers()`.
- Add a for loop to iterate through activePlayers.
- In the loop, call `respawnPlayerInLobby()` and pass in the player as the parameter.
```lua
function PlayerManager.resetPlayers()
for playerKey, whichPlayer in activePlayers do
respawnPlayerInLobby(whichPlayer)
end
end
-- Events
Players.PlayerAdded:Connect(onPlayerJoin)
return PlayerManager
```
2. Make sure the `activePlayers` table is empty for the next match by setting it equal to `{}`, which is a quick way of resetting to an empty table.
```lua
function PlayerManager.resetPlayers()
for playerKey, whichPlayer in activePlayers do
respawnPlayerInLobby(whichPlayer)
end
activePlayers = {}
end
```
3. Open MatchManager. Code a new module function named `resetMatch()` and call `playerManager.resetPlayers()`.
```lua
function MatchManager.resetMatch()
playerManager.resetPlayers()
end
matchStart.Event:Connect(startTimer)
matchEnd.Event:Connect(stopTimer)
return MatchManager
```
4. Go back to **GameManager**. At the end of the while true do loop, call `matchManager.resetMatch()`.
```lua
while true do
displayManager.updateStatus("Waiting for Players")
repeat
task.wait(gameSettings.intermissionDuration)
until #Players:GetPlayers() >= gameSettings.minimumPlayers
displayManager.updateStatus("Get ready!")
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
local endState = matchEnd.Event:Wait()
local endStatus = matchManager.getEndStatus(endState)
displayManager.updateStatus(endStatus)
matchManager.cleanupMatch()
task.wait(gameSettings.transitionTime)
matchManager.resetMatch()
end
```
5. Start a test server and run a match. Confirm that you can at least go through two game loops without any errors.
## Completed scripts
Below are completed scripts to double check your work.
### GameManager script
```lua
-- Services
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local matchManager = require(moduleScripts:WaitForChild("MatchManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
local displayManager = require(moduleScripts:WaitForChild("DisplayManager"))
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchEnd = events:WaitForChild("MatchEnd")
while true do
displayManager.updateStatus("Waiting for Players")
repeat
task.wait(gameSettings.intermissionDuration)
until #Players:GetPlayers() >= gameSettings.minimumPlayers
displayManager.updateStatus("Get ready!")
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
local endState = matchEnd.Event:Wait()
local endStatus = matchManager.getEndStatus(endState)
displayManager.updateStatus(endStatus)
matchManager.cleanupMatch()
task.wait(gameSettings.transitionTime)
matchManager.resetMatch()
end
```
### MatchManager script
```lua
local MatchManager = {}
-- Services
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local playerManager = require(moduleScripts:WaitForChild("PlayerManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
local displayManager = require(moduleScripts:WaitForChild("DisplayManager"))
local timer = require(moduleScripts:WaitForChild("Timer"))
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchStart = events:WaitForChild("MatchStart")
local matchEnd = events:WaitForChild("MatchEnd")
-- Values
local displayValues = ReplicatedStorage:WaitForChild("DisplayValues")
local timeLeft = displayValues:WaitForChild("TimeLeft")
-- Creates a new timer object to be used to keep track of match time.
local myTimer = timer.new()
-- Local Functions
local function stopTimer()
myTimer:stop()
end
local function timeUp()
matchEnd:Fire(gameSettings.endStates.TimerUp)
end
local function startTimer()
myTimer:start(gameSettings.matchDuration)
myTimer.finished:Connect(timeUp)
while myTimer:isRunning() do
-- Adding +1 makes sure the timer display ends at 1 instead of 0.
timeLeft.Value = (myTimer:getTimeLeft() + 1) // 1
-- By not setting the time for wait, it offers more accurate looping
task.wait()
end
end
-- Module Functions
function MatchManager.prepareGame()
playerManager.sendPlayersToMatch()
matchStart:Fire()
end
function MatchManager.getEndStatus(endState)
local messageToReturn
if endState == gameSettings.endStates.FoundWinner then
local winnerName = playerManager.getWinnerName()
messageToReturn = "Winner is : " .. winnerName
elseif endState == gameSettings.endStates.TimerUp then
messageToReturn = "Time ran out!"
else
messageToReturn = "Error found"
end
return messageToReturn
end
function MatchManager.cleanupMatch()
playerManager.removeAllWeapons()
end
function MatchManager.resetMatch()
playerManager.resetPlayers()
end
matchStart.Event:Connect(startTimer)
matchEnd.Event:Connect(stopTimer)
return MatchManager
```
### PlayerManager script
```lua
local PlayerManager = {}
-- Services
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Modules
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchEnd = events:WaitForChild("MatchEnd")
-- Map Variables
local lobbySpawn = workspace.Lobby.StartSpawn
local arenaMap = workspace.Arena
local spawnLocations = arenaMap.SpawnLocations
-- Values
local displayValues = ReplicatedStorage:WaitForChild("DisplayValues")
local playersLeft = displayValues:WaitForChild("PlayersLeft")
-- Player Variables
local activePlayers = {}
local playerWeapon = ServerStorage.Weapon
local function checkPlayerCount()
if #activePlayers == 1 then
matchEnd:Fire(gameSettings.endStates.FoundWinner)
print("Found winner")
end
end
local function removeActivePlayer(player)
print("removing player")
for playerKey, whichPlayer in activePlayers do
if whichPlayer == player then
table.remove(activePlayers, playerKey)
playersLeft.Value = #activePlayers
checkPlayerCount()
end
end
end
local function respawnPlayerInLobby(player)
player.RespawnLocation = lobbySpawn
player:LoadCharacter()
end
local function preparePlayer(player, whichSpawn)
player.RespawnLocation = whichSpawn
player:LoadCharacter()
local character = player.Character or player.CharacterAdded:Wait()
-- Give the player a tool
local sword = playerWeapon:Clone()
sword.Parent = character
local humanoid = character:WaitForChild("Humanoid")
humanoid.Died:Connect(function()
respawnPlayerInLobby(player)
removeActivePlayer(player)
end)
end
local function onPlayerJoin(player)
player.RespawnLocation = lobbySpawn
end
local function removePlayerWeapon(whichPlayer)
-- Check to see if a player exist in case they disconnected or left.
if whichPlayer then
local character = whichPlayer.Character
-- If the player has it currently on their character
local weapon = character:FindFirstChild("Weapon")
if weapon then
weapon:Destroy()
end
-- If the player has the weapon in their backpack
local backpackWeapon = whichPlayer.Backpack:FindFirstChild("Weapon")
if backpackWeapon then
backpackWeapon:Destroy()
end
else
print("No player to remove weapon")
end
end
function PlayerManager.sendPlayersToMatch()
local availableSpawnPoints = spawnLocations:GetChildren()
for playerKey, whichPlayer in Players:GetPlayers() do
table.insert(activePlayers,whichPlayer)
-- Gets a spawn location and then removes it from the table so the next player gets the next spawn
local spawnLocation = table.remove(availableSpawnPoints, 1)
preparePlayer(whichPlayer, spawnLocation)
end
playersLeft.Value = #activePlayers
end
function PlayerManager.getWinnerName()
local winningPlayer = activePlayers[1]
if winningPlayer then
return winningPlayer.Name
else
return "Error: No player found"
end
end
function PlayerManager.removeAllWeapons()
for playerKey, whichPlayer in activePlayers do
removePlayerWeapon(whichPlayer)
end
end
function PlayerManager.resetPlayers()
for playerKey, whichPlayer in activePlayers do
respawnPlayerInLobby(whichPlayer)
end
activePlayers = {}
end
-- Events
Players.PlayerAdded:Connect(onPlayerJoin)
return PlayerManager
```

View file

@ -0,0 +1,335 @@
---
title: Code the game loop
description: Create a battle royale experience in Roblox Studio. Code a round manager for a game.
next: /education/battle-royale-series/managing-players
prev: /education/battle-royale-series/project-setup
---
With the map created, it's time to start building out the scripts. The remainder of this course will heavily focus on scripting all different elements of the game loop.
## Set up the scripts
The battle royale will use a combination of module scripts and normal scripts. Below are the scripts and their functions.
<table>
<tbody>
<tr>
<td><b>GameManager </b></td>
<td>Script. Runs functions from the Match Manager using variables from the Game Settings</td>
</tr>
<tr>
<td><b>MatchManager </b></td>
<td>Module Script. Runs functions like sending players into an arena or keeping track of time in a match. </td>
</tr>
<tr>
<td><b>GameSettings </b></td>
<td>Module Script. Stores commonly used variables used by other scripts.</td>
</tr>
</tbody>
</table>
### GameSettings script
Create a module script named GameSettings to store variables used by other scripts, like match and intermission duration. These variables will be used by the GameManager script later.
1. In **ServerStorage**, create a folder named **ModuleScripts**. In that folder, create a new module script named **GameSettings**.
<img src="../../assets/education/battle-royale-series/arena_2_showGameSettingsCreated.png" />
2. Open GameSettings and rename the module table to match the name of the script.
```lua
local GameSettings = {}
return GameSettings
```
3. In the module table, add variables for the following uses. Take your best guess for each value, you can always change it later as you test.
- **Intermission Duration** - Seconds players wait before a match.
- **Match Duration** - Length of a match in seconds.
- **Minimum Players** - Smallest number of players needed to start.
- **Transition Time** - Time before and after a match in seconds. Makes transitioning between parts of the game loop less sudden.
```lua
local GameSettings = {}
-- Game Variables
GameSettings.intermissionDuration = 5
GameSettings.matchDuration = 10
GameSettings.minimumPlayers = 2
GameSettings.transitionTime = 5
return GameSettings
```
### MatchManager script
The second script connected to the GameManager is the MatchManager. This script manages tasks like starting the timer or resetting players once the match ends.
Within MatchManager is a function named `prepareGame()` starts the game by transitioning players into the match.
1. In ServerStorage > ModuleScripts > add a module script named MatchManager. Rename the module table.
```lua
local MatchManager = {}
return MatchManager
```
2. Add a new module function to MatchManager named `prepareGame()`. Include a print statement to test the script later.
```lua
local MatchManager = {}
function MatchManager.prepareGame()
print("Game starting!")
end
return MatchManager
```
## Code the game loop
The main game loop will be coded in the GameManager script using the variables just created. Remember, there are three phases in the game loop: intermission, competition, and cleanup & reset.
### GameManager script
This script is a normal server script, so put it in ServerScriptService, rather than the module scripts folder. The actual game loop will be in a while true do loop.
1. In **ServerScriptService**, create a new script named **GameManager**.
<img src="../../assets/education/battle-royale-series/arena_2_showGameManagerCreated.png" />
2. Add a variable for the service "ServerStorage", which is where the Modulescripts are. Then add a variable for the service "Players", which will be needed to check player count during intermissions.
```lua
-- Services
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
```
3. To use the previously created modules:
- Set a variable named `moduleScripts` to the location of the ModuleScripts folder.
- Add variables named `matchManager` and `gameSettings`. Set each variable to require their respective script.
```lua
-- Services
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local matchManager = require(moduleScripts:WaitForChild("MatchManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
```
4. After the variables, add a `while true do` loop. All phases of the game loop will go inside to repeat indefinitely.
```lua
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local matchManager = require(moduleScripts:WaitForChild("MatchManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
-- Main game loop
while true do
end
```
### Code the intermission
While the game loop runs indefinitely, the intermission should pause the loop and only continue when there are enough players for a match. To code this pause, include a nested repeat loop for the intermission in the while loop. That nested loop will repeat until there are enough players, pausing the main loop. Once there are enough players, it'll exit and transition players into a match.
With a **repeat loop**, the code in the loop will run at least once. Unlike a while loop, it doesn't check it's condition until the loop ends. This ensures players always go to the lobby before a match.
1. In the `while true do` loop, type `repeat` and press <kbd>Enter</kbd> to autocomplete with the keyword `until`.
```lua
while true do
repeat
until
end
```
2. Check if the current number of players `(#Players:GetPlayers())` is greater or equal to the `minimumPlayers` variable created earlier in the GameSettings module.
```lua
while true do
repeat
until #Players:GetPlayers() >= gameSettings.minimumPlayers
end
```
3. In the repeat loop, add a print statement saying the intermission is starting. Use `Library.task.wait()` to pause for the intermission using `intermissionDuration` from GameSettings.
```lua
while true do
repeat
print("Starting intermission")
task.wait(gameSettings.intermissionDuration)
until #Players:GetPlayers() >= gameSettings.minimumPlayers
end
```
4. Playtest and check that the print statement `"Starting intermission"` is shown at least twice. Seeing the message twice proves the repeat loop didn't find enough players and ran again. You'll have to wait the length of intermission before seeing the message a second time.
### Troubleshooting tips
At this point, if you're not spawning as intended, try one of the following below.
- `Library.task.wait()` should be inside the repeat loop. Without the wait, the script will run too many times in a second, overloading Roblox Studio and causing an error.
- In the Game Settings module, the variable `intermissionDuration` should be greater than 1. If lower, the script can repeat too often, causing slow down issues.
### End the intermission
Once there are enough players, have them wait a short transition time. Then, send them into the match by calling the `prepareGame()` function in MatchManager. Remember, that function just prints a line, but you'll add more code later.
1. At the end of the repeat loop, add a print statement saying the intermission is over to test your code. Then, follow it with a `Library.task.wait()` using GameSetting's `transitionTime` variable.
```lua
while true do
repeat
print("Starting intermission")
task.wait(gameSettings.intermissionDuration)
until #Players:GetPlayers() >= gameSettings.minimumPlayers
print("Intermission over")
task.wait(gameSettings.transitionTime)
end
```
<Alert severity="warning">
Keep the code within the scope of the while loop (between `do` and `end`). If code is outside, parts of your game loop might not repeat and players will just be stuck in the intermission phase.
</Alert>
2. After the wait, call the `prepareGame()` from the MatchManager module. When the code runs, this will just print text to the Output window. Wait until the next section to test this code.
```lua
while true do
repeat
print("Starting intermission")
task.wait(gameSettings.intermissionDuration)
until #Players:GetPlayers() >= gameSettings.minimumPlayers
print("Intermission over")
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
end
```
<Alert severity="warning">
If you add onto the script after this project, keep in mind code below the while true do loop **won't** run. Keep relevant code inside the while loop or call module functions from the main loop.
</Alert>
## Test multiplayer games
Right now, to have the code run `prepareGame()`, it needs to exit the repeat loop. But, to do that, there needs to be more than one player. This means if you use the playtest button, the function will never run because you're the only player in the game (unless your minimum players is one). To test this out, you'll need to simulate a multiplayer game.
### Start a local server
To test code requiring more than one player, create a local server. While published games are normally on Roblox servers, a **local server** simulates a multiplayer game on your computer with simulated players.
1. To start a local server, navigate to the **Test** menu's **Start Test Session** ⟩ **Server and Clients**. This lesson uses 2 players.
2. Wait a few seconds for the server to set up. Multiple windows will open in addition to your original Studio window. You may need to allow access to Roblox Studio from firewalls or other online security software.
### Troubleshooting tips
At this point, you're unable to see the test servers, try one of the following below.
- If you have any issues with the server starting, double check the article <a href="https://en.help.roblox.com/hc/en-us/articles/203312840-Firewall-and-Router-Issues">Firewall and Router Issues</a>.
- Set the number of players to a small amount, like 2 or 3.
- If the issue doesn't resolve, try restarting Studio or restarting your computer.
### Test in the local server
You'll see multiple windows when the server starts. Each one represents a different part of the server/client relationship.
- **Server** (green border) runs the game.
- **Client** (blue borders) simulates a player's experience.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/education/battle-royale-series/arena_2_showClientServerWindows_server.png" />
<figcaption>Server with green border</figcaption>
</figure>
<figure>
<img src="../../assets/education/battle-royale-series/arena_2_showClientServerWindows_client.png" />
<figcaption>Client with blue border</figcaption>
</figure>
</GridContainer>
With the server up, you can check if the code worked.
1. Find the **Server** window with the green border. Check for the print statement called from the MatchManager script. Because there is a repeat loop, you'll see the same print statements repeating.
<img src="../../assets/education/battle-royale-series/arena_2_showFinishedOutput.png" />
2. Once you're done testing, click the **End Session** button in any window to close all server and client windows and return to your normal Studio window.
### Troubleshooting tips
At this point, if the intended print statements didn't appear, try one of the following below.
- Check that functions like `prepareGame()` are in scope of the while true do loop.
- If the print from MatchManager didn't work, check some common troubleshooting with module scripts, like making sure that the MatchManager script is required in GameManager or that `prepareGame()` is added to that module's table.
## Completed scripts
Below are completed scripts to double check your work.
### GameManager script
```lua
-- Services
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local matchManager = require(moduleScripts:WaitForChild("MatchManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
-- Main game loop
while true do
repeat
print("Starting intermission")
task.wait(gameSettings.intermissionDuration)
until #Players:GetPlayers() >= gameSettings.minimumPlayers
print("Intermission over")
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
end
```
### MatchManager script
```lua
local MatchManager = {}
function MatchManager.prepareGame()
print("Game starting!")
end
return MatchManager
```
### GameSettings script
```lua
local GameSettings = {}
-- Game Variables
GameSettings.intermissionDuration = 5
GameSettings.roundDuration = 10
GameSettings.minimumPlayers = 2
GameSettings.transitionTime = 5
return GameSettings
```

View file

@ -0,0 +1,527 @@
---
title: Create a GUI
description: Create a battle royale experience in Roblox Studio. Create and code a GUI that displays a game status during a match.
next: /education/battle-royale-series/ending-matches
prev: /education/battle-royale-series/timers-and-events
---
Right now, much of the game information is currently in the Output window, invisible to players. So players can be informed of what's happening in the game, you'll create a graphical user interface (GUI) and code it.
## Display information with a GUI
For this game a text label will display the current game status as well as the remaining player count and time.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/education/battle-royale-series/arena_5_showReadyDisplay.jpg" />
<figcaption>During Intermission</figcaption>
</figure>
<figure>
<img src="../../assets/education/battle-royale-series/arena_5_showTimerDisplay.jpg" />
<figcaption>During a Match</figcaption>
</figure>
</GridContainer>
### Set up the GUI
First, create a **Screen GUI** object to hold the different text elements. When the player moves the camera, the screen GUI stays in the same place on their screen.
To ensure all players see the same display, place the GUI in the **StarterGUI** folder. At game startup, this folder is copied to all players.
1. In the StarterGUI folder, create a new ScreenGUI. Then in ScreenGUI, add a new TextLabel named StatusText.
<img src="../../assets/education/battle-royale-series/arena_5_showLabelCreated.png" />
2. To move the label, in the Explorer, select StatusText. Then, in the game view, drag the label where you would like it. Your numbers may differ from the video. The label can also be resized using the anchor points on the corners.
<video controls src="../../assets/education/battle-royale-series/arena_5_showMovingScaleGUI.mp4" width="100%"></video>
## Script the GUI
To reflect changes in the game, scripts will need to update the GUI elements. For instance, the game status, whether it's an intermission or active round, will be stored in a StringValue and updated using local scripts.
<Alert severity="info">
Compared to scripts and module scripts which run Roblox servers, local scripts run on a player's device. They're often used for coding GUI elements like the timer or player input, like mouse or keyboard actions.
</Alert>
### Set up the script
The StatusDisplay script will be used to update the player's GUI whenever the game state changes.
1. In **ReplicatedStorage**, create a folder named DisplayValues. In that folder, add a StringValue named Status. To test the value later, give it a temporary value, like "Welcome to the Battle!".
<img src="../../assets/education/battle-royale-series/arena_6_showStatusCreated.png" />
<Alert severity="info">
Because local scripts only run on a player's device, they can't be stored in Server folders like ServerStorage. ReplicatedStorage is a folder that's available both to the client (device) and server.
</Alert>
2. In StarterGUI > ScreenGUI > Status, add a new local script named StatusDisplay. Scripts that affect the GUI are often parented to that GUI element.
<img src="../../assets/education/battle-royale-series/arena_5_addLocalScript.png" />
3. Open StatusDisplay and define the following variables for the follow:
- ReplicatedStorage service
- DisplayValues folder
- Status StringValue
- TextLabel - use `script.Parent`.
```lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local displayValues = ReplicatedStorage:WaitForChild("DisplayValues")
local status = displayValues:WaitForChild("Status")
local textLabel = script.Parent
```
### Change the text label
To change the text in the label, use a Changed event so whenever the Status string is changed by another script, the text label will be updated.
1. Code a new function named `updateText()`. In that function, set the Text property of `textLabel` to `status.Value`.
```lua
local textLabel = script.Parent
local function updateText()
textLabel.Text = status.Value
end
```
2. Connect the function to the Changed event.
```lua
local function updateText()
textLabel.Text = status.Value
end
status.Changed:Connect(updateText)
```
3. So players see the most up to date status when starting the game, run `updateText()` at the end of the script.
```lua
local function updateText()
textLabel.Text = status.Value
end
status.Changed:Connect(updateText)
updateText()
```
4. Run the game and confirm that you see the temporary value in the display.
<img src="../../assets/education/battle-royale-series/arena_5_showGUITest.png" />
## Create the display manager
During a game, the text label will need to get information from GameManager, MatchManager, and possibly other scripts. So these different scripts can update the text label when needed, create a module script named DisplayManager.
### Set up the script
Because DisplayManager needs to communicate with other scripts, it'll be a module script.
1. In **ServerStorage** > **ModuleScripts**, create a new module script named DisplayManager. Rename the module table to match the script name.
2. Add local variables for the following: Replicated Storage, DisplayValues folder, Status.
```lua
local DisplayManager = {}
-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Display Values used to update Player GUI
local displayValues = ReplicatedStorage:WaitForChild("DisplayValues")
local status = displayValues:WaitForChild("Status")
-- Local Functions
-- Module Functions
return DisplayManager
```
3. Create a new module function named `updateStatus()` that updates the string in the Status value. Other scripts will be able to call this function.
```lua
-- Local Functions
-- Module Functions
function DisplayManager.updateStatus(newStatus)
status.Value = newStatus
end
```
### Update the text status
With the Display Manager set up, it can be used in other scripts to update the GUI text label. As the first message in the GUI, show the start and end of the intermission through the GameManager script.
1. In **ServerScriptService** > GameManager, create a variable named `displayManager` and require the DisplayManager module in ServerStorage.
```lua
-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local roundManager = require(moduleScripts:WaitForChild("RoundManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
local displayManager = require(moduleScripts:WaitForChild("DisplayManager"))
```
2. As the **first** line after the while true do statement, call displayManager > `updateStatus()` and pass in a message about waiting for players.
```lua
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchEnd = events:WaitForChild("MatchEnd")
while true do
displayManager.updateStatus("Waiting for Players")
repeat
print("Starting intermission")
task.wait(gameSettings.intermissionDuration)
until #Players:GetPlayers() >= gameSettings.minimumPlayers
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
matchEnd.Event:Wait()
end
```
3. After the end of the repeat loop for the intermission, call `updateStatus()` and pass in a string announcing the match is starting. Since you'll be testing with the GUI, delete the two print statements for noting the start and end of the intermission.
```lua
while true do
displayManager.updateStatus("Waiting for Players")
repeat
task.wait(gameSettings.intermissionDuration)
until #Players:GetPlayers() >= gameSettings.minimumPlayers
displayManager.updateStatus("Get ready!")
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
matchEnd.Event:Wait()
end
```
4. Test the game **with** and **without** your minimum players. The message should read the following:
- Without the minimum players: `"Waiting for Players"`.
- With the minimum players: `"Get ready"`.
### Troubleshooting tips
At this point, if the text label doesn't display the first message, or still displays "Label", try one of the following below.
- Make sure in the StatusDisplay local script that `updateText()` is called at the bottom of the script. This ensures that the player gets the most up to date message.
- Check that the Status StringValue is in ReplicatedStorage. Due to the unique nature of client-server relations, if it's in ServerStorage, a local script won't be able to find it.
## Display match status
During a match, the GUI will display two numbers: remaining player count and time. As these numbers change, the text label will change as well.
## Set up values and functions
IntValues will be used to store the player count and time left.
1. In ReplicatedStorage > DisplayValues, create two IntValues named PlayersLeft and TimeLeft.
<img src="../../assets/education/battle-royale-series/arena_5_showIntValuesCreated.png" />
2. In DisplayManager, add variables to store the players left and time left values.
```lua
local DisplayManager = {}
-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Display Values used to update Player GUI
local displayValues = ReplicatedStorage:WaitForChild("DisplayValues")
local status = displayValues:WaitForChild("Status")
local playersLeft = displayValues:WaitForChild("PlayersLeft")
local timeLeft = displayValues:WaitForChild("TimeLeft")
```
3. Create a local function named `updateMatchStatus()`. Then, set the value of status to display the number of players left and the remaining time.
```lua
local displayValues = ReplicatedStorage:WaitForChild("DisplayValues")
local status = displayValues:WaitForChild("Status")
local playersLeft = displayValues:WaitForChild("PlayersLeft")
local timeLeft = displayValues:WaitForChild("TimeLeft")
-- Local Functions
local function updateRoundStatus()
status.Value = "Players Left: " .. playersLeft.Value .. " / Time Left: " .. timeLeft.Value
end
```
4. For **both** IntValue variables, connect `updateRoundStatus()` to the Changed event.
```lua
-- Module Functions
function DisplayManager.updateStatus(newStatus)
status.Value = newStatus
end
playersLeft.Changed:Connect(updateRoundStatus)
timeLeft.Changed:Connect(updateRoundStatus)
return DisplayManager
```
<Alert severity="warning">
Don't test yet. Nothing has updated the PlayersLeft or TimeLeft values, so the status won't be changed once a round starts.
</Alert>
### Display players
Next, add the code for displaying the number of players at the start of a game. Later lessons will update the PlayersLeft value as players are eliminated from the game.
1. In PlayerManager, add local variables for ReplicatedStorage service, DisplayValues folder and PlayersLeft IntValue.
```lua
local PlayerManager = {}
-- Services
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Map Variables
local lobbySpawn = workspace.Lobby.StartSpawn
local arenaMap = workspace.Arena
local spawnLocations = arenaMap.SpawnLocations
-- Values
local displayValues = ReplicatedStorage:WaitForChild("DisplayValues")
local playersLeft = displayValues:WaitForChild("PlayersLeft")
```
2. Show the starting player count by setting the value of `playersLeft` to the size of the active players array.
Then, in `sendPlayersToMatch()`, under the for loop, type: `playersLeft.Value = #activePlayers`
```lua
function PlayerManager.sendPlayersToMatch()
local availableSpawnPoints = spawnLocations:GetChildren()
for playerKey, whichPlayer in Players:GetPlayers() do
table.insert(activePlayers, whichPlayer)
local spawnLocation = table.remove(availableSpawnPoints, 1)
preparePlayer(whichPlayer, spawnLocation)
end
playersLeft.Value = #activePlayers
end
```
### Display the timer
Remember that module scripts are used to centralize similar code. Since the timer is tracked in MatchManager, update the TimeLeft value using functions from the Timer script. The display manager will listen for changes to the TimeLeft, and update to match the new value.
1. In MatchManager, create variables to store the **ReplicatedStorage** service, DisplayValues folder and TimeLeft value.
```lua
local MatchManager = {}
-- Services
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local playerManager = require(moduleScripts:WaitForChild("PlayerManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
local timer = require(moduleScripts:WaitForChild("Timer"))
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchStart = events:WaitForChild("MatchStart")
-- Values
local displayValues = ReplicatedStorage:WaitForChild("DisplayValues")
local timeLeft = displayValues:WaitForChild("TimeLeft")
local myTimer = timer.new()
```
2. Find the `startTimer()` function. **After** the timer's `Finished` event, copy and paste the whole, highlighted while loop below. The code runs a loop to update the `timeLeft` value as long as the timer is still active.
```lua
while myTimer:isRunning() do
-- Adding +1 makes sure the timer display ends at 1 instead of 0.
timeLeft.Value = (myTimer:getTimeLeft() + 1) // 1
-- By not setting the time for wait, it offers more accurate looping
task.wait()
end
```
When added in, the code should look like the sample below.
```lua
local function startTimer()
print("Timer started")
myTimer:start(gameSettings.matchDuration)
myTimer.finished:Connect(timeUp)
while myTimer:isRunning() do
-- Adding +1 makes sure the timer display ends at 1 instead of 0.
timeLeft.Value = (myTimer:getTimeLeft() + 1) // 1
-- By not setting the time for wait, it offers more accurate looping
task.wait()
end
end
```
3. Run the game with the minimum players. Check that the status text displays:
- The correct amount of starting players. Remember, this number won't change until additional code is added in a future lesson.
- Time decreases each second until it stops at 1.
<video controls src="../../assets/education/battle-royale-series/arena_5_showFinalizedGameLoop.mp4" width="100%"></video>
## Completed scripts
Below are completed scripts to double check your work.
### GameManager script
```lua
-- Services
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local matchManager = require(moduleScripts:WaitForChild("MatchManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
local displayManager = require(moduleScripts:WaitForChild("DisplayManager"))
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchEnd = events:WaitForChild("MatchEnd")
while true do
displayManager.updateStatus("Waiting for Players")
repeat
task.wait(gameSettings.intermissionDuration)
until #Players:GetPlayers() >= gameSettings.minimumPlayers
displayManager.updateStatus("Get ready!")
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
matchEnd.Event:Wait()
end
```
### DisplayManager script
```lua
local DisplayManager = {}
-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Display Values used to update Player GUI
local displayValues = ReplicatedStorage:WaitForChild("DisplayValues")
local status = displayValues:WaitForChild("Status")
local playersLeft = displayValues:WaitForChild("PlayersLeft")
local timeLeft = displayValues:WaitForChild("TimeLeft")
-- Local Functions
local function updateRoundStatus()
status.Value = "Players Left: " .. playersLeft.Value .. " / Time Left: " .. timeLeft.Value
end
-- Module Functions
function DisplayManager.updateStatus(newStatus)
status.Value = newStatus
end
playersLeft.Changed:Connect(updateRoundStatus)
timeLeft.Changed:Connect(updateRoundStatus)
return DisplayManager
```
### MatchManager script
```lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local playerManager = require(moduleScripts:WaitForChild("PlayerManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
local timer = require(moduleScripts:WaitForChild("Timer"))
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchStart = events:WaitForChild("MatchStart")
local matchEnd = events:WaitForChild("MatchEnd")
-- Values
local displayValues = ReplicatedStorage:WaitForChild("DisplayValues")
local timeLeft = displayValues:WaitForChild("TimeLeft")
local myTimer = timer.new()
-- Local Functions
local function timeUp()
print("Time is up!")
end
local function startTimer()
print("Timer started")
myTimer:start(gameSettings.matchDuration)
myTimer.finished:Connect(timeUp)
while myTimer:isRunning() do
-- Adding +1 makes sure the timer display ends at 1 instead of 0.
timeLeft.Value = (myTimer:getTimeLeft() + 1) // 1
-- By not setting the time for wait, it offers more accurate looping
task.wait()
end
end
-- Module Functions
function MatchManager.prepareGame()
playerManager.sendPlayersToMatch()
matchStart:Fire()
end
matchStart.Event:Connect(startTimer)
return MatchManager
```
### StatusDisplay script
```lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local displayValues = ReplicatedStorage:WaitForChild("DisplayValues")
local status = displayValues:WaitForChild("Status")
local textLabel = script.Parent
local function updateText()
textLabel.Text = status.Value
end
status.Changed:Connect(updateText)
updateText()
```

View file

@ -0,0 +1,581 @@
---
title: End matches
description: Create a battle royale experience in Roblox Studio. Write scripts that handle victory conditions or time running out.
next: /education/battle-royale-series/cleanup-and-reset
prev: /education/battle-royale-series/creating-a-gui
---
Matches can end in a few conditions including timers running out or a single player being left.
## Manage defeated players
Right now, defeated players respawn in the arena. Instead, send them back to the lobby to wait for the next match.
1. In PlayerManager, create a local function named `respawnPlayerInLobby()`. In that function, do the following:
- Set the player's RespawnLocation property to the lobbySpawn.
- Reload the character with `Class.Player:LoadCharacter()`. Remember, `Class.Player:LoadCharacter()|LoadCharacter()` recreates the player at their spawn, removing any tools they had.
## Use anonymous functions
Anonymous functions are commonly used in advanced scripts under specific situations. Before defining them, it's important to understand a situation your script may encounter.
Consider this: To teleport defeated players to the lobby, the respawn function will have to be connected to the player's Died event. There is a problem though. If you connect the event as we have in the past, there's no way to get the name of the player from the Died event. The script needs that name to remove a defeated player from the table tracking active players.
To get the name of the player that triggered the Died event, use an anonymous function. **Anonymous functions** don't have names, and can be created directly within `Connect()` rather than separately.
A sample anonymous function is below that shows the syntax.
```lua
myPlayer.Died:Connect(function()
print(player)
end)
```
### Code the died event
For this function, whenever a player's character dies, they'll trigger the function that respawns them.
1. To get access to the player's Died event, in PlayerManager > `preparePlayer()`, add a variable for the player's humanoid.
```lua
local function preparePlayer(player, whichSpawn)
player.RespawnLocation = whichSpawn
player:LoadCharacter()
local character = player.Character or player.CharacterAdded:Wait()
local sword = playerWeapon:Clone()
sword.Parent = character
local humanoid = character:WaitForChild("Humanoid")
end
```
2. Create an anonymous function that connects to the Died event. Start by typing `humanoid.Died:Connect()`. Then, **inside** `Connect()`, type `function()`, press <kbd>Enter</kbd> to autocomplete `end`, and delete the extra parenthesis.
```lua
local humanoid = character:WaitForChild("Humanoid")
humanoid.Died:Connect(function()
end)
```
3. In the anonymous function, call `respawnPlayerInLobby()`. Pass in `player`, a variable storing the player getting ready to enter a match.
```lua
local humanoid = character:WaitForChild("Humanoid")
humanoid.Died:Connect(function()
respawnPlayerInLobby(player)
end)
```
4. Start a **server** and play a match. Test that when a player dies, they respawn in the lobby. To defeat players, walk up to them and use your weapon until you can confirm they've respawned in the lobby.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/education/battle-royale-series/arena_6_showPlayersDefeated_showPlayerAttacked.jpg" />
<figcaption>Attacking Player2</figcaption>
</figure>
<figure>
<img src="../../assets/education/battle-royale-series/arena_6_showPlayersDefeated_inLobby.jpg" />
<figcaption>Player2 in Lobby after respawning</figcaption>
</figure>
</GridContainer>
Note, there are also some different ways of testing if desired.
- To kill a player, in the Server > Output window > Command Bar, copy and paste: `workspace.Player1.Humanoid.Health = 0`. Press <kbd>Enter</kbd> to run the command. To remove other players, use Player2, Player3, etc.
- Setting GameSettings > `matchDuration` to a longer time can give you more time to find and take out all players.
- To have quicker tests, change GameSettings > `minimumPlayers` to smaller number, like 2.
## End the game
Now that defeated players respawn, start working on ending the game. Remember creating the MatchEnd event? It'll be fired when either the timer runs out or a winner is found.
To tell other scripts which condition ended the game, create a table with variables for `TimerUp` and `FoundWinner`. When the match end event fires, it'll pass in a variable so other scripts can respond.
1. In GameSettings, create an empty module table named `endStates`.
```lua
local GameSettings = {}
-- Game Variables
GameSettings.intermissionDuration = 5
GameSettings.matchDuration = 10
GameSettings.minimumPlayers = 2
GameSettings.transitionTime = 5
-- Possible ways that the game can end.
GameSettings.endStates = {
}
return GameSettings
```
2. Create two variables named `TimerUp` and `FoundWinner`. Set each to a string matching their name. These strings will be used to test the code.
```lua
GameSettings.endStates = {
TimerUp = "TimerUp",
FoundWinner = "FoundWinner"
}
```
### End with a timer
When the timer ends, fire the Match End event and send the matching end state variable. That way, other scripts listening for that event can respond accordingly. Remember that while events fire signals, that can also send data that's received by listening scripts, like `TimerUp` or `FoundWinner`.
1. In **GameManager**, in the while true do loop, find the line `matchEnd.Event:Wait()`. At the start of the line, add `local endState =`.
```lua
while true do
displayManager.updateStatus("Waiting for Players")
repeat
task.wait(gameSettings.intermissionDuration)
until #Players:GetPlayers() >= gameSettings.minimumPlayers
displayManager.updateStatus("Get ready!")
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
local endState = matchEnd.Event:Wait()
end
```
2. To confirm that the correct state was received, add a print statement including `endState`.
```lua
while true do
displayManager.updateStatus("Waiting for Players")
repeat
task.wait(gameSettings.intermissionDuration)
until #Players:GetPlayers() >= gameSettings.minimumPlayers
displayManager.updateStatus("Get ready!")
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
local endState = matchEnd.Event:Wait()
print("Game ended with: " .. endState)
end
```
3. In MatchManager, find `timeUp()` and **remove** the print statement. Then, to fire the match end event, type `matchEnd:Fire()` and pass in `gameSettings.endStates.TimerUp`.
```lua
-- Values
local displayValues = ReplicatedStorage:WaitForChild("DisplayValues")
local timeLeft = displayValues:WaitForChild("TimeLeft")
-- Creates a new timer object to be used to keep track of match time.
local myTimer = timer.new()
-- Local Functions
local function timeUp()
matchEnd:Fire(gameSettings.endStates.TimerUp)
end
```
4. Test a match. Once the timer runs out, check that the print statement includes the string stored in the `TimerUp` variable.
<img src="../../assets/education/battle-royale-series/arena_6_showEndGame.png" />
### Troubleshooting tips
At this point, the message wasn't displayed, try one of the following below.
- Check that wherever the end state variables are called, that they're written exactly, like here: `gameSettings.endStates.TimerUp`.
- Make sure to use : (colon operator) with the `Class.Fire|Fire()` **instead** of the dot operator, like in `matchEnd:Fire()`.
## Code a winning match
Next, the script needs to identify winning players, remove losing players, and run cleanup such as stopping the timer. Matches will also end if one player is left. To see if the `FoundWinner` condition is met, you'll need a function that checks the number left in the table tracking players in a match.
### Check player count
Matches will end if there's only one player left. To check this, the script needs to track the players in a round. Once one player is left, a winner can be assigned.
1. In PlayerManager, define the following variables:
- ModuleScripts folder
- GameSettings module - Used to access end state variables
- Events folder and the MatchEnd event - Fires event
```lua
local PlayerManager = {}
-- Services
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Modules
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchEnd = events:WaitForChild("MatchEnd")
```
2. Above `respawnPlayerInLobby()`, add a new local function named `checkPlayerCount()`.
```lua
-- Player Variables
local activePlayers = {}
local playerWeapon = ServerStorage.Weapon
local function checkPlayerCount()
end
local function respawnPlayerInLobby(player)
```
<Alert severity="warning">
Remember, to be in scope, this function needs to come **before** the functions that will use it later in the script.
</Alert>
3. Within that function, use an if then statement to check for a winner. In that statement:
- Check if the size of the `activePlayers` table is 1.
- If so, fire `matchEnd` and pass in `gameSettings.endStates.FoundWinner`.
```lua
local function checkPlayerCount()
if #activePlayers == 1 then
matchEnd:Fire(gameSettings.endStates.FoundWinner)
end
end
```
### Remove a player
An accurate count can't be kept unless players are removed. When a player is defeated, keep an accurate player count by removing them from the player table. Then, check the size of the active player table to see if there's a winner.
1. Under `checkPlayerCount()`, create a new local function named `removeActivePlayer()` with a parameter named `player`.
```lua
local function checkPlayerCount()
if #activePlayers == 1 then
matchEnd:Fire(gameSettings.endStates.FoundWinner)
end
end
local function removeActivePlayer(player)
end
```
2. To find the player in the `activePlayers` table, iterate through it using a `for` loop. Then, add an `if` statement that runs if a player matching the name passed into the function is found.
```lua
local function removeActivePlayer(player)
for playerKey, whichPlayer in activePlayers do
if whichPlayer == player then
end
end
end
```
<Alert severity="warning">
Keep similar variable names **different**. As you code, may sure you don't have overlapping variable names, like the above function which includes two versions of player: `player` and `whichPlayer`.
For instance, adding which to a variable name in an iterating table is a good convention to avoid confusion, such as `whichPlayer` or `whichPart`.
</Alert>
3. To remove the player, in the if statement:
- Call `Library.table.remove()`. Inside the parentheses, pass in `activePlayers`, the table to look in, and `playerKey` - the player to remove from the table.
- Set the value of the `playersLeft` object to `#activePlayers`.
- Check for a winning player by running `checkPlayerCount()`.
```lua
local function removeActivePlayer(player)
for playerKey, whichPlayer in activePlayers do
if whichPlayer == player then
table.remove(activePlayers, playerKey)
playersLeft.Value = #activePlayers
checkPlayerCount()
end
end
end
```
### Connect events and test
To use the function just created, call it from within the anonymous function connected to the player's `Died` event.
1. Find `preparePlayer()`. In the anonymous function with the Died event connection, call `removeActivePlayer()`. Then, pass in the player as the parameter.
```lua
humanoid.Died:Connect(function()
respawnPlayerInLobby(player)
removeActivePlayer(player)
end)
```
2. To see if a winning player is found, start a **test server**. When there is only one player left you should see FoundWinner in the Output window.
<img src="../../assets/education/battle-royale-series/arena_6_showGameEndFoundWinner.png" />
3. Continue testing and let the match end. Notice as a new match starts, an error appears in the Output window:
<img src="../../assets/education/battle-royale-series/arena_6_showGameTimerError.png" />
This error is because the timer wasn't stopped, which will be fixed in the next section.
### Stop the timer
Whenever a match ends with a winning player, the timer should stop as well. To halt the timer before time is up, have the timer stop whenever the match end event fires. This is one of the benefits of creating events. They can be reused in multiple situations to script cause and effect relationships.
1. In MatchManager, create a new local function named `stopTimer()`. Inside, type `myTimer:stop()` to halt the timer.
```lua
-- Creates a new timer object to be used to keep track of match time.
local myTimer = timer.new()
-- Local Functions
local function stopTimer()
myTimer:stop()
end
local function timeUp()
matchEnd:Fire(gameSettings.endStates.TimerUp)
end
```
2. To stop the timer when the match ends, connect the matchEnd event to `stopTimer()`.
```lua
-- Module Functions
function MatchManager.prepareGame()
playerManager.sendPlayersToMatch()
matchStart:Fire()
end
matchStart.Event:Connect(startTimer)
matchEnd.Event:Connect(stopTimer)
return MatchManager
```
3. **Test** that the previous error no longer appears by starting a **server**. Eliminate all but one player and then wait a few seconds once the match ends.
## Next steps
While the two win conditions are finished, there are still some tasks left to finish the game loop. For instance, the winning player isn't ever teleported to the lobby. In the next lesson, you'll display how the match ended to players and reset the game, finally completing the whole loop.
## Completed scripts
Below are completed scripts to double check your work.
### PlayerManager script
```lua
local PlayerManager = {}
-- Services
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Modules
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchEnd = events:WaitForChild("MatchEnd")
-- Map Variables
local lobbySpawn = workspace.Lobby.StartSpawn
local arenaMap = workspace.Arena
local spawnLocations = arenaMap.SpawnLocations
-- Values
local displayValues = ReplicatedStorage:WaitForChild("DisplayValues")
local playersLeft = displayValues:WaitForChild("PlayersLeft")
-- Player Variables
local activePlayers = {}
local playerWeapon = ServerStorage.Weapon
-- Local Functions
local function checkPlayerCount()
if #activePlayers == 1 then
matchEnd:Fire(gameSettings.endStates.FoundWinner)
end
end
local function removeActivePlayer(player)
for playerKey, whichPlayer in activePlayers do
if whichPlayer == player then
table.remove(activePlayers, playerKey)
playersLeft.Value = #activePlayers
checkPlayerCount()
end
end
end
local function respawnPlayerInLobby(player)
player.RespawnLocation = lobbySpawn
player:LoadCharacter()
end
local function onPlayerJoin(player)
player.RespawnLocation = lobbySpawn
end
local function preparePlayer(player, whichSpawn)
player.RespawnLocation = whichSpawn
player:LoadCharacter()
local character = player.Character or player.CharacterAdded:Wait()
local sword = playerWeapon:Clone()
sword.Parent = character
local humanoid = character:WaitForChild("Humanoid")
humanoid.Died:Connect(function()
respawnPlayerInLobby(player)
removeActivePlayer(player)
end)
end
-- Module Functions
function PlayerManager.sendPlayersToMatch()
local arenaSpawns = spawnLocations:GetChildren()
for playerKey, whichPlayer in Players:GetPlayers() do
table.insert(activePlayers, whichPlayer)
local spawnLocation = table.remove(arenaSpawns, 1)
preparePlayer(whichPlayer, spawnLocation)
end
playersLeft.Value = #activePlayers
end
-- Events
Players.PlayerAdded:Connect(onPlayerJoin)
return PlayerManager
```
### GameSettings script
```lua
local GameSettings = {}
-- Game Variables
GameSettings.intermissionDuration = 5
GameSettings.matchDuration = 10
GameSettings.minimumPlayers = 2
GameSettings.transitionTime = 5
-- Possible ways that the game can end.
GameSettings.endStates = {
TimerUp = "TimerUp",
FoundWinner = "FoundWinner"
}
return GameSettings
```
### GameManager script
```lua
-- Services
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local matchManager = require(moduleScripts:WaitForChild("MatchManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
local displayManager = require(moduleScripts:WaitForChild("DisplayManager"))
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchEnd = events:WaitForChild("MatchEnd")
while true do
displayManager.updateStatus("Waiting for Players")
repeat
task.wait(gameSettings.intermissionDuration)
until #Players:GetPlayers() >= gameSettings.minimumPlayers
displayManager.updateStatus("Get ready!")
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
local endState = matchEnd.Event:Wait()
print("Game ended with: " .. endState)
end
```
### MatchManager script
```lua
local MatchManager = {}
-- Services
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local playerManager = require(moduleScripts:WaitForChild("PlayerManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
local timer = require(moduleScripts:WaitForChild("Timer"))
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchStart = events:WaitForChild("MatchStart")
local matchEnd = events:WaitForChild("MatchEnd")
-- Values
local displayValues = ReplicatedStorage:WaitForChild("DisplayValues")
local timeLeft = displayValues:WaitForChild("TimeLeft")
-- Creates a new timer object to be used to keep track of match time.
local myTimer = timer.new()
-- Local Functions
local function stopTimer()
myTimer:stop()
end
local function timeUp()
matchEnd:Fire(gameSettings.endStates.TimerUp)
end
local function startTimer()
print("Timer started")
myTimer:start(gameSettings.matchDuration)
myTimer.finished:Connect(timeUp)
while myTimer:isRunning() do
-- Adding +1 makes sure the timer display ends at 1 instead of 0.
timeLeft.Value = (myTimer:getTimeLeft() + 1) // 1
-- By not setting the time for wait, it offers more accurate looping
task.wait()
end
end
-- Module Functions
function MatchManager.prepareGame()
playerManager.sendPlayersToMatch()
matchStart:Fire()
end
matchStart.Event:Connect(startTimer)
matchEnd.Event:Connect(stopTimer)
return MatchManager
```

View file

@ -0,0 +1,125 @@
---
title: Finish the project
description: Create a battle royale experience in Roblox Studio. Wrap up the project with map improvements.
prev: /education/battle-royale-series/ending-matches
---
Congrats! You've just created a multiplayer battle royale! Over this series, you have:
- Created modular scripts that handled different game functions like teleporting players.
- Learned how to code custom events for the start and end of matches
- Used arrays to manage players as they join, win, or leave games.
But, your game is almost ready for others to play. Attract players to your game by making your arena unique and creating an eye-catching thumbnail.
## Optional improvements
Below are a few ways of improving your experience.
<img src="../../assets/education/battle-royale-series/roundBased_hero_lesson8.jpg" />
### Improve map visuals
Having a visually interesting map sets a strong first impression for your game, encouraging people to start playing. Take some time to turn your graybox level into a real map.
Remember writing a description of the setting for your game at the start of this series? As you build, ensure your map has a clear setting. For inspiration, below are some example maps built by Roblox developers.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/education/battle-royale-series/playerShowcase_luxeyes.jpg" />
<figcaption>Map by Luxeyes</figcaption>
</figure>
<figure>
<img src="../../assets/education/battle-royale-series/playerShowcase_Jandel.jpg" />
<figcaption>Map by Janedel</figcaption>
</figure>
</GridContainer>
You can either build in Studio, or use premade assets. Below are some suggested assets uploaded by Roblox that can be used to build an environment. Each pack includes high quality, fully textured models.
- <a href="https://www.roblox.com/library/6933438443/Synty-Nature-Pack" target="_blank" rel="noopener">Synty Nature Pack</a>
- <a href="https://www.roblox.com/library/6933556508/Synty-City-Pack" target="_blank" rel="noopener">Synty City Pack</a>
- <a href="https://www.roblox.com/library/6934021345/" target="_blank" rel="noopener">Synty Dungeon Pack: Cave & Castle Interiors</a>
### Changing the Forcefield
During a game, you may have noticed a force field when players respawn. Change how long the force field lasts in the SpawnLocation properties.
1. Click on the relevant Spawn Location.
2. In Properties > Forcefield, change the Duration value.
### Playtest and confirm variables
Successful games on Roblox are playtested frequently to ensure the gameplay is fun and fair.
Playtest your game with peers and check for the following:
- Does the duration of a match feel right? Do matches end too quickly without a winning player, or take too long?
- Does the size of the map feel right? Are there any areas that feel too empty? Will it take a long time to run into another player?
Test, evaluate, and modify variables to improve gameplay. Some examples:
- Change `GameSettings.matchDuration` to make matches longer in a larger map.
- Make the intermission duration longer if players feel it's too sudden.
### Make the lobby social
Popular games in Roblox often add mini-games to encourage players to have fun and be social while they wait for an intermission. This can include putting in sphere parts with physics so players can play, or even mini-obstacle courses.
## Optional challenges
Many Roblox experiences continue to get updates even after they release. Below are some optional challenges that can add new features for your project.
### Traps
Make maps more challenging by adding traps or obstacles that damage players. Learn more in [Creating Traps](../../tutorials/fundamentals/coding-3/traps-with-if-statements.md).
### Tracking score
Create a leaderboard that tracks how many times someone wins a round. Code one using this article on <a href="https://developer.roblox.com/articles/Leaderboards" target="_blank" rel="noopener">Leaderboards</a>.
### Powerups
Create scripted parts that make changes like modify a player's speed or tool's attack power. Remember, after the end of a match, use the `resetMatch()` to recreate the set of powerups. For reference, learn more in the [Powerups](../../tutorials/fundamentals/coding-3/powerups-with-if-statements.md) tutorial.
### Add more arenas
Build out more arenas with different settings and code a randomized map selection. Whenever players start a match, a module script named MapManager will pick a random map, and then assign players to those spawn locations as needed. Check the code box below if you need hints or to see one implementation.
```lua
--[[
Setup Notes:
1. In Workspace, create a folder named Maps. Store all parts of a map in individual folders.
2. For each individual map, include a folder named SpawnLocations
3. When starting a match, use pickNewMap() to get a random map. When assigning player spawn points, use
GetSpawnLocations() to get a table with all locations.
]]
local MapManager = {}
local mapsFolder = workspace.Maps
-- Stores all maps that can be rotated between
local availableMaps = mapsFolder:GetChildren()
-- Stores the current map in play
local activeMap
-- Used to get random maps.
local randomGenerator = Random.new()
-- Gets a random map from the available maps table
function MapManager.pickNewMap()
local whichMapKey = randomGenerator:NextInteger(1, #availableMaps)
activeMap = availableMaps[whichMapKey]
print("New map: " .. activeMap.Name)
end
-- returns a table with the maps current spawn points
function MapManager.getSpawnLocations()
local spawnPoints = activeMap:FindFirstChild("SpawnLocations")
local availableSpawnPoints = spawnPoints:GetChildren()
return availableSpawnPoints
end
return MapManager
```

View file

@ -0,0 +1,102 @@
---
title: Create a Battle Royale
description: How to make a game in Roblox Studio. Learn to design and code a battle royale experience for Roblox as a beginner.
next: /education/battle-royale-series/project-setup
---
### Series description
Create a round-based multiplayer experience where users compete to be the last player left! A great next lesson after the adventure game, this series expands on core concepts in game design and computer science. Once finished, the experience can be easily customized with unique gameplay elements and monetized.
<video controls src="../../assets/education/battle-royale-series/battleRoyal_webFinal.mp4" width="100%"></video>
### Objectives and prerequisites
<table>
<tbody>
<tr>
<td width="20%"><b>Learning Objectives</b></td>
<td>
Practice <b>modular programming</b> by creating scripts that separately handle game functions, like teleporting players or starting a match timer.
Implement <b>events</b> whenever game matches start and end to create cause and effect relationships between separate scripts.
Implement <b>arrays</b> to manage players and manipulate them as needed as players start, win, or leave games.
Understand the <b>coding architecture</b> of round based games, including how to cleanup and reset to making looping gameplay.
</td>
</tr>
<tr>
<td><b>Prerequisites</b></td>
<td>
Understand how to use if statements, arrays and for loops.
Have a general understanding of module scripts.
</td>
</tr>
</tbody>
</table>
### Series contents
<table>
<thead>
<tr>
<th>Article</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="../../education/battle-royale-series/project-setup.md">Project Setup</a></td>
<td>Plan out a vision for the experience and build out a map to test gameplay and movement with.</td>
</tr>
<tr>
<td><a href="../../education/battle-royale-series/coding-the-game-loop.md">Coding the Game Loop</a></td>
<td>Use module scripts to code a game loop that will run in the background of the experience.</td>
</tr>
<tr>
<td><a href="../../education/battle-royale-series/managing-players.md">Managing Players</a></td>
<td>Continue to use module scripts to manage an array of players and perform functions such as teleporting them to a match.</td>
</tr>
<tr>
<td><a href="../../education/battle-royale-series/timers-and-events.md">Timers and Events</a></td>
<td>Use events to track different states of the game and signal whenever a state changes, such as the end of a timer.</td>
</tr>
<tr>
<td><a href="../../education/battle-royale-series/creating-a-gui.md">Creating a GUI</a></td>
<td>Display the current game state and other information to players using a graphical user interface.</td>
</tr>
<tr>
<td><a href="../../education/battle-royale-series/ending-matches.md">Ending Matches</a></td>
<td>Track the current player count in matches and use that information to send events that trigger the game's end.</td>
</tr>
<tr>
<td><a href="../../education/battle-royale-series/cleanup-and-reset.md">Clean Up and Reset</a></td>
<td>Learn how code is cleaned up to ensure that each player experiences a continuous gameplay loop after a match.</td>
</tr>
<tr>
<td><a href="../../education/battle-royale-series/finishing-the-project.md">Finishing the Project</a></td>
<td>Find assets to decorate the map and see optional challenges to take the experience further.</td>
</tr>
</tbody>
</table>
### Example project
<table>
<tbody>
<tr>
<td><img src="../../assets/education/battle-royale-series/roundBased_thumbnail_alt.jpg" /></td>
<td>
<a href="https://www.roblox.com/games/3623999509/Battle-Royale-Template" target="_blank" rel="noopener">Battle Royale Example</a>
Play a version of the final project to see what you can develop with this series.
</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,422 @@
---
title: Manage players
description: Create a battle royale experience in Roblox Studio. Write scripts that handle player spawning and equipment.
next: /education/battle-royale-series/timers-and-events
prev: /education/battle-royale-series/coding-the-game-loop
---
With the game loop coded, it's time to start adding features into it. During a match, players can come and go. Because of this, code is needed for tasks like sending players into a match and keeping track of active players. To manage these tasks, create a module script named **PlayerManager**.
This script will start a function to send players into the arena with a weapon and be expanded later in the series.
<video controls src="../../assets/education/battle-royale-series/arena_3_showFinalResult.mp4" width="100%"></video>
### Set up the script
Because the player manager includes functions used by other scripts, it'll be a module script.
1. In ServerStorage > ModuleScripts, add a new module script named PlayerManager. Then, rename the module table to match the script name and add comments for local and module functions.
<img src="../../assets/education/battle-royale-series/arena_createPlayerManager.png" />
```lua
local PlayerManager = {}
-- Local Functions
-- Module Functions
return PlayerManager
```
2. Add local variables for the following:
**Services:**
- Players - Know what players have joined or left the game.
- ServerStorage - Storage for player weapons.
**Map and Player Variables:**
- Lobby Spawn, Arena Folder, and Arena Spawn Folder - Used to teleport players to different areas.
- An Array of Active Players - Keeps track of players currently in a game.
```lua
local PlayerManager = {}
-- Services
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
-- Map Variables
local lobbySpawn = workspace.Lobby.StartSpawn
local arenaMap = workspace.Arena
local spawnLocations = arenaMap.SpawnLocations
-- Player Variables
local activePlayers = {}
-- Local Functions
-- Module Functions
return PlayerManager
```
3. Create a module function named `sendPlayersToMatch()` with a test print inside.
```lua
-- Local Functions
-- Module Functions
function PlayerManager.sendPlayersToMatch()
print("Sending players to match")
end
return PlayerManager
```
### Spawn players in the lobby
Right now, there's multiple spawn locations, meaning that players spawn at a random one when joining the game. To ensure players spawn in the lobby, change the player's `RespawnLocation` property.
1. Create a new local function named `onPlayerJoin()` with a parameter of `player`. In that function, set the player's respawn location to the lobby spawn variable made earlier.
```lua
-- Local Functions
local function onPlayerJoin(player)
player.RespawnLocation = lobbySpawn
end
```
2. Add an events section beneath your module function. Then, connect `onPlayerJoin()` to Player Service's `PlayerAdded` event.
```lua
-- Module Functions
function PlayerManager.sendPlayersToMatch()
print("Sending players to match")
end
-- Events
Players.PlayerAdded:Connect(onPlayerJoin)
```
### Connect and test
Now the modules can be connected and tested. With the PlayerManager created, require it so that the code in that module script can then run and send players to the lobby.
1. Go back to **MatchManager** and create variables for the following:
- **ServerStorage** service.
- **ModuleScripts** folder, child of ServerStorage.
- **PlayerManager** module script, child of moduleScripts.
```lua
local MatchManager = {}
-- Services
local ServerStorage = game:GetService("ServerStorage")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local playerManager = require(moduleScripts:WaitForChild("PlayerManager"))
function MatchManager.prepareGame()
playerManager.sendPlayersToMatch()
end
return MatchManager
```
2. Use a **local server** with at least the minimum players to test. Confirm that you can see the following:
- All players spawn in the Lobby.
- The print statement from PlayerManager appears in the Output window.
<img src="../../assets/education/battle-royale-series/arena_3_showSendToMatch.png" />
3. Once finished, click Cleanup to shut down the server.
### Troubleshooting tips
At this point, parts of the script aren't working as intended, try one of the following below.
- Check the name of parts such as the Arena, or the location of Lobby > StartSpawn, especially if you named them differently than instructed in the lesson.
- Make sure that modules are required in each script using the `require()` function and correctly spelled.
## Send players to the arena
Now that players spawn in the lobby, teleport them into a match once the intermission is over. Change the player's `RespawnLocation` to a spawn location in the arena using a function in the Player object called `ReloadCharacter()`.
1. Go to the **PlayerManager** script, below `onPlayerJoin()`, add a new local function named `preparePlayer()`. Include two parameters: `player` and `whichSpawn`, the spawn location to send them to.
```lua
local activePlayers = {}
-- Local Functions
local function onPlayerJoin(player)
player.RespawnLocation = lobbySpawn
end
local function preparePlayer(player, whichSpawn)
end
-- Module Functions
function PlayerManager.sendPlayersToMatch()
print("Sending players to match")
end
```
2. Set the player's respawn location to `whichSpawn`.
```lua
local function preparePlayer(player, whichSpawn)
player.RespawnLocation = whichSpawn
end
```
3. Force the character to reload, using `LoadCharacter()`, and the player will respawn using their newly assigned location.
```lua
local function preparePlayer(player, whichSpawn)
player.RespawnLocation = whichSpawn
player:LoadCharacter()
end
```
<Alert severity="info">
Reloading a character ensures that players only start with tools provided at the start of a round. For instance, if you further developed this game and added tools with ammo and pickup weapons, it may be possible for a character to bring those tools in, giving them an unfair advantage.
</Alert>
### Send players to spawn
Make sure each player gets teleported to a different spawn location in the arena by using a `for` loop to iterate through the active players array. Using a `for` loop allows you to go through every value in the players array, allowing the script to adapt to a variety of player numbers.
1. In the `sendPlayersToMatch()` function, use a variable to create an array of all the arena spawn locations by getting the children of the Arena > SpawnLocations folder.
```lua
--Module Functions
function PlayerManager.sendPlayersToMatch()
local arenaSpawns = spawnLocations:GetChildren()
end
```
2. Add the `for` loop below to get an array of all players and then iterate through each of them. To get players, type: `Players:GetPlayers()`.
```lua
function PlayerManager.sendPlayersToMatch()
local arenaSpawns = spawnLocations:GetChildren()
for playerKey, whichPlayer in Players:GetPlayers() do
end
end
```
### Track and spawn
When the game runs, it needs to identify which users are playing so they can be spawned in the arena. At the start of a round, every player will be tracked in an array of active players. That array will be used for different functions, such as teleporting or assigning weapons, ensuring that players still in the lobby during a round aren't affected.
1. In the for loop, use `Library.table.insert()`, using the two parameters for the `activePlayers` array and the player to add.
```lua
function PlayerManager.sendPlayersToMatch()
local arenaSpawns = spawnLocations:GetChildren()
for playerKey, whichPlayer in Players:GetPlayers() do
table.insert(activePlayers, whichPlayer)
end
end
```
2. To get a spawn location from the arena, create a variable named `spawnLocation` and set it to the **first** index in the `arenaSpawns` table.
```lua
for playerKey, whichPlayer in Players:GetPlayers() do
table.insert(activePlayers, whichPlayer)
local spawnLocation = arenaSpawns[1]
end
```
3. Call `preparePlayer()` and pass in `whichPlayer` and `spawnLocation`. Then, since that spawn location was used, **remove** it from the table so the next player will get a different spawn.
```lua
for playerKey, whichPlayer in Players:GetPlayers() do
table.insert(activePlayers, whichPlayer)
local spawnLocation = table.remove(arenaSpawns, 1)
preparePlayer(whichPlayer, spawnLocation)
end
```
4. Test on a **local** server that players are sent to the arena. The players will continue to respawn at the same location because the code to send them back to the lobby isn't yet in place.
<video controls src="../../assets/education/battle-royale-series/arena_3_showRepeatArena.mp4" width="100%"></video>
### Troubleshooting tips
At this point, you didn't see the intended results, try one of the following below.
- In `GetPlayers()`, make sure there are **two** closing parentheses, such as `Class.Players.GetPlayers(|Players:GetPlayers())` in the statement.
- Check the series of function calls in the module scripts. For example, `matchManager.prepareGame()` should call `playerManager.sendPlayersToMatch()`.
## Giving Players Weapons
When a round starts, each player in the arena will be provided a weapon to use.
### Add a tool
Player weapons will be a tool. While any tool in Roblox can be used, we've provided a sample sword to start.
1. Import the weapon from the Toolbox, or create your own (see `Class.Tool|Tools`).
<a href="https://www.roblox.com/library/10202913115/Battle-Royale-Weapon" target="_blank" rel="noopener">
<Button variant="contained">Get Tool</Button>
</a>
2. Place the weapon into ServerStorage. If you're creating your own tool, make sure the tool is named Weapon, since that'll be used in later scripts.
<img src="../../assets/education/battle-royale-series/arena_showWeapon.png" />
### Give tools to players
Now that the tool is in storage, work on a script to go through the active player array and provide each user that tool.
1. In PlayerManager, add a variable named `playerWeapon` for the Weapon in ServerStorage.
```lua
-- Map Variables
local lobbySpawn = workspace.Lobby.StartSpawn
local arenaMap = workspace.Arena
local spawnLocations = arenaMap.SpawnLocations
-- Player Variables
local activePlayers = {}
local playerWeapon = ServerStorage.Weapon
```
2. In `preparePlayer()`, paste the following code to get the player's character.
```lua
local function preparePlayer(player, whichSpawn)
player.RespawnLocation = whichSpawn
player:LoadCharacter()
local character = player.Character or player.CharacterAdded:Wait()
end
```
<Alert severity="info">
It's possible that a player's character isn't loaded when the game starts. Adding the extra wait ensures that if a character isn't immediately available, the script will wait and not cause an error.
</Alert>
3. Create a new variable named sword and use the `Clone()` function to create a copy of the weapon in ServerStorage. Then, parent the sword to the player's character.
```lua
local function preparePlayer(player, whichSpawn)
player.RespawnLocation = whichSpawn
player:LoadCharacter()
local character = player.Character or player.CharacterAdded:Wait()
local sword = playerWeapon:Clone()
sword.Parent = character
end
```
4. Test on a **local server** to confirm that every player gets a tool when sent to the arena. Keep in mind, if you continue testing, the intermission will keep restarting and so players will respawn every few seconds. This will be resolved in the next lesson.
<video controls src="../../assets/education/battle-royale-series/show-weapon-spawned.mp4" width="100%"></video>
## Completed scripts
Below are completed scripts to double check your work.
### GameManager script
```lua
-- Services
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local matchManager = require(moduleScripts:WaitForChild("MatchManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
while true do
repeat
task.wait(gameSettings.intermissionDuration)
print("Restarting intermission")
until #Players:GetPlayers() >= gameSettings.minimumPlayers
print("Intermission over")
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
end
```
### MatchManager script
```lua
local MatchManager = {}
-- Services
local ServerStorage = game:GetService("ServerStorage")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local playerManager = require(moduleScripts:WaitForChild("PlayerManager"))
function MatchManager.prepareGame()
playerManager.sendPlayersToMatch()
end
return MatchManager
```
### PlayerManager module script
```lua
local PlayerManager = {}
-- Services
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
-- Map Variables
local lobbySpawn = workspace.Lobby.StartSpawn
local arenaMap = workspace.Arena
local spawnLocations = arenaMap.SpawnLocations
-- Player Variables
local activePlayers = {}
local playerWeapon = ServerStorage.Weapon
-- Local Functions
local function onPlayerJoin(player)
player.RespawnLocation = lobbySpawn
end
local function preparePlayer(player, whichSpawn)
player.RespawnLocation = whichSpawn
player:LoadCharacter()
local character = player.Character or player.CharacterAdded:Wait()
local sword = playerWeapon:Clone()
sword.Parent = character
end
-- Module Functions
function PlayerManager.sendPlayersToMatch()
print("Sending players to match")
local arenaSpawns = spawnLocations:GetChildren()
for playerKey, whichPlayer in Players:GetPlayers() do
table.insert(activePlayers, whichPlayer)
local spawnLocation = table.remove(arenaSpawns, 1)
preparePlayer(whichPlayer, spawnLocation)
end
end
--events
Players.PlayerAdded:Connect(onPlayerJoin)
return PlayerManager
```

View file

@ -0,0 +1,199 @@
---
title: Battle Royale
description: Create a Battle Royale experience in Roblox Studio. Learn coding in Roblox and how to make a game.
next: /education/battle-royale-series/coding-the-game-loop
prev: /education/battle-royale-series/landing
---
**Battle Royales** are a multiplayer game genre where opponents compete until only one player is left. While every battle royale is different, they all include a way to eliminate players such as freezing someone or knocking them off the map. When one player survives, or a timer ends, the match is over and a new round starts.
The genre is popular because rounds are quick, easy to pick-up, and challenging to master. Battle royales can be customized with different game mechanics to appeal to a wide audience, like unique weapons, platforming obstacles, or visual themes.
Some popular battle royale experiences in Roblox include Island Royale and Strucid.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/education/battle-royale-series/islandRoyale_example_2.jpg" />
<figcaption>Island Royale by LordJurrd</figcaption>
</figure>
<figure>
<img src="../../assets/education/battle-royale-series/Strucid_thumbnail.jpg" />
<figcaption>Strucid by Frosted Studios</figcaption>
</figure>
</GridContainer>
Battle royales generally follow a round-based **game loop**, or series of phases. In the project you'll make, players go through the game loop below:
<GridContainer numColumns="3">
<figure>
<img src="../../assets/education/battle-royale-series/arena_gamePhase_intermission.jpg" />
<figcaption>Intermission</figcaption>
</figure>
<figure>
<img src="../../assets/education/battle-royale-series/arena_gamePhase_compete.jpg" />
<figcaption>Competition</figcaption>
</figure>
<figure>
<img src="../../assets/education/battle-royale-series/arena_gamePhase_cleanup.jpg" />
<figcaption>Cleanup and Reset</figcaption>
</figure>
</GridContainer>
During each phase, a different set of tasks happened that you'll code during this series.
- **Intermission** - Players socialize or watch games in the lobby until a new round starts.
- **Match** - Timer starts and players are teleported to an arena where they compete. If a player loses, they're teleported back to the lobby.
- **Cleanup and Reset** - Happens when one player is left or the timer finishes. Players are then teleported back to the lobby where the loop restarts.
## Develop the experience
Battle royales are made of many elements, like code and art assets. To manage larger projects, developers plan a **workflow**, or a series of steps, to get to completion.
During this series, you'll go through the following workflow:
- **Preproduction** - Create a sketch of the game map.
- **Design a Test Map** - Develop a map using placeholder assets to test out the design, without worrying about visual look and feel.
- **Code and Test** - Begin the process of coding the game loop.
- **Polish and Improve** - Replace placeholder assets with finalized models, and improve code and design through frequent playtesting.
Instead of working on different parts of a project simultaneously, developers break large projects into manageable chunks. Each phase should have its own specific goal before going to the next phase. This makes it easier to catch potential errors and save time down the road. For instance, designing art to polish a map that hasn't been tested may result in wasted time if the map needs to be redesigned to be fun.
### Plan the project
The first phase is to plan your vision in a process called **preproduction**. Taking the time to make a plan helps you focus on important design choices, such as where to place obstacles and player spawns.
To plan, you'll create a layout map with paper or a drawing software. A layout map is the floor plan for the arena drawn in basic shapes, focusing on how players move through the world rather than visual details. Once the layout map is finished, you'll recreate it in Studio.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/education/battle-royale-series/vectorMap_arenaSteps_finalizedLayoutMap.png" />
<figcaption>Drawn Layout</figcaption>
</figure>
<figure>
<img src="../../assets/education/battle-royale-series/arenaObstacles_advanced_topDown.jpg" />
<figcaption>Layout in Studio</figcaption>
</figure>
</GridContainer>
### Design a map layout
For the sketch, the objective here is to create a design you can then replicate in Studio. The map arena should have enough variety to be fun, but also include balance as to not give users an unfair advantage.
1. Write a brief description of the **setting**, such as a jungle, an abandoned moon base, or a medieval castle. During the polishing phase, you'll add map details using this setting.
2. Identify the shape of the map and then draw it out using a combination of 1-3 **basic shapes** (square, rectangle, octagon). Even if you envision a more complex map, such as an island, try and break it down into basic shapes.
<Alert severity="info">
We recommend making a symmetrical map to easily balance the experience. As you gain more experience, you can start working with non-symmetrical map shapes, which can be more complicated to balance fairly for everyone.
</Alert>
3. Add player spawns. For now, use eight spawns, but you can always add more later. The example here uses a square for the map shape.
<img src="../../assets/education/battle-royale-series/vectorMap_playerSpawns.png" />
4. To make players less predictable and add interest, place obstacles that force them choose different directions. Draw 2-4 shapes (orange) that create choices in moving around the arena. Add secondary obstacles (yellow) that prevent players from fighting right at the start.
<img src="../../assets/education/battle-royale-series/vectorMap_example_obstacle.png" />
### Tips for layouts
Keep designs simple, but engaging. Since players make split second decisions in moving through a level, give them enough choices to make a level feel different each time, but not too much that they can't remember how to move through the map or feel overwhelmed.
<GridContainer numColumns="3">
<figure>
<img src="../../assets/education/battle-royale-series/roundBased_obstacleExampleComplexity_simple.png" />
<figcaption>Too simple</figcaption>
</figure>
<figure>
<img src="../../assets/education/battle-royale-series/roundBased_obstacleExampleComplexity_balanced.png" />
<figcaption>Balanced</figcaption>
</figure>
<figure>
<img src="../../assets/education/battle-royale-series/roundBased_obstacleExampleComplexity_tooComplex.png" />
<figcaption>Potentially complex</figcaption>
</figure>
</GridContainer>
## Create the map
Recreate the layout map quickly in Roblox Studio using basic parts in a process called grayboxing. Stay focused on designing a fun, playable map. Don't spend time adding textures or small details, like decorative props. After you've created a working map with code, then spend time designing art to fit the map's setting.
<img src="../../assets/education/battle-royale-series/arena_finishedProject_arenaHero.jpg" />
### Build a lobby
Before creating the arena, build the lobby, where players enter the experience and socialize between matches.
1. Create a new Baseplate project and delete the baseplate.
2. Construct a walled room with a spawn location.
<img src="../../assets/education/battle-royale-series/arenaIntro_lobby.jpg" />
<Alert severity="info">
Enable Snap to Grid to make moving and lining up objects easier. Rotating parts at 90 or 45 degree angles will also help result in cleaner designs.
</Alert>
3. Place all lobby parts into a folder named Lobby.
### Arena and spawns
The arena is where players will compete. When building the arena, you'll **graybox** the environment with simple parts and colors. A graybox environment is an approximation of the final design, so a large cylinder may be an equally sized tree in the final version.
This process, common in level design, gives designers a working prototype to test and iterate with. Once the map design feels good in playtesting, graybox assets are replaced with 3D assets and terrain.
1. Create a folder named Arena. Inside, add the floor for the arena. If you're using terrain, leave the folder empty for now.
Below are sample arenas.
<GridContainer numColumns="3">
<img src="../../assets/education/battle-royale-series/arenaExample_square.jpg" />
<img src="../../assets/education/battle-royale-series/arenaExample_circle.jpg" />
<img src="../../assets/education/battle-royale-series/arenaExample_terrain.jpg" />
</GridContainer>
2. Create eight spawn locations on the map. Create a new folder in Arena named SpawnLocations and move the eight spawns there.
<img src="../../assets/education/battle-royale-series/arena_addedSpawns.jpg" />
<Alert severity="info">
As you place spawns, keep in mind that all players will randomly spawn when joining.
</Alert>
### Graybox the arena
Remember that grayboxing is an approximation of the final design using simple parts. A grayboxed level should give designers an understanding of how players move through the arena. To create the arena, use an optional building kit or basic parts.
1. Import the Grayboxing Kit from the Toolbox. This includes a variety of common building features such as stairs, ramps, and walls.
<a href="https://www.roblox.com/library/10202876758/Graybox-Assets" target="_blank" rel="noopener">
<Button variant="contained">Get Kit</Button>
</a>
<img src="../../assets/education/battle-royale-series/roundBased_grayboxExample.jpg" />
<Alert severity="info">
Note that Roblox_Educators is the official, trusted account for assets from Roblox Education.
</Alert>
2. Using a combination of parts and assets from the grayboxing kit, create obstacles and barriers.
<img src="../../assets/education/battle-royale-series/arenaObstacles_sideBySide.png" />
As you build, some tips for map design are below.
- **Vary Height** - Flat maps can get repetitive for players. Use hills, stairs, and ramps of different heights to add variety to the map.
- **Build Half of the Map and then Duplicate** - This technique allows you to quickly build a symmetrical map. -**Test and Check for Scale** - While building, think about the map in relation to a player. For example, how spacious does an area feel or can a player fit easily through a door. Keep in mind the average avatar is 6.5 studs tall.
### Playtest the graybox
With the arena completed, it's important to see if it's fun and interesting to move around.
1. Click **Play Here** in the arena and test the map.
<img src="../../assets/education/battle-royale-series/arena_playtestGraybox_cropped.jpg" />
While testing, self-evaluate your work with the prompts below and make changes as needed to improve.
- Can players move around without getting confused or stuck?
- Does the size of the map feel right? Are there any areas that feel too empty? Will it take a long time to run into another player?
- Does anything look strange, such as floating or misaligned parts?

View file

@ -0,0 +1,354 @@
---
title: Timers and events
description: Create a battle royale experience in Roblox Studio. Code a timer and use bindable events for a match based game.
next: /education/battle-royale-series/creating-a-gui
prev: /education/battle-royale-series/managing-players
---
During the course of a round, the scripts will need to track time and send signals between different scripts. Time will be managed using a time script, while events, a concept in Roblox coding, will signal changes such as the end of a match.
## Send signals with events
With players now in the arena, events can be used to signal the start of the match and the code for the timer can begin. Later, an event can also be used to signal the end of the match, and that it's time to transition players back to the lobby.
These events aren't prebuilt, so custom event objects called **bindable events** will need to be created. Bindable events are often used for player-fired actions and are similar to events like `Touched` or `Changed`.
Multiple scripts can listen for the same bindable events. This keeps your code organized and makes it easier to add additional code for the start or end of the match later if needed.
### Create bindable events
Start by creating bindable event objects for the start and end of the match. Since bindable events don't interact with the client, they can be stored in Server Storage.
1. In ServerStorage, create a new folder named Events. In that folder, create two **BindableEvents** named MatchStart and MatchEnd.
<img src="../../assets/education/battle-royale-series/arena_4_showBindableEvents.png" />
### Use events
Right now, when players enter the arena, the intermission keeps restarting rather than beginning the timer. The main game loop needs to be told to stop and wait until the MatchEnd event fires before moving on to the next part of the code.
Events have two built-in functions: `Connect()` and `Wait()`. Instead of using `Connect()` like previously, call `Wait()` on MatchEnd to pause the game manager script until MatchEnd is fired. In this case, the wait function pauses the code until the game manager receives a signal that the match ended.
1. In **GameManager**, create variables for the `Events` folder and `MatchEnd` event.
```lua
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local matchManager = require(moduleScripts:WaitForChild("MatchManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchEnd = events:WaitForChild("MatchEnd")
```
2. Have the script wait for the match end event to fire before moving on. In the **loop**, at the **end**, type: `matchEnd.Event:Wait()`
```lua
while true do
repeat
task.wait(gameSettings.intermissionDuration)
print("Restarting intermission")
until #Players:GetPlayers() >= gameSettings.minimumPlayers
print("Intermission over")
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
-- Placeholder wait for the length of the game.
matchEnd.Event:Wait()
end
```
3. **Test** the game. Confirm that once players are in the arena, the intermission loop **does not** continue. The script is now waiting for the `matchEnd` signal to fire.
### Troubleshooting tips
At this point, the code doesn't work as expected, try one of the following below.
- Double check the usage of the dot or colon operators in `matchEnd.Event:Wait()`.
- Make sure that MatchEnd is a BindableEvent, and not another type, such as a RemoteEvent.
## Use a timer
One of the conditions that will cause the end of the match is a timer running out, which will be handled through the script.
### Set up the timer
To add a timer into the game, use the premade module script in the steps below. It includes functions to start and end a timer, as well as return the amount of time left.
1. In ServerStorage > ModuleScripts, create a new module script named Timer.
<img src="../../assets/education/battle-royale-series/arena_createTimer.png" />
Replace the code with the code below.
```lua
local Timer = {}
Timer.__index = Timer
function Timer.new()
local self = setmetatable({}, Timer)
self._finishedEvent = Instance.new("BindableEvent")
self.finished = self._finishedEvent.Event
self._running = false
self._startTime = nil
self._duration = nil
return self
end
function Timer:start(duration)
if not self._running then
task.spawn(function()
self._running = true
self._duration = duration
self._startTime = tick()
while self._running and tick() - self._startTime < duration do
task.wait()
end
local completed = self._running
self._running = false
self._startTime = nil
self._duration = nil
self._finishedEvent:Fire(completed)
end)
else
warn("Warning: timer could not start again as it is already running.")
end
end
function Timer:getTimeLeft()
if self._running then
local now = tick()
local timeLeft = self._startTime + self._duration - now
if timeLeft < 0 then
timeLeft = 0
end
return timeLeft
else
warn("Warning: could not get remaining time, timer is not running.")
end
end
function Timer:isRunning()
return self._running
end
function Timer:stop()
self._running = false
end
return Timer
```
2. In MatchManager, require the GameSettings and Timer modules.
```lua
local MatchManager = {}
-- Services
local ServerStorage = game:GetService("ServerStorage")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local playerManager = require(moduleScripts:WaitForChild("PlayerManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
local timer = require(moduleScripts:WaitForChild("Timer"))
```
3. Below the variables, create a new timer object by setting a variable named `myTimer` equal to `timer.new()`. This object will be used to call functions that start and stop the timer.
```lua
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
local timer = require(moduleScripts:WaitForChild("Timer"))
-- Creates a new timer object to be used to keep track of match time.
local myTimer = timer.new()
```
<Alert severity="info">
The timer module script can be called by other scripts to create more timers as needed, like if you wanted to have traps appear after a specific time. To use the timer in more than one situation, create a new Timer object, like `myTrapTimer = timer.new()`. Timer objects should only be used for a single purpose, rather than reused.
</Alert>
### Start and stop
Now that a timer is created, use the included functions `start()` and `stop()` during a match. Below is a description of each function and the parameter it accepts.
- `start(time)` - Starts the timer, with time in seconds as the parameter.
- `finished:Connect(functionName)` - When the timer finishes, runs the function passed as a parameter.
1. In **MatchManager**, create a new function named `timeUp()` to run whenever the timer has finished. Include a test print statement.
```lua
local myTimer = timer.new()
-- Local Functions
local function timeUp()
print("Time is up!")
end
-- Module Functions
function MatchManager.prepareGame()
playerManager.sendPlayersToMatch()
end
return MatchManager
```
2. Below `timeUp()`, add a function named `startTimer()` with a print statement. You'll display the timer in-game later.
```lua
-- Local Functions
local function timeUp()
print("Time is up!")
end
local function startTimer()
print("Timer started")
end
```
3. To start and stop the timer, in `startTimer()`:
- Call `myTimer.start()`. Pass in `gameSettings.matchDuration`.
- Call `myTimer.finished:Connect()`. Pass in `timeUp()`.
```lua
-- Local Functions
local function startTimer()
print("Timer started")
myTimer:start(gameSettings.matchDuration)
myTimer.finished:Connect(timeUp)
end
```
### Start the timer
The timer can be triggered at the start of a match using the Match Start event.
1. In MatchManager, under the module variables, create variables to store the Events folder, MatchStart, and MatchEnd (which is used in a future lesson).
```lua
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local playerManager = require(moduleScripts:WaitForChild("PlayerManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
local timer = require(moduleScripts:WaitForChild("Timer"))
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchStart = events:WaitForChild("MatchStart")
local matchEnd = events:WaitForChild("MatchEnd")
--Creates timer
local myTimer = timer.new()
```
2. Above `return MatchManager`, connect the match start event to `startTimer()`.
```lua
-- Module Functions
function MatchManager.prepareGame()
playerManager.sendPlayersToMatch()
end
matchStart.Event:Connect(startTimer)
return MatchManager
```
3. To fire the match start event, in `prepareGame()`, type `matchStart:Fire()`.
```lua
-- Module Functions
function MatchManager.prepareGame()
playerManager.sendPlayersToMatch()
matchStart:Fire()
end
```
4. Test the game. In the Output window, confirm that you can see the print statements for the timer's start and stop functions.
<img src="../../assets/education/battle-royale-series/arena_4_outputFinished.png" />
## Completed scripts
Below are completed scripts to double check your work.
### MatchManager script
```lua
local MatchManager = {}
-- Services
local ServerStorage = game:GetService("ServerStorage")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local playerManager = require(moduleScripts:WaitForChild("PlayerManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
local timer = require(moduleScripts:WaitForChild("Timer"))
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchStart = events:WaitForChild("MatchStart")
local matchEnd = events:WaitForChild("MatchEnd")
-- Creates a new timer object to be used to keep track of match time.
local myTimer = timer.new()
-- Local Functions
local function timeUp()
print("Time is up!")
end
local function startTimer()
print("Timer started")
myTimer:start(gameSettings.matchDuration)
myTimer.finished:Connect(timeUp)
end
-- Module Functions
function MatchManager.prepareGame()
playerManager.sendPlayersToMatch()
matchStart:Fire()
end
matchStart.Event:Connect(startTimer)
return MatchManager
```
### GameManager script
```lua
-- Services
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
-- Module Scripts
local moduleScripts = ServerStorage:WaitForChild("ModuleScripts")
local matchManager = require(moduleScripts:WaitForChild("MatchManager"))
local gameSettings = require(moduleScripts:WaitForChild("GameSettings"))
-- Events
local events = ServerStorage:WaitForChild("Events")
local matchEnd = events:WaitForChild("MatchEnd")
while true do
repeat
task.wait(gameSettings.intermissionDuration)
print("Restarting intermission")
until #Players:GetPlayers() >= gameSettings.minimumPlayers
print("Intermission over")
task.wait(gameSettings.transitionTime)
matchManager.prepareGame()
-- Placeholder wait for the length of the game.
matchEnd.Event:Wait()
end
```