I'm creating a free to play webgl game “Pirats” with combats and puzzles and wanted to create some gameplay diversity adding race levels, with two modes depending on the level, the classic end the first and another one to beat the record using bonus on the track to freeze the chrono timer, nothing new. Yes I'm a big fan of CTR. I'll write an small tutorial split in three articles on how I created a LapManager two handle the lap control, how I use my own simple AI engine and how I improved it to be challenging in the classical mode and how I implemented the fight against the clock.
First of all in our engine everything happens in a 2D plane, collisiones, AI and everything else besides 3D models. This is pretty common to save CPU computation time on another dimension, but everything could work on a 3D enviroment, instead of using 2D convex polygons one would use 3D convex hulls or instead of using lines one would use planes for example. My method is an approximation for performance improvements, JavaScript is not particullary fast, specially on mobile phones because of that I use segments to split the main route into segments, althought someone else could use a bezier curve or a catmull rom curve to be more precise I find it like overengineering, Keep It Simple!
The lap manager registers all the line segment in order that define the track natural racing order.
Just register the segments to store it in a map:
const route_segments_1 =
[
{ name: 'GvRay2(51,44)' }, { name: 'GvRay2(48,46)' }, { name: 'GvRay2(31,41)' },
{ name: 'GvRay2(13,49)' }, { name: 'GvRay2(9,31)' }, { name: 'GvRay2(14,1)' },
{ name: 'GvRay2(32,9)' }, { name: 'GvRay2(43,8)' }, { name: 'GvRay2(51,16)' }
];
level.lap_manager.set_route( level, route_segments_1 );
Beside that I register all the ships and assign a steer route defined as nodes to their Agent AI, the AI route uses poins in the 2D plane:
const route_marks_1 =
[
{ node: 'GvRaceMark4' }, { node: 'GvRaceMark12' }, { node: 'GvRaceMark1' },
{ node: 'GvRaceMark23' }, { node: 'GvRaceMark2' }, { node: 'GvRaceMark34' },
{ node: 'GvRaceMark3' }
];
ship_race_1 = level.get_object( 'GvRaceShip1' );
ship_race_1.set_auto_route( route_marks_1, 'GvRaceMark4', true, 0.4, 8.0 );
level.lap_manager.add_vehicle( ship_race_1 );
ship_race_2 = level.get_object( 'GvRaceShip2' );
ship_race_2.set_auto_route( route_marks_1, 'GvRaceMark4', true, 0.2, 8.0 );
level.lap_manager.add_vehicle( ship_race_2 );
ship_race_3 = level.get_object( 'GvRaceShip3' );
ship_race_3.set_auto_route( route_marks_1, 'GvRaceMark4', true, 0.3, 8.0 );
level.lap_manager.add_vehicle( ship_race_3 );
player = level.get_object( 'player' );
level.lap_manager.add_vehicle( player );
The lap manager receives collisions of rays when a registered ship circle collider enters in contact with a segment on the level registered as a track segment. The first segment is the one that increments the ship lap count or triggers the win/lose event. It's a simple approach that work for now until I control glitches and cheaters.
The positions are fixed once the ships finish the last lap in order and these will not change, the challenge is to calculate the dynamic position of the ships. It's all about priorities, I get all the ships that have not finish the race and I order in a certain order, first go the ships with more laps, if they have the same amout of laps then I sort it by the last race segment they have come over, as a last resource if they are in the same race segment I calcualte the distance to the line segment and the one with a further distance is the one ahead. It's an approximation but work good enough.
Nothing fancy, it works well if the ships go through the standard route, if they take a shortcut, the ship might seem to be behind once the other ships cross the next line segments but once the tricky player gets the a further line segment then he shoul be back on track, hopefully on first position or recovering a good ranking.
The algorithm doesn't take into account