Предисловие
Всем привет ещё раз, в предыдущей статье просили больше технических подробностей и подводных камей из процесса разработки игры, поэтому в этой статье я охвачу несколько основных "вещей" с которыми я столкнулся и для которых были написаны инструменты на MonoBehaviour. Надеюсь вам понравится :)
Устройство игровых комнат
Игровая комната состоит из "коробки" (стены, пол и потолок) и объектов наполняющих её. Учитывая то что в подавляющем большенстве случаев игрок будет взаимодействовать с окружением не от первого лица, а именно в изометрической проекции, то появилась необходимость скрывать часть этого игрового окружения для того чтобы игроку ничего не мешало видеть себя и передвигаться по сцене.
В случае со стенами, полом и потолком всё оказалось достаточно просто и без необходимости писать какой-то код. Этот лёгкий и эффективный приём знаком всем кто занимался моделированием трёхмерных объектов.
Для создания примитивных объектов в игре я использую абсолютно потрясающий ассет под названием UModeler. Он позволяет вам создавать трёхмерные объекты прямо внутри Unity без использования стороннего софта для моделирования, а также быстро и весело их редактировать, более того при помощи этого ассета вы можете изменять любой Mesh (Сетку) ранее созданный в Blender или любом другом редакторе. Идеальный инструмент для быстрого и удобного создания прототипов. Рекомендую!
Давайте возьмём обычный куб, как на картинке ниже.
Если удалить одну из сторон куба, то мы начнём видеть сквозь него:
Комната представляет из себя "вывернутый" куб у которого удалены все внешние стороны:
С какой бы стороны мы не смотрели на этот куб, мы будем видеть только его внутренние стороны, а если мы окажемся внутри него, то для нас он будет выглядеть как комната:
Таким образом выполнены все стены, пол и потолок в игровых комнатах, но что же с остальными игровыми объектами? Для них пришлось написать небольшой скрипт, который скрывает и показывает их в зависимости от положения камеры.
Hide On Rotation
Одна из главных задач для игрока - изучение игрового окружения, оно поможет ему найти необходимы предметы и объекты с которыми можно взаимодействовать для того чтобы продвинуться по сюжету, также это является неотъемлемой частью визуального стиля и левел дизайна игры.
В отличии от изометрических игр с фиксированной камерой, которую нельзя вращать, в данной игре именно её вращение станет одной из основополагающих игровых механик, ведь необходимость скрывать и показывать объекты, помимо своего прямого назначения, поможет сделать изучение окружения более интересным и разнообразным для игроков, так как мы можем не только прятать объекты, но и скрывать взаимодействие с ними.
В качестве примера приведу одну из первых игровых сцен - прихожая.
Для того чтобы выйти из квартиры, игроку, помимо всего прочего, необходимо найти ключи. В комнате выключен свет и даже подойдя к ключнице у вас не появится никаких вариантов взаимодействия с ней, а чтобы это произошло нам нужно включить свет. Нажимая "E" или "Q" ("LB" и "RB" на джойстике) вы можете вращать камеру в ту или иную сторону. Давайте сделаем это и изучим окружение.
Обратите внимание на то что взаимодействие с выключателем доступно только в тот момент, когда мы фактически видим его, если же он скрыт, то и взаимодействовать с ним мы не сможем. Таким образом путём вращения камеры игрок может изучать игровое окружение и взаимодействовать с ним.
А теперь немного подробностей о том как это работает.
Для того чтобы скрыть или показать любой игровой объект в игре, нужно добавить ему этот компонент и указать "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" представляет из себя большую сероватую пластину (серого цвета на изображении) в которую постоянно направляются лучи (красные на изображении) от каждого объекта, который хочет знать, показаться ему или скрыть себя. Как на картинке ниже.
Если в глобальных координатах наш объект выглядит вот так:
То есть его лицевая сторона смотрит в направлении "-1" по Z, то для того чтобы его скрыть, в "rayVector" нужно указать обратное направление, то есть "1". Да можно было бы следить за тем чтобы у каждого объекта его лицевая сторона всегда смотрела в "1" по Z и просто посылать луч в обратном направлении, без точного указания, но мне это решение показалось не таким удобным, так как на всё это вращение и корректное расположение в масштабе тратилось бы гораздо больше времени чем пара секунд на указания направления вектора в ручную.
Вот собственно и вся магия, заранее скажу что ниразу не претендую на изящность данного решения, но оно работает и работает хорошо, да и к тому же добавление любой новой модельки в эту "схему" занимает несколько секунд.
Используя это решение я столкнусь с подводным камнем для которого в последствии пришлось дописать небольшую обёртку в HideOnRotation. Забегая вперёд скажу что этим подводным камнем стала комната в форме буквы "Г", но об этом, если вам будет интересно, я напишу в следующей части статьи, а сейчас расскажу о самом первом подводном камнем с которым пришлось столкнуться.
Подводный камень #0
Одна из визуальных фич отличающая игру от множества других воксельных проектов - ортографическая проекция камеры, то есть изометрическая картинка с трёхмерными объектами на сцене, её я планировал использовать в основной части геймплея. Для реализации проекта решено было выбрать HDRP (High Definition Render Pipeline), тобишь Unity с поддержкой "high-fidelity graphics", всякими пост эффектами и прочими графическими ништяками прямо из коробки, и, именно тут я столкнулся с первым подводным камнем.
Попробуйте найти одно кардинальное отличие на нижеприведенных картинках, помимо частично ортографической картинки на втором изображении.
Отличие - отсутствие лучшей света пробивающихся сквозь окно. В Unity за это отвечает компонент Density Volume (Объём плотности). По сути этот компонент позволяет вам установить локальный туман, который как раз даёт возможность создавать красивый эффект лучей света, дак вот, вся проблема заключается в том, что это просто не работает с ортографической проекцией камеры, и точка, только с перспективной.
Решение данной проблемы нашлось практически сразу, и, заключалось оно в уменьшении параметра Field Of View (Поле зрения), а так же размещении камеры на достаточно удалённом от игрока расстоянии.
В результате чего мы получили нужную нам практически ортографическую картинку с рабочими объёмами и разобрались с первым подводным камнем.
А теперь небольшой подгончик для тех кто дочитал до конца - небольшая самописная тулза, которая лично мне сэкономила много времени и сделала разработку чуть удобнее и быстрее.
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)
dunsky
17.02.2022 12:45+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
nikkutuzov
17.02.2022 13:43Добрый день, спасибо за статью. Симпатично и интересно у вас получается. Хотелось бы поиграть ;) Жду новый статей и желаю удачи!
idmrtank
18.02.2022 11:20Классная игра, так держать! Я бы поспорил про куб без внешних сторон - в моделировании полигоны просто выворачивают наизнанку, нормалью внутрь. Тем самым получается подобный эффект просвечивания полигонов.
Вот тут можно упростить (в глаз сильно бросилось):if (dependOnStatus && status != null && status.value) Show(); else Hide();
alnite
Спасибо, интересно (поставил бы плюс, но кармы не хватает :)
Еще интересно, как "перегонять" модели из MagicaVoxel в Unity.
AlexanderKudryavy Автор
Спасибо :) Я использую Voxel Importer https://assetstore.unity.com/packages/tools/modeling/voxel-importer-62914