How to Create a Multiplayer Game in Unity

How to Create a Multiplayer Game in Unity

In this tutorial, we'll create a straight forward demo to explore?Unity's multiplayer?capabilities. Our game will consist of a single scene where we'll build a multiplayer Space Shooter. Players can join the game together and collaborate to shoot enemies that spawn randomly.

To clearly understand this tutorial, it is recommended that you must have a basic understanding of the following concepts:

  • C# programming
  • Navigating the Unity Editor, including importing assets, creating prefabs, and adding components.

1. Creating Project and Importing Assets

Before you start following the tutorial, you should first create a new Unity project and import all the provided sprites from the source code. To do this, make a folder named "Sprites" and copy all the sprite files into it. Unity's Inspector will automatically import these sprites into your project.

However, please note that some of the sprites are in spritesheets, like the enemies spritesheets, and they require slicing. To accomplish this, set the Sprite Mode to "Multiple" and open the Sprite Editor.

No alt text provided for this image
Asset store accessing

We also import the Mirror asset.

Go to Asset store Windows -> Asset Stor
Click on Download button and Download Mirror        

2. Background Canvas

To start, we will create a background canvas to display a background image.

To achieve this, follow these steps:

  1. Create a new Image object in the Hierarchy. This will automatically generate a new Canvas (rename it as "BackgroundCanvas").
  2. In the BackgroundCanvas, set its Render Mode to "Screen Space - Camera" and remember to attach your Main Camera to it.
  3. Next, set the UI Scale Mode of the canvas to "Scale With Screen Size" This ensures that the canvas remains in the background and doesn't overlay other objects.

No alt text provided for this image
Canva

In the BackgroundImage we only need to set the Source Image, which will be the space one.

3. Network Manager

To enable multiplayer functionality in our game, we must create a GameObject that incorporates the?NetworkManager?and?NetworkManagerHUD?components.

No alt text provided for this image
Network Management

This GameObject will handle the connection of different clients in the game and synchronize game objects across all clients. The Network Manager HUD will provide a simple interface for players to connect to the game.

In this tutorial, we will utilize the?LAN Host?and?LAN Client?options for Unity multiplayer games. The multiplayer functionality operates in the following manner: a player initiates the game as a host (by selecting LAN Host). As a host, the player functions as both a client and a server simultaneously.

Other players can then connect to this host as clients (by selecting LAN Client). The client communicates with the server but does not execute any server-only code. To test our game, we will open two instances of it one as the Host and another as the Client.

To run two instances of the game for testing multiplayer functionality, you cannot open both instances directly from the Unity Editor. Instead, you need to build your game first and run the first instance from the generated executable file. The second instance can be run from the Unity Editor in Play Mode.

To build your game, follow these steps:

  1. Add the Game scene to the build by going to File -> Build Settings and including the Game scene in the build.
  2. Generate an executable file by clicking on File -> Build & Run. This will create an executable for your game.
  3. Run the first instance by opening the executable file. A new window with the game will appear.
  4. After launching the first instance, you can enter Play Mode in the Unity Editor to run the second instance of the game.

By following this approach, you can effectively test your multiplayer game with two instances running simultaneously. One from the executable and the other from the Unity Editor in Play Mode. Remember to repeat this process every time you need to test the multiplayer features.

No alt text provided for this image
Network Manager (1)

4. Ship Movement

With the?NetworkManager?in place, we can now begin creating the game objects that it will manage. The first object we'll create is the player ship.

For the initial version, the ship will only move horizontally on the screen, and its position will be updated by the?NetworkManager. Later, we'll add the functionality for shooting bullets and receiving damage.

To begin, create a new GameObject called "Ship" and turn it into a prefab. To enable a game object to be managed by the NetworkManager, we must add the NetworkIdentity component to it. Additionally, since the ship will be controlled by the player, we'll check the "Local Player Authority" checkbox for it.

