Unity Micro Multiplayer RTS

Unity Micro Multiplayer RTS

Context

I'm a Developer Advocate for Unity, and thankfully, my team and I were helping out at this event called Nordic Game Jam, which happens in Copenhagen, Denmark. It was an amazing 4 days where the participants had the chance to attend talks from many professionals in the industry, and then have the chance to make their games in 48 hours.

There was a good variety of participants including industry professionals, students, and even some game industry veterans as well. We had a good time, and I don't think it's going to be the last time that I participate in this event. I definitely recommend it to any game developer near the area, or that can make the trip.

This year, there were 87 games made!

A sudden challenge appeared!

Our main purpose was to be there and help out all of the participants who had questions about coding, designing, needed some playtesting, etc. But during our visit, I got a question about how can you make a Real-time strategy (RTS) game using Unity's Netcode for GameObjects SDK. I answered the person’s question, but with more of how I would tackle it, not out of experience. I love RTS games, so my curiosity got the best of me and now I wanted to try it for myself!

From Wikipedia's article:

An RTS is a subgenre of strategy video games that do not progress incrementally in turns, but allow all players to play simultaneously, in "real-time".

Inspiration

I wasn't fully participating as a jammer, and I didn't have much time, so I needed a very small-scale scenario. Suddenly, I was looking at our table, and noticed that some of our SWAG was placed in a particular way, which gave me an idea:

It's a very simple, yet very common scenario that happens in RTS games. There will be two main bases located near a resource that is commonly wanted, gold. Here's an image of this scenario, from the famous RTS game, Age of Empires II:

I'll try to build something that resembles this scenario, let's go!

Note: I want to be honest with every reader that follows this guide. This is a small experiment, that does not reflect how you would create a production-ready RTS game.

This is more of a fun "Getting Started with Multiplayer" experiment, specifically with Unity technology.

Organizing the Scene

There are already some GameObjects shown, but I'll go over all of what's needed so that you can understand what's going on.

The requirements for this simulation are:

  • A network manager tha starts the network session and keeps the necessary info. about it.

  • A common gold resource that will be mined

  • A base spawner object that creates a base for each player that joins the network session, 2 in this case.

  • Those 2 bases will spawn their gold-mining workers

  • Each worker will then go to the gold, mine a little bit of it, and bring it back to the base

Network Manager

The NetworkManager is the game object that is responsible for holding important information about the networking sessions. It'll hold how packets of data will be sent back and forth between the client and server (or host), this is called the transport layer:

It also holds a list of game object prefabs that will be dynamically spawned at runtime in the networking session:

And here's the transport data, it's set by default, and it means that the simulation will run on localhost, or the same computer:

Gold Resource

For now, this is the only networking object that will be pre-spawned when the game starts:

Now this object is simply static, but still, we need to prepare it properly for network replication, here's the component layout:

Note: Notice that I added a new Physics layer to this object called GoldResource

Notice these 3 important built-in networking components:

We'll continue using these throughout the sample for the bases and workers.

The last important component inherits from the NetworkBehaviour class, this script defines the gold resource behavior for when it's mined:

using Unity.Netcode;
using UnityEngine;

public class GoldResourceNetBehavior : NetworkBehaviour
{
    public NetworkVariable<int> AmountOfGold { get; private set;} 
        = new(MAX_GOLD);

    private Vector3 m_originalScale;

    private const int MAX_GOLD = 50;

    public override void OnNetworkSpawn()
    {
        m_originalScale = gameObject.transform.localScale;

        if (!IsServer)
            return;

        AmountOfGold.OnValueChanged += OnAmountOfGoldChanged;
    }

    public override void OnNetworkDespawn()
    {
        if (!IsServer)
            return;

        AmountOfGold.OnValueChanged += OnAmountOfGoldChanged;
    }

    public bool MineGold()
    {
        if (AmountOfGold.Value > 0)
        {
            AmountOfGold.Value -= 1;
            return true;
        }
        return false;
    }

    private void OnAmountOfGoldChanged(int oldAmount, int newAmount)
    {
        if (!IsServer)
            return;

        //inform all of the clients(including host) that some gold was mined!
        UpdateXScaleBasedOnGoldAmountClientRpc();
    }

    [ClientRpc]
    private void UpdateXScaleBasedOnGoldAmountClientRpc()
    {
        float newXscale = 
            m_originalScale.x * ((float)AmountOfGold.Value / MAX_GOLD);

        transform.localScale = 
            new Vector3(newXscale, m_originalScale.y,m_originalScale.z);
    }
}

Notice that these functions are reactions to when a worker takes one piece of gold. There's additional functionality to make sure that every connected client receives an update.

Base Spawner

This object creates a base for each player that joins the network session:

