TAR-3D

Time Attack Racing in 3D

This project was a roller coaster ride to work on. It’s a 3D time attack racing game that uses 3D physics and utilizes the NVidia PhysX vehicle SDK. It features a waypoint system, local multiplayer for up to 4 players, and uses the Xbox controller as the input device. Over the course of developing this project, I learned a lot about task planning, time management, and communication with stakeholders when developing games.

Here’s a video of the full loop play through of the demo developed. Inputs driven by 2 Xbox controllers using local multiplayer split-screen.

Project Details:

  • Role: Engine and Gameplay Programmer
  • Engine: Prodigy C++ Engine
  • Development Time: 5 months
  • Languages/Tools: C++ and HLSL on Prodigy Game Engine

Development Plan:

When developing this project, the most difficult part was coming up with a usable development plan. Although I used Trello and Github boards to plan my project at the start, I found myself most comfortable planning tasks and accounting for time using simple markdown or text files. Below is a plan I made post-mid-term milestone completion for my project as well as the hours logged throughout the development process:

# Time breakdowns

//------------------------------------------------------------------------------------------------------------------------------
# Task:			Description								- Time Accounted For 		- Time Spent

//------------------------------------------------------------------------------------------------------------------------------
// WEEK 1:
//------------------------------------------------------------------------------------------------------------------------------

Task 1:			Source Control Setup					- 1Hr 						- 1Hr
Task 2:			Sourcing mod kit to use					- 1Hr 						- 1.5Hr
Task 3: 		Make the Project using DFS1 Project		- 1Hr						- 3 Hr
Task 4:			Update Vehicle Code from DFS 1			- 1Hr 						- 2Hr

/////////////////////
Week 1 Summary
Time planned: 4Hrs
Time spent: 7.5Hrs

Off by: 87.5%
Hours Off: +3.5

/////////////////////

//------------------------------------------------------------------------------------------------------------------------------
WEEK 2: Plan Revision
//------------------------------------------------------------------------------------------------------------------------------

Task 5:			Check Collision Filters on Car			- 1Hr 						- 1.5Hr
Task 6:			Make a Simple racetrack using modkit	- 4Hr 						- 7Hr
Task 7:			Load OBJ files as PhysX collision mesh	- 5Hr 						- 6Hr

/////////////////////
Week 2 Summary
Time planned: 10Hrs
Time spent: 14.5Hrs

Off by: 45%
Hours Off: +4.5

/////////////////////

//------------------------------------------------------------------------------------------------------------------------------
WEEK 3:
//------------------------------------------------------------------------------------------------------------------------------

Task 8:			Event for mesh->PhysXCollider from XML	- 3Hr 						- 3Hr
Task 9:			Road and Ramp mesh loading as obstacles	- 4Hr 						- 6Hr
Task 10:		Load Multiple Convex obstacles PhysX	- 5Hr 						- 5Hr
Task 11:		Check collision on collider corners		- 1Hr 						- 0.5Hr

/////////////////////
Week 3 Summary
Time planned: 13Hrs
Time spent: 14.5Hrs

Off by: 11.53%
Hours Off: +1.5

/////////////////////

//------------------------------------------------------------------------------------------------------------------------------
WEEK 4:
//------------------------------------------------------------------------------------------------------------------------------

Task 12:		Parent mesh meta-data setups 
				for child collision meshes				- 2Hr 						- 2Hr
Task 13:		Setting up common pivot on origin		- 2Hr 						- 3Hr
Task 14:		Separating road to multiple OBJs		- 4Hr 						- 5.5Hr
Task 15:		Bugs mesh loading interfering with PhysX -  						- 6Hr

Other tasks I planned for but didn't do:

Task 16:		Make the time tracker					- 1Hr 						- 
Task 17:		Create Checkpoint System				- 3Hr 						- 

/////////////////////
Week 4 Summary
Time planned: 12Hrs
Time spent: 16.5Hrs

Off by: 37.5%
Hours Off: +4.5

/////////////////////

//------------------------------------------------------------------------------------------------------------------------------
WEEK 5: Re-Plan Revision
//------------------------------------------------------------------------------------------------------------------------------

Task 18:		Make the time tracker					- 1Hr 						- 1.5Hr
Task 19:		Create Checkpoint System				- 3Hr 						- 3Hr
Task 20:		Renderer Pick Nvidia always				- 2Hr 						- 3Hr

DEBUG:			More performance issues (No fixed time step, memory leaks)			- 6 Hrs

Other tasks I planned for but didn't do:

Task 21:		Create Split Screen System 				- 4Hr						- 
Task 22:		XML loading for checkpoint data			- 3Hr 						- 

/////////////////////
Week 5 Summary
Time planned: 13Hrs
Time spent: 13.5Hrs

Off by: 3.8%
Hours Off: +0.5

/////////////////////

//------------------------------------------------------------------------------------------------------------------------------
WEEK 6:
//------------------------------------------------------------------------------------------------------------------------------

Task 23:		Create Split Screen System 				- 4Hr						- 5Hr
Task 24:		New Schedule on Github Boards			- 2Hr 						- 1.5Hr
DEBUG:			Performance Bug							- 3Hr 						- 3Hr
Task 25:		Steps to fix performance Bug 			- 3Hr						- 4Hr

/////////////////////
Week 6 Summary
Time planned: 12Hrs
Time spent: 13.5Hrs

Off by: 12.5%
Hours Off: +1.5

/////////////////////

//------------------------------------------------------------------------------------------------------------------------------
WEEK 7:
//------------------------------------------------------------------------------------------------------------------------------

Task 26: 		Waypoint Time Tracking tier 2			- 3Hr 						- 4Hr
Task 27:		Research on Vehicle Audio				- 3Hr 						- 3Hr

Other tasks I planned for but didn't do:

Task 28:		Game Menus								- 3Hr 						- 

/////////////////////
Week 7 Summary
Time planned: 9Hrs
Time spent: 7Hrs

Off by: 28.57%
Hours Off: -2

/////////////////////

//------------------------------------------------------------------------------------------------------------------------------
WEEK 8:
//------------------------------------------------------------------------------------------------------------------------------

Task 29:		Update Vehicle Creation Logic			- 3Hr 						- 6Hr
Task 30:		Tweaks from Presentation				- 2Hr						- 3Hr

/////////////////////
Week 7 Summary
Time planned: 5Hrs
Time spent: 9Hrs

Off by: 55.55%
Hours Off: +4

/////////////////////

//------------------------------------------------------------------------------------------------------------------------------
WEEK 9:
//------------------------------------------------------------------------------------------------------------------------------

// Other projects and professional development (Pre-allocated time)

//------------------------------------------------------------------------------------------------------------------------------
WEEK 10:
//------------------------------------------------------------------------------------------------------------------------------

Task 31:		Setup Vehicle Audio system				- 3Hr 						- 3Hr
Task 32:		Implement Audio for car 				- 3Hr						- 4Hr
Task 33:		AudioSystem changes required			- 3Hr						- 2Hr

DEBUG:			Reset on car not working				- 							- 2Hr

/////////////////////
Week 10 Summary
Time planned: 9Hrs
Time spent: 11Hrs

Off by: 22.22%
Hours Off: +2

/////////////////////

//------------------------------------------------------------------------------------------------------------------------------
WEEK 11: Re-Re-Plan Revision
//------------------------------------------------------------------------------------------------------------------------------

Task 34:		Async Resource Loading					- 3Hr 						- 3.5Hr
Task 35:		Game Menus								- 3Hr 						- 

DEBUG:			Waypoint Tracking not working 			- 							- 4Hr
DEBUG:			Car data retreival wrong in Game 		-							- 2Hr

Other tasks I planned for but didn't do:

Task 36:		Car Tuner								- 3Hr						-

/////////////////////
Week 11 Summary
Time planned: 9Hrs
Time spent: 9.5Hrs

Off by: 5.5%
Hours Off: +0.5

/////////////////////

//------------------------------------------------------------------------------------------------------------------------------
WEEK 12:
//------------------------------------------------------------------------------------------------------------------------------

Task 37:		Simple Menu								- 3Hr 						- 3.5Hr


Other tasks I planned for but didn't do:

Task 38:		Car Tuner Tool							- 3Hr						-
Task 39:		Car HUD									- 3Hr 						-
Task 40:		New Track								- 3Hr 						-


/////////////////////
Week 12 Summary
Time planned: 12Hrs
Time spent: 3.5Hrs

Off by: 70%
Hours Off: -8.5

/////////////////////

//------------------------------------------------------------------------------------------------------------------------------
WEEK 13: Re-Re-Re Plan Revision
//------------------------------------------------------------------------------------------------------------------------------

Task 41:		End Game Condition						- 3Hr 						- 5Hr
Task 42:		Quaternion Math Car Reset				- 1Hr 						- 4Hr
Task 43:		Car Tuner Tool							- 3Hr 						- 9Hr
Task 44:		Tuning Car								- 3Hr 						- 4Hr

DEBUG: 			PhysX bugs on car for tuning			- 							- 4Hr

Other tasks I planned for but didn't do:

Task 45:		Car HUD									- 3Hr 						-


/////////////////////
Week 13 Summary
Time planned: 13Hrs
Time spent: 26Hrs

Off by: 100%
Hours Off: +13

/////////////////////

//------------------------------------------------------------------------------------------------------------------------------
WEEK 14: Re-Re-Re-Re Plan Revision to fuck making the car tool
//------------------------------------------------------------------------------------------------------------------------------

Task 45:		Car HUD									- 3Hr 						- 3Hr
Task 46:		Complete Game Loop with Restart			- 3Hr 						- 4Hr
Task 47:		Fixing load and save best time			- 2Hr 						- 1Hr
Task 48:		Checkpoint art change					- 2Hr 						- 3Hr

Other tasks I planned for but didn't do:

Task 49:		Fix car velocity and input on restart	- 2Hr 						- 

/////////////////////
Week 14 Summary
Time planned: 12Hrs
Time spent: 11Hrs

Off by: 0.09%
Hours Off: -1

/////////////////////

//------------------------------------------------------------------------------------------------------------------------------
WEEK 15: 
//------------------------------------------------------------------------------------------------------------------------------

Task 49:		Fix car velocity and input on restart	- 2Hr 						- 2Hr
Task 50:		Fix track scaling issues				- 3Hr 						- 3Hr
Task 51:		Add borders to splits					- 1Hr 						- 1Hr
Task 52:		Add more assets to scene				- 3Hr 						- 3Hr
Task 53:		Audio Revisit							- 2Hr 						- 3Hr

Other tasks I planned for but didn't do:

Task 54:		3,2,1 Countdown before start			- 2Hr  							- 

/////////////////////
Week 15 Summary
Time planned: 13Hrs
Time spent: 12Hrs

Off by: 0.08%
Hours Off: -1

/////////////////////


//------------------------------------------------------------------------------------------------------------------------------

Total Hours planned: 144
Total Hours Worked: 169

Off by %: 0.15%
Hours off by: +25 Hours
#Mid-Project Plan Revision

Name : Pronay
Cohort: C28
Project: TAR (Time Attack Racer)

Learnings from previous plans:

	-	Need a better way to organize per week plans
	-	Need to handle hours planned vs hours worked better
	-	Need some form of scrum when developing just to maintain sanity and ensure the correct tasks are being tackled

Solution to the planning problem
	
##Task Management:
	-	Keep all tasks listed in Github board for speed and ease of use
	-	Keep To-Do, In-Progress, Done and Cancelled Task columns for general work flow
	- 	Keep a Debug Features column for when things that are implemented need to be re-worked

##Weekly plan management:
	-	Since Github can't do everything, maintain a .md file that has a weekly breakdown
	-	Account for hours on weekly breakdown for both planned and worked hours
	-	Have a completion section where I can add the date of completion for task and number of hours used

