Introduction
I developed an online multiplayer racing game with three artists and one other developer for a school project. We had to meet several requirements in the first two weeks which were:
- Game Loop:
- Return to the main menu
- Start the game
- Fail or die mechanics
- Main Player: Well-controllable car
- User Interface (UI):
- Countdown / Traffic light
- Speedometer
- Lap time
- Fastest lap time
- Overview of recorded times
- Time trials saving
After successfully implementing these features, we had two additional weeks to further enhance the game according to our preferences. And I decided to add online multiplayer to the game because I thought it would be a fun challange and if I actually manage to make it work it would be so worth it. And the result is showcased here:
The Making of Cybermania
The Car:
How I came up with the idea of the car is a long story. At first, I wanted to try implementing it myself using Unity's Rigidbody and applying forces directly to the car when certain buttons are pressed.
// Car movement
if (Input.GetAxisRaw("Vertical") > 0.1f)
{
_rb.AddForce(transform.forward * _speed); // Move forward
}
if (Input.GetAxisRaw("Vertical") < -0.1f)
{
_rb.AddForce(-transform.forward * _speed); // Move backward
}
For steering, I attempted to rotate the car, thereby applying forces in different directions.
// Car steering
if (Input.GetAxisRaw("Horizontal") > 0.1f)
{
Vector3 rotation = new Vector3(0, 1, 0);
transform.Rotate(rotation * _steeringSpeed * Time.deltaTime); // Turn right
}
if (Input.GetAxisRaw("Horizontal") < -0.1f)
{
Vector3 rotation = new Vector3(0, 1, 0);
transform.Rotate(-rotation * _steeringSpeed * Time.deltaTime); // Turn left
}
However, I soon realized that this approach was not going to work. So, I decided to search the internet for a better way to create a car, and that's when I stumbled upon an excellent video by Toyful Games.
So, after watching the video and gaining some insights, I revamped the car movement. Instead of directly adding forces, I implemented a better solution that made use of physics and wheel colliders. This resulted in a much smoother and realistic car movement.
And the special thing about the video is that it doesn't explain step by step and show what you have to program, but it gives you all the theory about how the car should work and which forces are involved. So, it was up to me to bring his theories to reality. It wasn't easy, but in the end, I figured it out.
The big difference between what I had before and what I have now is that instead of moving the car, I'm moving the wheels.
First, you need to make sure your car has suspension. This prevents the car from tipping over when forces are applied and also keeps the car up.
if (Physics.Raycast(transform.position, -transform.up, out RaycastHit hit, 1 + _wheelRadius, _canRideOn))
{
Vector3 springDir = transform.up;
Vector3 tireWorldVel = _rb.GetPointVelocity(transform.position);
float offset = _restLenght - hit.distance;
float vel = Vector3.Dot(springDir, tireWorldVel);
// Calculates the force with the damped variables
float force = (offset * _springStenght) - (vel * _damperStenght);
// Adds the force to the car at the position of the wheel.
_rb.AddForceAtPosition(springDir * force, transform.position);
}
And then the steering forces have to be applied, so when the wheel turns, the appropriate forces are applied, causing the car to turn and steer. I made sure that the faster you go, the less you can steer, making it more realistic. Otherwise, the car could make a super sharp turn while going at top speed. I achieved this using an animation curve, and it looks like this:
_yRotation = _steerCurve.Evaluate(GetComponentInParent().velocity.magnitude);
Vector3 wheelEulerAngles = transform.localRotation.eulerAngles;
wheelEulerAngles.y = (wheelEulerAngles.y > 180) ? wheelEulerAngles.y - 360 : wheelEulerAngles.y;
wheelEulerAngles.y = Mathf.Clamp(wheelEulerAngles.y, -_yRotation, _yRotation);
// Applies rotation to the wheels.
transform.localRotation = Quaternion.Euler(wheelEulerAngles);
Finally, there's the acceleration, which actually moves the car forward. Again, I used an animation curve for this. It ensures that the car accelerates quickly at first and then gradually accelerates less, so you don't have a constant super-fast car.
Vector3 accelDir = transform.forward;
if (Input.GetAxisRaw("Vertical") > 0.0f)
{
float carSpeed = Vector3.Dot(_carTransform.forward, _rb.velocity);
float normalizeSpeed = Mathf.Clamp01(Mathf.Abs(carSpeed) / _topSpeed);
float availableTorque = _powerCurve.Evaluate(normalizeSpeed) * Input.GetAxisRaw("Vertical") * _acceleration;
_rb.AddForceAtPosition(accelDir * availableTorque, transform.position);
}
And when you combine all these things, you have a working car, and you can change all the values yourself, like how fast it accelerates, the top speed, and how fast it steers.