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.
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 into my project.
Components for Runtime NavMesh Building
Building a Navigation System
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);
}
}
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);
}
}
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.
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.
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.
[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.
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.
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:
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 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)