Race system with player positions, 1st, 2nd, etc

Sorry I was generalizing. You are right, confusing is not the right word. They simply do not elaborate on how to sort the table with more than one variable.

I am assuming that the checkpoints are not in the straight line.

So the confusing part is that nobody explained how the sort function should look like, rather they tried to solve the issue with complicated spaghetti code or not at all.

1 Like

I have a few questions, what would the loops be? Why are there two of the same functions but doing different things? What’s the difference between entryA and entryB are they like the players? Sorry if these questions may seem may seem dumb, still learning along the way.

It’s a custom sort, like as I explained earlier, you need a function to define whether it’s ascending or descending, he just made a custom sort function.

  1. You can skip the loop code, if Your game does not use loops. (Loops as in players go around in circles)
  2. I made a typo, as the other one should be called ChangeLoop. I edited my answer.
  3. entryA and entryB are single entries provided to the sort function to compare. Single entry is a table with all the required player data. Such entry is created on CharacterAdded event.

This is required, since you have more than one condition to compare:

  • magnitude to the last checkpoint,
  • last used checkpoint
  • optional loop count, if the course circles back to the start and you want to support multi loop race.

So one the table to sort is made up of x amount of nested tables, where x is the amount of players currently in the game.

I can detect how far away the person is to the next checkpoints via magnitude, however I am unsure how proceed with detecting if two players are on the same checkpoints and which is first

OP stated that they already have the magnitude

Alright, but how would I implement that into the code that I already had though? How would I assign each player a position on the leaderstats, 1st, 2nd, etc?

I can help you with that, but before I provide a solution, I just need to know how do you detect when player has reached a checkpoint?

Also let me know if you plan to have those loop system (closed circuit) in the game.

This detects when the player hits the next checkpoint:

for _, v in pairs(workspace.Checkpoints:GetChildren()) do
	v.Touched:Connect(function(hit)
		if v ~= nil then
			if v.Parent == workspace.Checkpoints then
				local player = game:GetService("Players"):GetPlayerFromCharacter(hit.Parent)
				if player and player.Character.Humanoid.Health <= 0 then
					if tonumber(v.Name) == 0 and player.leaderstats.Checkpoints.Value == game.ReplicatedStorage.RoundInfo.NumOfCheckpoints.Value then
						player.leaderstats.Checkpoints.Value = 0
						player.leaderstats.Laps.Value += 1
					elseif tonumber(v.Name) == 0 and player.leaderstats.Laps.Value == game.ReplicatedStorage.RoundInfo.NumOfLaps.Value + 1 then
						-- win
					elseif player.leaderstats.Checkpoints.Value == tonumber(v.Name) - 1 then
						-- gained a checkpoint
						player.leaderstats.Checkpoints.Value += 1
					elseif  player.leaderstats.Checkpoints.Value == tonumber(v.Name) + 1 then
						--wrong way
					elseif player.leaderstats.Checkpoints.Value < tonumber(v.Name) - 1 then
						--skipped a checkpoints
					end
				end
			end
		end
	end)
end

laps is basically just loops

laps is basically just loops

Probably a better name lol. I changed it to laps in my code.

Please add a leaderstat value called Place, to make sure the code works. I have also switched the comparison in table sort, as the player with more loops and more checkpoints should be in the lead.

Final thought: The current code check for a magnitude to the last checkpoint, and further away player is in the lead. This is not ideal, and proper way should be to check magnitude to the NEXT checkpoint. Closest player should be in the lead. This is slightly more complicated, and you will need to sort out edge cases (for example if the current checkpoint is already last one). Something to think about.

Anyways, here goes:

-- keep all player data here
local Entries = {}

