

In this post I will go over the following aspects of RTS style unit selection:

Draw a selection box using the mouse

Determine which units are within the selection box

Highlight selected units

Drawing rectangles

There are many ways to draw rectangles in Unity. In this example, I will use GUI.DrawTexture, as it is an easy and straightforward way to achieve our goal. We can draw a simple colored Rect using GUI.DrawTexture by setting GUI.color to the desired color, and then passing a white 1×1 texture to GUI.DrawTexture. I wrote a short utility class to make this easier:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static class Utils { static Texture2D _whiteTexture ; public static Texture2D WhiteTexture { get { if ( _whiteTexture == null ) { _whiteTexture = new Texture2D ( 1 , 1 ) ; _whiteTexture . SetPixel ( 0 , 0 , Color . white ) ; _whiteTexture . Apply ( ) ; } return _whiteTexture ; } } public static void DrawScreenRect ( Rect rect , Color color ) { GUI . color = color ; GUI . DrawTexture ( rect , WhiteTexture ) ; GUI . color = Color . white ; } }

Keep in mind that GUI methods (and thus also our DrawScreenRect utility method) can only be called during OnGUI(), and make sure you create the white texture only once for performance reasons.

We can now draw screen rectangles from any component in OnGUI():

1 2 3 4 void OnGUI ( ) { Utils . DrawScreenRect ( new Rect ( 32 , 32 , 256 , 128 ) , Color . green ) ; }



With the following utility method we can also draw borders for a rect:

1 2 3 4 5 6 7 8 9 10 11 public static void DrawScreenRectBorder ( Rect rect , float thickness , Color color ) { // Top Utils . DrawScreenRect ( new Rect ( rect . xMin , rect . yMin , rect . width , thickness ) , color ) ; // Left Utils . DrawScreenRect ( new Rect ( rect . xMin , rect . yMin , thickness , rect . height ) , color ) ; // Right Utils . DrawScreenRect ( new Rect ( rect . xMax - thickness , rect . yMin , thickness , rect . height ) , color ) ; // Bottom Utils . DrawScreenRect ( new Rect ( rect . xMin , rect . yMax - thickness , rect . width , thickness ) , color ) ; }

1 2 3 4 5 6 7 8 void OnGUI ( ) { // Left example Utils . DrawScreenRectBorder ( new Rect ( 32 , 32 , 256 , 128 ) , 2 , Color . green ) ; // Right example Utils . DrawScreenRect ( new Rect ( 320 , 32 , 256 , 128 ) , new Color ( 0.8f , 0.8f , 0.95f , 0.25f ) ) ; Utils . DrawScreenRectBorder ( new Rect ( 320 , 32 , 256 , 128 ) , 2 , new Color ( 0.8f , 0.8f , 0.95f ) ) ; }

Using the mouse to draw a selection box

Screen space in Unity has its origin (0,0) at the bottom left of the screen. This is inconsistent with the Rect struct, which has its origin at the top left. Input.mousePosition gives us the position of the mouse in screen space, so we have to be careful when using mouse positions to create a Rect. Knowing this, it is fairly easy to create a Rect from 2 screen space (mouse) positions:

1 2 3 4 5 6 7 8 9 10 11 public static Rect GetScreenRect ( Vector3 screenPosition1 , Vector3 screenPosition2 ) { // Move origin from bottom left to top left screenPosition1 . y = Screen . height - screenPosition1 . y ; screenPosition2 . y = Screen . height - screenPosition2 . y ; // Calculate corners var topLeft = Vector3 . Min ( screenPosition1 , screenPosition2 ) ; var bottomRight = Vector3 . Max ( screenPosition1 , screenPosition2 ) ; // Create Rect return Rect . MinMaxRect ( topLeft . x , topLeft . y , bottomRight . x , bottomRight . y ) ; }

We can then write a simple script that allows us to draw a selection box with the mouse:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class UnitSelectionComponent : MonoBehaviour { bool isSelecting = false ; Vector3 mousePosition1 ; void Update ( ) { // If we press the left mouse button, save mouse location and begin selection if ( Input . GetMouseButtonDown ( 0 ) ) { isSelecting = true ; mousePosition1 = Input . mousePosition ; } // If we let go of the left mouse button, end selection if ( Input . GetMouseButtonUp ( 0 ) ) isSelecting = false ; } void OnGUI ( ) { if ( isSelecting ) { // Create a rect from both mouse positions var rect = Utils . GetScreenRect ( mousePosition1 , Input . mousePosition ) ; Utils . DrawScreenRect ( rect , new Color ( 0.8f , 0.8f , 0.95f , 0.25f ) ) ; Utils . DrawScreenRectBorder ( rect , 2 , new Color ( 0.8f , 0.8f , 0.95f ) ) ; } } }

Selecting Units

In order to determine which objects are within the bounds of the selection box, we need to bring both the selectable objects and the selection box into the same space.

Your fist idea might be to run these tests in world space, but I personally prefer doing it in post-projection (viewport) space because it can be a bit tricky to convert a selection box into world space if your camera uses a perspective projection. With a perspective projection, the world shape of the selection box is a frustum. You’d have to calculate the correct viewprojection matrix, extract the frustum planes, and then test against all 6 planes.

Getting the viewport bounds for a selection box is much easier:

1 2 3 4 5 6 7 8 9 10 11 12 13 public static Bounds GetViewportBounds ( Camera camera , Vector3 screenPosition1 , Vector3 screenPosition2 ) { var v1 = Camera . main . ScreenToViewportPoint ( screenPosition1 ) ; var v2 = Camera . main . ScreenToViewportPoint ( screenPosition2 ) ; var min = Vector3 . Min ( v1 , v2 ) ; var max = Vector3 . Max ( v1 , v2 ) ; min . z = camera . nearClipPlane ; max . z = camera . farClipPlane ; var bounds = new Bounds ( ) ; bounds . SetMinMax ( min , max ) ; return bounds ; }

And testing if a game object is within the bounds is trivial:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class UnitSelectionComponent : MonoBehaviour { // [...] public bool IsWithinSelectionBounds ( GameObject gameObject ) { if ( ! isSelecting ) return false ; var camera = Camera . main ; var viewportBounds = Utils . GetViewportBounds ( camera , mousePosition1 , Input . mousePosition ) ; return viewportBounds . Contains ( camera . WorldToViewportPoint ( gameObject . transform . position ) ) ; } }

Highlighting selected units using projectors

There are many different ways to highlight units. In RTS games it seems to be pretty popular to place a small circle below selected units, so that’s what we are going to do.

In order for the circles to work well with sloped terrain we are going to use projectors (rather than just drawing a circle sprite below selected units). Projectors can project materials onto other geometry, but they need a special type of shader in order to do so. The Unity 5 standard assets contain both a Project/Light shader and a Project/Multiply shader, but unfortunately neither of those shaders are appropriate for what we want to do: Add circles to geometry below selected units. We’ll have to write our own projector shader.

It will take 2 parameters: a cookie texture (alpha mask) that defines which pixels are going to be affected, and a tint color, which will define the color of the affected pixels:

1 2 3 4 Properties { _Color ( "Tint Color" , Color ) = ( 1 , 1 , 1 , 1 ) _ShadowTex ( "Cookie" , 2D ) = "gray" { } }

Set up shader states for projection, but use additive blending:

1 2 3 4 ZWrite Off ColorMask RGB Blend SrcAlpha One // Additive blending Offset - 1 , - 1

Combine the alpha mask and the tint color to determine the final color:

1 2 3 4 5 6 7 fixed4 frag ( v2f i ) : SV_Target { // Sample cookie texture fixed4 texCookie = tex2Dproj ( _ShadowTex , UNITY_PROJ_COORD ( i . uvShadow ) ) ; // Apply tint & alpha mask return _Color * texCookie . a ; }

Using this shader, we can set up a projector like we initially intended. In the following example I use a simple circular alpha mask to project a circle below a unit. Note the import settings I used for the alpha mask. It needs to be a cookie texture with light type spotlight (this sets the wrap mode to clamp) and alpha from grayscale needs to be checked.



While we are now capable of rendering our selection circles, there are still a few issues that we need to address.

Ignoring layers By default projectors project onto everything. In our situation, we want projectors to ignore other units in order to avoid the behaviour seen in this image. This can be achieved using the Ignore Layers property of the projector. Personally, I like to have a ground layer which contains all of the terrain, and I simply ignore all other layers in the projector.

Attenuation Projections can sometimes appear on objects outside of the projectors frustum. In this example, a projection appears on the terrain above the unit. This happens because the terrains bounding box intersects the projectors frustum (even though its geometry does not). Granted, this scenario is fairly unlikely in an RTS game, but it is easily solved by introducing an attenuation factor. This will also fade out the projection when a unit stands close to a cliff.

1 2 3 4 5 6 7 8 9 fixed4 frag ( v2f i ) : SV_Target { // Apply tint & alpha mask fixed4 texCookie = tex2Dproj ( _ShadowTex , UNITY_PROJ_COORD ( i . uvShadow ) ) ; fixed4 outColor = _Color * texCookie . a ; // Distance attenuation (_Attenuation = 1.0 works well) float depth = i . uvShadow . z ; // [-1(near), 1(far)] return outColor * clamp ( 1.0 - abs ( depth ) + _Attenuation , 0.0 , 1.0 ) ; }

Example Project

I have created a Unity 5 project that implements everything discussed in this post. I also extended the unit selection component to preview unit selection and output all selected units. You can find the download link below.

Download Links