Hey everyone, in this tutorial I will be teaching you how to make a fully functional and configurable round system that involves object oriented programming.
It’s recommended that you have a basic understanding on:
- Metatables and their use cases
- Creating your own classes in module scripts
- Remote events and how they are useful in server-client communication
- (optional) Guis, such as frames and labels, knowing their properties.
Okay, so before we actually create our scripts, we want to keep our game organised, right? So, I’ll load up a fresh baseplate and create some folders.
Firstly, let’s create two folders in ServerScriptService. We’ll create one called Modules to store our module scripts and one called Scripts to store our server scripts.
Next and lastly, let’s create a folder in ReplicatedStorage called RemoteEvents.
Now, this next part is optional and completely up to you, but it will guide you in creating the Gui for the round system, being the status of the game and the timer. The end product should look something like this:
Firstly, let’s create a ScreenGui in StarterGui and I’ll call it StatusGui, but really you can call it whatever you like as long as it is memorable and distinct to you. Make sure that IgnoreGuiInset is set to true so that your gui stays at the top, or else it will indent to the middle of your screen.
Next, we’ll add a frame in the Gui called MainFrame, which is essentially just a transparent frame which takes up the whole screen, and it’s a personal preference I have so that my guis are more compatible with tweening and enabling/disabling, but it’s up to you.
After that, let’s go ahead and add a frame called Container, which will actually contain the textLabels for both our status and our timer.
The container should be located around here, (this is near the top of the screen)
Once we’ve done that, lets add in two textLabels called Status and Timer
They should look like this, but it does not need to be exact, whatever style you’re going for works fine:
Oh yeah, you can also make the container fully transparent so you can see the text better. I just had it translucent so I could position the text easily.
And there you go! Everything is ready and we can now start with the main focus of this tutorial, scripting the round system. Let’s go ahead and create a ModuleScript in our Modules folder in ServerScriptService. Let’s call this script Round, as it is essentially a class that we can use in our server script that we will create later on.
Now, let’s create a simple constructor for our class.
local Players = game:GetService("Players")
local Round = {}
Round.__index = Round
function Round.new()
local self = setmetatable({}, Round)
return self
end
return Round
Let’s go ahead and define some properties for our Round class.
local Players = game:GetService("Players")
local Round = {}
Round.__index = Round
function Round.new()
local self = setmetatable({}, Round)
self.IntermissionTime = 15
self.GraceTime = 10 -- this is the period after intermission but before round
self.RoundTime = 60
self.IsRound = false -- this is quite useful for certain functions
return self
end
return Round
If you want your properties to be easily configurable, you can use parameters instead, but for my example I won’t use them.
local Players = game:GetService("Players")
local Round = {}
Round.__index = Round
function Round.new(intermissionTime, graceTime, roundTime, isRound)
local self = setmetatable({}, Round)
self.IntermissionTime = intermissionTime
self.GraceTime = graceTime
self.RoundTime = roundTime
self.IsRound = isRound
return self
end
return Round
Okay, let’s create a new function called Round.EnoughPlayers(), and the goal of this function is to return whether #Player:GetPlayers() is >= (number). This is so that you don’t start the round with only 1 player, for example.
function Round.EnoughPlayers()
return #Players:GetPlayers() >= 2 -- minimum of 2 players to start a round
end
return Round
The next step is to create a function that counts down from the intermissionTime of the Round class to 0. we can viszualise this with a for loop and print statements.
function Round:Intermission()
if not self.EnoughPlayers() then return end
for index = self.IntermissionTime, 0, -1 do
print(index)
task.wait(1)
end
end
return Round
Okay, we’ve gotten to the point where we might want to start testing out our class, so let’s insert a script into our Scripts folder in ServerScriptService called RoundServer.
Now, we want to require the Round module we have in our Modules folder, so let’s do that quickly.
local ServerScriptService = game:GetService("ServerScriptService")
local Round = require(ServerScriptService.Modules.Round)
Let’s also create a new Round object and lets run the Round:Intermission() function.
local ServerScriptService = game:GetService("ServerScriptService")
local Round = require(ServerScriptService.Modules.Round)
local newRound = Round.new()
newRound:Intermission()
Why is it not working? Have I done something wrong?
There’s nothing in the output, but remember how the Round.EnoughPlayers() was implemented?, Well, I set it to return whether #Players:GetPlayers() was greater than or equal to 2, but we ran the script in a server with only 1 player. Now, you may be thinking "oh that’s a pretty easy fix I’ll just set the number to 1. Well technically this does work but, it still doesn’t work!? Now, the second problem is we haven’t waited for the player object to actually load in. To fix this we can run the code within a Player.PlayerAdded event and now it should work.
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local Round = require(ServerScriptService.Modules.Round)
local newRound = Round.new()
Players.PlayerAdded:Connect(function()
newRound:Intermission()
end)
And alas, it works! But, we haven’t done much, so this is where the tutorial really starts to get fun as we make more progress through it.
There’s a term in programming known as Abstraction, where we want the surface level of our code to be as simple as just running a function. To implement this, we can create another function called Round:Initiate(), which will sort of daisy chain all of our other methods/functions together. In the server script, we look to just call this function once and have the entirety of our round system up and running seamlessly. So, let’s go ahead and make it.
function Round:Initiate()
if not self.EnoughPlayers() then return end -- we run this here so we can remove it from the Round:Intermission() function so it applies to the whole thread
if self.IsRound then return end -- to prevent Players.PlayerAdded() from creating a new round system, we will set this to true when the round starts.
self:Intermission() -- this is important, make sure it is self:Intermission() and not Round:Intermission() or else this will error; this applies for other functions that we will call
end
Our server script should now look like this:
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local Round = require(ServerScriptService.Modules.Round)
local newRound = Round.new()
Players.PlayerAdded:Connect(function()
newRound:Initiate()
end)
There we go! We have successfully abstracted our code! This abstraction will be very useful later on.
Now, let’s create a new function that teleports/brings the players to a random position. In my example, I will be using the baseplate as a map but you can make it whatever you like.
function Round:BringPlayers()
self.IsRound = true -- as mentioned earlier, prevents new players that join from creating a new round as we exit the Round:Initiate() function if this is set to true
for index, player in Players:GetPlayers() do
local character = player.Character
local randomNumberX = math.random(-100, 100)
local randomNumberZ = math.random(-100, 100)
character:SetPrimaryPartCFrame(CFrame.new(randomNumberX, 0, randomNumberZ)) -- using random number to randomise cframe of character
print("Working!")
end
end
There we go! It prints “Working!” and my character’s position has changed.
Okay, we’re getting progress in steadily, but I’m sorry this tutorial is quite long. Anyways, let’s create a function for the grace period (the time after intermission has ended and before the round has started)
function Round:GracePeriod()
for index = self.GraceTime, 0, -1 do
print(index .. " seconds left till the round starts.") -- i concatenated this so that you don't get confused with the intermission print statement.
task.wait(1)
end
end
Also, have you noticed how we haven’t changed the server script at all? We just add the functions into the Round:Initiate() function so that it chains for us, making our code really neat and organised.
function Round:Initiate()
if not self.EnoughPlayers() then return end
if self.IsRound then return end
self:Intermission()
self:BringPlayers()
self:GracePeriod()
end
return Round
-- module script with chaining functions in Round:Initiate()
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local Round = require(ServerScriptService.Modules.Round)
local newRound = Round.new()
Players.PlayerAdded:Connect(function()
newRound:Initiate()
end)
-- server script is still the same
This is why abstraction is really nice when it comes to programming, and this is also one of the reasons why I use object oriented programming in my projects, as it reduces alot of clutter in my scripts and makes them truly organised.
Let’s create a new function called Round:Start() which will create a for loop similar to the intermission and grace period functions, but, when this timer finishes, self.IsRound will be set to false.
function Round:Start()
for index = self.RoundTime, 0, -1 do
print(index .. " seconds left till the round ends.")
task.wait(1)
end
self.IsRound = false
end
Now, we’ve basically finished the round system, but it doesn’t loop, so to do that we need to run a while true do loop in Round:Initiate()
function Round:Initiate()
while true do
if not self.EnoughPlayers() then return end
if self.IsRound then return end
self:Intermission()
self:BringPlayers()
self:GracePeriod()
self:Start()
end
end
return Round
We’ve completed the main part of our round system! It’s not as complicated as it looks, but we still need to send information about the status of our round to the client (whether or not it is intermission or grace period, etc).
This is what our module script currently looks like:
local Players = game:GetService("Players")
local Round = {}
Round.__index = Round
function Round.new()
local self = setmetatable({}, Round)
self.IntermissionTime = 15
self.GraceTime = 15
self.RoundTime = 60
self.IsRound = false
return self
end
function Round.EnoughPlayers()
return #Players:GetPlayers() >= 1
end
function Round:Intermission()
for index = self.IntermissionTime, 0, -1 do
print(index .. " seconds left till the intermission ends.")
task.wait(1)
end
end
function Round:BringPlayers()
self.IsRound = true
for index, player in Players:GetPlayers() do
local character = player.Character
local randomNumberX = math.random(-100, 100)
local randomNumberZ = math.random(-100, 100)
character:SetPrimaryPartCFrame(CFrame.new(randomNumberX, 0, randomNumberZ))
end
task.wait(1)
end
function Round:GracePeriod()
for index = self.GraceTime, 0, -1 do
print(index .. " seconds left till the round starts.")
task.wait(1)
end
end
function Round:Start()
for index = self.RoundTime, 0, -1 do
print(index .. " seconds left till the round ends.")
task.wait(1)
end
self.IsRound = false
end
function Round:Initiate()
while true do
if not self.EnoughPlayers() then return end
if self.IsRound then return end
self:Intermission()
self:BringPlayers()
self:GracePeriod()
self:Start()
end
end
return Round
And our server script is still the same!
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local Round = require(ServerScriptService.Modules.Round)
local newRound = Round.new()
Players.PlayerAdded:Connect(function()
newRound:Initiate()
end)
Let’s move on to the client-side of our round system. We need to create two remote events in our RemoteEvents folder in ReplicatedStorage. The first will be called TimerRemoteEvent and the second will be called StatusRemoteEvent. These will send information to the client about the timer and status respectively.
Now that we have done that, let’s create a script in StarterPlayerScripts called RoundClient.
And we’ll obviously get the two remote events we’ve just created.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local statusRemoteEvent = ReplicatedStorage.RemoteEvents.StatusRemoteEvent
local timerRemoteEvent = ReplicatedStorage.RemoteEvents.StatusRemoteEvent
Let’s go ahead and create functions for whenever FireAllClients() gets called for both remote events. We’ll also get the statusGui that we created at the start of the tutorial.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local statusRemoteEvent = ReplicatedStorage.RemoteEvents.StatusRemoteEvent
local timerRemoteEvent = ReplicatedStorage.RemoteEvents.TimerRemoteEvent
local player = Players.LocalPlayer
local playerGui = player.PlayerGui
local statusGui = playerGui:WaitForChild("StatusGui")
local function onStatusRemoteEvent(status: string)
-- our code
end
local function onTimerRemoteEvent(timer: number)
-- our code
end
statusRemoteEvent.OnClientEvent:Connect(onStatusRemoteEvent)
onTimerRemoteEvent.OnClientEvent:Connect(onTimerRemoteEvent)
Okay, we’ve missed two crucial steps. The first being adding two new properties to our Round class. Let’s do this quickly and add them; one being called self.Status and the other being called self.Timer.
local Round = {}
Round.__index = Round
function Round.new()
local self = setmetatable({}, Round)
self.IntermissionTime = 2
self.GraceTime = 5
self.RoundTime = 10
self.IsRound = false
self.Status = "" -- our new property
self.Timer = 0 -- our other new property
return self
end
And the second step is to update the status depending on the function and the timer depending on the index of the for loop.
local Players = game:GetService("Players")
local Round = {}
Round.__index = Round
function Round.new()
local self = setmetatable({}, Round)
self.IntermissionTime = 2
self.GraceTime = 5
self.RoundTime = 10
self.IsRound = false
self.Status = ""
self.Timer = 0
return self
end
function Round.EnoughPlayers() -- this is a dot (.) function so we use the metatable instead of self
Round.Status = "Not enough players"
return #Players:GetPlayers() >= 1
end
function Round:Intermission()
self.Status = "Intermission"
for index = self.IntermissionTime, 0, -1 do
self.Timer = index
task.wait(1)
end
end
function Round:BringPlayers()
self.IsRound = true
self.Status = "Bringing players..."
for index, player in Players:GetPlayers() do
local character = player.Character
local randomNumberX = math.random(-100, 100)
local randomNumberZ = math.random(-100, 100)
character:SetPrimaryPartCFrame(CFrame.new(randomNumberX, 0, randomNumberZ))
end
task.wait(1)
end
function Round:GracePeriod()
self.Status = "Grace"
for index = self.GraceTime, 0, -1 do
self.Timer = index
task.wait(1)
end
end
function Round:Start()
self.Status = "Round"
for index = self.RoundTime, 0, -1 do
self.Timer = index
task.wait(1)
end
self.IsRound = false
end
function Round:Initiate()
while true do
if not self.EnoughPlayers() then return end
if self.IsRound then return end
self:Intermission()
self:BringPlayers()
self:GracePeriod()
self:Start()
end
end
return Round
Our module script should now be looking like this with the new additions. But, we need to use the remote events to FireAllClients()
I mean, that’s pretty simple, but let’s actually put it into action. So, we need to get the remote events like we did in the client script. I’m assuming you know how to do that so let’s get right into implementing it into our functions, as this will be called within our methods.
function Round:GracePeriod()
self.Status = "Grace"
statusRemoteEvent:FireAllClients(self.Status) -- right after self.Status is being defined
for index = self.GraceTime, 0, -1 do
self.Timer = index
timerRemoteEvent:FireAllClients(self.Timer) -- right after self.Timer is being defined
task.wait(1)
end
end
Make sure you do this for every function where it’s needed. Don’t worry though. after we finish this tutorial I’ll paste all 3 scripts at the bottom. Okay, let’s move on to editing the statusGui Status and Timer with the information we pass from the module script. To test if it works, you can add a print statement.
ocal function onStatusRemoteEvent(status: string)
print(status)
end
local function onTimerRemoteEvent(timer: number)
print(timer)
end
statusRemoteEvent.OnClientEvent:Connect(onStatusRemoteEvent)
timerRemoteEvent.OnClientEvent:Connect(onTimerRemoteEvent)
There you go! we have the timer and status information on the client! (I changed the intermission time to 5 seconds so I could test faster but don’t worry about that).
Now, we move on to the final step of this tutorial. Updating the statusGui with the information we have now received on the client. We can remove the print statements now.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local statusRemoteEvent = ReplicatedStorage.RemoteEvents.StatusRemoteEvent
local timerRemoteEvent = ReplicatedStorage.RemoteEvents.TimerRemoteEvent
local player = Players.LocalPlayer
local playerGui = player.PlayerGui
local statusGui = playerGui:WaitForChild("StatusGui")
local container = statusGui.MainFrame.Container
local function onStatusRemoteEvent(status: string)
container.Status.Text = status
end
local function onTimerRemoteEvent(timer: number)
container.Timer.Text = timer
end
statusRemoteEvent.OnClientEvent:Connect(onStatusRemoteEvent)
timerRemoteEvent.OnClientEvent:Connect(onTimerRemoteEvent)
There you go. You’ve created your own simple round system with object oriented programming! I did mention I would paste the scripts so here they are.
Round module script
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local statusRemoteEvent = ReplicatedStorage.RemoteEvents.StatusRemoteEvent
local timerRemoteEvent = ReplicatedStorage.RemoteEvents.TimerRemoteEvent
local Round = {}
Round.__index = Round
function Round.new()
local self = setmetatable({}, Round)
self.IntermissionTime = 15
self.GraceTime = 15
self.RoundTime = 60
self.IsRound = false
self.Status = ""
self.Timer = 0
return self
end
function Round.EnoughPlayers()
Round.Status = "Not enough players"
statusRemoteEvent:FireAllClients(Round.Status)
return #Players:GetPlayers() >= 1
end
function Round:Intermission()
self.Status = "Intermission"
statusRemoteEvent:FireAllClients(self.Status)
for index = self.IntermissionTime, 0, -1 do
self.Timer = index
timerRemoteEvent:FireAllClients(self.Timer)
task.wait(1)
end
end
function Round:BringPlayers()
self.IsRound = true
self.Status = "Bringing players..."
statusRemoteEvent:FireAllClients(self.Status)
for index, player in Players:GetPlayers() do
local character = player.Character
local randomNumberX = math.random(-100, 100)
local randomNumberZ = math.random(-100, 100)
character:SetPrimaryPartCFrame(CFrame.new(randomNumberX, 0, randomNumberZ))
end
task.wait(1)
end
function Round:GracePeriod()
self.Status = "Grace"
statusRemoteEvent:FireAllClients(self.Status)
for index = self.GraceTime, 0, -1 do
self.Timer = index
timerRemoteEvent:FireAllClients(self.Timer)
task.wait(1)
end
end
function Round:Start()
self.Status = "Round"
statusRemoteEvent:FireAllClients(self.Status)
for index = self.RoundTime, 0, -1 do
self.Timer = index
timerRemoteEvent:FireAllClients(self.Timer)
task.wait(1)
end
self.IsRound = false
end
function Round:Initiate()
while true do
if not self.EnoughPlayers() then return end
if self.IsRound then return end
self:Intermission()
self:BringPlayers()
self:GracePeriod()
self:Start()
end
end
return Round
RoundServer server script (it’s still the same)
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local Round = require(ServerScriptService.Modules.Round)
local newRound = Round.new()
Players.PlayerAdded:Connect(function()
newRound:Initiate()
end)
RoundClient client script
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local statusRemoteEvent = ReplicatedStorage.RemoteEvents.StatusRemoteEvent
local timerRemoteEvent = ReplicatedStorage.RemoteEvents.TimerRemoteEvent
local player = Players.LocalPlayer
local playerGui = player.PlayerGui
local statusGui = playerGui:WaitForChild("StatusGui")
local container = statusGui.MainFrame.Container
local function onStatusRemoteEvent(status: string)
container.Status.Text = status
end
local function onTimerRemoteEvent(timer: number)
container.Timer.Text = timer
end
statusRemoteEvent.OnClientEvent:Connect(onStatusRemoteEvent)
timerRemoteEvent.OnClientEvent:Connect(onTimerRemoteEvent)
And there we have it. The end of this tutorial. If you have any suggestions or feedback it would be highly appreciated to leave a comment and I will try my best to respond. Have a great day guys
Made by @bosszemaa (my 1st post / tutorial so it’s probably not the best)