Как стать автором
Обновить

2d на Unity3d

Время на прочтение 14 мин
Количество просмотров 218K
В свое время, в начале разработки двухмерной игры на Unity3d, мне пришлось перерыть кучу документации, сайтов, проштудировать answers.unity3d.com и forum.unity3d.com на тему: как сделать 2d-игру на этом 3d-движке. Самыми первыми вопросами были: как правильно настроить камеру, как сделать 2d-спрайт так, чтобы он отображал текстуру «пиксель в пиксель». На то время уже существовал SpriteManager (далее – SM) и даже SM2 с его мощной поддержкой редактора. Судьба сложилась так, что я не мог его тогда купить, да и пробной версии не было. В итоге, мне пришлось самому написать несколько полезных скриптов для реализации 2d на Unity3d. О том, как это сделать, пойдет речь в этой статье.

"

Источники информации


Сразу напишу о некоторых полезных источниках информации по Unity3d для тех, кто вообще не знаком с движком, либо только начинает с ним знакомиться.
  • Unity3d.com – официальный сайт движка. Здесь полезными будут странички:
    • Unity User Manual – официальное руководство пользователя. Начинать изучать здесь.
    • Unity Reference Manual – более углубленное изучение.
    • Scripting Reference – здесь все по библиотеке Unity3d для всех трех поддерживаемых языков (Boo, JavaScript и C#).
    • Unity Resources – здесь можно найти видео-руководства, примеры, презентации.
    • Unity Answers – здесь ответят на ваши вопросы, либо можно найти готовый ответ. Часто пользовался этим ресурсом.
    • Unity Community — форум.
  • Unity3d по-русски — сайт разработчиков под Unity3d на русском языке. Если у вас неважно с английским, здесь есть руководства на русском, а на форуме можно получить ответы на интересующие вопросы.
  • Unity3d Wiki – очень полезный ресурс. Здесь множество рекомендаций, бесплатных полезных скриптов.
Также после установки Unity3d на вашем компьютере появятся локальные версии руководств и видео. При первом запуске их можно будет выбрать из окна приветствия или найти в меню Help.



Что понадобится


Версия используемого движка – 3.3, весь код написан на C# и будет работать на всех лицензиях Unity. Достаточно скачать бесплатную версию Unity. В комплекте идет MonoDevelop – бесплатная среда для разработки на .NET (на случай, если у вас нет Visual Studio). Среда легко настраивается «под себя», в ней есть все, что необходимо для разработки, поддерживает такие удобные функции, как автодополнение, шаблоны подстановки кода и многое другое. Сам я пользуюсь Visual Studio в связке с Resharper – так удобнее. Редактор Unity поддерживает обе среды.

Условия использования кода


Код я писал с нуля, прямо во время написания статьи, так что у вас не должно быть проблем с повторением моих шагов. Кодом можно свободно пользоваться, редактировать его, улучшать.

Условие только одно: при использовании кода (в том числе модифицированного) в коммерческом проекте необходимо указать ссылку на автора (т.е. fischer — меня).

Код примера, используемого в статье, можно скачать отсюда.

Настройка камеры


Для начала создайте пустую сцену без объектов. Удалите объект MainCamera, добавленный по умолчанию.

Нужно настроить камеру так, чтобы она стала «пригодна» для 2d. В Unity под камерой понимается класс Camera. Для того, чтобы ее использовать, нужно:
  1. Создать пустой объект (GameObject -> Create Empty).
  2. Выбрать его и добавить ему компонент Camera (Component -> Rendering -> Camera).
Изначально камера перпендикулярна плоскости XOY и направлена вдоль оси Z в положительном направлении. В дальнейшем для простоты спрайты будут лежать в плоскости XOY и по направлению к камере, так что поместите камеру в центр координат и отдалите ее по оси Z на необходимое расстояние в отрицательное полупространство (скажем, в точку [0, 0, -100]).

Для 2d-графики положение спрайтов в пространстве не важно. На много важнее, как спрайты друг друга перекрывают. Камера поддерживает два режима (вида проекции): перспективный (Perspective) и ортогональный (Orthographic). Первый используется во всех 3d-играх: объекты, расположенные дальше от камеры, выглядят меньше. Это почти то, как мы видим наш мир. Нам нужен второй режим, Orthographic – объекты всегда рисуются реального размера и перекрывают друг друга в зависимости от расстояния до камеры. Идеальный режим камеры для 2d и изометрии. В окне Inspector в компоненте Camera вновь созданного объекта в поле Projection выберите Orthographic. При этом некоторые параметры (соответствующие Perspective-режиму) пропадут, но появится параметр Size – размер ортогональной камеры.



Теперь настроим камеру так, чтобы каждый пиксель на экране соответствовал одной единице (unit) пространства в Unity. В дальнейшем это будет удобно при перемещении спрайтов и задании их размеров в пикселях. Для этого размер ортогональной камеры (параметр Size) должен равняться половине высоты экрана в пикселях. Например, если это экран iPhone 3G в портретном режиме, разрешение экрана которого 320x480, то Size = h/2 = 480/2 = 240.

Для того чтобы каждый раз не делать всего этого вручную, напишем скрипт:
using UnityEngine; 
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))] 
internal class Ortho2dCamera : MonoBehaviour
{
  [SerializeField] private bool uniform = true;
  [SerializeField] private bool autoSetUniform = false;