#TAR-3D Mid Term Plan:

NOTE: Each task has it's hours planned in [ ] before the task header. Each task header also has a (Task ID: #) where each task is assigned an ID, these IDs correspond to matching tasks on Github project boards. Weekly breakdowns below:

	-	[PlannedTime]	Task Header 	(Task ID : ##)

Find the TAR Project board here: https://github.com/pronaypeddiraju/TimeAttackRacerBoards/projects/1?fullscreen=true

NOTE: The entire plan has been listed in this file along with hourly breakdowns and space to update actual hours worked for each task. The GitHub board is just for my personal use and contains no informaiton that this file doesn't contain. All Task IDs mentioned in this file correspond to tasks in the GitHub project board linked above.

##WEEK 9: VEHICLE AUDIO IMPLEMENTATION

###Plan Proposed:

	-	[3] Research on Vehicle Audio (Task ID: 1)
		-	How is Audio generally implemented for Racing Games
		-	Types of Audio files I need to use
		-	Any systemic changes required in AudioSystem?

	-	[3]	Vehicle Audio Setup (Task ID: 2)
		-	[2] Design system for Audio playback on Car class
			-	Where and when to play 3D spatial audio 
			-	Implement m_car.GetCarController().GetGearRatiosForAudio()
			-	Implement m_car.PlayAudioForGear( gearRatios, currentGear);

	-	[3] AudioSystem Implementation Changes (Task ID: 3)
		-	Account for any changes required by Audio System class to handle playback changes from general 

###On Plan Completion:

Enter the actual house used here as well as the date you worked on this task in the only correct date format (DD/MM/YYYY). Fight me on date format.

	-	[4] (00/03/2020) Research on Vehicle Audio (Task ID: 1)
	-	[6] (00/03/2020) Vehicle Audio Setup (Task ID: 2)
	-	[2] (00/03/2020) AudioSystem Implementation Changes (Task ID: 3)

Additional Tasks Performed:

	-	[3]	Debugging and Bug fixing on SplitScreen System

Total Hours Planned: 9 Hours
Total Hours Worked: X Hours

##WEEK 10: WAYPOINT SYSTEM AND VEHICLE UPDATES

###Plan Proposed:

	-	[3]	Audio Debug and Integrate (Task ID: 4)

	-	[3] Waypoint Time Tracking (Task ID: 5)
		-	[1.5] Implement fastest time tracking on waypoint system for all 4P
		-	[1.5] Write functions for save and load using xml

	-	[3]	Update Vehicle Creation Logic (Task ID: 6)
		-	[1] Update vehicle mesh dimensions to be more accurate for car convex mesh
		-	[1] Expose engine, differential and transmission variables
		-	[1] Expose sprung mass settings for wheels

###On Plan Completion:

Enter the actual house used here as well as the date you worked on this task in the only correct date format (DD/MM/YYYY). Fight me on date format.

	-	[ ] (00/03/2020) Audio Debug and Integration (Task ID: 4)
	-	[2] (00/03/2020) Waypoint Time Tracking (Task ID: 5)
	-	[ ] (00/03/2020) Update Vehicle Creation Logic (Task ID: 6)

Other Tasks Performed:

	-	[5]	Fixing issues with Debug build cause by using static variables
	-	[1]	Fixed issues with Audio for other players

Total Hours Planned: 9 Hours
Total Hours Worked: X Hours

##WEEK 11: CAR TUNING AND ASYNC RESOURCE LOAD

###Plan Proposed:

	-	[3] Implement Car Tuning Tool (Task ID: 7)
		-	[1] Expose all car variables to ImGUI debug widget interface
		-	[2] Find good defaults for vehicle settings

	-	[3]	Async Resource Loader (Task ID: 8)
		-	[1] Implement async resource loading thread
		-	[1] Implement load logic for all game meshes
		-	[1] Debug and Integration

	-	[3] Make Game menus (Task ID: 9)
		-	[1] Implement a UI main menu 
		-	[2] Implement navigation and selection logic for controller input

###On Plan Completion:

Enter the actual house used here as well as the date you worked on this task in the only correct date format (DD/MM/YYYY). Fight me on date format.

	-	[ ] (00/03/2020) Implement Car Tuning Tool (Task ID: 7)
	-	[3] (00/03/2020) Async Resource Loader (Task ID: 8)
	-	[ ] (00/03/2020) Make Game Menus (Task ID: 9)
	-	[2] Debug Task for Car values (Multiple car data retrieval was not correct)
	-	[4] Fixing timing on waypoint system on End of Race

Total : 

Total Hours Planned: 9 Hours
Total Hours Worked: X Hours

##WEEK 12: HUD IMPLEMENTATION AND NEW RACETRACK

###Plan Proposed:

	-	[3]	Create Vehicle HUD  (Task ID: 10)
		-	[1] Create a speedometer widget to use car momentum
		-	[1] Display lap information and best times
		-	[1] Render HUD with both UI widgets

	-	[3] Test and Refine HUD  (Task ID: 11)
		-	[1] Adjust widget placement
		-	[1] Debug speedometer using debug values on screen
		-	[1] Debug lap system display for all 4P recorded times

	-	[3]	Model New Race Track (Task ID: 12)
		-	[1]	Scale all mod kit pieces to be larger
		-	[2]	Implement new tack design

	-	[3] Model Race Track Collisions and meta-data (Task ID: 13)
		-	[2] Model collision meshes to be used for PhysX
		-	[1] Setup .mesh file to load all the collision mesh information


###On Plan Completion:

Enter the actual house used here as well as the date you worked on this task in the only correct date format (DD/MM/YYYY). Fight me on date format.

	-	[ ] (00/03/2020) Create Vehicle HUD (Task ID: 10)
	-	[ ] (00/03/2020) Test and Refine HUD (Task ID: 11)
	-	[ ] (00/04/2020) Model New Race Track (Task ID: 12)
	-	[ ] (00/03/2020) Model Race Track Collisions and meta-data (Task ID: 13)

Total Hours Planned: 12 Hours
Total Hours Worked: X Hours

##WEEK 13: 

###Plan Proposed: GHOST CAR AND DATA_DRIVEN CHECKPOINTS

	-	[3]	Implement Ghost Car Section 1 (Task ID: 14)
		-	[1] Track car positions through run 
		-	[1] Save off ghost car positions in xml 
		-	[1] Load ghost car positions from xml

	-	[3] Implement Ghost Car Section 2 (Task ID: 15)
		-	[1] Track wheel rotation for vehicle
		-	[2] Debug and Integration

	-	[3] Checkpoint System Data Driving (Task ID: 16)
		-	Implement fastest time tracking on waypoint system for all 4P
		-	Write functions for save and load using xml

###On Plan Completion:

Enter the actual house used here as well as the date you worked on this task in the only correct date format (DD/MM/YYYY). Fight me on date format.

	-	[ ] (00/03/2020) Implement Ghost Car Section 1 (Task ID: 14)
	-	[ ] (00/03/2020) Implement Ghost Car Section 2 (Task ID: 15)
	-	[ ] (00/03/2020) Checkpoint System Data Driving (Task ID: 16)

Total Hours Planned: 9 Hours
Total Hours Worked: X Hours

##WEEK 14: 

###Plan Proposed: CONVEYANCE AND JUICE

	-	[3]	Add Player Vehicle Conveyance (Task ID: 17)
		-	[1] Setup different colors for different player cars
		-	[1] Setup some on-screen P1,2,3,4 identification info
		-	[1] Test and Debug

	-	[3]	Implement Controller Join Screen (Task ID: 18)
		-	[1] Check for input before assigning contoller to cars
		-	[1] Show player IDs for connected controllers
		-	[1] Add join screen to OnClick play button

	-	[3]	Tier 1 Juice (Task ID: 19)
		-	[2] Implement camera shake on hit cars
		-	[1] Implement a skybox tier 1

	-	[3]	Tier 2 Juice (Task ID: 20)
		-	[2] Implement sky box tier 2
		-	[1] Refine UI

###On Plan Completion:

Enter the actual house used here as well as the date you worked on this task in the only correct date format (DD/MM/YYYY). Fight me on date format.

	-	[ ] (00/03/2020) Add Player Vehicle Conveyance (Task ID: 17)
	-	[ ] (00/03/2020) Implement Controller Join Screen (Task ID: 18)
	-	[ ] (00/03/2020) Tier 1 Juice (Task ID: 19)
	-	[ ] (00/03/2020) Tier 2 Juice (Task ID: 20)


Total Hours Planned: 12 Hours
Total Hours Worked: X Hours

##WEEK 15: 

###Plan Proposed: BUG FIXING AND WISHLIST TASKS

	-	[3] Bug Fixing (Task ID: 21)

Wishlist Tasks:

	-	[5]	Model 2nd Track (Wishlist ID: 1)
		-	[2] Model a new track
		-	[2] Model collisions
		-	[1] Setup .mesh file

	-	[6] Add a second car (Wishlist ID: 2)
		-	[1] Acquire car off the internet 
		-	[2] Setup mesh file for car
		-	[2] Tune car values

###On Plan Completion:

Enter the actual house used here as well as the date you worked on this task in the only correct date format (DD/MM/YYYY). Fight me on date format.

	-	[ ] (00/03/2020) Bug Fixing (Task ID: 21)
	-	[ ] (00/03/2020) Model 2nd Track (Wishlist ID: 1)
	-	[ ] (00/03/2020) Add a second car (Wishlist ID: 2)

Total Hours Planned: 14 Hours
Total Hours Worked: X Hours

Systems Developed:

When creating TAR 3D, I developed numerous Engine and Gameplay systems to assist in the rapid development and iteration of my project. Below are some of the notable systems I had developed during that time:

  • Car and Car Tuning
  • Data-driven OBJ file loader
  • Event-driven collision mesh generator
  • Waypoint system with time tracking
  • Custom binary formats for fast OBJ file loading
  • Split-screen system to support up to 4P split screen

Each of these systems have been described in detail below:

Car and Car Tuning

A racing game is not fun without a fun car. My first problem when developing this project was the results of my first play-test. The play-testers reported that my car was

  • Sluggish
  • Slow to turn
  • Not natural

To address this I took a closer look at my vehicle implementation and decided to tweak some values being used by the car. The structure of my Car class is highlighted below:

#pragma once
#include "Game/CarController.hpp"
#include "Game/CarCamera.hpp"
#include "Game/CarAudio.hpp"
#include "Game/WaypointSystem.hpp"
//Engine Systems
#include "Engine/Renderer/BitmapFont.hpp"

//------------------------------------------------------------------------------------------------------------------------------
class Shader;

//------------------------------------------------------------------------------------------------------------------------------
static std::vector<std::string> CAR_FILE_PATHS = {
	"/C_5_ExhL_02142.wav",
	"/C_6_ExhL_02412.wav",
	"/C_9_ExhL_03775.wav",
	"/C_10_ExhL_04308.wav",
	"/C_11_ExhL_04984.wav",
	"/C_12_ExhL_05652.wav",
	"/C_13_ExhL_06177.wav",
	"/C_14_ExhL_06669.wav"
};

//------------------------------------------------------------------------------------------------------------------------------
class Car
{
public:
	Car();
	~Car();

	void						StartUp(const Vec3& startPosition, int controllerID, float timeToBeat);
	void						Update(float deltaTime, bool isInputEnabled = true);
	void						FixedUpdate(float fixedTime);
	void						Shutdown();

	void						SetupCarAudio();

	const CarCamera&			GetCarCamera() const;
	CarCamera*					GetCarCameraEditable();
	const CarController&		GetCarController() const;
	CarController*				GetCarControllerEditable();
	const CarAudio&				GetCarAudio() const;
	CarAudio*					GetCarAudioEditable();
	int							GetCarIndex() const;
	PxRigidDynamic*				GetCarRigidbody() const;
	Camera&						GetCarHUDCamera() const;

	WaypointSystem&				GetWaypointsEditable();
	const WaypointSystem&		GetWaypoints() const;

	void						SetupNewPlaybackIDs();

	//For reset car transform we will just orient it at current forward direction and set position at current pos and y += 10;
	void						ResetCarPosition();
	void						ResetWaypointSystem();

	void						SetCameraColorTarget(ColorTargetView* colorTargetView);
	void						SetCameraPerspectiveProjection(float m_camFOVDegrees, float nearZ, float farZ, float aspect);
	void						UpdateCarCamera(float deltaTime);

	double						GetRaceTime();

	void						RenderUIHUD() const;
private:

	void						RenderBackgroundBoxes() const;
	void						RenderLapCounter() const;
	void						RenderTimeTaken() const;
	void						RenderTimeToBeat() const;
	void						RenderGearIndicator() const;
	void						RenderRevMeter() const;

private:
	CarController*				m_controller = nullptr;
	CarCamera*					m_camera = nullptr;
	CarAudio*					m_audio = nullptr;
	Camera*						m_carHUD = nullptr;

	WaypointSystem				m_waypoints;

	const std::string			m_BASE_AUDIO_PATH = "Data/Audio/Ferrari944";

	int							m_carIndex = -1;

	float						m_HUD_WIDTH = 300.f;
	float						m_HUD_HEIGHT = 150.f;

	BitmapFont*					m_HUDFont = nullptr;
	float						m_HUDFontHeight = 5.f;

	Shader*						m_HUDshader = nullptr;
	std::string					m_shaderPath = "default_unlit.xml";

	double						m_raceTime = 0.0;
	float						m_resetHeight = 2.0f;

	double						m_timeToBeat = 0.0;
};
#pragma once
#include "Engine/PhysXSystem/PhysXSystem.hpp"

class CarController
{
public:
	CarController();
	~CarController();

	void						HandleKeyPressed(unsigned char keyCode);
								
	void						SetupVehicle();
	void						SetDigitalControlMode(bool digitalControlEnabled);
								
	bool						IsDigitalInputEnabled() const;
								
	void						Update(float deltaTime);
	void						FixedUpdate(float deltaTime);
	void						UpdateInputs();
	void						VehiclePhysicsUpdate(float deltaTime);
								
	void						SetControllerIDToUse(int controllerID);

	//Vehicle Setter
	void						SetVehicleDefaultOrientation();
	void						SetVehiclePosition(const Vec3& targetPosition);
	void						SetVehicleTransform(const Vec3& targetPosition, const PxQuat& quaternion);
	void						SetVehicleTransform(const PxTransform& transform);
	void						SetNewPxVehicle(PxVehicleDrive4W* vehicle);

	//Vehicle Getters
	PxVehicleDrive4W*			GetVehicle() const;
	PxVehicleDrive4WRawInputData* GetVehicleInputData() const;
	Vec3						GetVehiclePosition() const;
	Vec3						GetVehicleForwardBasis() const;
	Vec3						GetVehicleRightBasis() const;

	//Vehicle Controls
	void	AccelerateForward(float analogAcc = 0.f);
	void	AccelerateReverse(float analogAcc = 0.f);
	void	Brake();

	void	Steer(float analogSteer = 0.f);
	void	Handbrake();

	void	ReleaseAllControls();
	void	RemoveVehicleFromScene();
	void	ReleaseVehicle();
	bool	IsControlReleased();

private:

	int			m_controllerID = 0;

private:
	bool		m_digitalControlEnabled = false;
	bool		m_isVehicleInAir = false;

	PxVehicleDrive4W*					m_vehicle4W = nullptr;
	PxVehicleDrive4WRawInputData*		m_vehicleInputData = nullptr;

public:
	bool								m_controlReleased = false;

	PxFixedSizeLookupTable<8>			m_SteerVsForwardSpeedTable;
	PxVehicleKeySmoothingData			m_keySmoothingData =
	{
		{
			6.0f,	//rise rate eANALOG_INPUT_ACCEL
			6.0f,	//rise rate eANALOG_INPUT_BRAKE		
			6.0f,	//rise rate eANALOG_INPUT_HANDBRAKE	
			2.5f,	//rise rate eANALOG_INPUT_STEER_LEFT
			2.5f,	//rise rate eANALOG_INPUT_STEER_RIGHT
		},
		{
			10.0f,	//fall rate eANALOG_INPUT_ACCEL
			10.0f,	//fall rate eANALOG_INPUT_BRAKE		
			10.0f,	//fall rate eANALOG_INPUT_HANDBRAKE	
			5.0f,	//fall rate eANALOG_INPUT_STEER_LEFT
			5.0f	//fall rate eANALOG_INPUT_STEER_RIGHT
		}
	};

	PxVehiclePadSmoothingData			m_padSmoothingData =
	{
		{
			6.0f,	//rise rate eANALOG_INPUT_ACCEL
			6.0f,	//rise rate eANALOG_INPUT_BRAKE		
			6.0f,	//rise rate eANALOG_INPUT_HANDBRAKE	
			2.5f,	//rise rate eANALOG_INPUT_STEER_LEFT
			2.5f,	//rise rate eANALOG_INPUT_STEER_RIGHT
		},
		{
			10.0f,	//fall rate eANALOG_INPUT_ACCEL
			10.0f,	//fall rate eANALOG_INPUT_BRAKE		
			10.0f,	//fall rate eANALOG_INPUT_HANDBRAKE	
			5.0f,	//fall rate eANALOG_INPUT_STEER_LEFT
			5.0f	//fall rate eANALOG_INPUT_STEER_RIGHT
		}
	};
};
#pragma once
#include "Engine/Renderer/Camera.hpp"

class CarCamera : public Camera
{
public:

	CarCamera();
	~CarCamera();

	void Update(const Vec3& carForward, float deltaTime);
	void SetFocalPoint(Vec3 const &pos);
	void SetZoom(float zoom); //Manipulates distance
	void SetAngleOffset(float angleOffset); // really is setting an angle offset
	void SetZoomDelta(float delta);

	void SetTiltValue(float tilt);
	void SetAngleValue(float angle);
	void SetHeightValue(float height);
	void SetDistanceValue(float distance);
	void SetLerpSpeed(float lerpSpeed);

	float GetAngleValue() const;
	float GetTiltValue() const;
	float GetHeightValue() const;
	float GetDistanceValue() const;
	float GetLerpSpeed() const;

private:
	Vec3			m_focalPoint = Vec3::ZERO;
	float			m_distance = 7.f;
	float			m_height = 2.f;

	float			m_currentZoom = 0.f;

	// configuration - mess with these numbers to get a view you like; 
	float			m_minDistance = 1.0f;
	float			m_maxDistance = 32.0f;

	float			m_defaultAngle = 90.0f;
	float			m_defaultTilt = -70.0f;
	float			m_lerpSpeed = 7.f;

	Vec2			m_tiltBounds = Vec2(10.f, 40.f);

	Vec3			m_camPosition = Vec3::ZERO;
	Vec3			m_targetPosition = Vec3::ZERO;

	Matrix44		m_modelMatrix = Matrix44::IDENTITY;

	//The actual tilt and angle for the camera
	float			m_tilt;
	float			m_angle;
};

To quickly iterate on the car and be able to tune it, I implemented a car tuner using ImGUI where I was able to quickly iterate and change vehicle values that allowed me to test better vehicle settings. Unfortunately, I was unable to use the tool in real-time due to some bugs I encountered when using the NVidia Vehicle system but this tool still allowed me to achieve better vehicle tuning results.

Here’s how the tool’s logic was setup in code:

#pragma once
#include "Engine/PhysXSystem/PhysXSystem.hpp"

class CarTool
{
public:
	CarTool();
	~CarTool();

	void					UpdateImGUICarTool();
	PxVehicleDrive4W*		MakeNewCar();

private:
	void		SetAllDefaults();
	void		SetDefaultVehicleDesc();
	void		SetDefaultDifferentialData();
	void		SetDefaultEngineData();
	void		SetDefaultGearData();
	void		SetDefaultClutchData();

	void		UpdateWheelData();
	void		UpdateChassisData();
	void		UpdateDifferentialData();
	void		UpdateEngineData();
	void		UpdateGearData();
	void		UpdateClutchData();

private:

	VehicleDesc						m_vehicleDesc;
	PxVehicleDriveSimData4W			m_driveSimData;

	//Internal for DriveSimData
	PxVehicleDifferential4WData		m_differnetialData;
	PxVehicleEngineData				m_engineData;
	PxVehicleGearsData				m_gearData;
	PxVehicleClutchData				m_clutchData;
	PxVehicleAckermannGeometryData  m_ackermanGeometry;

	//Internal for VehicleDesc
	PxFilterData					m_chassisSimFilter;
	PxFilterData					m_wheelSimFilter;
	ActorUserData					m_actorUserData;
	ShapeUserData					m_shapeUserData[PX_MAX_NB_WHEELS];

	float							m_defaultColumnHeight = 150.f;
	float							m_defaultColumnWidth = 500.f;
};
//------------------------------------------------------------------------------------------------------------------------------
void CarTool::UpdateImGUICarTool()
{
	ImGui::Begin("Car Tuner");

	ImGui::Columns(3, NULL, false);
	ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f);

	UpdateWheelData();
	ImGui::NextColumn();

	UpdateChassisData();
	ImGui::NextColumn();

	UpdateDifferentialData();
	ImGui::NextColumn();

	UpdateEngineData();
	ImGui::NextColumn();

	UpdateGearData();
	ImGui::NextColumn();

	ImGui::PopStyleVar();
	ImGui::End();
}

