Build a Unity RTS Skirmish Controller
Build a two-faction Unity RTS with selection and NavMesh movement.
Introduction
30 Second Summary
A strategy game feels responsive when each click affects the right unit. Clear ownership rules make that interaction possible.
In this project, you will build a compact real-time strategy controller in Unity. Your faction choice decides which unit accepts commands across an obstacle-filled arena.
What You'll Build
Your finished arena lets you command the chosen faction through an obstacle-aware movement route around the central wall.
By the end of this project, you'll have:
- A faction picker that lets you choose Sanctus or Unionis before issuing commands.
- A friendly-only selection system where left-clicking your unit reveals its selection ring while enemy clicks are rejected.
- A pathfinding system that sends the selected unit around the central wall after a right-click order.
- Secret Mission: Shift-select a squad from your chosen faction to move multiple friendly units with one order.
Are there any prerequisites?
You'll need a supported Windows PC with a mouse. Internet access plus a Unity ID are required for setup. Basic familiarity with Unity and C# is expected.
Before We Start
Your starting kit is ready: a supported Windows 10 or Windows 11 PC, a mouse, internet access, and Visual Studio Code. This checkpoint locks in why your Unity RTS command prototype needs a faction choice to control which units accept selection and movement commands.
Set Up Unity and the RTS Project
Your RTS controller depends on precise input behavior. Version drift can turn that foundation into unrelated compile problems.
This step pins the Unity Editor before any battlefield work begins.
You also prepare the project packages. A saved scene gives the next step a reliable starting point.
In this step, get ready to:
- Install the required Unity Editor through Unity Hub.
- Create the FactionSkirmishRTS 3D Core project.
- Prepare the project for C# editing, input, navigation, and scene work.
Install Unity Hub and the pinned Editor
Unity Hub manages Editor installations from one place. It also connects the installation to your Unity ID.
- Visit the official Unity Hub download page.
- Download the Windows installer.
- Open the downloaded .exe file.
- Follow the setup prompts to install Unity Hub.
Unity Personal Eligibility
Activating a license can feel like a paid commitment. Unity Personal is free for eligible individuals or small organizations with less than $200K USD in revenue plus funds raised during the last 12 months.
- Press the Windows key to open Windows search.
- Type Unity Hub and press Enter.
- Create a Unity ID through the sign-in flow if you do not have one.
- Sign in to Unity Hub with your Unity ID.
- Follow Unity Hub's licensing prompts to activate an eligible Unity Personal license.
The project uses Editor version 6000.3.24f1. Choose the tab that matches the Editor installations shown in Unity Hub.
✔️ I see the required Editor version
- Select Installs in Unity Hub.
- Confirm that Unity 6000.3.24f1 appears in the installed Editors list.
Great, the pinned Editor is already available for the project.
ⓧ I see an older Editor version
The older Editor can stay installed. This project needs its pinned version to keep the package behavior consistent.
- Select Installs in Unity Hub.
- Click Install Editor.
- Select Unity 6000.3.24f1 from the official releases.
- Leave the default modules selected.
- Click Install to start the Editor installation.
- Keep Unity Hub open until Unity 6000.3.24f1 appears under Installs.
The full Editor download takes longer than the Hub setup. The installed version is ready when it appears in the Editors list.
ⓧ No Unity Editor is installed
Unity Hub is ready to add the pinned Editor. The module screen appears before the download begins.
- Select Installs in Unity Hub.
- Click Install Editor.
- Select Unity 6000.3.24f1 from the official releases.
- Leave the default modules selected.
- Click Install to start the Editor installation.
- Keep Unity Hub open until Unity 6000.3.24f1 appears under Installs.
That is the largest download in this setup. The pinned Editor is ready once Unity Hub lists it as installed.
Having Trouble Installing the Editor?
- Confirm that your internet connection remains active during the Editor download.
- Return to the official releases if Unity Hub displays a different Editor version.
- Ask for help with my Unity Editor installation.
Create the project and connect Visual Studio Code
A Unity project stores the assets plus settings for one game. The 3D Core template provides the blank 3D starting point needed for the arena.
- Click New project in Unity Hub.
- Choose Unity 6000.3.24f1 as the Editor version.
- Select the 3D Core template.
- Enter FactionSkirmishRTS in the Project name field.
- Choose your Desktop as the Location.
- Click Create project.
The first project launch can spend a few minutes importing the 3D Core template. You should then see the open FactionSkirmishRTS project in Unity.
Project Not Opening?
- Confirm that Unity Hub still lists Unity 6000.3.24f1 under Installs.
- Check that the selected project location is writable on your Windows account.
- Ask for help with my Unity project launch.
Unity opens C# scripts in an external editor. Visual Studio Code gains Unity-aware editing through Microsoft's extension.
- Press the Windows key to open Windows search.
- Type Visual Studio Code and press Enter.
- Press Ctrl+Shift+X to open the Extensions view.
- Search for Unity for Visual Studio Code.
- Select the extension published by Microsoft.
- Click Install.
You should see a Manage control replace the installation control. That confirms the extension is installed.
- Switch back to the open Unity Editor.
- Click Edit in the top menu.
- Select Preferences.
- Select External Tools.
- Choose Visual Studio Code under External Script Editor.
The External Script Editor field should now display Visual Studio Code. Unity can use that editor when you open the C# scripts created later.
Cannot Find the Unity Extension?
- Check that the extension result is published by Microsoft.
- Confirm that Visual Studio Code has internet access to load Marketplace results.
- Ask for help with my Visual Studio Code Unity extension.
Install packages and save the scene
The Input System provides the mouse input used by the controller. AI Navigation later gives selected units obstacle-aware movement.
- Click Window in the Unity menu bar.
- Select Package Manager.
- Choose Unity Registry as the package source.
- Select Input System.
- Choose version 1.20.0.
- Click Install.
You should see Input System version 1.20.0 listed as installed in Package Manager.
The new input backend must be active before the controller can read mouse clicks. Unity may request an Editor restart after this setting changes.
- Click Edit in the Unity menu bar.
- Select Project Settings.
- Select Player.
- Expand Other Settings.
- Set Active Input Handling to Input System Package (New).
- Accept the Editor restart if Unity requests it.
After any restart, the FactionSkirmishRTS project should reopen. The Active Input Handling field should show Input System Package (New).
Input Setting Did Not Stick?
- Confirm that Input System version 1.20.0 is installed before changing the input backend.
- Allow Unity to finish reopening the project before checking the setting again.
- Ask for help with my Active Input Handling setting.
- Return to Package Manager from the Window menu.
- Choose Unity Registry as the package source.
- Select AI Navigation.
- Choose version 2.0.14.
- Click Install.
You should see AI Navigation version 2.0.14 listed as installed in Package Manager.
Package Version Missing?
- Confirm that Unity Registry remains selected as the package source.
- Confirm that the project is open with Unity 6000.3.24f1.
- Ask for help with my Unity package versions.
Unity stores project content beneath the Assets folder. Separate folders keep scripts, materials, and scenes easy to find as the prototype grows.
- Select Assets in Unity's Project window.
- Right-click an empty area in the Project window.
- Select Create.
- Select Folder.
- Name the folder Scripts.
You should now see Assets/Scripts in the Project window.
- Repeat the folder workflow to create Assets/Materials.
You should now see Assets/Materials beside the scripts folder.
- Repeat the folder workflow to create Assets/Scenes.
You should now see all three project folders beneath Assets.
- Click File in the Unity menu bar.
- Select Save As.
- Choose the Assets/Scenes folder.
- Enter Skirmish as the scene name.
- Click Save.
The Project window should now contain Assets/Scenes/Skirmish.unity.
Setup Checkpoint
- The FactionSkirmishRTS 3D Core project is open with Unity 6000.3.24f1.
- Input System version 1.20.0 is installed.
- AI Navigation version 2.0.14 is installed.
- Active Input Handling uses Input System Package (New).
- The Assets/Scripts, Assets/Materials, and Assets/Scenes folders exist.
- The empty scene is saved as Assets/Scenes/Skirmish.unity.
Before you enter Play mode, do you expect the empty project to run cleanly with the pinned packages?
- Click the Play button at the top of the Unity Editor.
You should see the Game view running in Play mode without a compile-error interruption.
- Click the Play button again to stop Play mode.
- Select the Console tab.
You should see no compile errors in the Console. That clean run proves the pinned Editor plus packages are ready for the RTS scripts.
Seeing Compile Errors?
- Wait for Unity to finish importing packages before entering Play mode again.
- Confirm that both pinned package versions appear as installed in Package Manager.
- Ask for help with my clean Unity Play mode check.
That is the setup locked in. Next, you will turn the empty scene into a battlefield with two clickable faction units.
Build the Arena and Add Unit Selection
Your Unity project is ready. Now you can turn the empty scene into a battlefield that responds to your mouse.
A visible arena makes unit selection testable before ownership rules add complexity. This gives you the first playable slice of the RTS controller.
In this step, get ready to:
- Build a battlefield with a central wall.
- Create visually distinct Sanctus and Unionis units.
- Add left-click selection with visible feedback.
Build the battlefield
A layer separates objects according to their role. The controller later uses these layers to aim selection rays only at units.
- Create a Plane primitive from the Hierarchy window.
- Keep the Plane centered in the scene.
- Create a layer named Ground through the Plane's Layer dropdown in the Inspector window.
- Assign the Plane to the Ground layer.
- Create a Cube primitive from the Hierarchy window.
- Scale the Cube into a wide wall.
- Position the wall near the center of the Plane.
Why put the wall in now?
The wall makes the arena readable today. It also creates a clear obstacle for the movement comparison later in the project.
- Select Main Camera in the Hierarchy window.
- Position Main Camera above the arena at an angle.
- Aim Main Camera so the Plane and central wall fill the Game view.
- Save Assets/Scenes/Skirmish.unity.
- Press Play to preview the battlefield.
You should see the Plane from an angled top-down view. The wide wall should divide part of the arena.
- Stop Play mode.
Create the faction units
Distinct materials make faction identity visible before the controller understands ownership. Each unit also gets a hidden ring that the selection code can reveal.
- Select Assets/Materials in the Project window.
- Create a Material asset named Sanctus.
- Set the Sanctus material to a clearly visible color.
- Create a Material asset named Unionis.
- Set the Unionis material to a contrasting color.
What do the materials prove?
The colors let you identify each faction at a glance. You can later verify whether the same controller treats the two units differently.
- Create a Capsule primitive from the Hierarchy window.
- Rename the Capsule to Sanctus.
- Position Sanctus on one side of the wall.
- Assign the Sanctus material to the Capsule's Material property.
- Create a Cylinder primitive as a child of Sanctus.
- Rename the child to SelectionIndicator.
- Flatten SelectionIndicator into a thin ring beneath the Capsule.
- Remove the Collider from SelectionIndicator with Remove Component.
Why remove the ring collider?
The Capsule collider represents the selectable unit. Removing the ring collider prevents the visual feedback object from becoming a second raycast target.
- Deactivate SelectionIndicator with the active-state checkbox beside its name in the Inspector window.
- Duplicate the Sanctus unit root.
- Rename the duplicate to Unionis.
- Position Unionis on the opposite side of the wall.
- Assign the Unionis material to the duplicate's Material property.
- Create a layer named RTSUnit through a unit root's Layer dropdown.
- Assign the Sanctus unit root to the RTSUnit layer.
- Assign the Unionis unit root to the RTSUnit layer.
If Unity asks whether the children should use the new layer, keep the layer change on the unit roots. The collider-free rings do not need to receive selection raycasts.
- Save Assets/Scenes/Skirmish.unity.
- Press Play to inspect the faction units.
You should see two differently colored Capsules on opposite sides of the wall. Both selection rings should remain hidden.
- Stop Play mode.
Wire click selection
The selection system needs three small C# components. One defines factions. Another stores each unit's state. The controller turns a mouse position into a physics raycast.
- Create a C# script named Faction inside Assets/Scripts.
- Open Faction.cs in Visual Studio Code.
- Define the two faction values by replacing the file with this code:
public enum Faction
{
Sanctus,
Unionis
}
What does this code do?
- The Faction enum creates one shared type for ownership data.
- The Sanctus and Unionis values give each unit one valid faction.
- Save Faction.cs.
- Return to Unity.
- Check the Console after compilation finishes.
The Console should remain free of compile errors.
Seeing a compile error?
Check that the file contains both faction values inside the enum braces. Confirm that the file is named Faction.cs.
Still stuck? Help me debug the Faction enum in my Unity project.
✔️ Awesome, I've got everything!
Great. Save Faction.cs before continuing.
ⓧ I'd like to double check the full code
public enum Faction
{
Sanctus,
Unionis
}
- Create a C# script named RTSUnit inside Assets/Scripts.
- Open RTSUnit.cs in Visual Studio Code.
- Add the unit's faction data and selection state by replacing the file with this code:
using UnityEngine;
public class RTSUnit : MonoBehaviour
{
[SerializeField] private Faction faction;
[SerializeField] private GameObject selectionIndicator;
public Faction Faction => faction;
private void Awake()
{
SetSelected(false);
}
public void SetSelected(bool isSelected)
{
selectionIndicator.SetActive(isSelected);
}
}
How does a unit track selection?
- The serialized faction field stores the unit's ownership value.
- The serialized selectionIndicator field points to the ring child.
- The Faction property lets another component read the ownership value.
- The Awake() method hides the ring when the scene starts.
- The SetSelected() method controls the ring's active state.
- Save RTSUnit.cs.
- Return to Unity.
- Check the Console after compilation finishes.
The Console should remain free of compile errors. Unity should expose the faction and selection-indicator references when this component is attached.
Is RTSUnit failing to compile?
Check that Faction.cs is saved. Compare the spelling of selectionIndicator with every reference inside RTSUnit.cs.
Need another pair of eyes? Help me debug RTSUnit.cs.
✔️ Awesome, I've got everything!
Your unit component is ready. Confirm that RTSUnit.cs is saved.
ⓧ I'd like to double check the full code
using UnityEngine;
public class RTSUnit : MonoBehaviour
{
[SerializeField] private Faction faction;
[SerializeField] private GameObject selectionIndicator;
public Faction Faction => faction;
private void Awake()
{
SetSelected(false);
}
public void SetSelected(bool isSelected)
{
selectionIndicator.SetActive(isSelected);
}
}
The controller uses the Input System to detect a left-click. A camera ray converts the pointer position into a line through the 3D scene.
- Create a C# script named RTSController inside Assets/Scripts.
- Open RTSController.cs in Visual Studio Code.
- Create the controller references and selection-state helper by replacing the file with this code:
using UnityEngine;
using UnityEngine.InputSystem;
public class RTSController : MonoBehaviour
{
[SerializeField] private Camera gameplayCamera;
[SerializeField] private LayerMask unitLayer;
private RTSUnit selectedUnit;
private Ray PointerRay()
{
return gameplayCamera.ScreenPointToRay(Mouse.current.position.ReadValue());
}
private void SetSelectedUnit(RTSUnit unit)
{
if (selectedUnit != null)
{
selectedUnit.SetSelected(false);
}
selectedUnit = unit;
if (selectedUnit != null)
{
selectedUnit.SetSelected(true);
}
}
}
What does this controller foundation do?
- The gameplayCamera reference turns the current mouse position into a world-space ray.
- The unitLayer mask limits later raycasts to unit objects.
- The selectedUnit field remembers the current selection.
- The SetSelectedUnit() method hides the old ring before showing the new ring.
- Save RTSController.cs.
- Return to Unity.
- Check the Console after compilation finishes.
The controller foundation should compile without errors.
Is the controller foundation failing?
Confirm that Input System is installed. Check that the namespace line matches the code block.
Need help finding the mismatch? Help me debug the RTSController foundation.
- In RTSController.cs find the closing brace of PointerRay().
- Insert the selection raycast below PointerRay() with this code:
private void HandleSelection()
{
if (Physics.Raycast(PointerRay(), out RaycastHit hit, Mathf.Infinity, unitLayer.value))
{
RTSUnit candidate = hit.collider.GetComponentInParent<RTSUnit>();
SetSelectedUnit(candidate);
return;
}
SetSelectedUnit(null);
}
How does the raycast choose a unit?
- The Physics.Raycast() call checks the pointer ray against the unitLayer mask.
- The hit collider searches its parent objects for an RTSUnit component.
- A missed raycast clears the current selection.
- Save RTSController.cs.
- Return to Unity.
- Check the Console after compilation finishes.
The raycast method should compile without errors.
Is HandleSelection failing?
Check that HandleSelection() sits inside the RTSController class. Confirm that it appears between PointerRay() and SetSelectedUnit().
Still seeing a problem? Help me debug HandleSelection().
- In RTSController.cs find PointerRay().
- Insert the mouse-input loop above PointerRay() with this code:
private void Update()
{
if (Mouse.current == null)
{
return;
}
if (Mouse.current.leftButton.wasPressedThisFrame)
{
HandleSelection();
}
}
How does the click reach the raycast?
- The null check prevents input work when no current mouse is available.
- The left-button transition triggers HandleSelection() once for each click.
- Save RTSController.cs.
- Return to Unity.
- Check the Console after compilation finishes.
The completed controller should compile without errors.
Is mouse input failing to compile?
Confirm that Update() is inside the class. Check that using UnityEngine.InputSystem; remains at the top of the file.
Need help tracing the error? Help me debug the controller's mouse input.
✔️ Awesome, I've got everything!
Your three scripts are ready. Save every file before returning to the scene.
ⓧ I'd like to double check the full code
using UnityEngine;
using UnityEngine.InputSystem;
public class RTSController : MonoBehaviour
{
[SerializeField] private Camera gameplayCamera;
[SerializeField] private LayerMask unitLayer;
private RTSUnit selectedUnit;
private void Update()
{
if (Mouse.current == null)
{
return;
}
if (Mouse.current.leftButton.wasPressedThisFrame)
{
HandleSelection();
}
}
private Ray PointerRay()
{
return gameplayCamera.ScreenPointToRay(Mouse.current.position.ReadValue());
}
private void HandleSelection()
{
if (Physics.Raycast(PointerRay(), out RaycastHit hit, Mathf.Infinity, unitLayer.value))
{
RTSUnit candidate = hit.collider.GetComponentInParent<RTSUnit>();
SetSelectedUnit(candidate);
return;
}
SetSelectedUnit(null);
}
private void SetSelectedUnit(RTSUnit unit)
{
if (selectedUnit != null)
{
selectedUnit.SetSelected(false);
}
selectedUnit = unit;
if (selectedUnit != null)
{
selectedUnit.SetSelected(true);
}
}
}
- Select both unit roots in the Hierarchy window.
- Add the RTSUnit component through Add Component.
- Select the Sanctus unit root.
- Set its faction field to Sanctus.
- Assign its SelectionIndicator child to the selectionIndicator field.
- Select the Unionis unit root.
- Set its faction field to Unionis.
- Assign its SelectionIndicator child to the selectionIndicator field.
Why use serialized references?
Serialized fields keep scene-specific objects visible in the Inspector window. The same script can control either unit because each Capsule supplies its own faction and ring.
- Create an empty GameObject named GameManager in the Hierarchy window.
- Add the RTSController component to GameManager.
- Assign Main Camera to the gameplayCamera field.
- Set the unitLayer mask to RTSUnit.
- Save Assets/Scenes/Skirmish.unity.
Before you press Play, which ring do you expect to remain visible after you click both units in sequence?
- Press Play.
- Left-click the Sanctus unit.
You should see the Sanctus selection ring become visible. The Unionis ring should remain hidden.
- Left-click the Unionis unit.
You should see the Unionis ring become visible. The Sanctus ring should hide because the controller keeps one selected unit.
Does the selection ring stay hidden?
Confirm that each unit root uses the RTSUnit layer. Check that GameManager has Main Camera and the unit mask assigned.
Check that each selectionIndicator field points to its own Cylinder child. Help me debug my Unity unit selection.
- Stop Play mode.
That is your first playable RTS interaction working. Next up, you will add a faction picker that decides which side the player intends to command.
Add the Skirmish Faction Picker
Your arena already responds to left-click selection. The missing piece is a skirmish choice that tells the controller when play can begin.
A faction picker must pause world input until the player chooses a side. This step adds that startup boundary before testing whether the existing selector respects the choice.
In this step, get ready to:
- Build a centered startup picker for Sanctus and Unionis.
- Store the chosen faction in SkirmishSetup.
- Pause world input until the player chooses a faction.
Build the faction picker
Unity UI places the faction picker in screen space. The EventSystem routes button clicks through the Input System UI module.
- In Unity's Hierarchy, create a Canvas.
- Under Canvas, create a Panel named factionPanel.
- Switch to the Game view.
You should see the new panel covering part of the battlefield. This confirms that the Canvas renders in screen space.
- Return to the Scene view.
- Choose the centered anchor preset for factionPanel.
- Set its horizontal anchored position to 0.
- Set its vertical anchored position to 0.
- Switch to the Game view.
You should now see the panel centered over the arena.
- Return to the Scene view.
- Add a Button under factionPanel.
- Set its visible label to Command Sanctus.
- Switch to the Game view.
You should see the Sanctus choice inside the centered panel.
- Return to the Scene view.
- Add a second Button under factionPanel.
- Set its visible label to Command Unionis.
- Switch to the Game view.
Your startup choice now presents both factions in the Game view.
- Select EventSystem in the Hierarchy.
- Confirm that its input component is named Input System UI Input Module.
- Save Assets/Scenes/Skirmish.unity.
Why Does the EventSystem Matter?
The EventSystem sends pointer activity to your Unity UI controls. The Input System UI Input Module lets the two Buttons receive clicks while the new input backend is active.
Missing the Buttons or Input Module?
- Check that both Buttons are children of factionPanel.
- Check that factionPanel is a child of Canvas.
- Use Help me fix a Unity UI faction picker that is missing its Buttons or Input System UI Input Module.
Store the player's choice
A small C# component can hold the chosen faction for the rest of the scene. It also hides the panel once the player commits to a side.
- In Unity's Project window, select Assets/Scripts.
- Create a C# script named SkirmishSetup.
You should see SkirmishSetup.cs inside Assets/Scripts.
- Replace the generated contents of SkirmishSetup.cs with this code:
using UnityEngine;
public class SkirmishSetup : MonoBehaviour
{
[SerializeField] private GameObject factionPanel;
public bool HasSelectedFaction { get; private set; }
public Faction PlayerFaction { get; private set; }
public void ChooseSanctus()
{
ChooseFaction(Faction.Sanctus);
}
public void ChooseUnionis()
{
ChooseFaction(Faction.Unionis);
}
private void ChooseFaction(Faction faction)
{
PlayerFaction = faction;
HasSelectedFaction = true;
factionPanel.SetActive(false);
}
}
What Does This Code Do?
- The factionPanel field gives the component control over the startup panel.
- HasSelectedFaction records whether the player has made a choice.
- PlayerFaction stores the selected Faction value.
- ChooseFaction() records the choice before hiding the panel.
- Save SkirmishSetup.cs.
- Return to the Unity Editor.
- Wait for Unity to finish compiling the script.
- Confirm that the Console shows no compile errors.
Seeing a Compile Error?
- Check that the file is named SkirmishSetup.cs.
- Check that Faction.cs still contains the Sanctus and Unionis values.
- Use Help me diagnose the compile error in my SkirmishSetup component.
✔️ Awesome, I've got everything!
Your saved SkirmishSetup.cs now stores the player's faction choice.
ⓧ I'd like to double check the full code
using UnityEngine;
public class SkirmishSetup : MonoBehaviour
{
[SerializeField] private GameObject factionPanel;
public bool HasSelectedFaction { get; private set; }
public Faction PlayerFaction { get; private set; }
public void ChooseSanctus()
{
ChooseFaction(Faction.Sanctus);
}
public void ChooseUnionis()
{
ChooseFaction(Faction.Unionis);
}
private void ChooseFaction(Faction faction)
{
PlayerFaction = faction;
HasSelectedFaction = true;
factionPanel.SetActive(false);
}
}
Each Button needs a UnityEvent listener that calls the matching public method. Wiring these callbacks is the fiddly part because each listener needs the correct target object and function.
- Select GameManager in the Hierarchy.
- Add the SkirmishSetup component in the Inspector.
You should see the new component with an empty factionPanel field.
- Drag factionPanel from the Hierarchy into the matching field.
The field should now show the factionPanel GameObject reference.
- Select the Button labelled Command Sanctus.
- Click + in its On Click event.
You should see an empty listener row in the Button's Inspector.
- Drag GameManager into the listener's target field.
- Choose SkirmishSetup.ChooseSanctus() from the function menu.
- Press Play.
- Click Command Sanctus.
The panel should disappear. This proves that the Sanctus callback reaches SkirmishSetup.
- Stop Play mode.
- Select the Button labelled Command Unionis.
- Click + in its On Click event.
You should see a new empty listener row for the Unionis Button.
- Drag GameManager into the listener's target field.
- Choose SkirmishSetup.ChooseUnionis() from the function menu.
- Press Play.
- Click Command Unionis.
The panel should disappear again. Both choices now record a faction through the same component.
- Stop Play mode.
- Save Assets/Scenes/Skirmish.unity.
Does a Button Leave the Panel Open?
- Check that the Button's listener targets GameManager.
- Check that the Sanctus Button calls ChooseSanctus().
- Check that the Unionis Button calls ChooseUnionis().
- Use Help me debug a Unity Button that is not hiding my factionPanel.
Gate input and test ownership
The chosen faction state only helps when the controller checks whether a choice exists. The controller will use HasSelectedFaction as a gate for world input.
- Switch back to RTSController.cs in Visual Studio Code.
- Find this serialized camera field:
[SerializeField] private Camera gameplayCamera;
Where Does the New Reference Go?
The camera field marks the start of the controller's serialized references. The setup reference belongs directly below it.
- Add the skirmishSetup field below the camera field so the pair looks like this:
[SerializeField] private Camera gameplayCamera;
[SerializeField] private SkirmishSetup skirmishSetup;
What Does This Reference Provide?
The serialized reference connects the controller to the setup component on GameManager. The controller can now read whether the player has selected a faction.
- Save RTSController.cs.
- Return to the Unity Editor.
After Unity compiles, you should see an empty Skirmish Setup field on the RTSController component.
Missing the Setup Field?
- Check that the new field sits inside the RTSController class.
- Check the Console for a compile error that prevented the Inspector from refreshing.
- Use Help me find why my serialized SkirmishSetup field is missing from the Unity Inspector.
- In RTSController.cs, replace the existing Update() method with this version:
private void Update()
{
if (!skirmishSetup.HasSelectedFaction || Mouse.current == null)
{
return;
}
if (Mouse.current.leftButton.wasPressedThisFrame)
{
HandleSelection();
}
}
What Does This Gate Do?
- The first condition stops Update() while the faction picker is waiting for a choice.
- The second condition keeps the existing protection for systems without a current mouse.
- The left-click handler runs once both requirements pass.
- Save RTSController.cs.
- Return to the Unity Editor.
- Select GameManager in the Hierarchy.
- Drag its SkirmishSetup component into the Skirmish Setup field on RTSController.
- Save Assets/Scenes/Skirmish.unity.
Before you press Play, do you think either unit can be selected while the picker is still open?
- Press Play.
- Left-click either unit while the faction picker is open.
The unit's selection ring should stay hidden. The faction gate is blocking world input until you choose a side.
- Click Command Sanctus.
The faction panel should disappear. World input is now active.
With Sanctus chosen, do you think the Unionis unit can still light its selection ring?
- Left-click the Unionis unit.
The Unionis selection ring still lights up. This is the intended shortfall because HandleSelection() still accepts any RTSUnit hit by the ray.
- Stop Play mode.
Does the Test Behave Differently?
- If a unit responds before the choice, check the HasSelectedFaction guard in Update().
- If the panel stays open, check the Command Sanctus Button listener.
- If the Unionis ring stays hidden, compare HandleSelection() with the full code below.
- Use Help me debug the faction gate and opposing-unit selection test in my Unity RTS controller.
✔️ Awesome, I've got everything!
Your saved controller now waits for a faction choice before processing left-click selection.
ⓧ I'd like to double check the full code
using UnityEngine;
using UnityEngine.InputSystem;
public class RTSController : MonoBehaviour
{
[SerializeField] private Camera gameplayCamera;
[SerializeField] private SkirmishSetup skirmishSetup;
[SerializeField] private LayerMask unitLayer;
private RTSUnit selectedUnit;
private void Update()
{
if (!skirmishSetup.HasSelectedFaction || Mouse.current == null)
{
return;
}
if (Mouse.current.leftButton.wasPressedThisFrame)
{
HandleSelection();
}
}
private Ray PointerRay()
{
return gameplayCamera.ScreenPointToRay(Mouse.current.position.ReadValue());
}
private void HandleSelection()
{
if (Physics.Raycast(PointerRay(), out RaycastHit hit, Mathf.Infinity, unitLayer.value))
{
RTSUnit candidate = hit.collider.GetComponentInParent<RTSUnit>();
SetSelectedUnit(candidate);
return;
}
SetSelectedUnit(null);
}
private void SetSelectedUnit(RTSUnit unit)
{
if (selectedUnit != null)
{
selectedUnit.SetSelected(false);
}
selectedUnit = unit;
if (selectedUnit != null)
{
selectedUnit.SetSelected(true);
}
}
}
Your faction picker now controls when the skirmish begins. Next, you will turn that choice into a real ownership boundary before adding movement orders.
Enforce Ownership and Add Movement Orders
Your faction picker now records a side in the Unity scene. Selection still ignores that choice.
This step turns the chosen faction into a command boundary. You will also use mouse raycasting to send the selected unit toward a ground point.
In this step, get ready to:
- Filter unit selection by the chosen faction.
- Add a direct movement method to each unit.
- Send movement orders with the right mouse button.
Enforce faction ownership
A selected unit becomes eligible for player commands. The faction comparison decides which units can enter that selection state.
- In the Unity Editor's project file list, double-click Assets/Scripts/RTSController.cs to switch to Visual Studio Code.
- Find the current HandleSelection() method:
private void HandleSelection()
{
if (Physics.Raycast(PointerRay(), out RaycastHit hit, Mathf.Infinity, unitLayer.value))
{
RTSUnit candidate = hit.collider.GetComponentInParent<RTSUnit>();
SetSelectedUnit(candidate);
return;
}
SetSelectedUnit(null);
}
Why does selection leak?
- The raycast finds any unit on the unitLayer mask.
- The current method passes every candidate directly to SetSelectedUnit().
- Replace the entire method with this ownership-aware version:
private void HandleSelection()
{
if (Physics.Raycast(PointerRay(), out RaycastHit hit, Mathf.Infinity, unitLayer.value))
{
RTSUnit candidate = hit.collider.GetComponentInParent<RTSUnit>();
if (candidate != null && candidate.Faction == skirmishSetup.PlayerFaction)
{
SetSelectedUnit(candidate);
return;
}
}
SetSelectedUnit(null);
}
How does the ownership check work?
- The candidate != null check confirms that the clicked collider belongs to an RTSUnit.
- The faction comparison accepts units owned by skirmishSetup.PlayerFaction.
- An invalid click reaches SetSelectedUnit(null).
- Save Assets/Scripts/RTSController.cs.
- Switch back to the Unity Editor from earlier.
- Confirm the Console shows no compile errors.
Before you test this change, do you think clicking the opposing unit will leave the friendly selection ring visible or clear it?
- Click Play in the Unity Editor toolbar.
- Click Command Sanctus.
- Left-click the Sanctus unit.
- Left-click the Unionis unit.
You will see the Sanctus ring appear after the friendly click. Both rings stay hidden after the opposing click.
You have closed the ownership leak. The faction choice now controls which unit can receive commands.
- Stop Play mode.
Can the opposing unit still be selected?
Check that the condition compares candidate.Faction with skirmishSetup.PlayerFaction.
Confirm that each unit still has the correct Faction value in the Inspector.
Ask for help checking the ownership filter.
Give each unit a movement method
A MoveTo() method gives the controller one movement entry point. The controller can pass a world position without managing the unit's internal movement.
- In the Unity Editor's project file list, double-click Assets/Scripts/RTSUnit.cs.
- In Assets/Scripts/RTSUnit.cs, find the closing brace below SetSelected().
- Add this method before the class's final closing brace:
public void MoveTo(Vector3 destination)
{
transform.position = destination;
}
What does this method do?
- The destination parameter holds the clicked world position.
- The assignment applies that position directly to the unit's transform.
- Save Assets/Scripts/RTSUnit.cs.
- Switch back to the Unity Editor.
- Confirm the Console remains free of compile errors.
Good progress. Every unit now exposes the same movement command to the controller.
Seeing a compile error?
Check that MoveTo() sits inside the RTSUnit class.
Check that the method uses the existing UnityEngine import for Vector3.
Ask for help placing the movement method.
✔️ Awesome, I've got everything!
Your RTSUnit.cs file now exposes selection feedback plus direct movement.
ⓧ I'd like to double check the full code
using UnityEngine;
public class RTSUnit : MonoBehaviour
{
[SerializeField] private Faction faction;
[SerializeField] private GameObject selectionIndicator;
public Faction Faction => faction;
private void Awake()
{
SetSelected(false);
}
public void SetSelected(bool isSelected)
{
selectionIndicator.SetActive(isSelected);
}
public void MoveTo(Vector3 destination)
{
transform.position = destination;
}
}
Send right-click ground orders
The controller needs a second LayerMask for movement targets. This keeps the movement raycast focused on the Ground layer.
- Return to Assets/Scripts/RTSController.cs in Visual Studio Code.
- Find the three serialized fields at the top of the class:
[SerializeField] private Camera gameplayCamera;
[SerializeField] private SkirmishSetup skirmishSetup;
[SerializeField] private LayerMask unitLayer;
What is missing here?
The existing mask identifies unit colliders. Movement needs its own mask for valid ground destinations.
- Replace those fields with this four-field group:
[SerializeField] private Camera gameplayCamera;
[SerializeField] private SkirmishSetup skirmishSetup;
[SerializeField] private LayerMask unitLayer;
[SerializeField] private LayerMask groundLayer;
Why use a separate ground mask?
- The groundLayer field limits movement raycasts to the arena floor.
- Separate masks prevent unit colliders from becoming movement destinations.
- Save Assets/Scripts/RTSController.cs.
- Return to the Unity Editor.
- Confirm the Console shows no compile errors.
- Select GameManager in the scene object list.
- Set the Ground Layer field to Ground in the Inspector.
- Save Assets/Scenes/Skirmish.unity.
The controller now has a ground-only movement mask assigned in the scene.
Can't find the Ground Layer field?
Confirm that groundLayer includes the SerializeField attribute.
Resolve any compile errors before checking the component again.
Ask for help restoring the serialized field.
- Switch back to Assets/Scripts/RTSController.cs in Visual Studio Code.
- Find SetSelectedUnit() near the bottom of the class.
- Add this method immediately above SetSelectedUnit():
private void HandleMoveCommand()
{
if (selectedUnit == null)
{
return;
}
if (Physics.Raycast(PointerRay(), out RaycastHit hit, Mathf.Infinity, groundLayer.value))
{
selectedUnit.MoveTo(hit.point);
}
}
How does the movement order work?
- The first check ignores movement input when selectedUnit is empty.
- The raycast searches for a hit on groundLayer.
- The hit's point becomes the destination passed to MoveTo().
- Save Assets/Scripts/RTSController.cs.
- Return to the Unity Editor.
- Confirm the Console remains free of compile errors.
Seeing an error after adding the method?
Confirm that HandleMoveCommand() sits inside the RTSController class.
Check that groundLayer matches the serialized field name exactly.
Ask for help checking the movement handler.
- Switch back to Assets/Scripts/RTSController.cs in Visual Studio Code.
- Find the current Update() method:
private void Update()
{
if (!skirmishSetup.HasSelectedFaction || Mouse.current == null)
{
return;
}
if (Mouse.current.leftButton.wasPressedThisFrame)
{
HandleSelection();
}
}
What input does Update handle now?
The current method reacts only to leftButton presses. Right-click input never reaches the movement handler.
- Replace the entire method with this version:
private void Update()
{
if (!skirmishSetup.HasSelectedFaction || Mouse.current == null)
{
return;
}
if (Mouse.current.leftButton.wasPressedThisFrame)
{
HandleSelection();
}
if (Mouse.current.rightButton.wasPressedThisFrame)
{
HandleMoveCommand();
}
}
How are the mouse buttons divided?
- A left-button press calls HandleSelection().
- A right-button press calls HandleMoveCommand().
- Each press runs once through wasPressedThisFrame.
- Save Assets/Scripts/RTSController.cs.
✔️ Awesome, I've got everything!
Your RTSController.cs file now filters ownership plus sends right-click ground orders.
ⓧ I'd like to double check the full code
using UnityEngine;
using UnityEngine.InputSystem;
public class RTSController : MonoBehaviour
{
[SerializeField] private Camera gameplayCamera;
[SerializeField] private SkirmishSetup skirmishSetup;
[SerializeField] private LayerMask unitLayer;
[SerializeField] private LayerMask groundLayer;
private RTSUnit selectedUnit;
private void Update()
{
if (!skirmishSetup.HasSelectedFaction || Mouse.current == null)
{
return;
}
if (Mouse.current.leftButton.wasPressedThisFrame)
{
HandleSelection();
}
if (Mouse.current.rightButton.wasPressedThisFrame)
{
HandleMoveCommand();
}
}
private Ray PointerRay()
{
return gameplayCamera.ScreenPointToRay(Mouse.current.position.ReadValue());
}
private void HandleSelection()
{
if (Physics.Raycast(PointerRay(), out RaycastHit hit, Mathf.Infinity, unitLayer.value))
{
RTSUnit candidate = hit.collider.GetComponentInParent<RTSUnit>();
if (candidate != null && candidate.Faction == skirmishSetup.PlayerFaction)
{
SetSelectedUnit(candidate);
return;
}
}
SetSelectedUnit(null);
}
private void HandleMoveCommand()
{
if (selectedUnit == null)
{
return;
}
if (Physics.Raycast(PointerRay(), out RaycastHit hit, Mathf.Infinity, groundLayer.value))
{
selectedUnit.MoveTo(hit.point);
}
}
private void SetSelectedUnit(RTSUnit unit)
{
if (selectedUnit != null)
{
selectedUnit.SetSelected(false);
}
selectedUnit = unit;
if (selectedUnit != null)
{
selectedUnit.SetSelected(true);
}
}
}
Before you test the full order, where do you think the selected unit will end up when you right-click across the wall?
- Switch back to the Unity Editor.
- Click Play.
- Click Command Sanctus.
- Left-click the Sanctus unit.
- Right-click a point on the Plane beyond the central wall.
You will see the Sanctus unit jump directly to the clicked point. The unit does not travel around the wall.
That shortfall is intentional. Pathfinding is the missing behavior.
- Stop Play mode.
- Click Play again.
- Click Command Unionis.
- Left-click the Sanctus unit.
The Sanctus selection ring stays hidden. The opposing unit remains outside your command boundary.
- Left-click the Unionis unit.
- Right-click a point on the Plane beyond the central wall.
You will see the Unionis selection ring remain visible. The Unionis unit teleports to the ground point while Sanctus remains uncommandable.
Unit does not move on right-click?
Confirm that the Plane uses the Ground layer.
Confirm that the controller's Ground Layer field contains Ground.
Ask for help tracing the movement order.
- Stop Play mode.
- Save Assets/Scenes/Skirmish.unity.
Your command boundary now holds. Next, AI Navigation will turn that jump into a route around the wall.
Route Units Around Obstacles
Your faction boundary is working. The selected unit now accepts a destination from the ground beneath your cursor.
Direct position changes ignore geometry. In this step, AI Navigation turns the same right-click order into a path that travels around the wall.
In this step, get ready to:
- Bake a walkable route that excludes the central wall.
- Give both faction units a pathfinding component.
- Replace teleport movement with a destination request.
Bake a route around the wall
A NavMesh describes the parts of the arena that agents can cross. The baked surface marks the Plane as walkable while the wall occupies a blocked region.
- Right-click an empty area in the Hierarchy.
- Select Create Empty.
- Enter Navigation as the GameObject name.
- Select Navigation in the Hierarchy.
That container now has a clear job. It holds the arena's navigation data.
- Click Add Component in the Inspector.
- Select Navigation from the component categories.
- Select NavMesh Surface.
- Click Bake in the NavMesh Surface component.
Your route map is now visible. You should see a blue walkable area across the Plane with a gap around the central wall.
No Blue NavMesh?
- Select Navigation in the Hierarchy to display its navigation data.
- Confirm that the Inspector contains a NavMesh Surface component.
- Click Bake again after confirming that the Plane remains active.
- Help me diagnose why my baked NavMesh is not visible around the wall.
Give both units a NavMesh Agent
A NavMesh Agent follows paths across the baked surface. Each unit needs this component before it can respond to a path request.
- Select the Sanctus unit in the Hierarchy.
- Click Component in the top menu.
- Select Navigation.
- Select NavMesh Agent.
The Sanctus unit's Inspector now shows its movement component.
- Select the Unionis unit in the Hierarchy.
- Click Component in the top menu.
- Select Navigation.
- Select NavMesh Agent.
- Save Assets/Scenes/Skirmish.unity.
Both faction units can now receive paths from the baked surface. Their existing faction data remains unchanged.
Missing an Agent Component?
- Select each unit root instead of its selection indicator child.
- Confirm that both unit Inspectors contain NavMesh Agent.
- Help me check whether I added NavMesh Agent to both RTS unit roots.
Replace teleporting with a path request
The RTSUnit component currently fulfills MoveTo by writing to transform.position. A cached NavMeshAgent can request a route to the same destination.
- Double-click Assets/Scripts/RTSUnit.cs in the Editor's file list.
- Replace the file's contents with the code below:
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]
public class RTSUnit : MonoBehaviour
{
[SerializeField] private Faction faction;
[SerializeField] private GameObject selectionIndicator;
private NavMeshAgent agent;
public Faction Faction => faction;
private void Awake()
{
agent = GetComponent<NavMeshAgent>();
SetSelected(false);
}
public void SetSelected(bool isSelected)
{
selectionIndicator.SetActive(isSelected);
}
public void MoveTo(Vector3 destination)
{
agent.SetDestination(destination);
}
}
What Does This Code Do?
- The RequireComponent attribute declares that an RTS unit depends on a NavMeshAgent.
- The Awake method caches the unit's agent before movement begins.
- The MoveTo method sends the clicked ground position to SetDestination.
- The selection methods stay intact so the ownership boundary still controls which unit receives the order.
- Save Assets/Scripts/RTSUnit.cs.
- Return to the Editor.
The script recompiles with the pathfinding dependency. The Console should remain free of compile errors.
Seeing a Compile Problem?
- Confirm that the file includes the UnityEngine.AI namespace.
- Confirm that both unit roots still contain NavMesh Agent components.
- Help me compare my RTSUnit script with the required NavMeshAgent version.
✔️ Awesome, I've got everything!
Your unit script now converts movement orders into path requests. Make sure Assets/Scripts/RTSUnit.cs is saved.
ⓧ I'd like to double check the full code
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]
public class RTSUnit : MonoBehaviour
{
[SerializeField] private Faction faction;
[SerializeField] private GameObject selectionIndicator;
private NavMeshAgent agent;
public Faction Faction => faction;
private void Awake()
{
agent = GetComponent<NavMeshAgent>();
SetSelected(false);
}
public void SetSelected(bool isSelected)
{
selectionIndicator.SetActive(isSelected);
}
public void MoveTo(Vector3 destination)
{
agent.SetDestination(destination);
}
}
The controller's existing gameplayCamera, skirmishSetup, unitLayer, and groundLayer assignments remain in place. The existing factionPanel reference also remains connected.
The selection indicators remain assigned to their unit roots. The final test now checks pathfinding without changing the ownership rules.
- Click Play.
- Click Command Sanctus.
- Left-click the Sanctus unit.
Before you send the order, which route do you expect the Sanctus unit to take now that the wall is excluded from the NavMesh? Make your prediction now.
- Right-click a Ground-layer point on the far side of the wall.
You should see the selected Sanctus unit travel around the wall to reach the destination. The movement now follows a route instead of teleporting.
- Left-click the Unionis unit.
The Unionis selection ring should remain hidden. The Sanctus selection clears because the opposing unit is outside your command boundary.
- Click Play to stop the first session.
- Click Play to start a new session.
- Click Command Unionis.
- Left-click the Unionis unit.
Before the final order, do you expect the same controller path to work for Unionis? Make your prediction now.
- Right-click a Ground-layer point on the far side of the wall.
You should see the selected Unionis unit travel around the wall. The Sanctus unit remains uncommandable during this session.
- Click Play to exit Play mode.
That is the command layer behaving like an RTS. Each faction can now issue obstacle-aware orders through the same controller.
Secret mission
Command a Shift-Selected Squad
Your controller can command one friendly unit at a time. Extend it with Shift-based squad selection so multiple friendly units can share one movement order.
Clean Up Your Resources
Clean Up Your Resources
Your prototype lives entirely in a local Unity project with no cloud resources or ongoing charges. Decide whether to keep it ready, pause your work, or remove its local files.
Resources you used:
- Your local FactionSkirmishRTS project folder, including Assets/Scenes/Skirmish.unity, four C# scripts, materials, layers, scene objects, and the baked NavMesh data.
Keep everything running
No deletion is needed. Choose this if you are still building on the skirmish controller.
- Save Assets/Scenes/Skirmish.unity in the Unity Editor.
- Close the Unity Editor after you finish working.
- Return to FactionSkirmishRTS when you are ready to add combat or enemy AI.
Pause - I'll come back to this later
Shut down the open Unity tools to free up memory while preserving every project file.
- Exit Play mode in the Unity Editor if it is active.
- Save Assets/Scenes/Skirmish.unity.
- Close the Unity Editor.
- Close Unity Hub.
- Confirm no Unity window remains open.
- Confirm FactionSkirmishRTS remains in its original location.
Delete - I don't want to use this again
Deleting the project folder removes the arena, scripts, materials, and baked navigation data together. This affects only the local prototype because the project created no cloud resources.
- Exit Play mode in the Unity Editor if it is active.
- Close the Unity Editor.
- Close Unity Hub.
- Copy any files you want to reuse to a location outside FactionSkirmishRTS.
- In File Explorer, locate the FactionSkirmishRTS folder in the location you chose during project creation.
- Remove the FactionSkirmishRTS folder through File Explorer.
- Confirm the FactionSkirmishRTS folder no longer appears in that location.
This removes the local prototype
Keep a copy of anything you may want for another Unity project. Removing the folder takes the saved scene, scripts, materials, and baked navigation data with it.
Nice Work!
Nice Work!
Mission complete! You built a playable Unity RTS command prototype that turns a faction choice into ownership-aware control.
Your selection rings show which units answer to you. Right-click orders now send those units around the wall through the baked NavMesh.
You've learned how to:
- Model faction identity across a visible battlefield by pairing distinct Sanctus and Unionis units with reusable ownership data.
- Turn the skirmish faction picker into a command boundary that accepts friendly selections while rejecting opposing units.
- Replace direct teleport movement with NavMeshAgent pathfinding. Right-click orders now route selected units around the central wall.
- Secret Mission: Extend the controller with shift-selected squads. Every selected friendly unit receives the same movement order.
Ready to quiz yourself?