  private void Awake()
  {
    camera.orthographic = true;

    if (uniform)
      SetUniform();
  } 
  private void LateUpdate()
  {
    if (autoSetUniform && uniform)
      SetUniform();
  } 
  private void SetUniform()
  {
    float orthographicSize = camera.pixelHeight/2;
    if (orthographicSize != camera.orthographicSize)
      camera.orthographicSize = orthographicSize;
  }
}

Если добавить этот скрипт на любой игровой объект (GameObject), то:
  1. Автоматически этому объекту добавится компонент Camera. За это отвечает атрибут RequireComponent.
  2. Выполнится функция Awake. За это отвечает атрибут ExecuteInEditMode, который заставляет выполняться скрипты прямо в редакторе.
  3. В результате вызова этой функции камера станет ортогональной.
  4. Ее размер будет установлен таким, чтобы один пиксель на экране соответствовал одной единице Unity (вызов функции SetUniform). Это будет выполняться автоматически для любого экрана.
Теперь камера должна выглядеть так:



Улучшения


  1. Если размер экрана может меняться во время выполнения (поворот экрана смартфона, смена разрешения пользователем), неплохо бы автоматически менять размер камеры. Это можно делать в функции LateUpdate.
  2. Если освещение использоваться не будет (как и бывает в большинстве 2d-игр), рекомендую в настройках проекта (File->Build Settings->Player Settings->Other Settings) установить параметр Rendering Path в значение Vertex Lit. Это самый простой способ отрисовки объектов (каждый объект за один шаг для всех источников света), поддерживаемый большинством устройств. В моем случае для iOS-устройств это дало скачок в производительности. То же самое можно сделать для конкретной камеры. По умолчанию камера используют значение из Player Settings.

Спрайт


Спрайт — прямоугольник с наложенной на него текстурой. Договоримся, что он по умолчанию будет расположен в плоскости XOY. Тогда за взаимное расположение спрайтов (слои) будет отвечать координата Z.

Подсказка: для того, чтобы легче было увидеть спрайты, повернуть координатную ось можно щелкая на осях сцены, справа вверху, пока они не примут надлежащий вид (в нашем случае положение Back).



Спрайт будет задаваться несколькими параметрами:
  [SerializeField] private Vector2 size = Vector2.one;
  [SerializeField] private Vector2 zero = Vector2.one / 2;
  [SerializeField] private Rect textureCoords = Rect.MinMaxRect(0, 0, 1, 1);
  [SerializeField] private bool pixelCorrect = true;

Расшифруем их:
  • zero – положение нулевой точки спрайта относительно его нижнего левого угла. Измеряется в долях спрайта, т.е. (0.5, 0.5) – это центр спрайта. Нужен для правильного смещения спрайта в не зависимости от того, как он расположен на текстуре.
    Подсказка: чтобы в редакторе увидеть на спрайте оси поворота/перемещения именно в нулевой точке, а не в центре (по умолчанию), необходимо на элементе управления Transform Gizmo Toggles панели инструментов выбрать Pivot.

  • textureCoords – текстурные координаты. Представляют собой координаты левого верхнего угла и размеры области на текстуре. Измеряются так же, как в OpenGL – в долях текстуры. Для каждой текстуры настраивается параметр, обозначающий будут ли координаты обрезаться при выходе из отрезка [0, 1], или текстура будет повторяться (параметр импорта текстуры Wrap Mode).
  • pixelCorrect – булева величина, означает будет ли спрайт отображаться «пиксель в пиксель», чтобы каждому пикселю на спрайте соответствовал пиксель на экране.
