Make a Roblox voting system

Create a Roblox voting system

A Roblox voting system gives players the power to select what they want to happen next. Players can vote on anything you as the developer want to empower players to choose. Players can choose the next level to play, who should be the team leader, or which rewards should become available in the next release. 

Player voting is also a great way to get feedback and keep players engaged. If you have your own player survey API you can also use the Roblox HttpService to get surveys and receive player feedback.

Different ways to show voting options

For players to submit their vote we can use several methods. Players can walk onto a pad to select their desired choice or make a selection from a Gui screen.

Take a player poll by touching world parts

With just Roblox events and functions we can use the Touched event for each voting pad. We can also create a voting controller object using the Knit framework.

Using Touched event only

local votingPad = script.Parent
local padTouched = false
local function onVotingPadTouched(part)
  if not padTouched then
    padTouched = true
    -- validate part is a player
    -- store player’s vote with VotingService
    wait(1) -- give player one second to get off pad
    padTouched = false
  end 
end

votingPad.Touched:Connect(function(part)
  onVotingPadTouched(part)
end)

Using a Knit framework controller

local VotingController = Knit.CreateController { Name = "VotingController" }
function VotingController:SubmitVote()
  Knit.GetService(“VotingService”):SubmitVote(self.PlayerVote):andThen(function()
    -- close voting UI
  end)
end

We’ll cover how the voting service works a bit later.

Let players choose with a GUI

To set up our GUI we can use the built-in Roblox APIs or use the Roact framework. The Roact framework is best for games with a lot of UI components. Built-in APIs are good for simple UIs and games with a very minimal need for on-screen UI elements.

Build voting GUI using a Screen Gui

We’ll use a ScreenGui object to display the voting options to players. Each option gets placed into a grid layout with buttons to submit their choice. Under each choice, you can use a focus border, a radio button to indicate the player’s selection, or change the opacity of all unselected options.

Let’s create a LocalScript under the PlayerStarterScripts folder.

We’ll use constant values to define and create the grid for all choices available for an election. For each choice cell in our grid, we’ll use scaling values for responsive design.

local GuiService = game:GetService("GuiService")
local ContextActionService = game:GetService("ContextActionService")

-- use scaling values for cell dimensions
local CHOICE_CELL_WIDTH = 0.25
local CHOICE_CELL_X_MARGIN = 0.04
local CHOICE_CELL_HEIGHT = 0.25
local CHOICE_CELL_Y_MARGIN = 0.02

local CHOICE_COLUMNS = 2
local CHOICE_ROWS = 0

local UI_OPEN_POSITION = UDim2.new(0.05, 0, 0.05, 0)
local UI_CLOSED_POSITION = UDim2.new(0.05, 0, -.9, -36)

local CLOSE_VOTING_UI_BINDING = "CloseVotingUI"
local EXIT_VOTING_UI_BINDING = "ExitVotingUI"

local player = game.Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")

Next, we’ll define our UI structure with ScreenGui as the parent and Frames to break up our layout into logical sections.

-- Create UI screen
local screenGui = Instance.new("ScreenGui", playerGui)

-- button to open voting UI
local votingButton = Instance.new("TextButton", screenGui)
votingButton.Name = "votingButton"
votingButton.Text = "Vote!"
votingButton.Size = UDim2.new(0, 300, 0, 75)
votingButton.Font = Enum.Font.SourceSans
votingButton.FontSize = Enum.FontSize.Size60

-- main Frame UI
local menuFrame = Instance.new("Frame", screenGui)
menuFrame.Size = UDim2.new(0.75, 0, 0.75, 0)
menuFrame.Position = UI_CLOSED_POSITION
menuFrame.BackgroundTransparency = 1

-- voiting choices frames
local choicesFrame = Instance.new("Frame", menuFrame)
choicesFrame.Name = "choicesFrame"
choicesFrame.Size = UDim2.new(1, 0, 1, 0)
choicesFrame.Position = UDim2.new(0, 0, 0, 0)

