This started as a project for the first course at Futuregames: make a very small game in Unity with C#. I went a bit overkill on the assignment, as I made something that far exceeded the requirements of the task, and I continued to work further on it on my spare time. Inspired by the chaotic fun of Mario Kart and my love for destructible enviroments, I created a racing game where you drive a bumper car and race around in a destructible area.
During the race, Events will pop up that alters the variables of the players, mostly to create a challenge for the players but also to create a laugh-out-loud moment when your opponents suddenly crashes through a pile of crates and alters the racing area. There are seven events in total that are activated randomly during the race, with their duration and how often it can happen customizable in the Options menu.
Two different Power-up items are placed on the level; small coin-shapes circles, in either red or blue color. The red one increases the steering capacity of the player when picked up and the blue one increases the maximum velocity of the player, with their duration also changeable in the Options menu.
Events
Crazy Carnival (Walls are extra bouncy)
Jet Speed (Going very fast)
Total Chaos (Every event is activated)
Oily Hands (Steering is faster)
Assassination (Players are extra bouncy)
No breaks
Eyes Crossed (Invert controls)
One of the major challenges that I had during the development of the game was the scaling of the project. As the complexity of the project grew, the scripts became long and hard to read and manage. As the game grew, I realized my scripts became too large and difficult to manage. At first, I tried organizing them with regions, which helped somewhat, but looking back, I would now structure my code into smaller scripts to have them more readable.
Another challenge I encountered was when the player became immobilized. Certain questions came to mind: how would I determine if the player was stuck, how would I help the player to get unstuck and how would it all work when you're racing other opponents? My first thought was to have the player return to the latest checkpoint. After trying it out, it felt stiff and punishing, so instead of this approach I decided to apply force to the car. Since the shape of the collision of the car is like a cone, it would naturally flip back to a position where it would be driveable again.
using UnityEngine;
using UnityEngine.InputSystem;
using TMPro;
public class CarController : MonoBehaviour
{
#region Values
public Rigidbody rb;
public Game game;
private Vector3 mass;
public Laps laps;
public float speed;
public float bounce;
public float rotation;
private bool grounded = false;
public TMP_Text PText;
private bool activeEvent;
public bool raceStarted = false;
private float inputForwardReverse;
private float inputRotation;
public float distance;
private Vector2 move;
private Vector2 rotate;
[Header("Car Standard Values")]
[SerializeField] float forwardSpeed = 1500f;
[SerializeField] float reverseSpeed = 1000f;
[SerializeField] float rotationSpeed = 140f;
[SerializeField] float disabledTime = 1.0f;
private float disabledTimeTick = 0;
[Header("Car Item Values")]
[SerializeField] bool speedItemActive = false;
[SerializeField] float speedItem = 2500f;
public float speedItemTime = 15f;
private float speedItemTimer;
[SerializeField] bool rotationItemActive = false;
[SerializeField] float rotationItem = 280f;
[SerializeField] float rotationItemTime = 15f;
private float rotationItemTimer;
[Header("Car Event Values")]
[SerializeField] public float speed_jetSpeed = 3000f;
[SerializeField] public float rotation_event = 500f;
[SerializeField] float bounce_assassination = 50;
[SerializeField] float bounce_crazyCarnival = 1.3f;
[Header("Event Values")]
[SerializeField] public bool eyesCrossed = false;
[SerializeField] public bool noBreaks = false;
[SerializeField] public bool jetSpeed = false;
[SerializeField] public bool crazyCarnival = false;
[SerializeField] public bool assassination = false;
[SerializeField] public bool oilyHands = false;
#endregion
public void Starting()
{
raceStarted = true;
}
public void Stopping()
{
raceStarted = false;
}
public void OnMove(InputAction.CallbackContext context)
{
if (gameObject.name == "player_1_car")
{
move = context.ReadValue<Vector2>();
inputForwardReverse = move.y;
}
}
public void OnRotate(InputAction.CallbackContext context)
{
if (gameObject.name == "player_1_car")
{
rotate = context.ReadValue<Vector2>();
inputRotation = rotate.x;
}
}
private void Start()
{
speedItemTime = PlayerPrefs.GetInt("PwDur");
rotationItemTime = PlayerPrefs.GetInt("PwDur");
speed = forwardSpeed;
rotation = rotationSpeed;
speedItemTimer = speedItemTime;
rotationItemTimer = rotationItemTime;
mass = transform.GetChild(0).localPosition;
rb.centerOfMass = mass;
}
public void Awake()
{
StartText();
Invoke("Startup", 10);
}
private void StartText()
{
if (gameObject.name == "player_1_car") { PText.SetText("Player 1"); }
if (gameObject.name == "player_2_car") { PText.SetText("Player 2"); }
if (gameObject.name == "player_3_car") { PText.SetText("Player 3"); }
if (gameObject.name == "player_4_car") { PText.SetText("Player 4"); }
}
public void Startup()
{
PText.SetText("");
raceStarted = true;
}
private void Update()
{
if (game.isActivated == true && activeEvent == false)
{
activeEvent = true;
eventHandling();
}
if (game.isActivated == false && activeEvent == true)
{
activeEvent = false;
eventHandling();
}
}
private void FixedUpdate()
{
if (raceStarted == true)
{
//handlingControls();
disabledCar();
itemTimers();
playerMovement();
}
}
#region Item Pickups
private void OnTriggerEnter(Collider col)
{
//Collision with Power Ups
if (col.gameObject.tag == "SpeedItem")
{
speed += speedItem;
speedItemActive = true;
}
if (col.gameObject.tag == "RotationItem")
{
rotation += rotationItem;
rotationItemActive = true;
}
#region If Grounded
if (col.gameObject.tag == "Ground")
{
grounded = true;
disabledTimeTick = 0;
}
#endregion
}
#endregion
#region Collisions
private void OnCollisionEnter(Collision col)
{
#region Event Collisions (Crazy Carnival & Assassination)
if (crazyCarnival == true && grounded == true)
{
Vector3 collisionNormal = col.contacts[0].normal;
Vector3 bounceForce = collisionNormal * bounce_crazyCarnival;
rb.AddForce(bounceForce, ForceMode.Impulse);
}
if (col.gameObject.tag == "Cars" && assassination == true)
{
Vector3 collisionNormal = (transform.position - col.transform.position).normalized;
Vector3 bounceForce = collisionNormal * bounce_assassination / 2;
rb.AddForce(bounceForce, ForceMode.Impulse);
}
#endregion
}
public void OnTriggerExit(Collider col)
{
if (col.gameObject.tag == "Ground")
{
grounded = false;
}
}
// If the player is stuck
private void disabledCar()
{
if (grounded == false)
{
if (Mathf.Approximately(rb.velocity.magnitude, 0f))
{
disabledTimeTick += Time.deltaTime;
if (disabledTimeTick >= disabledTime)
{
rb.AddTorque(100, 100, 600);
rb.AddForce(400, 400, 1000);
disabledTimeTick = 0;
}
}
}
}
#endregion
#region Item Timers
private void itemTimers()
{
if (speedItemActive == true)
{
speedItemTimer -= Time.deltaTime;
if (speedItemTimer <= 0)
{
speedItemActive = false;
speed -= speedItem;
speedItemTimer = speedItemTime;
}
}
if (rotationItemActive == true)
{
rotationItemTimer -= Time.deltaTime;
if (rotationItemTimer <= 0)
{
rotationItemActive = false;
rotation = rotationSpeed;
rotationItemTimer -= rotationItemTime;
}
}
}
#endregion
#region Event Handling
void eventHandling()
{
if (game.randomEvents == "Eyes Crossed" | game.randomEvents == "Total Chaos")
{
if (activeEvent == true)
{
eyesCrossed = true;
}
else
{
eyesCrossed = false;
}
}
if (game.randomEvents == "No Breaks" | game.randomEvents == "Total Chaos")
{
if (activeEvent == true)
{
noBreaks = true;
}
else
{
noBreaks = false;
}
}
if (game.randomEvents == "Jet Speed" | game.randomEvents == "Total Chaos")
{
if (activeEvent == true)
{
jetSpeed = true;
speed += speed_jetSpeed;
}
else
{
jetSpeed = false;
speed -= speed_jetSpeed;
}
}
if (game.randomEvents == "Crazy Carnival" | game.randomEvents == "Total Chaos")
{
if (activeEvent == true)
{
crazyCarnival = true;
}
else
{
crazyCarnival = false;
}
}
if (game.randomEvents == "Assassination" | game.randomEvents == "Total Chaos")
{
if (activeEvent == true)
{
assassination = true;
}
else
{
assassination = false;
}
}
if (game.randomEvents == "Oily Hands" | game.randomEvents == "Total Chaos")
{
if (activeEvent == true)
{
oilyHands = true;
rotation += rotation_event;
}
else
{
oilyHands = false;
rotation -= rotation_event;
}
}
}
#endregion
}
using UnityEngine;
public class PowerUps : MonoBehaviour
{
[Header("Stats")]
public float timeRespawn = 30;
private bool startTimer;
private float theTimer;
private void Start()
{
timeRespawn = PlayerPrefs.GetInt("PwRes");
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "Cars")
{
gameObject.GetComponent<MeshRenderer>().enabled = false;
gameObject.GetComponent<BoxCollider>().enabled = false;
startTimer = true;
}
}
private void FixedUpdate()
{
transform.Rotate(0, 0, 150 * Time.deltaTime);
if(startTimer == true)
{
theTimer += Time.deltaTime;
if (theTimer >= timeRespawn)
{
startTimer = false;
theTimer = 0;
gameObject.GetComponent<MeshRenderer>().enabled = true;
gameObject.GetComponent<BoxCollider>().enabled = true;
}
}
}
}
Link to download the game