1. Building an RTS game in Unity - Basic Unit Navigation and selection tool
Tags: unity3d, devlog, gamedev, csharp
Published: 2024-01-26 14:46:21

---

## Introduction
**Devlog Part 1:**
In my [previous article](https://dev.to/kbrddestroyer/i-started-an-rts-game-project-hot-heads-in-the-firefight-jhf), I introduced the *Hot Heads* project and discussed the fundamental need for a unit navigation system in RTS games. I opted for Unity's NavMesh system, a well-established solution. In this article, I'll share my experience with Unity's NavMesh system.

## Unity NavMesh

Unity's NavMesh, based on the A* algorithm, provides a comprehensive pathfinding system. All you need in your scene is a NavMesh Surface and some NavMesh Agents.

![Unity Documentation example](https://docs.unity3d.com/ru/530/uploads/Main/NavMeshAgentSetup.svg)*NavMesh example from Unity Documentation*

> NavMeshAgent components assist in creating characters that navigate while avoiding collisions with each other. Agents leverage the NavMesh to navigate and intelligently avoid both static and dynamic obstacles. Pathfinding and spatial reasoning are handled using the scripting API of the NavMesh Agent.

To accommodate different unit types, ranging from small infantry units to large tanks, I decided to incorporate [NavMesh Components](https://github.com/Unity-Technologies/NavMeshComponents/tree/master/Assets/NavMeshComponents/Scripts) into my project.

> Components for Runtime NavMesh Building

**Building a Navigation System**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/71r8fr0wa52zx57hlmtz.png)

I attached the NavMesh Agent to my unit and implemented the movement logic in the following code within my UnitBase class.

```c#
private void MoveToClickPoint(Vector3 mouseClickPosition)
{
    Ray ray = mainCamera.ScreenPointToRay(mouseClickPosition);
    RaycastHit hit;
    if (Physics.Raycast(ray, out hit, 1000f, mask))
    {
        agent.SetDestination(hit.point);
    }
}
```

Here, **mask** represents the LayerMask for the Ground layer in the game. The line `if (Physics.Raycast(ray, out hit, 1000f, mask))` ensures that the hit point is obtained only from objects on the *Ground* layer.

This function is called when the mouse0 button is pressed and is checked in the **Update** function.

```c#
if (Selected)
{
    // Movement

    if (Input.GetKeyDown(KeyCode.Mouse1))
    {
        // Compute onTerrain position;

        MoveToClickPoint(Input.mousePosition);
    }
}
```

## Selection tool

One of the most important RTS mechanics is the ability to select one or multiple units. Currently, the game lacks this mechanic. Let's add the selection tool.

Firstly, I added a Canvas gameObject to my scene and an Image object to it.

![Canvas and Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fac2bn4pf5c9ughun88f.png)

![Initial settings for Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fl3bdnp9r7fow1l365un.png)*Initial settings for the selection box*

Now, with a selection box in place, it's time to add some logic. I created the **ISelectable** interface and the **Selection** class to control the selection box and the selectable items.

```c#
public interface ISelectable
{
    Vector3 WorldPosition { get; }

    void Register();

    void ToggleSelection(bool state);
}
```

The **ISelectable** interface is implemented by every class that can be selected by an on-screen box. It includes methods for obtaining the world position, registering a unit upon spawn, and toggling the selection state.

Now, let's discuss the **Selection** class. It has references to the selection tool's RectTransform component and a **delta** value that defines the delta position of the mouse cursor while the mouse0 button is being held to initiate a selection.

```c#
[SerializeField, Range(0f, 40f)] private float delta;
[SerializeField] private RectTransform selection;
```

The core logic involves updating two main parameters, **position** and **sizeDelta**, when a mouse drag is detected.

```c#
private void UpdateSelectionBox(Vector2 curMousePos)
{
    if (!selection.gameObject.activeInHierarchy)
        selection.gameObject.SetActive(true);
    float width = curMousePos.x - startPos.x;
    float height = curMousePos.y - startPos.y;
    selection.sizeDelta = new Vector2(Mathf.Abs(width), Mathf.Abs(height));
    selection.position = startPos + new Vector2(width / 2, height / 2);
}
```

In the **ReleaseSelectionBox** method, every selectable object within the selection box is tracked and selected.

```c#
void ReleaseSelectionBox()
{
    selection.gameObject.SetActive(false);
    Vector2 min = selection.anchoredPosition - (selection.sizeDelta / 2);
    Vector2 max = selection.anchoredPosition + (selection.sizeDelta / 2);
    
    foreach (ISelectable _selectable in selectables)
    {
        Vector2 screenPosition = mainCamera.WorldToScreenPoint(_selectable.WorldPosition);
        if (
            (screenPosition.x > min.x && screenPosition.y > min.y) &&
            (screenPosition.x < max.x && screenPosition.y < max.y)
            )
        {
            _selectable.ToggleSelection(true);
        }
        else _selectable.ToggleSelection(false);
    }
}
```

This code disables the selection box, retrieves box borders, and selects every ISelectable item within the screen region.

The complete code is provided below:

```c#
public class Selection : MonoBehaviour
{
    [SerializeField, Range(0f, 40f)] private float delta;
    [SerializeField] private RectTransform selection;

    private Camera mainCamera;
    private bool isSelection = false;
    private bool selectionEnabled = true;
    private Vector2 startPos, endPos;

    private static List<ISelectable> selectables = new List<ISelectable>();
    public static void RegisterNewSelectable(ISelectable selectable)
    {
        if (!selectables.Contains(selectable))
            selectables.Add(selectable);
    }

    public static void RemoveSelectable(ISelectable selectable)
    {
        if (selectables.Contains(selectable))
            selectables.Remove(selectable);
    }

    // Start is called before the first frame update
    void Start()
    {
        mainCamera = Camera.main;
    }

    // Update is called once per frame
    void Update()
    {
        if (selectionEnabled)
        {
            Vector2 mousePos = Input.mousePosition;
            if (Input.GetKeyDown(KeyCode.Mouse0))
            {
                startPos = mousePos;
            }
            if (Input.GetKey(KeyCode.Mouse0))
            {
                if (
                    Vector2.Distance(mousePos, startPos) > delta
                    )
                {
                    isSelection = true;
                    UpdateSelectionBox(mousePos);
                }
            }
            if (Input.GetKeyUp(KeyCode.Mouse0) && isSelection)
            {
                ReleaseSelectionBox();
                isSelection = false;
            }
        }

        if (Input.GetKeyDown(KeyCode.Escape)) selectionEnabled = !selectionEnabled;
    }

    private void UpdateSelectionBox(Vector2 curMousePos)
    {
        if (!selection.gameObject.activeInHierarchy)
            selection.gameObject.SetActive(true);
        float width = curMousePos.x - startPos.x;
        float height = curMousePos.y - startPos.y;
        selection.sizeDelta = new Vector2(Mathf.Abs(width), Mathf.Abs(height));
        selection.position = startPos + new Vector2(width / 2, height / 2);
    }

    void ReleaseSelectionBox()
    {
        selection.gameObject.SetActive(false);
        Vector2 min = selection.anchoredPosition - (selection.sizeDelta / 2);
        Vector2 max = selection.anchoredPosition + (selection.sizeDelta / 2);
        
        foreach (ISelectable _selectable in selectables)
        {
            Vector2 screenPosition = mainCamera.WorldToScreenPoint(_selectable.WorldPosition);
            if (
                (screenPosition.x > min.x && screenPosition.y > min.y) &&
                (screenPosition.x < max.x && screenPosition.y < max.y)
                )
            {
                _selectable.ToggleSelection(true);
            }
            else _selectable.ToggleSelection(false);
        }
    }
}

```

I followed [this tutorial](https://gamedevacademy.org/rts-unity-tutorial/) while implementing this, making a few modifications to the code.

This marks the second part of my devlog; stay tuned for more updates on my project. Feel free to share your thoughts and suggestions in the comments.

---