Data-driven OBJ file loader

To allow for fast OBJ file loading and handling pivot, scale and orientation changes quickly, I utilized a data-driven OBJ file loader which would perform these steps and generate a mesh that can be used within Prodigy Engine.

To do so, I created a custom .mesh file that was an XML formatted file describing some information to the Object loader when loading and generating OBJ mesh files. Here’s an example of the custom .mesh file used to load the vehicle mesh in game:

<mesh id="PhysX/car"
	src="Car/Rally_Fighter.obj"
	invert="false"
	tangents="true"
	scale = "0.025f"
	transform="x y -z">

	<material index="0" src="Car/Car.mat" />
</mesh>

Here’s the interface code for the Object loader which loads and creates a usable mesh:

//------------------------------------------------------------------------------------------------------------------------------
#pragma once
// Engine Systems
#include "Engine/Math/Vec2.hpp"
#include "Engine/Math/Vec3.hpp"
#include "Engine/Core/BufferWriteUtils.hpp"
#include "Engine/Core/BufferReadUtils.hpp"
#include "Engine/Commons/EngineCommon.hpp"
// Others
#include <string>
#include <vector>

class CPUMesh;
class GPUMesh;
class RenderContext;

//------------------------------------------------------------------------------------------------------------------------------
struct ObjIndex
{
	int vertexIndex = 0;
	int uvIndex = 0;
	int normalIndex = 0;
};

//------------------------------------------------------------------------------------------------------------------------------
class ObjectLoader
{
public:
	ObjectLoader();
	~ObjectLoader();
	static ObjectLoader*	MakeLoaderAndLoadMeshFromFile(RenderContext* renderContext, const std::string& filePath, bool isDataDriven);

	void					LoadMeshFromFile(RenderContext* renderContext, const std::string& fileName, bool isDataDriven);
	
	void					LoadFromPMSH(const std::string& fileName, Buffer& readBuffer);
	void					LoadFromXML(const std::string& fileName);
	void					CreateFromString(const char* data);
	void					AddIndexForMesh(const std::string& indices);
	void					CreateCPUMesh();
	void					CreateGPUMesh();