game.Players.PlayerAdded:Connect(function(plr)

   -- add a new entry when character is spawned
   plr.CharacterAdded:Connect(function(chr)

      table.insert(Entries,{
          player = plr,
          root = chr.HumanoidRootPart,
          checkpointRef = game.Workspace.Checkpoints:FindFirstChild("0"), 
          checkpointValue = 0 -- first checkpoint has a value of 0
          laps = 0, 
      }
   end)
   -- remove an entry when player leaves or despawns
   plr.CharacterRemoving:Connect(function()
      for i, entry in pairs(Entries) do
         if entry.player == plr then
             table.remove(Entries,i)
             break
         end
      end
   end)

end)

-- adjust the checkpoint with this function
local function ChangeCheckpoint(plr,newCheckpoint,newCheckpointValue)
   for _, entry in pairs(Entries) do
      if entry.player == plr then
          entry.checkpointRef = newCheckpoint
          entry.checkpointValue = newCheckpointValue
          break
      end
   end
end
 
-- adjust the laps with this function
--EDIT: Corrected the name of the function
local function ChangeLaps(plr,newLapsValue)
   for _, entry in pairs(Entries) do
      if entry.player == plr then
          entry.laps = newLapsValue
          break
      end
   end
end


for _, v in pairs(workspace.Checkpoints:GetChildren()) do

	v.Touched:Connect(function(hit)

	-- if v ~= nil then -- this check is redundant
	-- if v.Parent == workspace.Checkpoints then -- this check is redundant

		local player = game:GetService("Players"):GetPlayerFromCharacter(hit.Parent)

		if player and player.Character.Humanoid.Health <= 0 then -- why? is the character supposed to be dead?

			if tonumber(v.Name) == 0 and player.leaderstats.Checkpoints.Value == game.ReplicatedStorage.RoundInfo.NumOfCheckpoints.Value then

				player.leaderstats.Checkpoints.Value = 0
				player.leaderstats.Laps.Value += 1
				--update checkpoint here
				ChangeCheckpoint(player,v,0)
				--update laps
				ChangeLaps(player,player.leaderstats.Laps.Value)

			elseif tonumber(v.Name) == 0 and player.leaderstats.Laps.Value == game.ReplicatedStorage.RoundInfo.NumOfLaps.Value + 1 then

						-- win
			elseif player.leaderstats.Checkpoints.Value == tonumber(v.Name) - 1 then
						-- gained a checkpoint
				player.leaderstats.Checkpoints.Value += 1
				--update checkpoint here too
				ChangeCheckpoint(player,v,player.leaderstats.Checkpoints.Value)

			elseif  player.leaderstats.Checkpoints.Value == tonumber(v.Name) + 1 then
						--wrong way
			elseif player.leaderstats.Checkpoints.Value < tonumber(v.Name) - 1 then
						--skipped a checkpoints
			end

		end
		--end
		--end
	end)

end


-- finally the sort function
local function CustomSort(entryA, entryB)
   --first check the laps 
   if entryA.laps ~= entryB.laps then
      -- players have different amount of laps, return the result
      return entryA.laps > entryB.laps
   -- if loop is the same, check if the checkpoints are different.
   elseif entryA.checkpointValue ~= entryB.checkpointValue then
      -- players have different checkpoints, return the result (each checkpoint has a different value)
      return entryA.checkpointValue > entryB.checkpointValue
   else
      -- finally if checkpoints are the same, see which player is further away from said checkpoint
      return (entryA.root.Position - entryA.checkpointRef.Position).Magnitude >
         (entryB.root.Position - entryA.checkpointRef.Position).Magnitude
   end
end

-- Example of sorting loop each second (adjust if needed)
task.defer(function()
   while true do
      task.wait(1) -- check every second
      table.sort(Entries,CustomSort)
      --update leaderstats
      for place, entry in pairs(Entries) do
         entry.player.leaderstats.Place.Value = place
      end
   end
end)

Whoops, my bad I was just cleaning up the code for you and I accidentally made it so the character had to be dead! My fault, should be > instead of <.

Also, I forgot to ask this before, but what does defer do?

Creates a coroutine. This is similar to task.spawn() and coroutine.wrap(), but does not force the thread to be run immediately. I prefer it to other methods to avoid inexplicable lag spikes.

Of course sometimes task.spawn() is better, for example projectile handlers or timers.

If you are unfamiliar with coroutines, I am afraid I have no time to explain it today, but I am sure you can find plenty of tutorial here on forum.

Long story short, when you create coroutine, you are like creating separate almost independent script, that runs along side the main script.

It works perfectly! Also,

There should be a comma after the 0

There should also be a ) after the }

Also I fixed it so it works when you join the game, not only after you respawn/die

Other than those minor errors, it works great!

1 Like

Why he wasn’t unsatisfied with my code then? After all, none of both solved the problem.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.