The?NetworkTransform?component is responsible for updating the ship's position across the server and all clients. This ensures that the ship's position stays synchronized across all screens. Enabling "Client Authority" on the NetworkTransform component is essential for proper multiplayer functionality.

To handle movement and collisions, we add a?RigidBody2D?and a?BoxCollider2D?to the ship. The?BoxCollider2D?is set as a trigger (Is Trigger set to true) to prevent collisions from affecting the ship's physics.

Now, we introduce a script called "MoveShip" with a "Speed" parameter. Although other scripts will be added later, this is the current setup.

The?MoveShip?script is straightforward; in the?FixedUpdate?method, we retrieve the movement from the Horizontal Axis and set the ship's velocity accordingly. However, two essential network-related aspects must be clarified.

First, to utilize the Network API, scripts must inherit NetworkBehaviour instead of MonoBehaviour. To do this, include the Networking namespace (using?UnityEngine.Networking).

Additionally, in a Unity multiplayer game, the same code is executed in all instances of the game (host and clients). To ensure players only control their ships and not all ships in the game, we use an If condition at the beginning of the FixedUpdate method to check if this is the local player. Without this condition, all ships would move together across all screens, which is not desired.

using System.Collectio
using System.Collections.Generic;
using UnityEngine;
using Mirror;

public class MoveShip : NetworkBehaviour 
{
    [SerializeField]
    private float speed;

    void FixedUpdate () 
    {
        if(this.isLocalPlayer) 
        {
            float movement = Input.GetAxis("Horizontal");  
            GetComponent<Rigidbody2D>().velocity = new Vector2(movement * speed, 0.0f);
        }
};        

Before starting the game, we need to specify to the NetworkManager that the Ship prefab will serve as the Player Prefab. To do this, we select the Ship prefab in the "Player Prefab" attribute within the NetworkManager component. Consequently, whenever a player initiates a new game instance, the NetworkManager will automatically instantiate a Ship for them to control.

No alt text provided for this image
Ship Prefab

5. Spawn Position

To enhance the gameplay, let's set up predefined spawn positions for the ships instead of spawning them in the middle of the screen. This can be easily achieved using Unity's multiplayer API.

First, create a new GameObject to serve as the spawn position. Position this GameObject at the desired location where you want the ships to spawn. Then, add the?NetworkStartPosition?component to this GameObject. This will ensure that ships spawned from this position will have their initial locations set accordingly during multiplayer gameplay.

To determine how the NetworkManager utilizes the spawn positions, we need to configure the "Player Spawn Method" attribute. Two options are available: "Random" and "Round Robin".

  • "Random" means that for each game instance, the manager will randomly select one of the spawn positions as the starting point for the player.
  • "Round Robin" means it will sequentially cycle through all the spawn positions until each one has been used, starting from the first position in the list and repeating the cycle.

In this case, we'll select "Round Robin" as the method to use for spawning players, ensuring that the NetworkManager assigns player start positions sequentially from the available spawn positions in the list.

No alt text provided for this image
Player spawn method

6: Shooting Bullets

The next addition to our game is giving ships the ability to shoot bullets, which must be synchronized among all instances of the game.

To start, let's create the Bullet prefab. Create a new GameObject called "Bullet" and turn it into a prefab. Similar to the ship prefab, the Bullet prefab needs the NetworkIdentity and NetworkTransform components to manage it within the network. However, once a bullet is created, its position does not need to be propagated through the network, as the physics engine handles its position updates. To prevent network overload, we'll change the Network Send Rate in the Network Transform to 0.

Bullets will have a speed and should collide with enemies later in the game. To achieve this, we'll add a?RigidBody2D?and a?CircleCollider2D?to the prefab. Once again, note that the?CircleCollider2D?will be a trigger to ensure proper collision detection.

No alt text provided for this image
Bullet prefab

The ShootBullets script is another NetworkBehaviour, shown below. In its update method, it checks if the local player has pressed the Space key. If so, it calls a method to shoot bullets. This method instantiates a new bullet, sets its velocity, and schedules its destruction after one second, once the bullet has left the screen.

Here, we encounter some crucial network concepts that require explanation. First, there is a?[Command]?tag above the CmdShoot method. This tag, along with the "Cmd" prefix in the method name, designates it as a special method known as a Command.

In Unity, a command is a method that executes on the server, even though it's called on the client. In this case, when the local player shoots a bullet, instead of executing the method on the client, the game sends a command request to the server, and the server performs the method's execution.

Additionally, the?CmdShoot?method includes a call to?NetworkServer.Spawn.?This Spawn method is responsible for creating the bullet in all instances of the game. Consequently, CmdShoot generates a bullet on the server, and then the server replicates this bullet across all clients. It's worth noting that this behavior is possible only because CmdShoot is a Command and not a regular method.

using System.Collection
using System.Collections.Generic;
using UnityEngine;
using Mirror;

public class ShootBullets : NetworkBehaviour
{
    [SerializeField]
    private GameObject bulletPrefab;

