DEV Community

Cover image for 1. Building an RTS game in Unity - Basic Unit Navigation and selection tool
Alexey
Alexey

Posted on

1. Building an RTS game in Unity - Basic Unit Navigation and selection tool


Introduction

Devlog Part 1:
In my previous article, 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 exampleNavMesh 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 into my project.

Components for Runtime NavMesh Building

Building a Navigation System

Image description

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

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

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.

if (Selected)
{
    // Movement

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

        MoveToClickPoint(Input.mousePosition);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Initial settings for ImageInitial 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.

public interface ISelectable
{
    Vector3 WorldPosition { get; }

    void Register();

    void ToggleSelection(bool state);
}
Enter fullscreen mode Exit fullscreen mode

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.

[SerializeField, Range(0f, 40f)] private float delta;
[SerializeField] private RectTransform selection;
Enter fullscreen mode Exit fullscreen mode

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

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);
}
Enter fullscreen mode Exit fullscreen mode

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

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

The complete code is provided below:

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);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

I followed this 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.


Top comments (0)