We first need to define a custom class that stores the data of what prefab to spawn, and where:

using System;
using UnityEngine;

[Serializable]
class BaseData
{
    public GameObject m_basePrefab;
    public Transform m_baseSpawnPoint;
}

Now we need to define a class that handles the networking behavior. This class will handle the event of a client connecting to the network session, and spawn a base GameObject:

using UnityEngine;
using Unity.Netcode;

public class BaseSpawnerNetBehavior : NetworkBehaviour
{
    [SerializeField]
    private BaseData m_blackBaseData;

    [SerializeField]
    private BaseData m_whiteBaseData;

    [SerializeField]
    private GoldResourceNetBehavior m_goldResourceTarget = null;

    private NetworkVariable<byte> m_amountOfBasesSpawned = new(0);

    private void Start()
    {
        NetworkManager.Singleton.ConnectionApprovalCallback +=
            CheckIfBothBasesAreSpawned;

        NetworkManager.Singleton.OnClientConnectedCallback +=
            OnNewClientConnect;
    }

    private void CheckIfBothBasesAreSpawned(
        NetworkManager.ConnectionApprovalRequest request,
        NetworkManager.ConnectionApprovalResponse response)
    {
        response.Approved = m_amountOfBasesSpawned.Value < 2;
    }

    private void OnNewClientConnect(ulong newClientId)
    {
        if (!IsServer)
            return;

        if(m_amountOfBasesSpawned.Value == 0)
        {
            SpawnNewBase(m_blackBaseData);
        }
        else
        {
            SpawnNewBase(m_whiteBaseData);
        }
    }

    private void SpawnNewBase(BaseData baseData)
    {
        var newInstanceOfBase = Instantiate(
            baseData.m_basePrefab,
            baseData.m_baseSpawnPoint);

        var networkObject = newInstanceOfBase.GetComponent<NetworkObject>();
        networkObject.Spawn(true);

        var baseNetBehavior =
            newInstanceOfBase.GetComponent<BaseNetBehavior>();
        baseNetBehavior.SetGoldResourceTarget(m_goldResourceTarget);

        m_amountOfBasesSpawned.Value += 1;
    }
}

Note: The networkObject.Spawn(true); is the line of code that spawns the object in both the server and client!

And here's the component composition for this object:

After setting the right data, we see that the base spawner creates a new base game object for each player that joins the network session. Notice how on the left we start a new networking session as a host (client + server), and on the right, it joins the active session as a client:

As a quick note, to test with 2 instances of the Unity editor, I'm using this awesome Unity package called ParrelSync:

Bases

This is a very simple game object:

Nothing fancy, other than a NetworkObject component, and its networking behavior, which all it does is spawn workers that'll mine the gold with a max amount of 10:

using UnityEngine;
using Unity.Netcode;

public class BaseNetBehavior : NetworkBehaviour
{
    [SerializeField]
    private GameObject m_workerPrefabToSpawn;

    private GoldResourceNetBehavior m_goldResourceTarget = null;

    private float m_currentSpawnTime = 0f;

    private int m_workersSpawned = 0;

    private const int k_MAX_WORKERS = 10;

    public override void OnNetworkSpawn()
    {
        if (!IsServer)
            return;

        m_currentSpawnTime = Random.Range(1f, 3.5f);
    }

    public void SetGoldResourceTarget(
        GoldResourceNetBehavior goldResourceTarget)
    {
        m_goldResourceTarget = goldResourceTarget;
    }

    private void Update()
    {
        if (!IsServer || m_workersSpawned >= k_MAX_WORKERS)
            return;

        m_currentSpawnTime -= Time.deltaTime;

        if (m_currentSpawnTime >= 0f)
            return;

        SpawnNewBaseWorker();
    }

    private void SpawnNewBaseWorker()
    {
        var newInstanceOfBaseWorker = Instantiate(
            m_workerPrefabToSpawn,
            transform.position,
            Quaternion.identity);

        var networkObject = newInstanceOfBaseWorker.
            GetComponent<NetworkObject>();
        networkObject.Spawn(true);

        var baseWorkerNetBehavior =
            newInstanceOfBaseWorker.GetComponent<BaseWorkerNetBehavior>();

        baseWorkerNetBehavior.SetGoldResourceTarget(
            this,
            m_goldResourceTarget);

        m_currentSpawnTime = Random.Range(1f, 5.5f);

        m_workersSpawned++;
    }
}

The base also tells the spawned workers where the gold is, and where to return.

Workers

And last but not least, we have our gold miners, the base workers:

Note: Notice that I added a new Physics layer to this object called BaseWorker

The worker prefab also contains a little sprite that represents the little bit of gold being "carried" back to the base:

So still nothing fancy, but this is the game object in the sample that is doing the most activity. It's just a simple state machine that runs four states which are, idle, chasing gold, mining the gold, and finally returning the gold to its base:

using UnityEngine;
using Unity.Netcode;

public enum BaseWorkerState
{
    IDLE,
    CHASING_GOLD,
    MINING_GOLD,
    RETURNING_GOLD
}

public class BaseWorkerNetBehavior : NetworkBehaviour
{
    public BaseWorkerState baseWorkerState { get; private set; }
        = BaseWorkerState.IDLE;

    [SerializeField]
    private SpriteRenderer m_resourceSprite;

    private BaseNetBehavior m_owningBase = null;
    private GoldResourceNetBehavior m_goldResourceTarget = null;

    private float m_miningGoldTime = 3f;

    private float m_moveSpeed = 0f;
    private Vector3 m_moveDirection = Vector3.zero;

    public override void OnNetworkSpawn()
    {
        if (!IsServer)
            return;

        m_moveSpeed = Random.Range(.35f, 1.2f);
    }

    public void SetGoldResourceTarget(
        BaseNetBehavior ownerBase,
        GoldResourceNetBehavior goldResourceTarget)
    {
        m_owningBase = ownerBase;

        m_goldResourceTarget = goldResourceTarget;

        UpdateMovementDirection(goldResourceTarget.transform.position);

        baseWorkerState = BaseWorkerState.CHASING_GOLD;
    }

    private void Update()
    {
        if (!IsServer || m_goldResourceTarget == null)
            return;

        if (baseWorkerState == BaseWorkerState.CHASING_GOLD)
        {
            transform.Translate(
                Time.deltaTime * m_moveSpeed * m_moveDirection);
        }
        else if(baseWorkerState == BaseWorkerState.MINING_GOLD)
        {
            m_miningGoldTime -= Time.deltaTime;

            if (m_miningGoldTime <= 0f)
            {
                m_miningGoldTime = 3f;
                m_goldResourceTarget.MineGold();
                UpdateMovementDirection(m_owningBase.transform.position);

                //reduce speed to simulate slower movement
                // due to carrying "heavy stuff"
                m_moveSpeed *= 0.75f;

                //tell all clients that this worker is carrying some gold
                ToggleResourceSpriteClientRpc();

                baseWorkerState = BaseWorkerState.RETURNING_GOLD;
            }
        }
        else if(baseWorkerState == BaseWorkerState.RETURNING_GOLD)
        {
            transform.Translate(
                Time.deltaTime * m_moveSpeed * m_moveDirection);

            float distanceOfWorkerToBase =
                Vector3.Distance(
                    m_owningBase.transform.position,
                    transform.position);

            // Not the best way to check for this, just for testing purposes
            if (Mathf.Abs(distanceOfWorkerToBase) <= 0.002f)
            {
                //set target back to gold
                UpdateMovementDirection(
                    m_goldResourceTarget.transform.position);

                m_moveSpeed = Random.Range(.35f, 1.2f);

                // Inform that worker is NOT carrying gold anymore!
                ToggleResourceSpriteClientRpc();

                baseWorkerState = BaseWorkerState.CHASING_GOLD;
            }
        }
    }

    [ClientRpc]
    private void ToggleResourceSpriteClientRpc()
    {
        m_resourceSprite.enabled = !m_resourceSprite.enabled;
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (!IsServer)
            return;

        if(baseWorkerState == BaseWorkerState.CHASING_GOLD)
        {
            baseWorkerState = BaseWorkerState.MINING_GOLD;
        }
    }

    private void UpdateMovementDirection(Vector3 newTargetPosition)
    {
        m_moveDirection = newTargetPosition - transform.position;
        m_moveDirection.Normalize();
    }
}

One final Change, Physics layers!

Remember that I added new physics layers for the gold resource and the base workers? We need to do one final change. Head over to the Project Settings window, and under the Physics2D section, we'll change the Layer Collision Matrix to be like so:

For now, since our gold will only be mined by workers, and workers only care about gold, we'll make those types of objects only collide with themselves.

Everything put together

And check it out!

Once everything is put together, you see the workers going towards the gold, bringing back a bit of gold after mining it! You can even notice that the gold bar is shrinking as it's being mined.

Lessons learned

  • Even though this is a very simple and small sample, one could already start to tell how many more pieces working pieces are required to make a full RTS game!

  • I enjoyed making this a lot even though it was very simple. I could see myself working at a fuller RTS game sample if given the chance!

Please let me know what you think!

  • Are you building any multiplayer games yourself?

  • What would you build next if you were working on this?

  • What other game genres would you like to see being built as a multiplayer game?

And now, I think I'm going to play some AOE 2!

🎉🎉🎉 Happy building! 🎉🎉🎉