local choiceScrollFrame = Instance.new("ScrollingFrame", choicesFrame)
choiceScrollFrame.Size = UDim2.new(1, 0, 1, 0)
choiceScrollFrame.CanvasSize = UDim2.new(0.9, 0, 1.8, 0)
choiceScrollFrame.Position = UDim2.new(0.0, 0, 0.0, 0)
choiceScrollFrame.Selectable = false
choiceScrollFrame.ScrollBarThickness = 0

We’ll use the playerSelected variable to store the player’s choice until it’s ready to submit. We’ll also include a function to close our UI once everything is done or the player decides to cancel.

local playerSelected = 'none'

local function CloseVotingUI()
	-- animate screen out of view
	menuFrame:TweenPosition(UI_CLOSED_POSITION, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.25, false, function(status)
		-- remove exit button binding once animation finishes		
		ContextActionService:UnbindAction(CLOSE_VOTING_UI_BINDING)
	end)
end

Using the constant values, we’ll create each choice as an ImageButton for each row and column. This also includes the button activation function that you can customize. Possible options include just storing the player’s choice or completing the entire process in one step.

-- Create voting choice grid cells
for y = 0, CHOICE_ROWS do
	for x = 0, CHOICE_COLUMNS do
		local choiceCell = Instance.new("ImageButton", choiceScrollFrame)
		choiceCell.Image = "rbxassetid://133293265"
		choiceCell.Name = "choiceCell(" .. x .. "," .. y .. ")"
		choiceCell.Size = UDim2.new(CHOICE_CELL_WIDTH, 0, CHOICE_CELL_HEIGHT, 0)
		choiceCell.Position = UDim2.new(CHOICE_CELL_X_MARGIN + CHOICE_CELL_X_MARGIN * x + CHOICE_CELL_WIDTH * x, 0,
			CHOICE_CELL_Y_MARGIN + CHOICE_CELL_Y_MARGIN * y + CHOICE_CELL_HEIGHT * y, 0)
		
		-- choice selected function
		choiceCell.Activated:Connect(function()
			playerSelected = choiceCell.Name
			print(playerSelected)
			CloseVotingUI()
		end)
	end
end

Finally, we’ll use the openVotingUI function to animate the UI onto the player’s screen. Using ContextActionService, we can control which buttons close the UI when a player wants to cancel. In this example, we’re binding the B key button but this can work with any button from a keyboard or gamepad.

local function openVotingUI()
	-- animate screen into view
	menuFrame:TweenPosition(UI_OPEN_POSITION, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.25, false, function(status)
		-- bind exit button once animation finishes
		ContextActionService:BindAction(CLOSE_VOTING_UI_BINDING, function(actionName, inputState, inputObject)
			if inputState == Enum.UserInputState.Begin then
				CloseVotingUI()
			end
		end, false, Enum.KeyCode.B)
	end)
end

votingButton.MouseButton1Click:Connect(openVotingUI)

Here’s the final result. Of course, you can take this layout and functionality to fit your needs.

roblox voting system gui

Build voting GUI using Roact framework

We’ll start with the Gui service declarations.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Roact = require(ReplicatedStorage.Roact)

We’ll use the PlayerChoice variable to store the player’s choice in memory.

local PlayerChoice = nil

Using Roact Fragments, we’ll loop through all available choices to add to the grid layout. Fragments help us maintain element hierarchy and structure. For example, the UIGridLayout object expects its siblings to include all elements that it will constrain in its grid layout.

local function ChoiceList(props)
	print(props.Choices)
	local elements = {}
	for _, choice in pairs(props.Choices) do
		elements[choice.Name] = Roact.createElement("TextButton", {
			Text = choice.Name,
			[Roact.Event.Activated] = function()
				print('selected '.. choice.Name)
				PlayerChoice = choice
			end
		})
	end
	return Roact.createFragment(elements)
end

We’ll define a few choices for this example though these could come from a remote API or even the Roblox data store

local VotingChoices = {
	{
		Name = "option #1"
	},
	{
		Name = "option #2"
	},
	{
		Name = "option #3"
	},
	{
		Name = "option #4"
	},
	{
		Name = "option #5"
	}
}