	void					MakeCookedVersion();
public:
	std::vector<Vec3>				m_positions;
	std::vector<Vec2>				m_uvs;
	std::vector<Vec3>				m_normals;
	std::vector<ObjIndex>			m_indices;
	
	RenderContext*					m_renderContext = nullptr;
	CPUMesh*						m_cpuMesh = nullptr;
	GPUMesh*						m_mesh = nullptr;

	std::string						m_source = "";
	std::string						m_fullFileName = "";
	std::string						m_transform = "";
	std::string						m_defaultMaterialPath = "";
	bool							m_invert = false;
	bool							m_tangents = false;
	bool							m_isCooked = false;
	float							m_scale = 0.f;

	bool							m_cookingRun = RUN_COOKING;
};

Event-driven collision mesh generator

In this game, I also needed to have numerous collision meshes for the racetrack. Although the racetrack currently being used in-game is fairly simple, it still required 210 different collision meshes for the track along with a few other static meshes that allowed for ramps to be added in the game.

In order to account for collision meshes, I created a single render mesh defined in a custom .mesh file with some set of collision meshes to be loaded separately which will not be used to render the scene but be used for the scene’s physics.

Below is the logic in Object Loader that determines what to do when a Collision tag is encountered, the logic on the Physics system that is event-driven that creates the required collision meshes and the .mesh file implementation I used for the racetrack mesh:

//------------------------------------------------------------------------------------------------------------------------------
void ObjectLoader::LoadFromXML(const std::string& fileName)
{
	//Open the xml file and parse it
	tinyxml2::XMLDocument meshDoc;
	meshDoc.LoadFile(fileName.c_str());

	if (meshDoc.ErrorID() != tinyxml2::XML_SUCCESS)
	{
	
		ERROR_AND_DIE(">> Error loading Mesh XML file ");
		return;
	}
	else
	{
		//We loaded the file successfully
		XMLElement* root = meshDoc.RootElement();

		if (root->FindAttribute("src"))
		{
			m_source = ParseXmlAttribute(*root, "src", m_source);
		}
		
		if (root->FindAttribute("invert"))
		{
			m_invert = ParseXmlAttribute(*root, "invert", false);
		}

		if (root->FindAttribute("tangents"))
		{
			m_tangents = ParseXmlAttribute(*root, "tangents", false);
		}

		if (root->FindAttribute("scale"))
		{
			m_scale = ParseXmlAttribute(*root, "scale", 1.f);
		}
		
		m_transform = ParseXmlAttribute(*root, "transform", "");

		CreateFromString((MODEL_PATH + m_source).c_str());
		CreateCPUMesh();

		XMLElement* elem = root->FirstChildElement("material");
		if (elem != nullptr)
		{
			//Set the default material path for this model from XML
			m_defaultMaterialPath = ParseXmlAttribute(*elem, "src", "");
		}

		elem = root->FirstChildElement("collision");
		while (elem != nullptr)
		{
			//We requested to create a static collider with this model so generate that using the PhysX System
			NamedProperties eventArgs;
			eventArgs.SetValue("id", ParseXmlAttribute(*root, "id", ""));
			eventArgs.SetValue("src", ParseXmlAttribute(*elem, "src", ""));
			eventArgs.SetValue("physXFlags", ParseXmlAttribute(*elem, "physXFlags", ""));
			eventArgs.SetValue("position", ParseXmlAttribute(*elem, "position", Vec3::ZERO));

			eventArgs.SetValue("transform", m_transform);
			eventArgs.SetValue("scale", m_scale);
			eventArgs.SetValue("invert", m_invert);
			eventArgs.SetValue("tangents", m_tangents);

			g_eventSystem->FireEvent("ReadCollisionMeshFromData", eventArgs);

			elem = elem->NextSiblingElement("collision");
		}
	}
}
//------------------------------------------------------------------------------------------------------------------------------
STATIC bool PhysXSystem::LoadCollisionMeshFromData(EventArgs& args)
{
	//Load the mesh
	DebuggerPrintf("\n\n Called event LoadCollisionMeshFromData");
	ObjectLoader loader;

	std::string id = "";
	std::string src = "";
	id = args.GetValue("id", id);
	src = args.GetValue("src", src);

	if (id == "")
	{
		//The id is invalid so just return
		return false;
	}
	else
	{
		//Id was valid so now let's load the mesh
		//Check the file extension
		std::vector<std::string> strings = SplitStringOnDelimiter(src, '.');
		bool isDataDriven = false;
		if (strings.size() > 1)
		{
			if (strings[(strings.size() - 1)] == "mesh")
			{
				isDataDriven = true;
			}
		}

		//Before we can CreateMeshFromFile we need to send it the required transform, scale, invert, tangent information
		loader.m_tangents = args.GetValue("tangents", loader.m_tangents);
		loader.m_scale = args.GetValue("scale", loader.m_scale);
		loader.m_transform = args.GetValue("transform", loader.m_transform);
		loader.m_invert = args.GetValue("invert", loader.m_invert);

		//This will now load the mesh from file
		loader.LoadMeshFromFile(g_renderContext, MODEL_PATH + src, isDataDriven);
	}

	//Create a PxConvexMesh from the vertex array 
	PxVec3* convexVerts = new PxVec3[loader.m_cpuMesh->GetVertexCount()];
	g_PxPhysXSystem->AddVertMasterBufferToPxVecBuffer(convexVerts, loader.m_cpuMesh->GetVertices(), loader.m_cpuMesh->GetVertexCount());

	//Create a pxDescription for the convexmesh
	PxConvexMeshDesc desc;
	desc.points.count = (PxU32)loader.m_cpuMesh->GetVertexCount();
	desc.points.stride = sizeof(PxVec3);
	desc.points.data = convexVerts;
	desc.flags = PxConvexFlag::eCOMPUTE_CONVEX;

	//Use PxCooking to construct the PxConvexMesh
	PxDefaultMemoryOutputStream buffer;
	PxConvexMeshCookingResult::Enum result;
	if (!g_PxPhysXSystem->GetPhysXCookingModule()->cookConvexMesh(desc, buffer, &result))
	{
		//There was a problem making the mesh
		//For now we are going to just yell and die
		ERROR_AND_DIE("Failed to cook collision mesh for object in LoadCollisionMeshFromData");

		return false;
	}

	//We can actually create the convexMesh
	PxDefaultMemoryInputData input(buffer.getData(), buffer.getSize());
	PxConvexMesh* convexMesh = g_PxPhysXSystem->GetPhysXSDK()->createConvexMesh(input);

	//Maintain a map of std::string id to std::vector<PxConvexMesh*> 
	std::map<std::string, std::vector<PxConvexMesh*>>::iterator itr = g_PxPhysXSystem->m_collisionMeshRepository.find(id);
	if (itr != g_PxPhysXSystem->m_collisionMeshRepository.end())
	{
		//We need to add to the existing vector of collision meshes
		itr->second.push_back(convexMesh);
	}
	else
	{
		//We need to make a new entry with the key recieved
		std::vector<PxConvexMesh*> meshVector{ convexMesh };
		g_PxPhysXSystem->m_collisionMeshRepository[id] = meshVector;
	}

	//Finally add the new PxConvexMesh to the scene as a shape
	//Make the geometry
	PxConvexMeshGeometry geometry = PxConvexMeshGeometry(convexMesh);
	const PxMaterial* material = g_PxPhysXSystem->GetDefaultPxMaterial();

	PxShape* shape = g_PxPhysXSystem->GetPhysXSDK()->createShape(geometry, *material);

	PxFilterData groundPlaneSimFilterData(COLLISION_FLAG_OBSTACLE, COLLISION_FLAG_OBSTACLE_AGAINST, 0, 0);
	PxFilterData qryFilterData;
	setupDrivableSurface(qryFilterData);
	shape->setQueryFilterData(qryFilterData);
	shape->setSimulationFilterData(groundPlaneSimFilterData);

	Vec3 position = args.GetValue("position", position);

	PxTransform localTm(VecToPxVector(position), PxQuat(PxIdentity));
	PxRigidStatic* body = g_PxPhysXSystem->GetPhysXSDK()->createRigidStatic(localTm);
	body->attachShape(*shape);
	g_PxPhysXSystem->GetPhysXScene()->addActor(*body);

	PX_ASSERT(convex);

	return true;
}
<mesh id="PhysX/ScaledTrack1Colliders"
	src="ScaledTrack/ScaledTrack1CollidersOnly.obj"
	invert="false"
	tangents="true"
	scale = "1.5f"
	transform="x y -z"
	position = "0.f, 0.f, 0.f">

	<material index="0" src="ScaledTrack/defaultTrack.mat" />

	
	<collision index="0" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_1.obj" physXFlags = "obstacle"/>
	<collision index="1" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_2.obj" physXFlags = "obstacle"/>
	<collision index="2" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_3.obj" physXFlags = "obstacle"/>
	<collision index="3" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_4.obj" physXFlags = "obstacle"/>
	<collision index="4" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_5.obj" physXFlags = "obstacle"/>
	<collision index="5" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_6.obj" physXFlags = "obstacle"/>
	<collision index="6" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_7.obj" physXFlags = "obstacle"/>
	<collision index="7" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_8.obj" physXFlags = "obstacle"/>
	<collision index="8" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_9.obj" physXFlags = "obstacle"/>
	<collision index="9" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_10.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_11.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_12.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_13.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_14.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_15.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_16.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_17.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_18.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_19.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_20.obj" physXFlags = "obstacle"/>

	
	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_21.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_22.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_23.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_24.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_25.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_26.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_27.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_28.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_29.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_30.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_31.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_32.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_33.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_34.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_35.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_36.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_37.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_38.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_39.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_40.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_41.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_42.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_43.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_44.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_45.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_46.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_47.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_48.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_49.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_50.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_51.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_52.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_53.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_54.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_55.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_56.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_57.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_58.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_59.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_60.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_61.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_62.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_63.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_64.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_65.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_66.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_67.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_68.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_69.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_70.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_71.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_72.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_73.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_74.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_75.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_76.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_77.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_78.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_79.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_80.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_81.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_82.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_83.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_84.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_85.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_86.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_87.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_88.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_89.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_90.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_91.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_92.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_93.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_94.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_95.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_96.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_97.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_98.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_99.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_100.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_101.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_102.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_103.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_104.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_105.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_106.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_107.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_108.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_109.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_110.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_111.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_112.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_113.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_114.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_115.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_116.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_117.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_118.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_119.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_120.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_121.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_122.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_123.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_124.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_125.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_126.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_127.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_128.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_129.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_130.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_131.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_132.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_133.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_134.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_135.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_136.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_137.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_138.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_139.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_140.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_141.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_142.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_143.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_144.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_145.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_146.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_147.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_148.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_149.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_150.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_151.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_152.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_153.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_154.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_155.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_156.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_157.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_158.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_159.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_160.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_161.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_162.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_163.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_164.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_165.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_166.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_167.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_168.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_169.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_170.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_171.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_172.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_173.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_174.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_175.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_176.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_177.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_178.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_179.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_180.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_181.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_182.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_183.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_184.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_185.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_186.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_187.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_188.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_189.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_190.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_191.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_192.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_193.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_194.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_195.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_196.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_197.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_198.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_199.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_200.obj" physXFlags = "obstacle"/>

	<collision index="10" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_201.obj" physXFlags = "obstacle"/>
	<collision index="11" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_202.obj" physXFlags = "obstacle"/>
	<collision index="12" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_203.obj" physXFlags = "obstacle"/>
	<collision index="13" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_204.obj" physXFlags = "obstacle"/>
	<collision index="14" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_205.obj" physXFlags = "obstacle"/>
	<collision index="15" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_206.obj" physXFlags = "obstacle"/>
	<collision index="16" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_207.obj" physXFlags = "obstacle"/>
	<collision index="17" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_208.obj" physXFlags = "obstacle"/>
	<collision index="18" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_209.obj" physXFlags = "obstacle"/>
	<collision index="19" src="ScaledTrack/SimplexCollisions/ScaledTrack1_Simplex_210.obj" physXFlags = "obstacle"/>

	<collision index="98" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_9.obj" physXFlags = "obstacle"/>
	<collision index="99" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_10.obj" physXFlags = "obstacle"/>
	<collision index="910" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_11.obj" physXFlags = "obstacle"/>
	<collision index="911" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_12.obj" physXFlags = "obstacle"/>
	<collision index="912" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_13.obj" physXFlags = "obstacle"/>
	<collision index="913" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_14.obj" physXFlags = "obstacle"/>
	<collision index="914" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_15.obj" physXFlags = "obstacle"/>
	<collision index="915" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_16.obj" physXFlags = "obstacle"/>
	<collision index="916" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_17.obj" physXFlags = "obstacle"/>
	<collision index="917" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_18.obj" physXFlags = "obstacle"/>
	<collision index="918" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_19.obj" physXFlags = "obstacle"/>
	<collision index="919" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_20.obj" physXFlags = "obstacle"/>

	<collision index="63" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_RoadPiece_1.obj" physXFlags = "obstacle"/>
	<collision index="64" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_RoadPiece_2.obj" physXFlags = "obstacle"/>
	<collision index="65" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_RoadPiece_3.obj" physXFlags = "obstacle"/>
	<collision index="66" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_RoadPiece_4.obj" physXFlags = "obstacle"/>
	<collision index="67" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_RoadPiece_5.obj" physXFlags = "obstacle"/>
	<collision index="68" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_RoadPiece_6.obj" physXFlags = "obstacle"/>
	
	<collision index="93" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_RoadPiece_7.obj" physXFlags = "obstacle"/>
	<collision index="94" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_RoadPiece_8.obj" physXFlags = "obstacle"/>
	<collision index="95" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_RoadPiece_9.obj" physXFlags = "obstacle"/>
	<collision index="96" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_RoadPiece_10.obj" physXFlags = "obstacle"/>
	<collision index="97" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_RoadPiece_11.obj" physXFlags = "obstacle"/>
	<collision index="98" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_RoadPiece_12.obj" physXFlags = "obstacle"/>
	<collision index="297" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_RoadPiece_13.obj" physXFlags = "obstacle"/>
	<collision index="298" src="ScaledTrack/RampRoadCollisions/ScaledTrack1_Collider_RoadPiece_14.obj" physXFlags = "obstacle"/>