Для отображения прямоугольника будем использовать объект класса Mesh. В самом простом случае он инициализируется:
  1. списком вершин,
  2. списком индексов вершин, составляющих вершины треугольников, из которых состоит прямоугольник,
  3. списком соответствующих вершинам текстурных координат.

Учитывая параметры спрайта указанные выше, объект класса Mesh будем создавать так:

  private static Mesh CreateMesh(Vector2 size, Vector2 zero, Rect textureCoords)
  {
    var vertices = new[]
                       {
                         new Vector3(0, 0, 0),          // 1 ___  2
                         new Vector3(0, size.y, 0),     //   |  |
                         new Vector3(size.x, size.y, 0),//   |  |
                         new Vector3(size.x, 0, 0)      // 0 ---- 3
                       };

    Vector3 shift = Vector2.Scale(zero, size);
    for (int i = 0; i < vertices.Length; i++)
    {
      vertices[i] -= shift;
    }

    var uv = new[]
        {
          new Vector2(textureCoords.xMin, 1 - textureCoords.yMax),
          new Vector2(textureCoords.xMin, 1 - textureCoords.yMin),
          new Vector2(textureCoords.xMax, 1 - textureCoords.yMin),
          new Vector2(textureCoords.xMax, 1 - textureCoords.yMax)
        };

    var triangles = new[]
      {
        0, 1, 2,
        0, 2, 3
      };

    return new Mesh { vertices = vertices, uv = uv, triangles = triangles };
  }

Чтобы нарисовать меш, понадобятся компоненты MeshRenderer и MeshFilter. Первый компонент содержит ссылки на материалы текстур для спрайта. Второй из них содержит объект MeshFilter.mesh, который он и рисует. Для изменения спрайта нужно, соответственно, изменять этот объект. Сам спрайт реализуется через компонент SampleSprite. Для того чтобы у спрайта эти два компонента были всегда, добавим ему соответствующие атрибуты RequireComponent:

using UnityEngine;  
[ExecuteInEditMode]
[AddComponentMenu("Sprites/Sample Sprite")]
[RequireComponent (typeof(MeshFilter))]
[RequireComponent (typeof(MeshRenderer))]
public class SampleSprite : MonoBehaviour
{
 …
}

Атрибут AddComponentMenu добавляет в меню Component редактора пункт Sprites->Sample Sprite. Используя его можно добавить к любому объекту Unity наш компонент SampleSprite.



Для того чтобы можно было видеть спрайт во время редактирования, атрибут ExecuteInEditMode позволяет вызывать функции Awake и Start класса SampleSprite прямо в редакторе. Внутри этих функций создается меш:

  private MeshFilter meshFilter;
  private MeshRenderer meshRenderer;

  #region Unity messages

  // Use this for initialization
  private void Awake()
  {
    meshFilter = GetComponent<MeshFilter>();
    meshRenderer = GetComponent<MeshRenderer>();
  }

  private void Start()
  {
    // NOTE: initializing mesh here because our camera is initialized in Awake()
    InitializeMesh();
  }

  #endregion

При инициализации меша нужно учитывать флаг pixelCorrect. Проверяем, во сколько раз высота экрана отличается от размера камеры — во столько раз нужно увеличить меш, учитывая, что при нормальном варианте (высота экрана равна удвоенному размеру ортогональной камеры) размер меша равен размеру области текстуры для спрайта:

  private void InitializeMesh()
  {
    Camera cam = Camera.main;
    if (pixelCorrect && cam != null)
    {
      float ratio = cam.pixelHeight / (2 * cam.orthographicSize);
      size.x = NonNormalizedTextureCoords.width * ratio;
      size.y = NonNormalizedTextureCoords.height * ratio;
    }
    meshFilter.mesh = CreateMesh(size, zero, textureCoords);
  }