    [SerializeField]
    private float bulletSpeed;

    void Update () 
    {
        if(this.isLocalPlayer && Input.GetKeyDown(KeyCode.Space)) 
        {
            this.CmdShoot();
        }
    }

    [Command]
    void CmdShoot ()
    {
        GameObject bullet = Instantiate(bulletPrefab, this.transform.position, Quaternion.identity);
        bullet.GetComponent<Rigidbody2D>().velocity = Vector2.up * bulletSpeed;
        NetworkServer.Spawn(bullet);
        Destroy(bullet, 1.0f);
    }
};s        

7: Spawning Enemies

We must first create an?Enemy prefab. As a result, build a new GameObject called?Enemy?and prefab it. Enemies will have a?Rigidbody2D?and?BoxCollider2D?to manage movements and collisions, much as how ships do. It will also require a?NetworkIdentity?and?NetworkTransform, which the NetworkManager will manage. Later on, we'll also add a script to it, but for now, that will have to do.

Next, we will create a GameObject named "EnemySpawner" Similar to the previous objects, the EnemySpawner will have a NetworkIdentity component. However, this time we'll select the "Server Only" field in the component. This ensures that the spawner exists only on the server, as we don't want enemies to be created in each client separately.

The?EnemySpawner?will also include a?SpawnEnemies?script. This script will be responsible for spawnin. The script for SpawnEnemies is displayed below. You'll see that we are employing a fresh?OnStartServer?Unity method here. The only distinction between this method and OnStart is that this one is only used for the server. We will call?InovkeRepeating?to call the?SpawnEnemy?method every second (in accordance with the spawnInterval) when this occurs.

The?SpawnEnemy?function uses?NetworkServer?to spawn an opponent in a chosen location at?random.spawn?to duplicate it across all game instances. After 10 seconds, the opponent will finally be destroyed enemies at regular intervals, with parameters such as the enemy prefab, the spawn interval, and the enemy speed. The?SpawnEnemies?script allows the server to manage the spawning of enemies, and the changes will be replicated across all clients for consistent gameplay experience.

using System.Collection
using System.Collections.Generic;
using UnityEngine;
using Mirror;

public class SpawnEnemies : NetworkBehaviour 
{
    [SerializeField]
    private GameObject enemyPrefab;

    [SerializeField]
    private float spawnInterval = 1.0f;

    [SerializeField]
    private float enemySpeed = 1.0f;

    public override void OnStartServer () 
    {
        InvokeRepeating("SpawnEnemy", this.spawnInterval, this.spawnInterval);
    }