</mesh>

Waypoint system with time tracking

It’s not a time attack racing game without checkpoints/waypoints now is it? To implement waypoints in the game, I created a gameplay system called the WaypointSystem and a WaypointRegionBased class which defined the boundary and attributes of a waypoint.

The waypoint system would own all the waypoints in the level and each level is to have 1 waypoint system. The waypoints owned by the waypoint system would be stored in a list with waypoint IDs. On crossing a waypoint, the waypoint system will update and change the next waypoint ID to be crossed by the player.

Below is the implementation of the region based waypoint and waypoint system:

#pragma once
#include "Engine/Math/Vec3.hpp"
#include "Engine/Math/AABB3.hpp"
#include "Engine/Commons/EngineCommon.hpp"

//------------------------------------------------------------------------------------------------------------------------------
class WaypointRegionBased
{
public:
	WaypointRegionBased();
	~WaypointRegionBased();
	explicit WaypointRegionBased(const Vec3& wayPointPosition, const Vec3& halfExtents, uint waypointIndex);
	explicit WaypointRegionBased(const Vec3& waypointPosition, const AABB3& waypointShape, uint waypointIndex);

	bool				HasPointCrossedWaypoint(const Vec3& pointToCheck);

	const Vec3&			GetWaypointMins() const;
	const Vec3&			GetWaypointMaxs() const;

	void				AssignWaypointNumber(uint numberToAssign);
	uint				GetWaypointNumber() const;

	const Vec3&			GetWaypointPosition() const;

private:
	uint		m_waypointIndex = UINT_MAX;

	Vec3		m_position = Vec3::ZERO;
	Vec3		m_halfExtents = Vec3::ZERO;
	AABB3		m_shape;
};
#include "Game/WaypointRegionBased.hpp"
//Engine Systems
#include "Engine/Commons/EngineCommon.hpp"
#include "Engine/Math/MathUtils.hpp"
#include "Engine/Math/AABB3.hpp"

//------------------------------------------------------------------------------------------------------------------------------
WaypointRegionBased::WaypointRegionBased()
{

}

//------------------------------------------------------------------------------------------------------------------------------
WaypointRegionBased::WaypointRegionBased(const Vec3& wayPointPosition, const Vec3& halfExtents, uint waypointIndex)
{
	m_position = wayPointPosition;
	m_halfExtents = halfExtents;

	m_shape = AABB3(m_position - m_halfExtents, m_position + m_halfExtents);
	
	m_waypointIndex = waypointIndex;
}

//------------------------------------------------------------------------------------------------------------------------------
WaypointRegionBased::WaypointRegionBased(const Vec3& waypointPosition, const AABB3& waypointShape, uint waypointIndex)
{
	m_position = waypointPosition;
	m_shape = waypointShape;
	m_waypointIndex = waypointIndex;
}

//------------------------------------------------------------------------------------------------------------------------------
bool WaypointRegionBased::HasPointCrossedWaypoint(const Vec3& pointToCheck)
{
	return m_shape.IsPointInsideAABB3(pointToCheck);
}

//------------------------------------------------------------------------------------------------------------------------------
const Vec3& WaypointRegionBased::GetWaypointMins() const
{
	return m_shape.GetMins();
}

//------------------------------------------------------------------------------------------------------------------------------
const Vec3& WaypointRegionBased::GetWaypointMaxs() const
{
	return m_shape.GetMaxs();
}

//------------------------------------------------------------------------------------------------------------------------------
void WaypointRegionBased::AssignWaypointNumber(uint numberToAssign)
{
	m_waypointIndex = numberToAssign;
}

//------------------------------------------------------------------------------------------------------------------------------
uint WaypointRegionBased::GetWaypointNumber() const
{
	return m_waypointIndex;
}

//------------------------------------------------------------------------------------------------------------------------------
const Vec3& WaypointRegionBased::GetWaypointPosition() const
{
	return m_position;
}

//------------------------------------------------------------------------------------------------------------------------------
WaypointRegionBased::~WaypointRegionBased()
{

}
#pragma once
//Engine Systems
#include "Engine/Commons/EngineCommon.hpp"
//Game Systems
#include "Game/WaypointRegionBased.hpp"
#include <vector>

//------------------------------------------------------------------------------------------------------------------------------
class WaypointSystem 
{
public:
	WaypointSystem();
	~WaypointSystem();

	void					AddNewWayPoint(const Vec3& waypointPosition, const Vec3& waypointHalfExtents, uint waypointIndex);
	uint					GetNextWaypointIndex() const;
	uint					GetCurrentWaypointIndex() const;
	const Vec3&				GetNextWaypointPosition() const;
	Matrix44				GetNextWaypointModelMatrix() const;

	uint					GetCurrentLapNumber() const;
	uint					GetMaxLapCount() const;
	void					SetMaxLapCount(uint maxLapCount);

	double					GetTotalTime() const;
	bool					AreLapsComplete() const;

	void					Startup();
	void					Update(const Vec3& carPosition);
	
	void					RenderNextWaypoint() const;
	void					DebugRenderWaypoints() const;

	void					UpdateImGUIForWaypoints();

	void					Reset();

private:
	void					SetSystemToNextWaypoint();
	void					AddTimeStampForLap();
	double					GetLastLapTime();
	double					GetAccumulatedLapTimes() const;

	void					ComputeBestLapTimeForRun();

private:
	std::vector<WaypointRegionBased> m_waypointList;
	uint					m_crossedIndex = UINT_MAX;	//This is Uint max. I'm not stupid this was on purpose
	uint					m_lapIndex = 1;
	uint					m_maxLaps = 1;

	bool					m_lapsCompleted = false;

	double					m_startTime = 0.0;

	std::vector<double>		m_timeStamps;
};
#include "Game/WaypointSystem.hpp"
//Engine Systems
#include "Engine/Math/Vertex_Lit.hpp"   
#include "Engine/Renderer/CPUMesh.hpp"
#include "Engine/Renderer/GPUMesh.hpp"
#include "Engine/Renderer/RenderContext.hpp"
#include "Engine/Core/DevConsole.hpp"
#include "Engine/Core/Time.hpp"
#include "Engine/Core/NamedProperties.hpp"

//------------------------------------------------------------------------------------------------------------------------------
WaypointSystem::WaypointSystem()
{

}

//------------------------------------------------------------------------------------------------------------------------------
WaypointSystem::~WaypointSystem()
{

}

//------------------------------------------------------------------------------------------------------------------------------
void WaypointSystem::AddNewWayPoint(const Vec3& waypointPosition, const Vec3& waypointHalfExtents, uint waypointIndex)
{
	m_waypointList.emplace_back(waypointPosition, waypointHalfExtents, waypointIndex);
}

//------------------------------------------------------------------------------------------------------------------------------
uint WaypointSystem::GetNextWaypointIndex() const
{
	uint nextIndex = m_crossedIndex + 1;
	if (nextIndex < m_waypointList.size())
	{
		return nextIndex;
	}
	else
	{
		return 0;
	}
}

//------------------------------------------------------------------------------------------------------------------------------
uint WaypointSystem::GetCurrentWaypointIndex() const
{
	return m_crossedIndex;
}

//------------------------------------------------------------------------------------------------------------------------------
const Vec3& WaypointSystem::GetNextWaypointPosition() const
{
	int index = GetNextWaypointIndex();
	return m_waypointList[index].GetWaypointPosition();
}

//------------------------------------------------------------------------------------------------------------------------------
Matrix44 WaypointSystem::GetNextWaypointModelMatrix() const
{
	Matrix44 modelMatrix = Matrix44::IDENTITY;

	modelMatrix.SetTranslation3D(GetNextWaypointPosition(), modelMatrix);

	return modelMatrix;
}

//------------------------------------------------------------------------------------------------------------------------------
uint WaypointSystem::GetCurrentLapNumber() const
{
	return m_lapIndex;
}

//------------------------------------------------------------------------------------------------------------------------------
uint WaypointSystem::GetMaxLapCount() const
{
	return m_maxLaps;
}

//------------------------------------------------------------------------------------------------------------------------------
void WaypointSystem::SetMaxLapCount(uint maxLapCount)
{
	m_maxLaps = maxLapCount;
}

//------------------------------------------------------------------------------------------------------------------------------
double WaypointSystem::GetTotalTime() const
{
	if (m_lapsCompleted)
	{
		return m_timeStamps[m_timeStamps.size() - 1];
	}
	else
	{
		return GetAccumulatedLapTimes();
	}
}

//------------------------------------------------------------------------------------------------------------------------------
bool WaypointSystem::AreLapsComplete() const
{
	return m_lapsCompleted;
}

//------------------------------------------------------------------------------------------------------------------------------
void WaypointSystem::Startup()
{
	m_startTime = GetCurrentTimeSeconds();
}