NonNormalizedTextureCoords – текстурные координаты в пикселях. Определяются через нормализованные текстурные координаты (параметр спрайта) и размер самой текстуры TextureSize:

  private Rect NonNormalizedTextureCoords
  {
    get
    {
      Rect coords = textureCoords;
      Vector2 texSize = TextureSize;
      if (texSize != Vector2.zero)
      {
        coords.xMin *= texSize.x;
        coords.xMax *= texSize.x;
        coords.yMin *= texSize.y;
        coords.yMax *= texSize.y;
      }
      return coords;
    }
  }

  private Vector2 TextureSize
  {
    get
    {
      if (meshRenderer == null)
        return Vector2.zero;
      Material mat = meshRenderer.sharedMaterial;
      if (mat == null)
        return Vector2.zero;
      Texture tex = mat.mainTexture;
      if (tex == null)
        return Vector2.zero;
      return new Vector2(tex.width, tex.height);
    }
  }

Заметьте, что меш инициализируется в функции Start, потому что при его инициализации используется информация из камеры, а она инициализируется нами в Awake, т.е. в Start такая информация уже доступна для других объектов (в Unity сначала вызываются все Awake, затем все Start, но порядок вызова одной и той же функции для различных объектов не определён). Так же в этом примере используется Camera.main — главная камера на сцене. Т.е. наша камера должна быть помечена тегом MainCamera.



В принципе, на этом этапе со спрайтом уже можно работать. Для этого к любому объекту нужно прикрепить компонент SampleSprite (например, через меню Component или перетянув на него файл скрипта). Автоматически к нему добавятся компоненты MeshFilter и MeshRenderer. Теперь если перетянуть на этот объект материал текстуры (или текстуру, а материал создастся автоматически), и настроить параметры, то можно будет увидеть готовое 2d-изображение.



Настройка параметров текстуры


Для правильного отображения спрайта необходимо в свойствах текстуры изменить следующие параметры:
  1. чтобы движение спрайта было плавным, чтобы при изменении размеров спрайт выглядел сглаженным, необходимо параметр экспорта текстуры Filter Mode установить в Bilinear или Trilinear);
  2. установите Texture Type в значение GUI;
  3. не забудьте убрать компрессию, установив значение параметра Format в Truecolor.



Освещение


Обычно в 2d-играх не используется освещение. Эти эффекты задаются заранее художниками на текстурах, системой частиц и другими способами. К тому же освещение влияет на скорость отрисовки. Поэтому в случае спрайтов для используемых материалов необходимо выбрать соответствующий шейдер:
  1. В версии 3.3 есть группа шейдеров Unlit с отключенным освещением. Для спрайтов с прозрачностью подойдет шейдер Unlit->Transparent, для заднего фона Unlit->Texture.
  2. В старых версиях Unity можно использовать шейдер Transparent->Diffuse. Но тогда надо не забыть в Edit->Render Settings проставить Ambient Light в белый, чтобы все спрайты были натурального цвета. Иначе они будут затемненными, потому что по умолчанию в качестве Ambient Light стоит оттенок серого.
  3. Можно написать свой шейдер, в котором освещение будет отключено. О том, как это сделать, можно посмотреть в официальном пособии по шейдерам Unity.

Использование возможностей редактора


Unity позволяет расширить возможности редактора. Для этого используются, например:
  1. Тег EditorOnly.
  2. Создание редакторов игровых объектов в Инспекторе Компонентов путем наследования от класса Editor.
  3. Создание окон редактора путем наследования от класса EditorWindow.

Можно предоставить пользователю возможность задавать некоторые параметры спрайта в более удобных единицах и тут же видеть результат. Например, нулевую точку лучше всего хранить нормализованной, т.к. если размер объекта на текстуре изменится, то ее не нужно будет исправлять ручками. Но задавать ее в редакторе удобнее в пикселях, чтобы представлять, как она будет расположена относительно размеров спрайта. Для всех этих нужд подойдет вот такая реализация класса Editor:

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(SampleSprite))]
public class SampleSpriteEditor : Editor
{
  public override void OnInspectorGUI()
  {
    Target.Size = EditorGUILayout.Vector2Field("Size", Target.Size);
    Target.Zero = EditorGUILayout.Vector2Field("Zero Point", Target.Zero);
    Target.TextureCoords = EditorGUILayout.RectField("Texture Coordinates", Target.TextureCoords);
    Target.PixelCorrect = EditorGUILayout.Toggle("Pixel Correct", Target.PixelCorrect);

    if (GUI.changed)
    {
      Target.UpdateMesh();
      EditorUtility.SetDirty(target);
    }
  }

