Racing: How To Keep Track Of Race Positions

During a race, I would like to keep track of which place each player is in (1st, 2nd, 3rd, …). The race has several laps. The race track has 10 gates that the player needs to go through in sequence to complete a lap. The racetrack is not confined, and the player can go anywhere on the map.

So how would I keep track of which place each player is in, at any given time in the race. Thank you for any suggestions.

3 Likes

You could define the race course as a path between a set of nodes (more nodes = more accurate around corners but might not be necessary). You can then find the closest point on that path from the player. Ray objects have a ClosestPoint method or you could make your own. This can give you a value from 0% to 100% of how far the player is along the track. If you sort these values, then you have an ordered list of player positions.

4 Likes

I personally use a system with only 4 checkpoints that tracks position at each checkpoint. Mine is lap based though, so it needs to be able deal with that too.

I track each player entering checkpoints use the distributedgametime function to figure out when they hit it. Then I use they lap they are on, and which checkpoint. So in my case, if they just hit Checkpoint 3 then I make their checkpoint value be 1. If they hit Checkpoint 4, I make their checkpoint value be 4. If they hit Checkpoint 1, 3, and so on. When they hit the Checkpoint 4, I divide their lap value by 5. The reason for this is to make their lap count count for more than checkpoints. The Initial Lap Value is 100,000 just so that way it’s big enough that it’ll never matter really. (I also use this in conjunction which checking their checkpoint value when they hit the checkpoint to make sure they’re going the right way. They can’t hit Checkpoint 4 again until they hit Checkpoint 3. They can’t hit Checkpoint 3 again until they hit Checkpoint 2, and so on.)

(In the case of a 10 gated thing, either use halves for your Checkpoints, or tenths, or use something larger than ten to divide the lap value by. The dividing of the lap value should be larger than the highest checkpoint value.)

So, how does this all end up working out then?
image

Car 1 has a Lap value of 160 and a Checkpoint Value of 2.
Car 4 has a Lap value of 160 and a Checkpoint Value of 4.
Car 3 has a Lap Value of 800 and a Checkpoint Value of 1.

Now, I multiply these values together.

Car 1’s overall value is 320.
Car 4’s overall value is 640.
Car 3’s overall value is 800.

I use a system that takes the smallest value and puts them in First, and then the next Smallest in Second, and so on. However, if two people were on the same checkpoint and lap, they’d be tied, right? Well, I also combine in the distributedgametime, and add that number in. By adding that number in the first place car will be the driver who hit that checkpoint on that lap first because their distributedgametime value will be smallest.

That being said, that’s just how I do it, and it’s probably extremely inefficient. I use a script inside the vehicleseat of every car to change the values, I use scripts inside the checkpoints to determine when a car hits it and re-run all the values for sorting them into order, I use num values in a folder in the map’s model grouping to store all the values, and I use a script inside each driver’s section of values to determine if the player left, and if they have to remove them from the list.

10 Likes

That makes sense, thank you. The only thing I did not quite understand is how to use DistributedGameTime. In the Wiki it defines this as:

DistributedGameTime
The amount of time, in seconds, that the game has been running.

How do you use this to determine when a player hits a checkpoint?

When a server starts DistributedGameTime is 0.
1 second in, it’s 1.
2 seconds in, it’s 2. And so on.
So the sooner you hit a checkpoint and get a time value, the smaller it is. The value actually goes down to thousandth of a second, if I recall correctly though, so you’ll always have a decimal on the end of it. You just have to call up game.workspace.distributedgametime to get the value. Nothing else. I store this in an object value, but you can also store it in a script by making a value in the script to hold it.

I like how @GeorgeOfAIITrades uses checkpoints, but I’d like to note a few things:

  1. You can use any time-getting method with decimal accuracy. I would personally use os.clock() instead of workspace.DistributedGameTime, but it’s all up to preference.

    You should choose a time source that is accurate for your use case:

    • os.clock() if you want accurate real-world time
    • time() if you want physics simulation elapsed time

    See the os. enhancements section of Luau Recap: June 2020

  2. You can actually check the distance to the next checkpoint instead of checking the time. This should be more accurate. It’s slightly less performant, but that will only matter at all if you have hundreds of cars.

  3. There are other ways to store and sort placement positions…

    • When using distance, you can store total checkpoints passed + percent from last checkpoint to next checkpoint.
    • You can store checkpoints, laps, and distance separately, then write your own sort function .

Here’s an example using the distance checking and percent method:

local function setupRace(checkpoints, laps)
	table.sort(checkpoints, function(a, b)
		return tonumber(a.Name) < tonumber(b.Name)
	end)

	for i, checkpoint in ipairs(checkpoints) do
		local lastCheckpoint = checkpoints[i - 1] or checkpoints[#checkpoints]
		local nextCheckpoint = checkpoints[i + 1] or checkpoints[1]

		checkpoint.Touched:Connect(function(hit)
			local carInfo = getCarInfoFromPart(hit)
			if carInfo and carInfo.lastCheckpoint == lastCheckpoint then
				carInfo.totalCheckpoints = carInfo.totalCheckpoints + 1
				carInfo.lastCheckpoint = checkpoint
				carInfo.nextCheckpoint = nextCheckpoint

				if carInfo.totalCheckpoints%(#checkpoints) == 0 then
					carInfo.completedLaps = carInfo.completedLaps + 1
					-- car has finished a lap, do lappy things like lap count notifications
				end

				if carInfo.totalCheckpoints == #checkpoints*laps then
					-- car has finished the race, do race finish things
				end
			end
		end)
	end
end

local function updateScores()
	for _, carInfo in ipairs(cars) do
		local checkpointDistance = (carInfo.lastCheckpoint.Position - carInfo.nextCheckpoint.Position).magnitude
		local carDistance = (carInfo.car.PrimaryPart.Position - carInfo.nextCheckpoint.Position).magnitude
		-- carDistance gets smaller as the car gets closer to the next checkpoint
		local sectionPercentage = 1 - carDistance/checkpointDistance
		-- sectionPercentage is ~0 when around the lastCheckpoint, and 1 when at the next checkpoint
		carInfo.score = carInfo.totalCheckpoints + sectionPercentage
	end

	table.sort(cars, function(a, b)
		return a.score > b.score  -- puts the highest scores first
	end)
	-- cars is now sorted by race placement
end

This is only an example and is clearly not complete. This is only supposed to give you an idea of how it would work. You should learn from this and implement it yourself. This code has not been tested.

12 Likes

Afaik DistribGameTime isn’t read only so I’d trust tick() in case you want to do something different with DGT later on and in case a different script is tampering with the value.

My only concern with doing distance to a checkpoint is for example, if a track has a lot of hairpins it’ll make weird things unless you put a checkpoint at the beginning of the hairpin, the middle of the hairpin and the end.

So for example, if this is a corner on a track, and you place two checkpoints like this:
image
You get further away from the 2nd checkpoint, and then closer.
image
To fix this you just add another checkpoint. But that being said, having to do that on a track that has a lot of hairpin corners could turn out to frustrating over time.

For example, this track would require a lot of “correction” checkpoints to solve that issue.
image

This should be obvious, but it’s better to raycast between previous and new player positions (instead of reacting to BasePart.Touched) in order to see whether they’ve past through a checkpoint. It’s also good for anti noclip but that’s a different conversation.

If you just place all the checkpoints at the apex of every corner this works itself out. The only problem then is the car on the inside always will appear slightly ahead on the leaderboard, but at long as the important checkpoints, like the start finish line, are on a straight it doesn’t matter much.

It’s not possible for it to be perfect. With your method, if Player2 speeds up and passes Player1 before either player reaches the next checkpoint, then it will still say that Player1 is first despite Player2 having passed them. With my method, it will say that Player2 is in first place like it should. I think slight inaccuracy between checkpoints is better than no accuracy at all between checkpoints.

I agree. The score between checkpoints is only useful for visuals/gameplay. Who hits what checkpoint first is what ultimately determines the final placement/winners. If I were to add on to my example, I’d save the tick() when the player finishes the race so that can be compared to figure out the winners.

No. With my method it updates the order anytime that a person passes any checkpoint. So when Player2 hits the next checkpoint after passing Player1, it’ll update before Player1 hits the next checkpoint because the multiplicative difference is higher than the distributedtime gap.

Edit: I just realized what you meant. I read that wrong in my head. Yea. Well, 4 checkpoints throughout a track still updates better than iRacing only updating positions every lap.

Note the following on DistributedGameTime:

  • It’s not really distributed, you can’t use it as a global timestamp from client → server
  • It updates at 1/60 so I would highly recommend using tick(), otherwise you can easily get 0 when calculating delta.
  • It’s probably slower to read than tick()
2 Likes

Could you explain to me what the “next” part in the for loop does? I understand normal for loops and such, but I have never seen

for _, carInfo in next, cars do

What is that supposed to do?

I’ve amended the example to look normal.

next, cars is the same as pairs(cars): pairs just returns next, cars when you call it, so…

for _, carInfo in next, cars do

is the same as

for _, carInfo in pairs(cars) do

My example was done this way because next, cars use to be slightly faster. With Luau improvements, pairs(cars) and next, cars have no performance difference, so it’s better to use pairs(cars) since it’s more clear.

Oh ok I see, thanks! Great code example by the way!

Good day! I implemented your example into my game, and with a couple tweaks, it works great! The only thing is is that every time I try to sort it, it always sorts backwards for some reason.Screenshot 2021-10-03 085358

I even tried doing it backwards, with <= and it still did the same thing. Any ideas?

It looks like your using keys of 1.0342..., 1.6333..., and2.2157...?

To sort a table, it needs to be an array (keys of 1, 2, 3, etc.). What you have is a dictionary with some decimal number keys instead.

My example code above expects cars to be an array. Something that looks like:

local cars = {
   {
       player = player,
       lastCheckpoint = part,
       nextCheckpoint = part,
       car = model,
       totalCheckpoints = 1,
       score = 0,
   },
   -- and more...
}

Oh I see thanks! That makes sense.