//------------------------------------------------------------------------------------------------------------------------------
void WaypointSystem::Update(const Vec3& carPosition)
{
	if (m_lapsCompleted)
	{
		//We finished the track. There are no more waypoints
		return;
	}

	std::vector<WaypointRegionBased>::iterator waypointItr;
	waypointItr = m_waypointList.begin();

	while (waypointItr != m_waypointList.end())
	{
		bool result = waypointItr->HasPointCrossedWaypoint(carPosition);

		if (result)
		{
			//Check if this is the valid next way point
			uint wayPointIndex = waypointItr->GetWaypointNumber();
			if (wayPointIndex == GetNextWaypointIndex())
			{
				g_devConsole->PrintString(Rgba::YELLOW, "Reached Next Waypoint");
				//Call update waypoint and return from here
				SetSystemToNextWaypoint();
			}
		}

		waypointItr++;
	}

}

//------------------------------------------------------------------------------------------------------------------------------
void WaypointSystem::RenderNextWaypoint() const
{
	if (m_lapsCompleted)
		return;

	const WaypointRegionBased* nextWaypoint = &m_waypointList[GetNextWaypointIndex()];

	CPUMesh boxMesh;

	CPUMesh postLeftMesh;
	CPUMesh postRightMesh;
	CPUMesh postTopMesh;

	Vec3 mins = nextWaypoint->GetWaypointMins();
	Vec3 maxs = nextWaypoint->GetWaypointMaxs();
	CPUMeshAddCube(&postLeftMesh, AABB3(mins - Vec3(0.25f, 0.f, 0.25f), mins + Vec3(0.25f, maxs.y * 2.f, 0.25f)), Rgba::ORGANIC_BLUE);
	CPUMeshAddCube(&postRightMesh, AABB3(Vec3(maxs.x, mins.y, maxs.z) - Vec3(0.25f, 0.f, 0.25f), Vec3(maxs.x, mins.y, maxs.z) + Vec3(0.25f, maxs.y * 2.f, 0.25f)), Rgba::ORGANIC_BLUE);

	Vec3 dir = maxs - mins;
	dir.y = 0.5f;
	CPUMeshAddCube(&postTopMesh, AABB3(Vec3(mins.x, maxs.y, mins.z), Vec3(mins.x, maxs.y, mins.z) + dir), Rgba::ORGANIC_BLUE);
	
	GPUMesh renderMeshLeftPost = GPUMesh(g_renderContext);
	renderMeshLeftPost.CreateFromCPUMesh<Vertex_Lit>(&postLeftMesh, GPU_MEMORY_USAGE_STATIC);

	GPUMesh renderMeshRightPost = GPUMesh(g_renderContext);
	renderMeshRightPost.CreateFromCPUMesh<Vertex_Lit>(&postRightMesh, GPU_MEMORY_USAGE_STATIC);
	
	GPUMesh renderMeshTopPost = GPUMesh(g_renderContext);
	renderMeshTopPost.CreateFromCPUMesh<Vertex_Lit>(&postTopMesh, GPU_MEMORY_USAGE_STATIC);

	g_renderContext->DrawMesh(&renderMeshLeftPost);
	g_renderContext->DrawMesh(&renderMeshRightPost);
	g_renderContext->DrawMesh(&renderMeshTopPost);
}

//------------------------------------------------------------------------------------------------------------------------------
void WaypointSystem::DebugRenderWaypoints() const
{
	CPUMesh boxMesh;
	GPUMesh mesh = GPUMesh(g_renderContext);

	std::vector<WaypointRegionBased>::const_iterator waypointItr = m_waypointList.begin();
	while (waypointItr != m_waypointList.end())
	{
		boxMesh.Clear();

		CPUMeshAddCube(&boxMesh, AABB3(waypointItr->GetWaypointMins(), waypointItr->GetWaypointMaxs()), Rgba::GREEN);
		mesh.CreateFromCPUMesh<Vertex_Lit>(&boxMesh, GPU_MEMORY_USAGE_STATIC);
		g_renderContext->DrawMesh(&mesh);

		waypointItr++;
	}

}

//------------------------------------------------------------------------------------------------------------------------------
void WaypointSystem::UpdateImGUIForWaypoints()
{
	//Create the actual ImGUI widget
	ImGui::Begin("PhysX Scene Controls");

	ImGui::Text("Number of Waypoints in System : %d", m_waypointList.size());
	ImGui::Text("Max Number of laps allowed for this track: %d", m_maxLaps);

	ImGui::End();
}

//------------------------------------------------------------------------------------------------------------------------------
void WaypointSystem::Reset()
{
	m_lapsCompleted = false;
	m_startTime = GetCurrentTimeSeconds();
	m_timeStamps.clear();
	m_lapIndex = 1;
	m_crossedIndex = UINT_MAX;
}

//------------------------------------------------------------------------------------------------------------------------------
void WaypointSystem::SetSystemToNextWaypoint()
{
	//Increase m_crossedIndex;
	m_crossedIndex += 1;

	if (m_crossedIndex == m_waypointList.size() - 1)
	{
		g_devConsole->PrintString(Rgba::ORGANIC_PURPLE, "Entered the next Lap");
		AddTimeStampForLap();
		std::string printString = "Time Taken: " + ToString(m_timeStamps[m_timeStamps.size() - 1]);
		g_devConsole->PrintString(Rgba::GREEN, printString);
		m_crossedIndex = UINT_MAX;
		m_lapIndex += 1;
	}

	if (m_lapIndex > m_maxLaps)
	{
		g_devConsole->PrintString(Rgba::GREEN, "Completed Race");

		m_lapIndex = m_maxLaps; //Get it back to maxLaps just so that our UI doesn't say lap 4 of 3
		m_lapsCompleted = true;
	}
}

//------------------------------------------------------------------------------------------------------------------------------
void WaypointSystem::AddTimeStampForLap()
{
	m_timeStamps.push_back(GetLastLapTime());
}

//------------------------------------------------------------------------------------------------------------------------------
double WaypointSystem::GetLastLapTime()
{
	std::vector<double>::const_iterator timeItr = m_timeStamps.begin();
	if (timeItr == m_timeStamps.end())
	{
		return GetCurrentTimeSeconds() - m_startTime;
	}
	else
	{
		double accumulatedTime = 0.0;

		while (timeItr != m_timeStamps.end())
		{
			accumulatedTime += *timeItr;

			timeItr++;
		}
		
		return GetCurrentTimeSeconds() - m_startTime - accumulatedTime;
	}
}

//------------------------------------------------------------------------------------------------------------------------------
double WaypointSystem::GetAccumulatedLapTimes() const
{
	std::vector<double>::const_iterator timeItr = m_timeStamps.begin();
	double accumulatedTime = 0.0;

	while (timeItr != m_timeStamps.end())
	{
		accumulatedTime += *timeItr;

		timeItr++;
	}

	if (accumulatedTime == 0.0)
	{
		//We didn't get any accumulated time yet
		accumulatedTime = GetCurrentTimeSeconds() - m_startTime;
	}

	return accumulatedTime;
}

Custom binary formats for fast OBJ file loading

The Prodigy C++ Engine used text parsing to load OBJ files which initially proved to be an acceptable solution. However, when over 200 OBJ files are to be loaded and the complexity of each of the OBJ files is high, the application would spend significant time on loading up meshes to use in-game. To counter this problem, I implemented a custom binary 3D file format to speed up the loading process. Below are the results of changing from a text parsing OBJ loader to .pmsh custom file loader:

---------------------------
TAR Cooked OBJ test
---------------------------
Base Meshes = (2 Files)
Track Meshes = (220 files approx)
---------------------------
Pre-Cooking Performance:
---------------------------
Release:
Base Meshes: 1.632079
Track Meshes: 1.499031
---------------------------
DebugInline:
Base Meshes: 227.814758
Track Meshes: 16.094894
---------------------------
Debug:
Base Meshes: 726.031677
Track Meshes: 51.747803
---------------------------
---------------------------
Post Cooking Performance
---------------------------
---------------------------
Release: 
Base Meshes: 0.019409
Track Meshes: 0.281304
---------------------------
DebugInline:
Base Meshes: 0.128675
Track Meshes: 16.159714
---------------------------
Debug:
Base Meshes: 0.289492
Track Meshes: 49.004589
---------------------------
---------------------------

To do this, I implemented a cooking feature in my Object Loader class that allowed me to load OBJ files via XML and text parsing for the first mesh load and then saving a custom .pmsh file format. For each subsequent loads, the object loader would check if a .pmsh file exists and prefer to load that instead.

Below is the implementation of the custom file format:

# Cooking utils file specification

	.PMSH	- Prodigy Mesh file

# File Header

//Header

(4 Byte Array) FourCC : PMSH
(1 Byte) Reserved
(1 Byte) Major File Version Number: 1 currently
(1 Byte) Minor File Version Number: 0 currently
(1 Byte) Endianness (1 = little, 2 = Big)

# Mesh Data

//Mesh Data

(4 Byte - uint32)	Num Vertices
(4 Byte - uint32)	Num indices

# Vertex Data

//Vertex Data (Set of all Vertex info)

....

(12 Bytes - Vec3) - position
(12 Bytes - Vec3) - normal
(12 Bytes - Vec3) - tangent
(12 Bytes - Vec3) - biTangent
(16 Bytes - Rgba) - color
(8 Bytes - Vec2) - UVs

...

# Index Data

//Index Data (Set of all indics)

...

(4 Bytes - uint32) - indice

...
//------------------------------------------------------------------------------------------------------------------------------
void ObjectLoader::MakeCookedVersion()
{
	if (m_isCooked || !m_cookingRun)
	{
		DebuggerPrintf("\n Mesh is already a cooked mesh");
		return;
	}

	//Write cooked version to disk

	Buffer buffer;
	BufferWriteUtils writeBufferUtil(buffer);

	//Header
	//Four CC
	writeBufferUtil.AppendByte('P');
	writeBufferUtil.AppendByte('M');
	writeBufferUtil.AppendByte('S');
	writeBufferUtil.AppendByte('H');

	//Reserved Byte
	writeBufferUtil.AppendByte(0);

	//File Number
	writeBufferUtil.AppendByte(1);
	writeBufferUtil.AppendByte(0);

	//Endianness
	writeBufferUtil.AppendByte(writeBufferUtil.m_endianMode);

	//Mesh Data
	uint numVertices = m_cpuMesh->GetVertexCount();
	uint numIndices = m_cpuMesh->GetIndexCount();
	writeBufferUtil.AppendUint32(numVertices);
	writeBufferUtil.AppendUint32(numIndices);

	//Vertex Data
	const VertexMaster* vertices = m_cpuMesh->GetVertices();
	for (int vertexIndex = 0; vertexIndex < (int)numVertices; vertexIndex++)
	{
		writeBufferUtil.AppendVertexMaster(vertices[vertexIndex]);
	}

	//Index Data
	const uint* indices = m_cpuMesh->GetIndices();
	for (int indiceIndex = 0; indiceIndex < (int)numIndices; indiceIndex++)
	{
		writeBufferUtil.AppendUint32(indices[indiceIndex]);
	}

	std::string fileSavePath = "";
	std::vector<std::string> splits = SplitStringOnDelimiter(m_fullFileName, '.');
	if (splits[splits.size() - 1] == "mesh" || splits[splits.size() - 1] ==  "obj")
	{
		//Write source except the extention
		for (int i = 0; i < splits.size() - 1; i++)
		{
			fileSavePath += splits[i];
		}

		fileSavePath += ".pmsh";
	}

	bool success = SaveBinaryFileFromBuffer(fileSavePath, buffer);
	if (success)
	{
		DebuggerPrintf("\n Sucessfully cooked %s PMSH to disk", fileSavePath.c_str());
	}
	else
	{
		DebuggerPrintf("\n Failed to cook %s PMSH to disk", fileSavePath.c_str());
	}
}
//------------------------------------------------------------------------------------------------------------------------------
void ObjectLoader::LoadFromPMSH(const std::string& fileName, Buffer& readBuffer)
{
	BufferReadUtils readUtils(readBuffer);

	//Check FourCC
	uchar* fourCC = new uchar[4];
	readUtils.ParseByteArray(fourCC, 4);

	if (fourCC[0] != 'P' || fourCC[1] != 'M' || fourCC[2] != 'S' || fourCC[3] != 'H')
	{
		ERROR_AND_DIE("FourCC code mismatch for PMSH");
	}

	uchar byte = readUtils.ParseByte();
	uchar versionMajor = readUtils.ParseByte();
	if (versionMajor != 1)
	{
		ERROR_AND_DIE("Major Version mismatch for PMSH");
	}
	uchar versionMinor = readUtils.ParseByte();
	if (versionMinor != 0)
	{
		ERROR_AND_DIE("Minor Version mismatch for PMSH");
	}

	eBufferEndianness endianNess = (eBufferEndianness)readUtils.ParseByte();
	readUtils.SetEndianMode(endianNess);

	uint numVerts = readUtils.ParseUint32();
	uint numIndices = readUtils.ParseUint32();

	m_cpuMesh = new CPUMesh();
	m_cpuMesh->ReserveForNumVertices(numVerts);
	m_cpuMesh->ReserveForNumIndices(numIndices);

	//Copy all the verts
	VertexMaster tempVertex;
	for (int vertexIndex = 0; vertexIndex < (int)numVerts; vertexIndex++)
	{
		tempVertex = readUtils.ParseVertexMaster();
		m_cpuMesh->AddVertex(tempVertex);
	}

	//Indice data
	uint tempIndice = 0;
	for (int indiceIndex = 0; indiceIndex < (int)numIndices; indiceIndex++)
	{
		tempIndice = readUtils.ParseUint32();
		m_cpuMesh->AddIndex(tempIndice);
	}
}

