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.

Online Multiplayer:

Of course, a big part of what made this game so exceptional is its online multiplayer feature. With the assistance of Unity Netcode for GameObjects and Unity Relay, I eventually achieved success. Let me take you through the journey of creating this game with multiplayer functionality.

To implement multiplayer capabilities, various services like Photon, Mirror, and Netcode for GameObjects are available. Due to my experience with it, I chose Netcode for GameObjects, even though there were limited tutorials, which led me to explore and learn a lot on my own.

After deciding to use Unity Netcode for GameObjects, I began by implementing the necessary network manager. I had to provide a client network transform to the objects I wanted to synchronize with the server, which, in this case, was solely the car, as no other object needed movement synchronization within the scene.

I intentionally disabled synchronization for the scale, as it did not require synchronization.

However, making multiplayer functional required additional adjustments since, without them, all players could move any car in the game. So, I had to add a few things to the code.


// First, you need to use Unity Netcode to access the netcode functionalities.
using Unity.Netcode;

// Then, instead of inheriting from MonoBehaviour, you should inherit from NetworkBehaviour.
public class NetcodeExplanesion : NetworkBehaviour
{
	  // Lastly, you need to ensure that if you don't want others to execute your code, 
	  // check if your object is the owner.
	  private void Update()
	  { 
		  if (!IsOwner) return;
	  }
}
						  

I also wanted players to have personalized names, so they could easily identify each other in the game. To achieve this, I used a NetworkVariable, which is a variable that can be synchronized over the network, but it cannot hold null values. This posed a challenge because I usually use strings for names, but they can be null. After researching, I discovered the FixedString128Bytes, which is essentially a string with a maximum of 128 characters, and it can never be null.


// To create player overhead displays with personalized names, I used Unity.Netcode.
using Unity.Netcode;
using Unity.Collections;

public class PlayerOverheadDisplay : NetworkBehaviour
{
	  [SerializeField] private NetworkVariable _displayName = new NetworkVariable("", NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);

	  private void Update()
	  {
		  // Check if the player is the owner before changing the display name.
		  if (IsOwner)
			  _displayName.Value = _multiplayerManager._nameInputfield.text;

		  // Set the display name text to the value of the FixedString128Bytes.
		  _displayNameText.text = _displayName.Value.ToString();
	  }
}
						  

Additionally, to create personalized player overhead displays with customized names, I utilized Unity.Netcode. By using the NetworkVariable with FixedString128Bytes, I ensured that players' names could be synchronized over the network without any issues. When a player updates their name in the name input field, it is reflected across the server, allowing others to see their chosen name displayed above their cars in the game.

Upon implementing all these crucial components, the multiplayer functionality was already operational in terms of code. The final step was enabling players to connect and play together via a server. This was achieved using a Unity service called Relay. As a result, players can either create or join a game. If they create a game, they receive a code that others can use to join and participate in the multiplayer experience.

Discover Cybermania

Watch another video of cybermania