Предисловие

Всем привет ещё раз, в предыдущей статье просили больше технических подробностей и подводных камей из процесса разработки игры, поэтому в этой статье я охвачу несколько основных "вещей" с которыми я столкнулся и для которых были написаны инструменты на MonoBehaviour. Надеюсь вам понравится :)

Устройство игровых комнат

Игровая комната состоит из "коробки" (стены, пол и потолок) и объектов наполняющих её. Учитывая то что в подавляющем большенстве случаев игрок будет взаимодействовать с окружением не от первого лица, а именно в изометрической проекции, то появилась необходимость скрывать часть этого игрового окружения для того чтобы игроку ничего не мешало видеть себя и передвигаться по сцене.

В случае со стенами, полом и потолком всё оказалось достаточно просто и без необходимости писать какой-то код. Этот лёгкий и эффективный приём знаком всем кто занимался моделированием трёхмерных объектов.

Для создания примитивных объектов в игре я использую абсолютно потрясающий ассет под названием UModeler. Он позволяет вам создавать трёхмерные объекты прямо внутри Unity без использования стороннего софта для моделирования, а также быстро и весело их редактировать, более того при помощи этого ассета вы можете изменять любой Mesh (Сетку) ранее созданный в Blender или любом другом редакторе. Идеальный инструмент для быстрого и удобного создания прототипов. Рекомендую!

Давайте возьмём обычный куб, как на картинке ниже.

Куб созданный при помощи ассета UModeler
Куб созданный при помощи ассета UModeler

Если удалить одну из сторон куба, то мы начнём видеть сквозь него:

Куб без одной из сторон
Куб без одной из сторон

Комната представляет из себя "вывернутый" куб у которого удалены все внешние стороны:

Куб без внешних сторон
Куб без внешних сторон

С какой бы стороны мы не смотрели на этот куб, мы будем видеть только его внутренние стороны, а если мы окажемся внутри него, то для нас он будет выглядеть как комната:

Куб без внешних сторон
Куб без внешних сторон

Таким образом выполнены все стены, пол и потолок в игровых комнатах, но что же с остальными игровыми объектами? Для них пришлось написать небольшой скрипт, который скрывает и показывает их в зависимости от положения камеры.

Hide On Rotation

Одна из главных задач для игрока - изучение игрового окружения, оно поможет ему найти необходимы предметы и объекты с которыми можно взаимодействовать для того чтобы продвинуться по сюжету, также это является неотъемлемой частью визуального стиля и левел дизайна игры.

В отличии от изометрических игр с фиксированной камерой, которую нельзя вращать, в данной игре именно её вращение станет одной из основополагающих игровых механик, ведь необходимость скрывать и показывать объекты, помимо своего прямого назначения, поможет сделать изучение окружения более интересным и разнообразным для игроков, так как мы можем не только прятать объекты, но и скрывать взаимодействие с ними.

В качестве примера приведу одну из первых игровых сцен - прихожая.

Для того чтобы выйти из квартиры, игроку, помимо всего прочего, необходимо найти ключи. В комнате выключен свет и даже подойдя к ключнице у вас не появится никаких вариантов взаимодействия с ней, а чтобы это произошло нам нужно включить свет. Нажимая "E" или "Q" ("LB" и "RB" на джойстике) вы можете вращать камеру в ту или иную сторону. Давайте сделаем это и изучим окружение.

Обратите внимание на то что взаимодействие с выключателем доступно только в тот момент, когда мы фактически видим его, если же он скрыт, то и взаимодействовать с ним мы не сможем. Таким образом путём вращения камеры игрок может изучать игровое окружение и взаимодействовать с ним.

А теперь немного подробностей о том как это работает.

Скрипт HideOnRotation
Скрипт HideOnRotation

Для того чтобы скрыть или показать любой игровой объект в игре, нужно добавить ему этот компонент и указать "Ray Vector" (направление луча по X или Z) и то что нам нужно скрыть, а также указать параметр "Hide Mesh". После этого можно спокойно вращать камеру и наслаждаться.

За проверку того что нам нужно сделать, скрыть или показать, отвечает функция Check(), код приведён ниже.

public void Check()
{
    if (!disabled)
    {
        RaycastHit hit;
        Ray ray = new Ray(transform.position, rayVector);
        Physics.Raycast(ray, out hit, 1000f, 1 << LayerMask.NameToLayer("Object Show"));
        if (hit.collider != null)
        {
            if (hided)
            {
                Show();
            }
        }
        else
        {
            if (!hided)
            {
                if (dependOnStatus && status != null)
                {
                    if (status.value)
                    {
                        Show();
                    }
                    else
                    {
                        Hide();
                    }
                }
                else
                {
                    Hide();
                }
            }
        }
        Debug.DrawLine(ray.origin, hit.point, Color.red);
    }
}

