mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-25 04:54:07 +00:00
update rules
This commit is contained in:
parent
4c390fdd37
commit
d5522b2fef
8549 changed files with 11783 additions and 1888 deletions
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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()
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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?
|
||||
|
|
@ -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
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue