The user can view race simulations. This includes giving control over the replay speed, simulations shown, cars' visibility, overlay data and being able to rewind and skip ahead in the replay.
This project is packaged with an interface which gives the user the necessary tools to plot data and gain insight into their race tracks.
The dashboard is customisable allowing the user to decide on the placement of components on their dashboard. Furthermore, there are options for changing the hotkeys and the colour associated with data metrics.
A race track generator which is capable of optimising its output towards evoking a specific player experience.
For my Master’s dissertation, I implemented a race track generator which optimises its output to evoke a specific player experience. Alongside this generator is an analytic tool that helps visualise the agent's experience playing on these generated race tracks.
The analytic tool intends to help the user more accurately evaluate the usability of the race track for a live game. The generator seeks to aid level designers, there may be design goals such as wanting a particular track shape but at its core, the objective of a level designer is to create a specific player experience.
This aspect of the project covers the ananlytic tool's features which seek to help visualise the agent's behavior while playing the race track. The user can add and remove components from their dashboard so that this dashboard is not cluttered with data they are not interested in viewing. These components are either a camera component or a graph component.
This is the generator aspect of the project which is using a Genetic Algorithm to optimise the generated race track towards providing a specific player experience. This process involves simulating a race and feeding the agent's simulation data into a human experience intensity model which calculates the agent's experience.
Through the camera component, the user can view race simulations. There are customisation options which enable the user to view multiple race simulations for the same track simultaneously. To avoid cluttering the race track with cars the user can individually toggle a car's visibility or apply a translucent filter.
A key feature of the camera component is the overlaying of data in the form of a gradient on top of the race track. The user can change which data metric that is being visualised through the overlay. Since this is a component the user can have multiple camera components with their dashboard. By adjusting the camera's position, rotation and anchor point the user can achieve different perspectives through which to view the replay.
To complement the replay system is a timeline at the bottom of the dashboard which enables being able to jump to a specific timestamp. Above certain slices of the timeline, there are icons to indicate points of interest in the replay such as a new lap.
The graph component is capable of plotting either a line or a scatter plot. The standard customisation options are present such as being able to label the axis, change the colours used, use delta difference, show confidence bounds and stack multiple graphs on top of one another. To help speed up the process of setting up a graph by default the data will be pulled from the currently selected race track, but should the user choose to they can compare the data across different race tracks.
Apart from building a tool's features, it is important to package the tool in a way which will result in a positive user experience. To help ensure this there are a set of guidelines known as Usability Heuristics. This is what inspired the flexible dashboard, being able to change hotkeys, hide UI elements, the inclusion of a help screen and the addition of an onboarding tutorial when the user opens the tool for the first time.
The generator uses a Genetic Algorithm (GA) to explore its search space and output the optimal race track which most closely matches the user target player experience. A population can also be referred to as a generation, with each generation containing a number of race tracks. A race track is represented as a chromosome, with each segment of the race track being represented by a gene. Therefore a chromosome (race track) is a collection of genes (segments of the race track). Each generation is evaluated using a fitness function and assigned a fitness score. The GA operates by creating a new generation based on the population of the previous generation. This is the core loop of the GA which uses the genetic operators: selection, crossover and mutation to produce a new set of race tracks.
Before starting the GA's core loop an initial population of race tracks needs to be generated. I took the approach of using a Voronoi diagram to obtain the points for a race track's spline.
A Voronoi diagram is a set of Voronoi cells and edges. A cell is created by randomly placing a point within the Voronoi’s grid space and expanding a circle from this point until it collides with another point’s circle bounds. A Voronoi edge is the boundary line separating different cells. The point which connects the two edges is the vertex. Therefore, within a Voronoi diagram, we have a series of vertex points with a clear path to all other points in the diagram without overlapping lines.
Extracting the race track's spline from the Voronoi diagram starts by randomly selecting an edge. Then creating a list of edges (referred to as neighbouring edges) which are in direct connection to our selected edge. A neighbour edge is randomly selected from this list and the vertex point between our edge and neighbour edge is stored as a point within the race track's spline. That neighbour edge is now the selected edge, repeating this process until we have returned to our initial edge.
This process ensures that the race track is a complete circuit with no segments overlapping as an explored edge can not be reused. On top of this by applying Perline Noise to the spline it was possible to add variation to the race track's Y-elevation throughout the spline.
A sped-up simulation of a race using the race track is performed. A race involves having four cars on the track known as agents, with one of these agents being the player agent. The player agent will be logging its data (speed, visible opponent’s speed, standing, etc) and passing this data through the human experience intensity model to obtain a player experience (PX) value between 0 and 1. After this conversion the PX value is placed in a temporary array and the lists of data are wiped.
The player agent is logging data every FixedUpdate (0.02 seconds). To avoid memory overflow a PX value is obtained when the data lists reach 50 elements, making it possible to frequently clear the data lists. At the end of each segment (gene) a final PX conversion if performed to use up the remaining logged data, followed by calculating the average PX for that segment based on the list of PX values obtained during that segment. The human experience intensity model functions by comparing the input data to the list of human play traces and outputting the experience intensity value of the human play trace which most closely resembles the current game state.
There are three scenarios in which a simulation ends. Either the player agent has completed one lap, the player agent got stuck or time ran out. An agent is considered stuck when it has not moved much distance within several frames. In this scenario, the agent is teleported to a previous waypoint. After doing this several times it is concluded that there must be an unplayable segment. The time limit is simply how long the race track is allowed to be active before being forced to end. The time limit takes into account the length of the race track and is a generous amount of time. This is more prevent any physics-based bugs such as the agent being out of bounds from halting the GA.
Once the race is concluded the program moves on to calculate the fitness score. In the scenario that a segment was not assigned an average PX value, it will be assigned a value of -1, which is unobtained. The fitness score starts at 0 and is subtracted by the difference between the simulated average PX and the target PX for that segment.
|Target PX − Simulated PX| = Amount To Subtract From Fitness
There are three possible scenarios for a race to end. Either all the player agents complete one lap, all the player agents get stuck or time runs out. An agent is considered stuck when it has moved much within several frames. In this scenario, the agent is teleported to a previous waypoint. After doing this several times in the same race it is concluded that there must be an unplayable segment. The time limit is simply how long the race track is allowed to be active before being forced to end. The time limit takes into account the length of the race track and is a generous amount of time which is intended to catch any physics-based bugs which may result in the agent being out of bounds and halting the GA from progressing.
With the race being concluded it is now possible to obtain the fitness of the race track using the array of average PX. In the scenario that the array of average PX is not equal to the number of segments in the track, it is concluded that the agent failed a segment and the ones that followed, therefore these segments are assigned an unobtained average PX of -1. The fitness starts at 0 and is subtracted by the difference between the simulated average PX and the target PX for that segment.
|Target PX − Simulated PX| = Amount To Subtract From Fitness
The Genetic Operators of selection, crossover and mutation are applied to the current population. This results in a new population of race tracks based on the data from the previous generation. This seeks to optimise the race tracks towards providing the best output, along with exploring search space.
Selection involves picking out the parent chromosomes which will be used to produce an offspring chromosome. I selected three chromosomes and had them face off in a tournament in which the winner was the chromosome with the highest fitness. This is repeated to obtain the second parent. Measures are in place to prevent the parent chromosomes from being the same, helping delay premature convergence.
Crossover oversees the transferring of gene data from the parents to the offspring. I used one-point crossover which randomly generate an index point in which to cut the list of genes from each parent. Combining the 1st half from one parent with the 2nd half from the other parent makes an offspring.
Mutation is used to explore the search space by introducing new data. The offspring's genes are iterated through and have a small chance of having their data altered. For this project's race track, this involves a 50/50 chance that the direction of that segment will now be a straight or a turn. Should it be a turn then the strength of the turn is randomly assigned between 1 and 90 along with the direction of the turn. While the elevation of the segment is simply either going to ramp up, remain level or dip down.
The output of this genetic operator is a new race track based on the existing race tracks. Doing this multiple times will create a new population of race tracks. For this generator, I took the Elistism approach which involves carrying over the highest fitness chromosome into the new population. Ensuring the population never regresses in terms of fitness.
To help reduce the time the generator takes to produce the output and efficiently use the hardware's resources several optimisations were implemented. A noticeable inclusion is the use of threading to run multiple races simultaneously, at the cost of placing a heavy burden on the hardware running the algorithm. To help reduce this stress on the hardware I used object pooling, removed all graphic rendering during simulations and converted where possible mesh colliders to box colliders.
Copyright © 2024 Kooldiagon - All Rights Reserved.
Powered by GoDaddy