Scripting a Map Voting GUI

I’ve been attempting to script something that allows players to vote on one map option, out of 3 randomly selected ones. Basically:

-Three options are randomly selected from a given bunch of maps from ServerStorage (:white_check_mark:)
-These three options are sent to the client and representing on a screen GUI with 3 buttons, one representing each choice.
-Once a player clicks a choice, it should update the votes for that map on the server, and then after a set amount of seconds establish a winner.

Basically something similar to Phantom Forces.

I’ve spent more time than I’d like to admit on this, trying to come up with solutions but I just don’t seem to know how to get it to work no matter what I do. I know that it’s most likely a flaw in the way(s) I tried to handle client-server communication. Any examples or in depth explanation on how I could tackle this would be highly appreciated.

4 Likes

I’m in the process of making something similar myself… and my draft will more or less be implemented like so:

Draft
--// Select the "3" random maps and setup a selection pool like so:
local SelectionPool = {Selection1 = 0; Selection2 = 0 Selection3 = 0}

--// Asynchronously send all three choice names to all clients (+ timeout)..

local getPlayerVotes;
function getPlayerVotes(timeout)
	local current = coroutine.running()
	
	local tasks = {}
	--|Participants| = players in-game;
		for player in Participants do
		--|GetPlayerVote| = relevant `RemoteFunction`
		--|Selection(n)| = choice from pool
		local vote = function()
			local success, vote = pcall(GetPlayerVote.InvokeClient, GetPlayerVote, player, {selection1, selection2, selection3})
			
			if success then
				-- update pool to reflect player choice
				SelectionPool[vote] = SelectionPool[vote] + 1;
			end
		end
		tasks[#tasks+1] = vote;
	end
	
	local sleep = tick() + timeout
	while true do
		if tick() < sleep then
			coroutine.resume(current)
            break;
		end
		for i = 1, #tasks do
			--InvokeClient = yielding function, so run in separate thread
			coroutine.wrap(tasks[i])()
		end
        coroutine.yield()
	end
	
end

--// Get choice with the most votes w/ table.sort
--//CLIENT

GetPlayerVote.OnClientInvoke = function(data)
	-- Create choices from `data`
	local connection;
	connection = choiceButton.MouseButtion1Clicked:connect(function()
		connection:Disconnect();
		return choiceButton.Name -- or Text
	end)
end

I haven’t tested these snippets myself. This is simply a general idea of how I’d go about doing it!

EDIT: I just finally got this this point in my own project. Using RemoteEvents instead, much more manageable than the draft above…

--|pool| = map choices
function getPlayerVotes(players, pool, timeout)
	local voteSignal = Instance.new("BindableEvent")
	local voteTimeout= tick() + (timeout or 30)
	local awaitVote;
			
	local votes = pool;
	local tasks = {}
	
	for _,player in next, players do
		local vote = function()
			local success, err = pcall(GetPlayerVote.FireClient, GetPlayerVote, player, votes)			
			if success then
				--//sent successfully
			end
		end
		tasks[#tasks+1] = vote;
	end
		
	coroutine.wrap(function()
		repeat wait() until tick() >= voteTimeout;
		voteSignal:Fire()
	end)
	
	for i, task in next, tasks do
		coroutine.wrap(tasks[i])()
	end		
	
	awaitVote = GetPlayerVote.OnServerEvent:Connect(function(player, vote)
		if player and vote then
			votes[vote] = (votes[vote] or 0) + 1
			voteSignal:Fire()
		end
	end)
	
	voteSignal.Event:Wait()
	awaitVote:Disconnect()
	table.sort(votes, function(x,y) return x > y end)
	return next(votes);
end
6 Likes

For something like this, seeing as RemoteFunction is yielding and players won’t instantly send in their vote, what about using a RemoteEvent which goes both ways?
It also means you could just use :FireAllClients (assuming you want all players = participants) to start the voting, and you don’t need to get into so many different coroutines

The workflow I had in my head was something like this:

  • New Vote Starts on Server with 3 random maps
  • Server uses :FireAllClients() to send the 3 new maps
  • Clients send back their vote with :FireServer()
  • Server checks if vote is still active and the user can vote

A few things that need to be picked up upon are:

  • Users should only receive one vote; if they select a map then change it, you need to make sure you remove the vote from the old selection and add it to the new one [this is assuming you can change your vote after you select it]
  • New players which join the server after the votes have been fired to the clients will need to retrieve these votes separately
2 Likes

Interesting… the way I saw it, assuming that RemoteFunctions did yield indefinitely, the timeout + remotefunction combination would eliminate the need for two separate RemoveEvents. My implementation would centralize all these “checks”, vs having to handle them in separate environments.

It doesn’t take into account new players (non-participants?) or vote changes (as you mentioned), but the that logic can squeezed somewhere in there.


Disconnecting the connection after the vote takes care of the one vote per player issue (but doesn’t allow changes)

1 Like