В двух словах, функция создаёт луч по направлению указанному в "rayVector", после чего проверяет столкнулась ли луч с объектом на слое "Object Show" и в зависимости от этого вызывает Hide() или Show().

Объект принадлежащий слою "Object Show" представляет из себя большую сероватую пластину (серого цвета на изображении) в которую постоянно направляются лучи (красные на изображении) от каждого объекта, который хочет знать, показаться ему или скрыть себя. Как на картинке ниже.

Сцена демонстрирующая работу HideOnRotation
Сцена демонстрирующая работу HideOnRotation

Если в глобальных координатах наш объект выглядит вот так:

Лицевая сторона объекта "-1" по Z
Лицевая сторона объекта "-1" по Z

То есть его лицевая сторона смотрит в направлении "-1" по Z, то для того чтобы его скрыть, в "rayVector" нужно указать обратное направление, то есть "1". Да можно было бы следить за тем чтобы у каждого объекта его лицевая сторона всегда смотрела в "1" по Z и просто посылать луч в обратном направлении, без точного указания, но мне это решение показалось не таким удобным, так как на всё это вращение и корректное расположение в масштабе тратилось бы гораздо больше времени чем пара секунд на указания направления вектора в ручную.

Вот собственно и вся магия, заранее скажу что ниразу не претендую на изящность данного решения, но оно работает и работает хорошо, да и к тому же добавление любой новой модельки в эту "схему" занимает несколько секунд.

Используя это решение я столкнусь с подводным камнем для которого в последствии пришлось дописать небольшую обёртку в HideOnRotation. Забегая вперёд скажу что этим подводным камнем стала комната в форме буквы "Г", но об этом, если вам будет интересно, я напишу в следующей части статьи, а сейчас расскажу о самом первом подводном камнем с которым пришлось столкнуться.

Подводный камень #0

Одна из визуальных фич отличающая игру от множества других воксельных проектов - ортографическая проекция камеры, то есть изометрическая картинка с трёхмерными объектами на сцене, её я планировал использовать в основной части геймплея. Для реализации проекта решено было выбрать HDRP (High Definition Render Pipeline), тобишь Unity с поддержкой "high-fidelity graphics", всякими пост эффектами и прочими графическими ништяками прямо из коробки, и, именно тут я столкнулся с первым подводным камнем.

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

Камера с ортографической проекцией
Камера с ортографической проекцией
Камера с перспективной проекцией
Камера с перспективной проекцией

Отличие - отсутствие лучшей света пробивающихся сквозь окно. В Unity за это отвечает компонент Density Volume (Объём плотности). По сути этот компонент позволяет вам установить локальный туман, который как раз даёт возможность создавать красивый эффект лучей света, дак вот, вся проблема заключается в том, что это просто не работает с ортографической проекцией камеры, и точка, только с перспективной.

Решение данной проблемы нашлось практически сразу, и, заключалось оно в уменьшении параметра Field Of View (Поле зрения), а так же размещении камеры на достаточно удалённом от игрока расстоянии.

Расположение камеры с перспективной проекцией и параметром Field Of View (Поле зрения) равным 10
Расположение камеры с перспективной проекцией и параметром Field Of View (Поле зрения) равным 10

В результате чего мы получили нужную нам практически ортографическую картинку с рабочими объёмами и разобрались с первым подводным камнем.

А теперь небольшой подгончик для тех кто дочитал до конца - небольшая самописная тулза, которая лично мне сэкономила много времени и сделала разработку чуть удобнее и быстрее.

Open In New Inspector

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

Для того чтобы сделать это вам необходимо нажать на троеточие в инспекторе, далее в контекстном меню выбрать "Add Tab" и затем "Inspector", в результате откроется новый инспектор во вкладках рядом с предыдущим, после чего вам нужно перетащить вкладку нового инспектора в пустое место, чтобы она отлипла и стала отдельным окном, а затем ещё и нажать на иконку замочка, дабы залочить объект сцены в этом инспекторе, ну очень муторно, долго и неудобно.

Но теперь это не проблема, достаточно лишь кликнуть по нужному объекту на сцене и нажать на клавиатуре "N" и он откроется в новом сразу залоченном инспекторе отдельной вкладкой, как показанно на видео ниже.

Код сохраняем в OpenInNewInspector.cs и размещаем в папку Editor внутри вашего проекта, надеюсь это сделает вашу работу в Unity чуть удобнее.