  private SampleSprite Target
  {
    get { return target as SampleSprite; }
  }

  [MenuItem("Sprites/Create/Sample")]
  private static void CreateSprite()
  {
    var gameObject = new GameObject("New Sample Sprite");
    gameObject.AddComponent<SampleSprite>();
    Selection.activeObject = gameObject;
  }
}
Внимание: все скрипты, связанные с редактором, должны располагаться в папке Assets/Editor.

Атрибут CustomEditor говорит о том, что данный класс будет использован в Инспекторе Компонентов как редактор для класса-компонента SampleSprite. Свойство Target введено для удобства обращения к полям редактируемого объекта, т.к. выдаваемый по умолчанию объект target имеет тип Object. В переопределенной функции OnInspectorGUI задается список параметров компонента SampleSprite, отображаемых в Инспекторе. Если хоть один из этих параметров изменится (GUI.changed), спрайт обновится, и мы увидим результат изменения на экране, а также сохранятся измененные параметры спрайта (EditorUtility.SetDirty).

Редактируемые параметры добавим в класс SampleSprite и сделаем их условно-компилируемыми (чтобы этот код не попал в конечный продукт, а работал только в редакторе):

#if UNITY_EDITOR

  public Vector2 Size
  {
    get { return size; }
    set { size = value; }
  }

  public Vector2 Zero
  {
    get { return Vector2.Scale(zero, size); }
    set
    {
      if (size.x != 0 && size.y != 0)
      {
        zero = new Vector2(value.x / size.x, value.y / size.y);
      }
    }
  }

  public Rect TextureCoords
  {
    get { return NonNormalizedTextureCoords; }
    set
    {
      textureCoords = value;
      Vector2 texSize = TextureSize;
      if (texSize != Vector2.zero)
      {
        textureCoords.xMin /= texSize.x;
        textureCoords.xMax /= texSize.x;
        textureCoords.yMin /= texSize.y;
        textureCoords.yMax /= texSize.y;
      }
    }
  }

  public bool PixelCorrect
  {
    get { return pixelCorrect; }
    set { pixelCorrect = value; }
  }

  public void UpdateMesh()
  {
    InitializeMesh();
  }

#endif

В данном случае параметр Zero измеряется в тех же единицах, что и size, а TextureCoords – в пикселях текстуры.

Оптимизация, улучшения и прочее


Уменьшение числа Draw Calls


Есть несколько способов это сделать.
  1. Static batching. Если объект никогда не изменяется, то его можно пометить как статический (галочка Static в Инспекторе). Все такие объекты будут объединены в один большой и будут рисоваться за один Draw Call. К сожалению, функция static batching доступна только в Pro-версии Unity.
  2. Dynamic batching. Если несколько объектов используют один и тот же материал, Unity перед отрисовкой объединяет их в один, и все они будут рисоваться за один Draw Call. Для достижения этого эффекта текстуры необходимо объединять в атлас – одну большую текстуру. Используйте атласы – они позволяют сократить как количество Draw Call (за счет dynamic batching), так и объем памяти, занимаемой текстурами (что очень актуально для мобильных платформ).
    Подсказка: включение/отключение видов batching для некоторых платформ осуществляется в File->Build Settings->Player Settings.
  3. Менеджер спрайтов. Одна из реализаций – SpriteManager. Спрайт добавляется в менеджер спрайтов, который использует одну текстуру-атлас (в компоненте MeshRenderer), и создает для спрайтов меш (в компоненте MeshFilter), состоящий из множества прямоугольников, по одному для каждого спрайта (к этой реализации автор прикрутил удобство редактора и получил SM2). Также улучшить менеджер спрайтов можно за счет задания спрайту ссылки на используемый материал, все материалы хранить в компоненте MeshRenderer менеджера спрайтов, а меш рисовать как совокупность более маленьких мешей (по одному на материал), пользуясь возможностями Mesh.CombineMeshes, Mesh.SetTriangles, Mesh.GetTriangles, Mesh.subMeshCount. Это позволит не заводить для каждого материала по менеджеру спрайтов.