    void SpawnEnemy () 
    {
        Vector2 spawnPosition = new Vector2(Random.Range(-4.0f, 4.0f), this.transform.position.y);
        GameObject enemy = Instantiate(enemyPrefab, spawnPosition, Quaternion.identity) as GameObject;
        enemy.GetComponent<Rigidbody2D>().velocity = new Vector2(0.0f, -this.enemySpeed);
        NetworkServer.Spawn(enemy);
        Destroy(enemy, 10);
    }
};s        

Before playing the game, we need to add the Enemy prefab to the Registered?Spawnable?Prefabs list.

No alt text provided for this image
Enemy prefab

8: Taking Damage

The final addition to our game is the ability to inflict damage on enemies and unfortunately, be defeated by them. To keep this tutorial straightforward, we will use the same script for both enemies and ships.

The script we'll utilize is called "ReceiveDamage" depicted below. It comes with configurable parameters like?maxHealth,?enemyTag,?and?destroyOnDeath.?The maxHealth parameter determines the initial health of the object. The enemyTag is used to detect collisions with specific tags. For instance, ships will have the "Enemy" tag, while enemies will have the "Bullet" tag. This way, we can ensure that ships collide only with enemies, and enemies collide only with bullets. Lastly, the "destroyOnDeath" parameter decides whether an object will be respawned or destroyed upon reaching zero health.

using System.Collection
using System.Collections.Generic;
using UnityEngine;
using Mirror;

public class ReceiveDamage : NetworkBehaviour 
{
    [SerializeField]
    private int maxHealth = 10;

    [SyncVar]
    private int currentHealth;

    [SerializeField]
    private string enemyTag;

    [SerializeField]
    private bool destroyOnDeath;

    private Vector2 initialPosition;

    // Use this for initialization
    void Start () 
    {
        this.currentHealth = this.maxHealth;
        this.initialPosition = this.transform.position;
    }

    void OnTriggerEnter2D (Collider2D collider) 
    {
        if(collider.tag == this.enemyTag) 
        {
            this.TakeDamage(1);
            Destroy(collider.gameObject);
        }
    }

    void TakeDamage (int amount) 
    {
        if(this.isServer) 
        {
            this.currentHealth -= amount;

            if(this.currentHealth <= 0) 
            {
                if(this.destroyOnDeath) 
                {
                    Destroy(this.gameObject);
                } 
                else 
                {
                    this.currentHealth = this.maxHealth;
                    RpcRespawn();
                }
            }
        }
    }

    [ClientRpc]
    void RpcRespawn () 
    {
        this.transform.position = this.initialPosition;
    }
};        

Let's examine the methods in the script. In the Start method, the script initializes the?currentHealth?to its maximum value and saves the initial position, which will be used for respawning ships later. Notice the?[SyncVar]?tag above the currentHealth attribute, indicating that this variable will be synchronized across the network.

Next, we check if the collider tag matches the enemyTag we are looking for. This ensures that collisions are handled only with the objects we intend to interact with (enemies against ships and bullets against enemies). If the conditions are met, we call the TakeDamage method and destroy the other collider.

The TakeDamage method is designed to be called exclusively on the server. Since currentHealth is already a SyncVar, updating it on the server will automatically synchronize it across all clients. The method is simple, decreasing the currentHealth and checking if it becomes less than or equal to 0.

The last method is the respawn method. Here, we employ another multiplayer feature known as?ClientRpc, indicated by the?[ClientRpc]?tag above the method definition. Unlike?[Command], which is sent from clients to the server, a ClientRpc is executed on the client, even though it was called from the server.

To complete the implementation, we add this script to both the Ship and Enemy prefabs. For ships, the Enemy Tag should be set as "Enemy" while for enemies, it should be set as "Bullet" (make sure to properly define the tags for the prefabs). Additionally, in the enemy prefab, we enable the "Destroy On Death" attribute as needed.

If you like the article please ?? it, wants to refer somebody ?? with him/her. We also provide Services of 2D/3D Game Development, 2D/3D Animations,Video Editing, UI/UX Designing.

If you have questions or suggestions about the game or want something to build from us, Feel free to reach out to us anytime!

?? Mobile: +971 544 614 238

?? Email: [email protected]

要查看或添加评论,请登录

VECTOR LABZ LLC的更多文章

社区洞察

其他会员也浏览了