Split-screen system to support up to 4P split-screen

To incorporate an arcade racing feel, I wanted to implement a split-screen local multiplayer system. To do so, I allowed the game to use a maximum of 4 Xbox controllers and at startup figuring out how many cars needed to be spawned along with the desired screen splits.

To create the split-screen view, I created a set of fixed screen ratios that would be applied to the camera viewports. By allowing the camera to render to a specific region on the screen, I was able to achieve the desired results. The logic for the split-screen system is highlighted below:

//------------------------------------------------------------------------------------------------------------------------------
void Game::SetFrameColorTargetOnCameras() const
{
	//Get the ColorTargetView from rendercontext
	ColorTargetView *colorTargetView = g_renderContext->GetFrameColorTarget();

	//Setup what we are rendering to
	m_mainCamera->SetColorTarget(colorTargetView);
	m_mainCamera->SetViewport(Vec2(0.5f, 0.f), Vec2::ONE);
	m_devConsoleCamera->SetColorTarget(colorTargetView);

	m_UICamera->SetColorTarget(colorTargetView);
	m_UICamera->SetViewport(Vec2::ZERO, Vec2::ONE);

	if (m_initiateFromMenu)
	{
		//The cars exist and we can set the split screen system's color targets
		m_splitScreenSystem.SetColorTargets(colorTargetView);
		m_splitScreenSystem.ComputeViewPortSplits(m_splitMode);
		SetCarHUDColorTargets(colorTargetView);
		SetupCarHUDsFromSplits(m_splitMode);
	}
}
//------------------------------------------------------------------------------------------------------------------------------
void Game::SetupCarHUDsFromSplits(eSplitMode splitMode) const
{
	switch (m_numConnectedPlayers)
	{
	case 1:
	{
		//We have only 1 player so we should be fine with the viewport being the whole screen
		m_cars[0]->GetCarHUDCamera().SetViewport(Vec2::ZERO, Vec2::ONE);
	}
	break;
	case 2:
	{
		//We have 2 players, check the split mode for 2P and split accordingly
		if (splitMode == PREFER_VERTICAL_SPLIT)
		{
			//We need to split the screen into vertical halfs
			m_cars[0]->GetCarHUDCamera().SetViewport(SplitScreenSystem::VERTICAL_SPLIT_2P_FIRST_MIN, SplitScreenSystem::VERTICAL_SPLIT_2P_FIRST_MAX);
			m_cars[1]->GetCarHUDCamera().SetViewport(SplitScreenSystem::VERTICAL_SPLIT_2P_SECOND_MIN, SplitScreenSystem::VERTICAL_SPLIT_2P_SECOND_MAX);
		}
		else
		{
			//We need to split the screen into horizontal halfs
			m_cars[0]->GetCarHUDCamera().SetViewport(SplitScreenSystem::HORIZONTAL_SPLIT_2P_FIRST_MIN, SplitScreenSystem::HORIZONTAL_SPLIT_2P_FIRST_MAX);
			m_cars[1]->GetCarHUDCamera().SetViewport(SplitScreenSystem::HORIZONTAL_SPLIT_2P_SECOND_MIN, SplitScreenSystem::HORIZONTAL_SPLIT_2P_SECOND_MAX);
		}
	}
	break;
	case 3:
	{
		if (splitMode == PREFER_VERTICAL_SPLIT)
		{
			m_cars[0]->GetCarHUDCamera().SetViewport(SplitScreenSystem::VERTICAL_SPLIT_3P_FIRST_MIN, SplitScreenSystem::VERTICAL_SPLIT_3P_FIRST_MAX);
			m_cars[1]->GetCarHUDCamera().SetViewport(SplitScreenSystem::VERTICAL_SPLIT_3P_SECOND_MIN, SplitScreenSystem::VERTICAL_SPLIT_3P_SECOND_MAX);
			m_cars[2]->GetCarHUDCamera().SetViewport(SplitScreenSystem::VERTICAL_SPLIT_3P_THIRD_MIN, SplitScreenSystem::VERTICAL_SPLIT_3P_THIRD_MAX);
		}
		else
		{
			m_cars[0]->GetCarHUDCamera().SetViewport(SplitScreenSystem::HORIZONTAL_SPLIT_3P_FIRST_MIN, SplitScreenSystem::HORIZONTAL_SPLIT_3P_FIRST_MAX);
			m_cars[1]->GetCarHUDCamera().SetViewport(SplitScreenSystem::HORIZONTAL_SPLIT_3P_SECOND_MIN, SplitScreenSystem::HORIZONTAL_SPLIT_3P_SECOND_MAX);
			m_cars[2]->GetCarHUDCamera().SetViewport(SplitScreenSystem::HORIZONTAL_SPLIT_3P_THIRD_MIN, SplitScreenSystem::HORIZONTAL_SPLIT_3P_THIRD_MAX);
		}
	}
	break;
	case 4:
	{
		if (splitMode == PREFER_VERTICAL_SPLIT)
		{
			m_cars[0]->GetCarHUDCamera().SetViewport(SplitScreenSystem::VERTICAL_SPLIT_4P_FIRST_MIN, SplitScreenSystem::VERTICAL_SPLIT_4P_FIRST_MAX);
			m_cars[1]->GetCarHUDCamera().SetViewport(SplitScreenSystem::VERTICAL_SPLIT_4P_SECOND_MIN, SplitScreenSystem::VERTICAL_SPLIT_4P_SECOND_MAX);
			m_cars[2]->GetCarHUDCamera().SetViewport(SplitScreenSystem::VERTICAL_SPLIT_4P_THIRD_MIN, SplitScreenSystem::VERTICAL_SPLIT_4P_THIRD_MAX);
			m_cars[3]->GetCarHUDCamera().SetViewport(SplitScreenSystem::VERTICAL_SPLIT_4P_FOURTH_MIN, SplitScreenSystem::VERTICAL_SPLIT_4P_FOURTH_MAX);
		}
		else
		{
			m_cars[0]->GetCarHUDCamera().SetViewport(SplitScreenSystem::HORIZONTAL_SPLIT_4P_FIRST_MIN, SplitScreenSystem::HORIZONTAL_SPLIT_4P_FIRST_MAX);
			m_cars[1]->GetCarHUDCamera().SetViewport(SplitScreenSystem::HORIZONTAL_SPLIT_4P_SECOND_MIN, SplitScreenSystem::HORIZONTAL_SPLIT_4P_SECOND_MAX);
			m_cars[2]->GetCarHUDCamera().SetViewport(SplitScreenSystem::HORIZONTAL_SPLIT_4P_THIRD_MIN, SplitScreenSystem::HORIZONTAL_SPLIT_4P_THIRD_MAX);
			m_cars[3]->GetCarHUDCamera().SetViewport(SplitScreenSystem::HORIZONTAL_SPLIT_4P_FOURTH_MIN, SplitScreenSystem::HORIZONTAL_SPLIT_4P_FOURTH_MAX);
		}
	}
	break;
	default:
		break;
	}
}
#pragma once
#include "Game/CarCamera.hpp"
#include <array>

enum eSplitMode
{
	PREFER_VERTICAL_SPLIT,
	PREFER_HORIZONTAL_SPLIT
};

//------------------------------------------------------------------------------------------------------------------------------
class SplitScreenSystem
{
public:

	friend class Game;

	SplitScreenSystem();
	~SplitScreenSystem();

	void		AddCarCameraForPlayer(CarCamera* camera, int playerID);
	int			GetNumPlayers() const;
	void		SetNumPlayers(int numPlayers);

	void		ComputeViewPortSplits(eSplitMode splitMode = PREFER_VERTICAL_SPLIT) const;

	const std::array<CarCamera*, 4>&	GetAllCameras() const;
	const CarCamera*					GetCameraForPlayerID(int playerID) const;


	void		SetColorTargets(ColorTargetView * colorTargetView) const;
private:

	//Vertical Split ratios for 2P
	const static Vec2	VERTICAL_SPLIT_2P_FIRST_MIN;
	const static Vec2	VERTICAL_SPLIT_2P_FIRST_MAX;
	const static Vec2	VERTICAL_SPLIT_2P_SECOND_MIN;
	const static Vec2	VERTICAL_SPLIT_2P_SECOND_MAX;

	//Horizontal Split ratios for 2P
	const static Vec2	HORIZONTAL_SPLIT_2P_FIRST_MIN;
	const static Vec2	HORIZONTAL_SPLIT_2P_FIRST_MAX;
	const static Vec2	HORIZONTAL_SPLIT_2P_SECOND_MIN;
	const static Vec2	HORIZONTAL_SPLIT_2P_SECOND_MAX;

	//Vertical Split ratios for 3P
	const static Vec2	VERTICAL_SPLIT_3P_FIRST_MIN;
	const static Vec2	VERTICAL_SPLIT_3P_FIRST_MAX;
	const static Vec2	VERTICAL_SPLIT_3P_SECOND_MIN;
	const static Vec2	VERTICAL_SPLIT_3P_SECOND_MAX;
	const static Vec2	VERTICAL_SPLIT_3P_THIRD_MIN;
	const static Vec2	VERTICAL_SPLIT_3P_THIRD_MAX;
	
	//Horizontal Split ratios for 3P
	const static Vec2	HORIZONTAL_SPLIT_3P_FIRST_MIN;
	const static Vec2	HORIZONTAL_SPLIT_3P_FIRST_MAX;
	const static Vec2	HORIZONTAL_SPLIT_3P_SECOND_MIN;
	const static Vec2	HORIZONTAL_SPLIT_3P_SECOND_MAX;
	const static Vec2	HORIZONTAL_SPLIT_3P_THIRD_MIN;
	const static Vec2	HORIZONTAL_SPLIT_3P_THIRD_MAX;