Now, we’ll extend the Frame object to include variable bindings. These bindings allow us to control the properties of our elements such as the Frame’s visibility.

local MainFrame = Roact.Component:extend("Frame")
function MainFrame:init()
	self.visibility, self.updateVisibility = Roact.createBinding(true)
end

function MainFrame:render()
	return Roact.createElement("Frame", {
		Position = UDim2.new(0, 10, 0, 10),
		Size = UDim2.new(0.75, 0, 0.75, 0),
		Visible = self.visibility
	}, {
		ChoicesFrame = Roact.createElement("Frame", {
			Position = UDim2.new(0, 0, 0, 0),
			Size = UDim2.new(1, 0, 0.75, 0)
		}, {
			Layout = Roact.createElement("UIGridLayout"),
			Items = Roact.createElement(ChoiceList, { Choices = VotingChoices })
		}),
		SubmitButton = Roact.createElement("TextButton", {
			Position = UDim2.new(0.45, 0, 0.75, 0),
			Size = UDim2.new(0, 100, 0, 60),
			Text = "Submit!",
			[Roact.Event.Activated] = function()
				print('submitting ' .. PlayerChoice.Name)
				self.updateVisibility(false)
			end
		})
	})
end

Finally, we’ll add the main container frame as a child to ScreenGui and use the mount function to render it on the player’s GUI.

local app = Roact.createElement("ScreenGui", {}, {
	MainFrame = Roact.createElement(MainFrame)
})
Roact.mount(app, Players.LocalPlayer.PlayerGui)

Instead of displaying this immediately once a player joins, we can instead call the mount function using the ContextActionService or bind it to another button that’s always on the player’s screen.

roblox voting system gui with roact

You can adjust this layout and style to fit your needs.

While using Roact has a bit of a learning curve, it helps you build reusable UI components and separates them into logical units.

How to store and count voting results

For elections that span long periods of time such as a week or more, it makes sense to use a Roblox data store to collect player votes.

Check out my Roblox data store guide to learn more.

In our data store, we’ll use several key schemas. First, we’ll need one for each election to store possible choices and the final result.

Election key schema and data structure

election_{unique election id}

For each unique id, we can use the HttpService:GenerateGUID function.

{
  ElectionId = 1234, -- unique election id
  StartTimestamp = 1640666295, -- when election begins
  EndTimestamp = 1643344695, -- when election ends and votes are tallied
  Options = {}, -- array of options
  Ballots = {}, -- all player submitted ballots
  Result = nil – table of final tallied result
}

Each voting option uses this table structure.

{
  OptionId = <someUniqueNumber>,
  Title = “option title”
}

Ballots use this table structure that we’ll use to validate when counting votes.

{
  ElectionId = 1234,
  OptionId = 2345,
  PlayerId = 3456,
  SubmittedOn = 4567, -- timestamp when it was received
}

When a player submits their vote, we’ll use this key schema. Use the os.time() function to get the current server UNIX timestamp.

election_{unique election id}_player_{playerId}

-- player Ballot structure
{
  PlayerBallotId = 2345, -- unique player ballot id
  ElectionId = 1234, -- unique election id
  Vote = {} -- copy of option table structure
}

Voting service example

local DataStoreService = game:GetService("DataStoreService")

-- Voting service constructor
local VotingService = {}
local function VotingService:new()
  local o = {}
  o.datastore = DataStoreService:GetDataStore("PlayerElections")
  self.__index = self
  return setmetatable(o, self)
end

local function VotingService:_GetElectionDataKey(electionId)
  local dataKey = "election_" .. electionId
  return dataKey  
end

local function VotingService:_GetPlayerVoteDataKey(electionId, playerId)
  local dataKey = "election_" .. electionId .. "_player_" .. playerId
  return dataKey  
end

local function VotingService:CreateElection(start, duration, options)
  local election = {
    ElectionId = HttpService:GenerateGUID(false),
    StartTimestamp = start,
    EndTimestamp = start + duration,
    Options = options,
    Ballots = {},
    Result = nil
  }
  self.datastore:SetAsync(electionDataKey, election)
end

