Best Game-Round System

Hi

FYI I’m a kind of noob but not really…

I’m making a round based game, and I’ve run into a bit of a problem…

Here is a basic round system (not my actual code):

while true do
	if not enoughPlayers() then repeat Players.PlayerAdded:Wait() until enoughPlayers() end
	intermission()
	wait(10)
	-- How would I reset the loop if players leave
	-- and there is not enoughPlayers() while it's yielding?
	-- I know I could just check if there is enoughPlayers()
	-- after the yielding, but I would prefer just breaking
	-- out of intermission.
	loadMap()
	teleportPlayers()
	wait(10)
	-- what is the best way to stop a round when a winner
	-- is found?
end

The questions I have are in the comments of the code.

Originally, I made a state machine and on the entering and exiting of the states I would do stuff like load the map, teleport players, etc. and when a winner was found, it would transition to the next state, and the callback for states leaving would handle the rest.

However, I don’t like this approach because I needed a new state for every code I did between yields, and then they aren’t really states or whatever…

Then I used Promises (current implementation) but I don’t know if I like that either… :frowning:

Edit: I would also like to avoid conditional polling.

If anyone has any suggestions, please tell me!

11 Likes

Isn’t this a #help-and-feedback:scripting-support type of post?

I think this is mostly a game design question, as it’s not the scripting I need help with, but I will move it there if people think it is a scripting help post.

1 Like

Something like this would go into the #help-and-feedback:scripting-support :: However it’s fine! Anyways, what I basically did was used a repeat loop until a winner was chosen aka not equal to nil. An example is below;

-- Services
local Players = game:GetService("Players");
local ReplicatedStorage = game:GetService("ReplicatedStorage");


-- Variables
local NumPlayersNeeded = 2;


-- Objects
local Winner = Instance.new("ObjectValue");
Winner.Name = "Winner";
Winner.Parent = ReplicatedStorage;