	//Vertical Split ratios for 4P
	const static Vec2	VERTICAL_SPLIT_4P_FIRST_MIN;
	const static Vec2	VERTICAL_SPLIT_4P_FIRST_MAX;
	const static Vec2	VERTICAL_SPLIT_4P_SECOND_MIN;
	const static Vec2	VERTICAL_SPLIT_4P_SECOND_MAX;
	const static Vec2	VERTICAL_SPLIT_4P_THIRD_MIN;
	const static Vec2	VERTICAL_SPLIT_4P_THIRD_MAX;
	const static Vec2	VERTICAL_SPLIT_4P_FOURTH_MIN;
	const static Vec2	VERTICAL_SPLIT_4P_FOURTH_MAX;

	//Horizontal Split ratios for 4P
	const static Vec2	HORIZONTAL_SPLIT_4P_FIRST_MIN;
	const static Vec2	HORIZONTAL_SPLIT_4P_FIRST_MAX;
	const static Vec2	HORIZONTAL_SPLIT_4P_SECOND_MIN;
	const static Vec2	HORIZONTAL_SPLIT_4P_SECOND_MAX;
	const static Vec2	HORIZONTAL_SPLIT_4P_THIRD_MIN;
	const static Vec2	HORIZONTAL_SPLIT_4P_THIRD_MAX;
	const static Vec2	HORIZONTAL_SPLIT_4P_FOURTH_MIN;
	const static Vec2	HORIZONTAL_SPLIT_4P_FOURTH_MAX;

	//Player and Camera data
	std::array<CarCamera*, 4>	m_playerCameras = {nullptr, nullptr, nullptr, nullptr };	//We know we won't have more than 4 so this is fixed size
	int							m_numPlayers = 0;

	eSplitMode					m_defaultSplitMode = PREFER_VERTICAL_SPLIT;
};
#include "Game/SplitScreenSystem.hpp"
//Engine Systems
#include "Engine/Math/Vec2.hpp"
#include "Engine/Commons/EngineCommon.hpp"

//------------------------------------------------------------------------------------------------------------------------------
//////////////////////////////////////////////////////////////////////////
//Static constant split values
//////////////////////////////////////////////////////////////////////////

//2P Vertical Splits
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_2P_FIRST_MIN = Vec2(0.f, 0.f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_2P_FIRST_MAX = Vec2(0.5f, 1.f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_2P_SECOND_MIN = Vec2(0.5f, 0.f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_2P_SECOND_MAX = Vec2(1.f, 1.f);

//2P Horizontal Splits
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_2P_FIRST_MIN = Vec2(0.f, 0.5f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_2P_FIRST_MAX = Vec2(1.f, 1.f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_2P_SECOND_MIN = Vec2(0.f, 0.f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_2P_SECOND_MAX = Vec2(1.f, 0.5f);

//3P Vertical Splits
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_3P_FIRST_MIN = Vec2(0.f, 0.5f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_3P_FIRST_MAX = Vec2(0.5f, 1.f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_3P_SECOND_MIN = Vec2(0.5f, 0.5f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_3P_SECOND_MAX = Vec2(1.f, 1.f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_3P_THIRD_MIN = Vec2(0.f, 0.f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_3P_THIRD_MAX = Vec2(1.f, 0.5f);

//3P Horizontal Splits
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_3P_FIRST_MIN = Vec2(0.f, 0.5f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_3P_FIRST_MAX = Vec2(1.f, 1.f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_3P_SECOND_MIN = Vec2(0.f, 0.f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_3P_SECOND_MAX = Vec2(0.5f, 0.5f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_3P_THIRD_MIN = Vec2(0.5f, 0.f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_3P_THIRD_MAX = Vec2(1.f, 0.5f);

//4P Vertical Splits
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_4P_FIRST_MIN = Vec2(0.f, 0.5f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_4P_FIRST_MAX = Vec2(0.5f, 1.f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_4P_SECOND_MIN = Vec2(0.5f, 0.5f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_4P_SECOND_MAX = Vec2(1.f, 1.f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_4P_THIRD_MIN = Vec2(0.f, 0.f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_4P_THIRD_MAX = Vec2(0.5f, 0.5f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_4P_FOURTH_MIN = Vec2(0.5f, 0.f);
const STATIC Vec2 SplitScreenSystem::VERTICAL_SPLIT_4P_FOURTH_MAX = Vec2(1.f, 0.5f);

//4P Horizontal Splits
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_4P_FIRST_MIN = Vec2(0.f, 0.5f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_4P_FIRST_MAX = Vec2(0.f, 1.f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_4P_SECOND_MIN = Vec2(0.f, 0.f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_4P_SECOND_MAX = Vec2(0.5f, 0.5f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_4P_THIRD_MIN = Vec2(0.5f, 0.5f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_4P_THIRD_MAX = Vec2(1.f, 1.f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_4P_FOURTH_MIN = Vec2(0.5f, 0.f);
const STATIC Vec2 SplitScreenSystem::HORIZONTAL_SPLIT_4P_FOURTH_MAX = Vec2(1.f, 0.5f);

//------------------------------------------------------------------------------------------------------------------------------
SplitScreenSystem::SplitScreenSystem()
{

}

//------------------------------------------------------------------------------------------------------------------------------
SplitScreenSystem::~SplitScreenSystem()
{

}

//------------------------------------------------------------------------------------------------------------------------------
void SplitScreenSystem::AddCarCameraForPlayer(CarCamera* camera, int playerID)
{
	ASSERT_OR_DIE(playerID < 4, "The player ID recieved by SplitScreenSystem is greater than  4");

	if (m_playerCameras[playerID] == nullptr)
	{
		m_playerCameras[playerID] = camera;
		m_numPlayers++;
	}
	else
	{
		ERROR_AND_DIE("PlayerID passed to the SplitScreenSystem was already assigned");
	}

}

//------------------------------------------------------------------------------------------------------------------------------
int SplitScreenSystem::GetNumPlayers() const
{
	return m_numPlayers;
}

//------------------------------------------------------------------------------------------------------------------------------
void SplitScreenSystem::SetNumPlayers(int numPlayers)
{
	m_numPlayers = numPlayers;
}

//------------------------------------------------------------------------------------------------------------------------------
void SplitScreenSystem::ComputeViewPortSplits(eSplitMode splitMode) const
{
	ASSERT_OR_DIE(m_numPlayers > 0, "The number of players in the Split Screen System is 0");

	switch (m_numPlayers)
	{
	case 1:
	{
		//We have only 1 player so we should be fine with the viewport being the whole screen
		m_playerCameras[0]->SetViewport(Vec2::ZERO, Vec2::ONE);
	}
	break;
	case 2:
	{
		//We have 2 players, check the split mode for 2P and split accordingly
		if (splitMode == PREFER_VERTICAL_SPLIT)
		{
			//We need to split the screen into vertical halfs
			m_playerCameras[0]->SetViewport(VERTICAL_SPLIT_2P_FIRST_MIN,		VERTICAL_SPLIT_2P_FIRST_MAX);
			m_playerCameras[1]->SetViewport(VERTICAL_SPLIT_2P_SECOND_MIN,	VERTICAL_SPLIT_2P_SECOND_MAX);
		}
		else
		{
			//We need to split the screen into horizontal halfs
			m_playerCameras[0]->SetViewport(HORIZONTAL_SPLIT_2P_FIRST_MIN,	HORIZONTAL_SPLIT_2P_FIRST_MAX);
			m_playerCameras[1]->SetViewport(HORIZONTAL_SPLIT_2P_SECOND_MIN,	HORIZONTAL_SPLIT_2P_SECOND_MAX);
		}
	}
	break;
	case 3:
	{
		if (splitMode == PREFER_VERTICAL_SPLIT)
		{
			m_playerCameras[0]->SetViewport(VERTICAL_SPLIT_3P_FIRST_MIN,		VERTICAL_SPLIT_3P_FIRST_MAX);
			m_playerCameras[1]->SetViewport(VERTICAL_SPLIT_3P_SECOND_MIN,	VERTICAL_SPLIT_3P_SECOND_MAX);
			m_playerCameras[2]->SetViewport(VERTICAL_SPLIT_3P_THIRD_MIN,		VERTICAL_SPLIT_3P_THIRD_MAX);
		}
		else
		{
			m_playerCameras[0]->SetViewport(HORIZONTAL_SPLIT_3P_FIRST_MIN,	HORIZONTAL_SPLIT_3P_FIRST_MAX);
			m_playerCameras[1]->SetViewport(HORIZONTAL_SPLIT_3P_SECOND_MIN,	HORIZONTAL_SPLIT_3P_SECOND_MAX);
			m_playerCameras[2]->SetViewport(HORIZONTAL_SPLIT_3P_THIRD_MIN,	HORIZONTAL_SPLIT_3P_THIRD_MAX);
		}
	}
	break;
	case 4:
	{
		if (splitMode == PREFER_VERTICAL_SPLIT)
		{
			m_playerCameras[0]->SetViewport(VERTICAL_SPLIT_4P_FIRST_MIN,		VERTICAL_SPLIT_4P_FIRST_MAX);
			m_playerCameras[1]->SetViewport(VERTICAL_SPLIT_4P_SECOND_MIN,	VERTICAL_SPLIT_4P_SECOND_MAX);
			m_playerCameras[2]->SetViewport(VERTICAL_SPLIT_4P_THIRD_MIN,		VERTICAL_SPLIT_4P_THIRD_MAX);
			m_playerCameras[3]->SetViewport(VERTICAL_SPLIT_4P_FOURTH_MIN,	VERTICAL_SPLIT_4P_FOURTH_MAX);
		}
		else
		{
			m_playerCameras[0]->SetViewport(HORIZONTAL_SPLIT_4P_FIRST_MIN,	HORIZONTAL_SPLIT_4P_FIRST_MAX);
			m_playerCameras[1]->SetViewport(HORIZONTAL_SPLIT_4P_SECOND_MIN,	HORIZONTAL_SPLIT_4P_SECOND_MAX);
			m_playerCameras[2]->SetViewport(HORIZONTAL_SPLIT_4P_THIRD_MIN,	HORIZONTAL_SPLIT_4P_THIRD_MAX);
			m_playerCameras[3]->SetViewport(HORIZONTAL_SPLIT_4P_FOURTH_MIN,	HORIZONTAL_SPLIT_4P_FOURTH_MAX);
		}
	}
	break;
	default:
		break;
	}
}

//------------------------------------------------------------------------------------------------------------------------------
const std::array<CarCamera*, 4>& SplitScreenSystem::GetAllCameras() const
{
	return m_playerCameras;
}

//------------------------------------------------------------------------------------------------------------------------------
const CarCamera* SplitScreenSystem::GetCameraForPlayerID(int playerID) const
{
	if (m_playerCameras[playerID] != nullptr)
	{
		return m_playerCameras[playerID];
	}
	else
	{
		return nullptr;
	}
}

void SplitScreenSystem::SetColorTargets(ColorTargetView * colorTargetView) const
{
	for (int cameraIndex = 0; cameraIndex < m_numPlayers; cameraIndex++)
	{
		m_playerCameras[cameraIndex]->SetColorTarget(colorTargetView);
	}
}

This is the end.

Yaaay! You have made it to the end (well almost). Now you get to see a little clip of me trying to attempt a super long jump at top speed:

Okay now this is the end.

Retrospectives

What went well 🙂

  • Completed a working demo of a racing game
  • Implemented a fairly decent feeling vehicle tune
  • Implemented a good track system and tools that allowed me to work rapidly
  • Got over major programmer morale problems when encountering bugs

What went bad 🙁

  • Had a very poorly structured project plan for most of the project duration
  • Issues with morale problems over Covid-19 quarantine
  • Sprint burn down charts were very inconsistent when reviewing time spent working

What I would do differently next time 🙂

  • Make a personal plan that works for me using a medium I know best (text files)
  • Don’t delay discussing road-blocks, keep stakeholders informed about problems
  • Eliminate project risks early on and re-plan immediately if tasks are not viable