local function VotingService:GetElectionData(electionId)
  return self.datastore:GetAsync(self:_GetElectionDataKey(electionId))
end

local function VotingService:SavePlayerVote(playerId, vote)
  local electionData = self:GetElectionData(vote.ElectionId)
  local dataKey = self:_GetPlayerVoteDataKey(vote.ElectionId, playerId)
  
  electionData.Ballots[dataKey] = vote

  local electionDataKey = self:_GetElectionDataKey(vote.ElectionId)
  self.datastore:UpdateAsync(electionDataKey, electionData)
end

local function VotingService:RemovePlayerVote(playerId, vote)
  local electionData = self:GetElectionData(vote.ElectionId)

  local dataKey = self:_GetPlayerVoteDataKey(vote.ElectionId, playerId)
  electionData.Ballots[dataKey] = nil

  local electionDataKey = self:_GetElectionDataKey(vote.ElectionId)
  self.datastore:UpdateAsync(electionDataKey, electionData)
end

When finishing an election by tallying all the ballots, it’s important to ensure that we only count valid ballots. We can validate ballots by confirming the ballot was submitted within the voting period and includes a valid option for the election.

local function VotingService:FinishElection(electionId)
  local dataKey = self:_GetElectionDataKey(electionId)
  local election = self.datastore:GetAsync(dataKey)

  -- loop through all ballots
  local validBallots = {}
  local now = os.time()

  -- validate each ballot was received during election period
  for ballotKey, ballot in pairs(election.Ballots) do
    if now >= election.StartTimestamp and now <= election.EndTimestamp then
      validBallots[ballotKey] = ballot
    end 
  end

  -- validate ballot refers to an option in the election list
  for ballotKey, ballot in pairs(validBallots) do
    results[ballot.OptionId] = results[ballot.OptionId] + 1
  end

  -- update results
  election.Result = results
  self.datastore:UpdateAsync(electionDataKey, election)
end

Connect Voting service between client and server

By using the Knit framework we can easily connect client scripts with the voting service.

Check out my Roblox Frameworks guide to learn more.

Knit Voting Service example

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local VotingService = require(ReplicatedStorage:WaitForChild("VotingService"))
local KnitVotingService = Knit.CreateService { Name = "KnitVotingService", Client = {} }

function KnitVotingService:SavePlayerVote(player, vote)
  VotingService:SavePlayerVote(player.UserId, vote)
end

function KnitVotingService:RemovePlayerVote(player, vote)
  VotingService:RemovePlayerVote(player.UserId, vote)
end

function KnitVotingService:FinishElection(electionId)
  VotingService:FinishElection(electionId)
end

-- periodically check if any elections are done
local lastChecked = os.time()
local finishElectionsInterval = 300 -- check every 5 minutes
while true do
  -- retrieve elections and filter for any that are completed
  local elections = VotingService:GetElections()
  for id, election in pairs(elections) do
    if election.EndTimestamp >= now and election.Result == nil then
      -- process finished election results
      VotingService:FinishElection(election.ElectionId)
    end
  end
end

We have this time interval between each election finalization to keep requests within the data store limits. Check out my Data Store guide to learn more about these limits.

Build a memory only Roblox Voting System Service

For short elections such as the next level to play in the next round, we can store player votes in memory in a server script. These types of elections don’t need a data store since they only run for a few minutes each time. 

We could get extra fancy and make a generic voting service that works for short and long-running elections. By using the strategy design pattern, the service will know where to obtain election data when it’s time to count all votes. 

So we’d have a data store strategy and an in-memory strategy with the same functions in its interface.

In either case, we’ll use the same table structures and use the same logic when processing final results.

We’ll also need to create a timer to keep the game moving forward. If a player doesn’t make a choice before the time is up, the majority still wins. In the case that no one votes, we can either give all players another period to choose or let the computer decide with a random choice.

Check out my Roblox timers guide to learn more.

What’s next

You now have a grasp of how to build a Roblox voting system service that allows players to vote. By using these patterns and concepts you can make similar Roblox game services such as this voting system.

Please consider joining my email newsletter for future guides and updates!

Thank you for reading, and stay curious.

Leave a Reply