-- Functions -------------------------------------------------
function checkPlayers()
	return (NumPlayersNeeded >= #Players:GetPlayers());
end

function startGame()
	-- Starting the minigame
	repeat
		wait();
		-- Game code here
		-- Countdown code / etc....
	until Winner.Value ~= nil
	print("Winner is: " .. tostring(Winner.Value));
end
--------------------------------------------------------------


-- Game Loop

while wait() do -- >> You can just use while true do <<
	if (checkPlayers()) then
		-- Run game code here
	else
		-- Display a message saying there's not enough players
	end
end
4 Likes

Thanks for the reply, I appreciate it.

However, and I’m sorry for not clarifying this in the original post, I would also like to avoid conditional polling like that. This DevForum post explains why.

The post does say how to get around conditional polling, but callbacks and events are also not ideal, as it would lead to callback hell, if I wanted to, say, add different “stages” to rounds (which is a very real possibility) and would look something like this:

while true do
	enoughPlayers(function()
		intermission(function()
			if isEnoughPlayers() then
				doRoundStage(1, function()
					if not winner() then
						doRoundStage(2)
					end
				end)
			end
		end)
	end)
end

and that is a big no for me. This leads to barely readable and really hard to maintain code.

Also I guess I will move this post to scripting help. Thanks again!

4 Likes

Thanks! I totally forgot about polling, so now I need to go redo some code of mine. :smiley:

Ah okay… I read through the post and using :Wait(); is perfectly fine. Here would be my way of doing this which should work like it is or with a little tweaking…

-- In that forum post :Wait() is perfectly fine, but wait() or :wait() isn't...
-- Remember how I made an ObjectValue called, "Winner"? >> I will be using that in this code <<

local WINNER_Event, WINNER_EVENT_CALLED = Instance.new("BindableEvent"), false; -- WINNER_EVENT_CALLED is a boolean

function GetWinner()
	if (Winner.Value) then
		WINNER_EVENT_CALLED = true;
		WINNER_Event:Fire()
	end
end

-- What I'm about to do is Get the property changed signal and fire this event...
Winner:GetPropertyChangedSignal("Value"):Connect(function()
	if (not Winner.Value) then return end
	GetWinner();
end)




-- Round Code
function startGame()
	WINNER_EVENT_CALLED = false; -- Reset this to false
	-- Game Mode functions
	
	if (not WINNER_EVENT_CALLED) then
		WINNER_Event:Wait();
		-- Winner display etc....
		print("Winner is: " .. tostring(Winner.Value));
	end
end


while true do
	game:GetService("RunService").Stepped:Wait() -- Again, this is perfectly fine as it's a yielding function.. Also this will prevent crashing
	
	-- Your intermission, etc. code here;
	
	
	-- Run the function
	startGame();
end
1 Like

Again, thanks for the reply, but I would still rather like to avoid callbacks and friends.

I wrote some theoretical code for using events and stuff and it still makes me want to commit toaster-bath:

-- theoretical Timer object defined here... :P

local timer = Timer.new()
local canContinue, connection

while true do
	canContinue = true

	if not enoughPlayers() then repeat Players.PlayerAdded:Wait() until enoughPlayers() end

	timer:Start(30)

	connection = Players.PlayerRemoving:Connect(function()
		if not enoughPlayers then
			connection:Disconnect()
			timer:Cancel()
			canContinue = false
		end
	end)

	timer.Finished:Wait()

	if canContinue then
		timer:Start(120)
		connection = ThingToHelpDetermineWinner.TeamWon:Connect(function()
			connection:Disconnect()
			timer:Cancel()
			canContinue = false
		end)

		timer.Finished:Wait()

		if canContinue then
			timer:Start(360)
			
			timer.Finished:Connect(function()
				if canContinue then -- also means no winners were found
					-- say it was a tie
				end
			end)
		end
	end
end

I really don’t like having to use that canContinue variable to determine if it was the timer that ran out or the game determined a winner or the game determined it cannot go on. But again, I really appreciate your time. <3

I also might be really dumb and am completely overthinking this.

1 Like

What if instead of canContinue you just see if the Winner.Value is not nil? << Oops just read another line and saw hmmm… it’s tricky

Still basically the same thing; actually it could be worse because then I would need a new variable to see if the game can continue after intermission if players left and there were not enough to start a round.

I think the answer lies in something much more complicated than checking to see if there is a winner and if the game may continue on.

I am currently working on a round based game and the main round based script is now almost a 1000 lines of code so how did I make it readable because you don’t want to add all the code inside the loop to read.

Here is an example

Place the runGame function at the end of the script and execute the function then you can keep all functions below the while loop.

Note 90% of the code inside the while loop should be functions that is to make it more readible.

So this post is pretty old but I would like to update it on how I solved it.

I ended up using promises, here is a post on why they are useful.

I’m not going to release the script because I feel like it’s pretty trivial to implement it yourself if you know what you are doing, and if you don’t, then you’re better off doing something you understand. :slight_smile:

Since I’m not an experienced programmer, I can’t explain how I made promises work without making it sound really confusing, but thanks to everyone who replied!

Edit: I was asked to release a code snippet, so here it is. Please do note that this is pretty much untested pseudocode, because I am lazy :stuck_out_tongue:

local ReplicatedStorage	= game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Teams = game:GetService("Teams")

local lib = ReplicatedStorage.lib
local Promise = require(lib.Promise)

local Team1 = Teams:WaitForChild("Team1")
local Team2 = Teams:WaitForChild("Team2")
-- there is a third team that players go to when they die :P

local function enoughPlayers()
	return #Players:GetPlayers() >= 2
end

-- promise that resolves when the passed in team has no players
local function teamWipedOutPromise(team)
	return Promise.fromEvent(team.PlayerRemoving, function()
		return #team:GetPlayers() <= 0
	end)
end

-- promise that resolves when enough players have joined the game
local function notEnoughPlayersPromise()
	return Promise.fromEvent(Players.PlayerRemoving, function()
		return #Players:GetPlayers() - 1 < 2
	end)
end

-- promise that resolves when there are no longer enough players
local function enoughPlayersPromise()
	return Promise.fromEvent(Players.PlayerAdded, function()
		return enoughPlayers()
	end)
end

function startGame()
	return Promise.race({
		teamWipedOutPromise(Team1) -- team2 win condition 1
			:andThenReturn(Team2), -- return team2, because they won :D

		teamWipedOutPromise(Team2) -- team1 win condition 1
			:andThenReturn(Team1), -- return team1, same reason :P

		Promise.resolve() -- team1 win condition 2
			:andThen(function()
				-- teleport + freeze all team1
				-- do a lil' countdown
			end)
			:andThenCall(Promise.delay, 5)
			:andThen(function()
				-- unfreeze team1
			end)
			:andThenCall(Promise.delay, 30)
			:andThen(function()
				-- teleport team2
			end)
			:andThenCall(Promise.delay, 300)
			:andThenReturn(Team1)
	})
		:andThen(function(winners)
			-- handle winners
		end)
		:andThenCall(Promise.delay, 5)
		:andThen(function()
			-- teleport players back to lobby
		end)
		:andThen(function() -- can't use andThenReturn here because of conditional
			return enoughPlayers() and intermit or waitForPlayers
			-- return the next block of code we should do!
		end)
end

function intermit()
	return Promise.race({
		notEnoughPlayersPromise()
			:andThenReturn(waitForPlayers),
			-- not enough players, so we go back to waiting for players :'(

		Promise.delay(30)
			:andThen(function()
				-- load the map
				-- assign teams
			end)
			:andThenReturn(startGame)
			-- we successfully finished intermission with enough players,
			-- so we should start the game >:D
	})
end

function waitForPlayers()
	return enoughPlayersPromise()
		:andThenReturn(intermit)
		-- enough players, so we can start the intermission! :D
end

local nextBlock = waitForPlayers

while true do
	nextBlock = nextBlock():expect()
end
23 Likes