Skip to main content

Making Your First Round

This page builds a complete round from scratch using the concepts introduced so far. Later sections add player state, reconnection, and custom events to the same round.

Setup

Require RoundKit like any other module.

local RoundKit = require(game.ReplicatedStorage.RoundKit)

We'll build the round one phase at a time, then put everything together at the end.

The lobby

Every round starts with a Phases table and an InitialPhase.

local Phases = {
Lobby = {
Name = "Lobby",
AllowedTransitions = {},
},
}

local manager = RoundKit.new({
Phases = Phases,
InitialPhase = "Lobby",
WinCondition = "TimeLimit",
})

manager:Start()

At this point the round starts and remains in Lobby. There are no valid transitions yet, and the TimeLimit win condition has not been registered.

Adding a countdown

Add a Countdown phase that transitions automatically after five seconds.

Phases.Countdown = {
Name = "Countdown",
AllowedTransitions = { "Active" },
Duration = 5,

OnEnter = function(context)
print("Countdown started")
end,
}

Now allow Lobby to transition into it.

Phases.Lobby = {
Name = "Lobby",
AllowedTransitions = { "Countdown" },

CanTransitionTo = function(context)
return context:GetReadyPlayerCount() >= 2
end,
}

GetReadyPlayerCount() reads from the player registry. Until players are added to the round, it always returns 0, so the round remains in Lobby.

For simplicity, this example uses the hardcoded value 2. In a real game, it's usually better to expose this as part of your round configuration:

local manager = RoundKit.new({
Phases = Phases,
InitialPhase = "Lobby",
WinCondition = "TimeLimit",
MinimumPlayers = 2,
})

Then reference it through context.Config:

CanTransitionTo = function(context)
return context:GetReadyPlayerCount() >= context.Config.MinimumPlayers
end

Adding a win condition

Register a win condition that ends the round after 30 seconds.

RoundKit.WinCondition.new("TimeLimit", function(context)
if context:GetElapsedTime() >= 30 then
return {
Type = "Draw",
Winners = {},
}
end

return nil
end)

Adding the active phase

The Active phase evaluates the win condition and returns the round to Lobby once a winner is reported.

Phases.Active = {
Name = "Active",
AllowedTransitions = { "Lobby" },
EvaluateWinCondition = true,

OnEnter = function(context)
context:Connect("OnWinConditionMet", function(outcome)
print("Round ended:", outcome.Type)
context:TransitionTo("Lobby")
end)
end,
}

Putting it together

local RoundKit = require(game.ReplicatedStorage.RoundKit)

RoundKit.WinCondition.new("TimeLimit", function(context)
if context:GetElapsedTime() >= 30 then
return {
Type = "Draw",
Winners = {},
}
end

return nil
end)

local Phases = {
Lobby = {
Name = "Lobby",
AllowedTransitions = { "Countdown" },

CanTransitionTo = function(context)
return context:GetReadyPlayerCount() >= 2
end,
},

Countdown = {
Name = "Countdown",
AllowedTransitions = { "Active" },
Duration = 5,

OnEnter = function(context)
print("Countdown started")
end,
},

Active = {
Name = "Active",
AllowedTransitions = { "Lobby" },
EvaluateWinCondition = true,

OnEnter = function(context)
context:Connect("OnWinConditionMet", function(outcome)
print("Round ended:", outcome.Type)
context:TransitionTo("Lobby")
end)
end,
},
}

local manager = RoundKit.new({
Phases = Phases,
InitialPhase = "Lobby",
WinCondition = "TimeLimit",
AutoWirePlayers = true,
})

manager:Start()

This round starts in Lobby, waits for two ready players, enters a five-second countdown, runs the active phase, then returns to Lobby after thirty seconds.

The round still doesn't track players, so GetReadyPlayerCount() always returns 0. The next section adds player state, allowing the lobby to transition as intended.

Next steps

The next section, Player & Global State, introduces context:AddPlayerState() and state:SetReady(), which complete this round. Later sections cover reconnection and custom events.