using System.Reflection;
using UnityEditor;
using UnityEditor.ShortcutManagement;
using UnityEngine;

public class OpenInNewInspector : EditorWindow
{
    static void NewInspector()
    {
        var inspectorWindowType = typeof(Editor).Assembly.GetType("UnityEditor.InspectorWindow");
        if (inspectorWindowType == null)
            return;
        EditorWindow newInspector = (EditorWindow) CreateInstance(inspectorWindowType);
        newInspector.Show(true);
        newInspector.Repaint();
        newInspector.Focus();
#if UNITY_2018_OR_NEWER
		var flipLockedMethod = inspectorWindowType.GetMethod("FlipLocked", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
		if (flipLockedMethod != null)
			flipLockedMethod.Invoke(newInspector, null);
#else
        var isLockedMethod = inspectorWindowType == null ? null :  inspectorWindowType.GetMethod("set_isLocked", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
        if (isLockedMethod != null)
            isLockedMethod.Invoke(newInspector, new object[]{true});
#endif
    }
    [Shortcut("GameObject/Open In New Inspector...", null, KeyCode.N)]
    static void OpenNewInspector(ShortcutArguments shortcutArguments)
    {
        NewInspector();
    }
    [MenuItem("GameObject/Open In New Inspector... %N", false, -1)]
    static void OpenNewInspector(MenuCommand command)
    {
        NewInspector();
    }
}

Заключение

Надеюсь, вам было интересно читать всю эту писанину, и если это так, то убедительно прошу вас вступить в наш Discord, где вы сможете найти много дополнительных материалов о разработке игры и быть в курсе последних новостей о проекте. Всем кто дочитал до сюда - низкий поклон от нас.

Discord

Если вы присоединились к нашему Discord'у, то обязательно не забудьте получить роль "habr" :)

Большое спасибо!

Комментарии (10)


  1. alnite
    16.02.2022 20:25

    Спасибо, интересно (поставил бы плюс, но кармы не хватает :)

    Еще интересно, как "перегонять" модели из MagicaVoxel в Unity.


    1. AlexanderKudryavy Автор
      16.02.2022 20:46

      Спасибо :) Я использую Voxel Importer https://assetstore.unity.com/packages/tools/modeling/voxel-importer-62914


  1. FrytechTV
    17.02.2022 04:59
    +1

    Красиво выглядит, желаю вам успеха!


    1. AlexanderKudryavy Автор
      17.02.2022 12:52

      Большое спасибо!


  1. dunsky
    17.02.2022 12:45
    +1

    Какая красивая у вас получается игра! Желаю вам с женой успехов в этом проекте. Было очень интересно почитать уже вторую статью от вас и наблюдать за прогрессом. Пожалуйста, продолжайте!


    1. AlexanderKudryavy Автор
      17.02.2022 12:53

      Ваши слова греют душу, спасибо :)


  1. domix32
    17.02.2022 13:03

    А вам удобно читать такие здоровенные лесенки?

    Что-то типа такого не лучше будет?
    public void Check()
    {
        if (disabled) return;
        
        RaycastHit hit;
        Ray ray = new Ray(transform.position, rayVector);
        Physics.Raycast(ray, out hit, 1000f, 1 << LayerMask.NameToLayer("Object Show"));
        if (hit.collider != null)
        {
            if (hided) Show();
            Debug.DrawLine(ray.origin, hit.point, Color.red);
            return;
        }
    
        if (hided) {
            Debug.DrawLine(ray.origin, hit.point, Color.red);
            return;
        }
        
        if (dependOnStatus && status != null)
        {
            if (status.value)
            {
                Show();
                Debug.DrawLine(ray.origin, hit.point, Color.red);
                return;
            }
        }
        Hide();
        Debug.DrawLine(ray.origin, hit.point, Color.red);
        
    }

    P.S. оно hidden, не hided


  1. nikkutuzov
    17.02.2022 13:43

    Добрый день, спасибо за статью. Симпатично и интересно у вас получается. Хотелось бы поиграть ;) Жду новый статей и желаю удачи!


  1. besska
    18.02.2022 09:54

    Я просто напишу, что в восторге от игры света в вашей графике.


  1. idmrtank
    18.02.2022 11:20

    Классная игра, так держать! Я бы поспорил про куб без внешних сторон - в моделировании полигоны просто выворачивают наизнанку, нормалью внутрь. Тем самым получается подобный эффект просвечивания полигонов.
    Вот тут можно упростить (в глаз сильно бросилось):

    if (dependOnStatus && status != null && status.value)
    	Show();
    else
    	Hide();