В этом уроке я расскажу как сделать первый шаг в создании RTS-игры на Unity 5. Мы научимся:
- Показывать область выделения с помощью мыши
- Определять, какие юниты находятся в области выделения
- Выделять выбранные юниты
Рисуем в Unity область выделения
Есть много способов, чтобы нарисовать прямоугольник в Unity. В этом примере мы будем использовать GUI.DrawTexture
, как наиболее простой путь к достижению цели. Мы нарисуем цветной прямоугольник установив GUI.color
в желаемый цвет, а затем передавая белую 1×1 текстуру в GUI.DrawTexture
:
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;
}
}
Имейте в виду, что методы GUI, а также наш вспомогательный метод DrawScreenRect может быть вызван только в OnGUI()
. Также убедитесь, что вы создаете белую текстуру только один раз из соображений производительности.
Теперь мы можем нарисовать на экране прямоугольник из любого компонента в OnGUI()
:
void OnGUI()
{
Utils.DrawScreenRect(new Rect(32,32,256,128), Color.green);
}
Отобразим границы прямоугольника следующим методом:
public static void DrawScreenRectBorder( Rect rect, float thickness, Color color )
{
// верх
Utils.DrawScreenRect( new Rect( rect.xMin, rect.yMin, rect.width, thickness ), color );
// лево
Utils.DrawScreenRect( new Rect( rect.xMin, rect.yMin, thickness, rect.height ), color );
// право
Utils.DrawScreenRect( new Rect( rect.xMax - thickness, rect.yMin, thickness, rect.height ), color);
// низ
Utils.DrawScreenRect( new Rect( rect.xMin, rect.yMax - thickness, rect.width, thickness ), color );
}
void OnGUI()
{
// пример слева
Utils.DrawScreenRectBorder( new Rect( 32, 32, 256, 128 ), 2, Color.green );
// пример справа
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 ) );
}
Рисуем мышью прямоугольную область выбора
Область экрана в Unity бначинается в левом нижнем углу экрана. Это несовместимо с Rect-структурой, у которой начало в левом верхнем углу. Input.mousePosition
находит положение курсора в игровом окне, а не на экране, поэтому мы должны быть внимательны при использовании позиции мыши, чтобы создать Rect. Зная это, легко создать Rect из 2 координат курсора мыши:
public static Rect GetScreenRect( Vector3 screenPosition1, Vector3 screenPosition2 )
{
// Перемещение координат из левого нижнего в левый верхний угол
screenPosition1.y = Screen.height - screenPosition1.y;
screenPosition2.y = Screen.height - screenPosition2.y;
// Рассчитать углы
var topLeft = Vector3.Min( screenPosition1, screenPosition2 );
var bottomRight = Vector3.Max( screenPosition1, screenPosition2 );
// Создать прямоугольник
return Rect.MinMaxRect( topLeft.x, topLeft.y, bottomRight.x, bottomRight.y );
}
Мы можем написать простой скрипт, чтобы отобразить область выбора с помощью мыши:
public class UnitSelectionComponent : MonoBehaviour
{
bool isSelecting = false;
Vector3 mousePosition1;
void Update()
{
// Если нажимаем на левую кнопку мыши, то
// сохраняем координаты курсора мыши и начинаем выбор
if( Input.GetMouseButtonDown( 0 ) )
{
isSelecting = true;
mousePosition1 = Input.mousePosition;
}
// Если мы отпускаем левую кнопку мыши - конец выбора
if( Input.GetMouseButtonUp( 0 ) )
isSelecting = false;
}
void OnGUI()
{
if( isSelecting )
{
// Создаем прямоугольник на основе начальных и конечных координат курсора
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 ) );
}
}
}
Выбор юнитов в Unity
Для того, чтобы определить, какие объекты находятся в пределах окна выбора, мы должны привести выбираемые объекты и окно выбора в в одно и то же пространство.
Вашей первой мыслью была бы идея сделать проверку в мировом пространстве, но я лично предпочитаю делать в пост-проекции пространства, потому что в общем пространстве немного трудно конвертировать выбранную область, если ваша камера использует перспективную проекцию. В перспективной проекции форма выбранной области является усеченной. Вам нужно вычислить правильное значение проекционной матрицы, извлечь усеченную плоскость, а затем протестировать все 6 плоскостей.
Получение границы области просмотра для области выделения намного проще:
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;
}
И тестирование, если объект игры в рамках тривиален:
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 ) );
}
}
Выделение выбранных единиц с помощью проекторов
Есть много различных способов, чтобы выделить юниты. В RTS играх (в т.ч. на Unity) чаще всего размещают небольшой круг под выбранными юнитами. Так и сделаем.
Для того, чтобы круги хорошо работали с наклонной местностью мы используем проекторы (а не просто нарисуем круговой спрайт). Проекторы можно проецировать на другие геометрические материалы, но для этого им необходим особый тип шейдеров. У Unity 5 в стандартных ассетах содержатся Project/Light и Project/Multiply шейдеры, но, к сожалению, ни один из них нам не подходит: добавить круг под выбранными юнитами в RTS игре. Мы должны написать свой собственный проектор шейдеров.
Потребуются 2 параметра: текстура (альфа маска), которая определяет, какие пиксели будут затронуты, и оттенок цвета, который будет определять цвет затрагиваемых пикселей:
Properties {
_Color ("Tint Color", Color) = (1,1,1,1)
_ShadowTex ("Cookie", 2D) = "gray" {}
}
Настройка состояния шейдеров для проекции, используя аддитивный блендинг:
ZWrite Off
ColorMask RGB
Blend SrcAlpha One // Аддитивный блендинг
Offset -1, -1
Объединим альфа-маску и оттенок цвета, для определения окончательного цвета:
fixed4 frag (v2f i) : SV_Target
{
// Пример cookie-текстуры
fixed4 texCookie = tex2Dproj (_ShadowTex, UNITY_PROJ_COORD(i.uvShadow));
// Применить оттенок и альфа-маску
return _Color * texCookie.a;
}
Используя этот шейдер, мы можем установить проектор так, как мы хотели. В следующем примере мы использовуем простую круговую альфа-маску для проецирования круга под юнитом. Обратите внимание на параметры импорта, которые используются для альфа-маски. Должны быть маркеры текстуры с типом света прожектора (это устанавливает режим обтекания для фиксации) и должна быть проверена альфа градаций серого.
Осталось еще несколько проблем, которые мы должны решить, но об этом ниже.
Игнорирование слоев
По умолчанию проекторы проецируются на все. Мы же хотим, чтобы проекторы игнорировали другие элементы, чтобы не получилось как на рисунке ниже. Это может быть достигнуто с помощью свойства проектора Ignore Layers (Игнорировать Слои).
Затухание
Проекции могут иногда появляться на объектах за пределами усеченных проекций. В этом примере проекция появляется над пространственным блоком. Это происходит потому, что ландшафты, ограничивающие пространство, пересекают усеченную проекцию (даже если их геометрия этого не делает). Конечно, этот сценарий довольно маловерояен в RTS-играх, но он легко решается путем введения коэффициента затухания. Проекция может также исчезнуть, если блок стоит близко к препятствию.
fixed4 frag (v2f i) : SV_Target
{
// Применить оттенок и альфа-маску
fixed4 texCookie = tex2Dproj (_ShadowTex, UNITY_PROJ_COORD(i.uvShadow));
fixed4 outColor = _Color * texCookie.a;
// Расстояние затухания (хорошо работает _Attenuation = 1.0)
float depth = i.uvShadow.z; // [-1(рядом), 1(далеко)]
return outColor * clamp(1.0 - abs(depth) + _Attenuation, 0.0, 1.0);
}
Пример проекта
Взгляните на проект RTS игры в Unity 5, который реализует все описанное в этом уроке. В нем также расширен компонент выбора юнита для предпросмотра выбранного юнита и снятие выделения с выбранных юнитов. Ссылки для скачивания приведены ниже: