Game
Full Contact Racing
7 years ago

Multiplayer devblog


Hi,

I don’t really feel like coding tonight so I thought I’d take the time to do a blog post on how I’m using Photon to build the networked part of full contact racing.
Photon is robust, easy to work with and reasonably priced.

It’s a fairly simple affair so far, players can join the lobby, create a game or join an existing one, choose the colour of the car they wish to drive and when then enter the game.

Lets take a look at some code.

First up, we get the players name from a text input box, connect to photons master server with our current game version number and update a text field to let the player know what is happening.

5d0b3658825a0.png

Once the player has entered a name they click a button which fires the EnteredNickName() method.
We use a version number so that older versions of the game won’t attempt to connect with hosts using newer versions and vice versa.

		
			public void EnteredNickname() {
        PhotonNetwork.player.name = editNickname.text;
        PhotonNetwork.ConnectUsingSettings("0.1.68");
        messageText.text = "Connecting...";
    }
		
	

Once the player has connected to the master server we then need to join a lobby. The lobby is where the player can choose to create a new game or jump into one of the open ones.
We join the lobby once we are connected to the master server by using photons built in OnConnectedToMaster() call back. We also update our message text field so the player is aware of what’s happening.

		
			public override void OnConnectedToMaster()
    {
        PhotonNetwork.JoinLobby ();
        messageText.text = "Entering lobby...";

    }
		
	

We are now connected to the lobby so we deactivate a UI panel which contained the player name input box and we activate a new UI panel which shows running games and gives the player the option to create a new game or join an existing one.
This is done using photons built in OnJoinedLobby() call back.

		
			public override void OnJoinedLobby () {
        multiPlayerPanel.gameObject.SetActive(false);
        messageText.gameObject.SetActive(false);
        racePanel.gameObject.SetActive(true);
    }
		
	

Our new panel gives us a list of open games by instantiating a new UI button for each open game.

5d0b36597612f.png

The RoomJoiner component mentioned in the code below handles joining the selected game.

		
			    public override void OnReceivedRoomListUpdate () {
        foreach (RoomJoiner roomButton in rooms) {
            Destroy(roomButton.gameObject);
        }
        rooms.Clear ();
        int i = 0;
        foreach (RoomInfo room in PhotonNetwork.GetRoomList()) {
            if (!room.open)
                continue;
            GameObject buttonPrefab = Resources.Load<GameObject>("GUI/RoomGUI");
            GameObject roomButton = Instantiate<GameObject>(buttonPrefab);
            roomButton.GetComponent<RoomJoiner>().RoomName = room.name;
            string info = room.name.Trim() + " (" + room.playerCount + "/" + room.maxPlayers + ")";
            roomButton.GetComponentInChildren<Text>().text = info;
            rooms.Add(roomButton.GetComponent<RoomJoiner>());
            roomButton.transform.SetParent(racePanel, false);
            roomButton.transform.position.Set(0, i * 120, 0);
        }
    }
		
	

We also have the option to create a new game.
We’ll cover creating an empty game first.
We create a game or room as photon calls it with a few options. Max players and visible. Neither really require any explanation.

		
			public void CreateGame () {
        RoomOptions options = new RoomOptions ();
        options.maxPlayers = 10;
        options.isVisible = true;
        PhotonNetwork.CreateRoom (PhotonNetwork.player.name, options, TypedLobby.Default);

    }
		
	

Now we’ve created the game, We will join it automatically and the OnCreatedRoom() callback will fire activating a start button and calling our method to set some player properties

		
			public override void OnCreatedRoom () {
        btStart.gameObject.SetActive(true);
        SetCustomProperties(PhotonNetwork.player, 0, PhotonNetwork.playerList.Length - 1);
    }
		
	

Each player joins the room and chooses a car colour using the UI.
We use an array of different coloured car sprites to communicate the vehicle colour to the player and use the selected position in the array to instantiate the correct prefab when the game starts.

5d0b365a5d029.png

We set these custom properties on each player while they are still in the lobby,

		
			private void SetCustomProperties(PhotonPlayer player, int car, int position) {
        ExitGames.Client.Photon.Hashtable customProperties = new ExitGames.Client.Photon.Hashtable() { { "spawn", position }, {"car", car} };
        player.SetCustomProperties(customProperties);
    }
		
	

, When players disconnect,

		
			    public override void OnPhotonPlayerDisconnected(PhotonPlayer disconnetedPlayer) {
        if (PhotonNetwork.isMasterClient) {
            int playerIndex = 0;
            foreach (PhotonPlayer p in PhotonNetwork.playerList) {
                SetCustomProperties(p, (int) p.customProperties["car"], playerIndex++);
            }
        }
    }
		
	

and when someone changes their properties like selecting a different coloured car.

		
			public void NextCar() {
        carIndex = (carIndex + 1) % carTextures.Length;
        SetCustomProperties (PhotonNetwork.player, carIndex, (int) PhotonNetwork.player.customProperties ["spawn"]);
    }

    public void PreviousCar() {
        carIndex--;
        if (carIndex < 0)
            carIndex = carTextures.Length - 1;
        SetCustomProperties (PhotonNetwork.player, carIndex, (int) PhotonNetwork.player.customProperties ["spawn"]);
    }
		
	

When everyone is ready, The host clicks the start button and a Remote Procedure Call is executed on each connected machine to start the game.

		
			public void CallLoadRace() {
        PhotonNetwork.room.open = false;
        photonView.RPC("LoadRace", PhotonTargets.All);
    }

[PunRPC]
    public void LoadRace () {
        PhotonNetwork.LoadLevel("MultiDD");
    }
		
	

The actual playable scene has the same amount of spawn points as the game has max players. A spawn point is chosen for the player in the order they connect to the game when the game starts.
When the game starts, the following code instantiates the players car at on the network at the correct spawn point.

		
			    public void CreateCar(){
        int pos = (int) PhotonNetwork.player.customProperties["spawn"];
        int carNumber = (int) PhotonNetwork.player.customProperties["car"];
        Transform spawn = spawnPoints[pos];
        GameObject car = PhotonNetwork.Instantiate("MultiPlayerCar" + carNumber, spawn.position, spawn.rotation, 0);
    }
		
	

We keep a track of all players which have loaded.

		
			photonView.RPC ("ConfirmLoad", PhotonTargets.All);

[PunRPC]
    public void ConfirmLoad () {
        loadedPlayers++;
    }
		
	

When they are all ready, the game starts..

VEHICLES

Our vehicles whether they are local, remote or AI controlled all share the same main script and input values are fed to this either from the players controller, an AI script which makes decisions about the speed to travel at, when to brake and which way to steer.

If the player is local, we feed the same input values to the main script as we would if the player was playing alone in offline mode.

		
			    {
        if (stream.isWriting) {
            //We own this car: send the others our input and transform data
            stream.SendNext((float)m_CarInput.steer);
            stream.SendNext((float)m_CarInput.accel);
            stream.SendNext((float)m_CarInput.ebrake);
            stream.SendNext(transform.position);
            stream.SendNext(transform.rotation);
            stream.SendNext(rb.velocity);
        }
		
	

at the same time, we also send the values we are using to the main server.
The server and clients have this code to move our remote vehicle representation.
This code runs for every remote vehicle in our game (including our own on other players machines).

		
			{
            if(!stream.isWriting){
            m_CarInput.steer = (float)stream.ReceiveNext();
            m_CarInput.accel = (float)stream.ReceiveNext();
            m_CarInput.ebrake = (float)stream.ReceiveNext();
            correctPlayerPos = (Vector3)stream.ReceiveNext();
            correctPlayerRot = (Quaternion)stream.ReceiveNext();
            currentVelocity = (Vector3)stream.ReceiveNext();
            updateTime = Time.time;
        } 
		
	

in FixedUpdate() we lerp the position of all the remote vehicles to their correct positions.

		
			   public void FixedUpdate()
    {
        if (!photonView.isMine) {
            Vector3 projectedPosition = this.correctPlayerPos + currentVelocity * (Time.time - updateTime);
            transform.position = Vector3.Lerp (transform.position, projectedPosition, Time.deltaTime * 4);
            transform.rotation = Quaternion.Lerp (transform.rotation, this.correctPlayerRot, Time.deltaTime * 4);
        }
    }
		
	

We’re doing other stuff like only turning on the script which handles input if the player is local and assigning cameras to local players and stuff but that’s basically it.
And there we have it. Each player can connect, choose a car colour and play together on the network.
This is as simple as it gets. I haven’t included any error checking for the sake of clarity (it’s in the game, just not in this blog post).
If you have any questions feel free to post in the comments section.

Paul.



0 comments

Loading...

Next up

New update - Race Mode

Multiplayer is live!!

Been working lately on lots of 'behind-the-scenes' boring stuff that no one really cares about, so here’s a guy playing the sax for some reason.

#screenshotsaturday

Fan art for Foolish I'm feeling kinda better so I drew this

Successful landing

#screenshotsaturday

I was bored, so... ManutKat.

Updated the chest in the maze, adding sound, particles and better animation. But what's in the chest?

"Our work is never over" they said.

A Shiny Mega Gengar 🌟 For @ManutkArt 's #ThreeColorsChallenge!

Rockin the Guitar, an Axe like Guitar I made for my Uni project