Overview
For Switchgrass, I wanted to use a simple, sector-based approach for track representation.
As I was designing both the racetrack and the AI, I decided on the following constraints:
- 2D Track Representation: simplifies math required while sufficient for my racetrack
- Piecewise-Linear Racing Line: this gave good results, partly due to the way AI forces were being applied
- Player-based Learning: I wanted to leverage a Player’s ‘run’ data to find an optimal line
- Simple Editor Tooling: my racetrack was still in progress and I needed to be able to make changes
Racetrack Representation
TrackNode
A TrackNode
consists of a single ‘node’ of the track. It stores a width and the point of the ‘racing line’ on it.
public class TrackNode : MonoBehaviour
{
public float width = 10;
[Range(-1, 1)] public float racingLine;
public TrackNode next;
public TrackNode prev;
}
This component is assigned to GameObjects and their Transforms are used to represent the Track.
TrackNodes form a doubly-linked list and include utilities to calculate length recursively, and 2D point-in-Quad testing.
Calculate Track Length
I wanted to dynamically calculate track length so I had greater flexibility with shortcuts later, as well as to avoid accidentally using old, invalid data.
public void CalculateLength()
{
// Start Node
if (prev is null)
{
length = 0;
return;
}
// Need to (re)calculate the length of previous nodes
if (prev.length < 0)
{
prev.CalculateLength();
}
var toPrev = Vector3.Distance(GetRacingLinePoint(), prev.GetRacingLinePoint());
length = prev.length + toPrev;
}
The member variable length
was initialized to -1
and updated as necessary.
Since the racing line could be player-defined, via tracking a ‘run’, a recursive approach gave me good results.
Sectors
A ‘Sector’ is defined as the area between the current TrackNode and the previous one. To track which sector an AI racer (or the Player) was in, I used a simple point in quad check.
For this simple approach to work, however, meant track sectors (the space between two consecutive TrackNodes
) were limited to being convex. This was fine, as concave track sectors would provide subpar results for AI steering logic, anyway.
public bool SectorContains(Vector3 testPosition)
{
// Get corner points (top left, top right, bottom left, bottom right) and test point in 2D
....
// Calculate edge vectors
var left = tl2D - bl2D;
var right = tr2D - br2D;
var top = tr2D - tl2D;
var bottom = br2D - bl2D;
// Bounds check using 2D 'Cross' (Wedge) product
return left.Cross(test2D - bl2D) < 0
&& right.Cross(test2D - tr2D) > 0
&& top.Cross(test2D - tr2D) < 0
&& bottom.Cross(test2D - br2D) > 0;
}
Alternate Paths and Shortcuts
To implement ‘fairer’ AI behaviour, I used the simplest trick in the book — a railroad-lever style shortcut. This is only activated when the Player has a sufficient lead, ensuring fairer-feeling gameplay. Since the AI was trained on player runs (mine, in particular), it was a fairly fearsome opponent.
Branched Track Node
A variant of TrackNode
that holds both a ‘normal’ and ‘alternate’ next node. The next node can then be toggled on-demand.
public override bool TryGetNextNode(out TrackNode nextNode)
{
if (!_useAlternate || alternate is null)
{
return base.TryGetNextNode(out nextNode);
}
nextNode = alternate;
return true;
}
Other than some helpful gizmo overrides, it was fairly simple.
Probabilistic Track Node
I also wanted to support a ‘failure’ path, since the payoff from the shortcut was too good, this would make it a more high-risk situation.
This was simply a BranchedTrackNode
with fuzzy logic. Uses a simple cutoff to decide whether to use the regular or alternate next node.
public override bool TryGetNextNode(out TrackNode nextNode)
{
// When asked for the next node, pick either the 'regular' one or alternate, based on a set probability.
SetUseAlternate(Random.value < alternateProbability);
return base.TryGetNextNode(out nextNode);
}
Editor Tooling
A combination of vertex snapping and custom editor tooling made setting up this track relatively easy.
The Track Node Editor tool has the following:
- Extend the track by holding
E
at the end node - Insert a node between two selected nodes by pressing
I
.
To make development easier, I bound this tool to the T
key (if a TrackNode
was selected).
Closing Thoughts
I enjoyed dipping my toes into racetrack-representation. The decision to spend some time making a simple editor tool paid off, and it’s something I plan to do a lot more of in the future.
References
- G. Biasillo. “Representing a race track for AI.” In AI Game Programming Wisdom, edited by Steve Rabin
- Simon Tomlinson and Nic Melder. “An Architecture Overview for AI in Racing Games.” In Game AI Pro, edited by Steve Rabin