Некоторые улучшения кода

  1. Конечно, лучше избавится от постоянного вызова функции CreateMesh, который приводит к созданию нового меша (в данном случае это несущественно, т.к. все происходит в редакторе, а в реальном приложении спрайт будет создаваться один раз и больше не будет меняться). Вместо этого достаточно изменять параметры Mesh.vertices, Mesh.uv, Mesh.triangles. Не забывайте вызвать mesh.RecalculateBounds(), если был изменен массив вершин vertices. Если же изменен массив triangles, то эта функция вызовется автоматически.
  2. Вместо Camera.main лучше задавать камеру как параметр скрипта.

Как делать анимации


Как делать полноценные спрайтовые анимации можно посмотреть на примере SpriteManager.
В нашей игре Papa Penguin спрайтовых анимаций не было. Вместо этого, например, пингвин скреплялся из частей по нулевым точкам спрайтов, а движение этих частей осуществлялось с помощью компонента Animation. Этого вполне хватило. В Unity это очень мощный инструмент. В анимацию, например, можно вставлять даже вызовы функций из скриптов.



2d-физика


К любому спрайту можно прикрепить компонент типа Collider: Component->Physics->BoxCollider для объектов прямоугольной формы и Component->Physics->SphereCollider для объектов сферической формы. Эти компоненты можно использовать в двух целях:
  1. Сделать объект триггером (флажок Is Trigger).
  2. Позволить объекту подвергаться физическим воздействиям. Для этого к объекту можно дополнительно прикрепить компонент Component->Physics->Rigidbody.
С помощью Rigidbody можно ограничить физику объекта двумя измерениями: в компоненте есть параметр Constraints. Нужно ограничить перемещение объекта по оси Z, поставив галочку на Rigidbody-> Constraints->Freeze Position->Z, и ограничить поворот по осям X и Y (Freeze Rotation->X и Freeze Rotation->Y).



Физика Unity3d предлагает также другие богатые возможности: силы, приложенные к определенным точкам объекта, гравитация, точки соединения (Fixed Joint, Spring Joint). Используя все это, можно создать неплохую игру, основанную на физике.

Альтернативы


  1. EZSprite – простая платная система для создания 2d-анимаций в Unity (на момент написания статьи плагин стоил $35).
  2. SpriteManager – система классов для создания спрайтовых анимаций. Поддерживает отрисовку спрайтов за один Draw Call, менеджер спрайтов, анимации и атласы. Довольно неудобен в настройке, но если бюджет ограничен, вполне пригоден для использования.
  3. SM2 (Sprite Manager 2) – платный менеджер спрайтов с поддержкой редактора. Поддерживает множество полезных функций в дополнение к бесплатной версии: создание атласов, использование возможностей редактора для выделения области текстуры, автоматического создания анимаций и многое другое. Если не хочется реализовывать спрайты вручную, то SM2, на мой взгляд – самый лучший выбор. Стоит $150 с 60-дневной возможностью вернуть деньги назад, если не понравится.
  4. RageSpline, $50. Этот плагин не совсем спрайтовый, а скорее векторный. Плагин обладает множеством интересных возможностей:
    • создание 2d-мешей и линий на основе кривых Безье;
    • контуры объектов различной ширины;
    • заливка одним цветом/градиент;
    • текстурированные объекты;
    • и многое другое.

Заключение


Всего того, о чем я написал, вполне хватит для написания своей 2d-системы и последующего ее использования при создании двухмерной игры. Трехмерность движка и поддержка физики предлагает нам новые богатые возможности, а высокая скорость разработки на Unity (что является большим плюсом движка) позволит сократить затраты.

P.S. Кому интересно, почитайте мои предыдущие посты: Игра за два дня и Как сделать промо-ролик игры малыми силами. Следующая моя статья будет про оптимизацию в Unity. При разработке игр под Unity я не раз сталкивался с необходимостью оптимизации и хочу с вами поделиться своим опытом в этом деле.
Теги:
Хабы:
+59
Комментарии 22
Комментарии Комментарии 22

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн