Урок на Unity. Интерактивное взаимодействие игрока с окружающими предметами в 3D с помощью меток

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Взаимодействия с окружением без коллайдеров и лучей, на простой математике.

Бонус урока. Делаем простое пианино!

Наверно всем знаком такой элемент в игре, как всплывающая иконка рядом с игровым объектом, позволяющая с ним интерактивно взаимодействовать.

Интерактивную иконку для взаимодействия с окружением будем далее называть меткой.

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

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

Это мой первый урок по программированию на Unity, поэтому прошу сильно не бить критика будет уместна. Если что не очень понятно – эти моменты разберу подробнее.

Система взаимодействия метки с игроком будет состоять из 2 скриптов.

Первый скрипт будет расположен на самой метке. А метка будет крепиться к нужному объекту в 3D мире (хотя можно ипользовать и чисто одну метку, например, для заскриптованных действий).

Скрипты для каждой метки будут работать независимо друг от друга.

Поэтому понадобится общий скрипт.

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

Приступим!

Урок будет разделен на 2 главы.

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

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

Содержание:

  1. Делаем метки для взаимодействия с окружением

    1. Как работает

    2. Как добавлять

    3. Параметры меток

  2. Изучаем скрипты

    1. Функция LateUpdate

    2. Функция ChangeState

    3. Функция NewActiveMark

    4. Функция LoadStartValue

    5. Функция UseMark

Глава 1. Делаем метки для взаимодействия с окружением

Принцип работы метки:

Подходя на определенное расстояние к объекту с меткой – метка включается (=расстояние включения ), по умолчанию это светлая текстура иконки.

Подходя очень близко –
включается активный режим метки (иконка меняет цвет) и появляется текст подсказки, например “Нажмите клавишу E для открытия двери”. Можно нажать нужную кнопку и будет выполнено ваше действие (скрипт). После этого метку можно удалить с объекта или оставить для дальнейшего использования.

Посмотреть работу интерактивных меток в действии можно на видео (сцена MarkScene из проекта):

Передвижение W A S D или стрелки на клавиатуре. Использовать метку – клавиша E или клик левой кнопкой мыши. Пауза – пробел.

Архив всего проекта в конце статьи.

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


Теперь рассмотрим, как добавлять такие метки в другой проект. Для примера создадим новую сцену TestScene (В вашем случае это может быть уже готовая сцена в своем проекте).

Важно: часть описанных действий, возможно, придется переделать под ваш проект (в основном это переименование имен в скрипте).

Копируем в ваш проект папку Mark из исходника:

Создаем на сцене отдельный элемент для скриптов (чтобы они работали при запуске приложения). Если у вас есть такой элемент, продолжаем:

Добавим этому элементу тег SceneScript, чтобы другие объекты могли найти его и по тегу:

Новый тег можно создать, выбрав в этом списке Add Tag...

Перенесем в этот элемент скрипт Assets – Mark – Script – MainScriptMark.cs :

В поле Scene Script переносим ваш скрипт сцены.

Под скриптом сцены понимается скрипт вашего проекта, в котором уже есть какая-то логика и можно просто ее доработать. Или создать новый скрипт.

Если имя вашего основного скрипта сцены отличается от SceneScript

Переименуйте его в скриптах Mark.cs и MainScriptMark.cs:

Для активации подходящей метки на клавишу E или левый клик мыши, добавим в скрипте в функции Update строчки:

if (Input.GetKeyDown(KeyCode.E) || Input.GetMouseButtonDown(0))
mark_script.UseMark();

И если вам нужно включать / отключать текстовые подсказки во время показа паузы игры, то нужно добавить глобальную функцию:

public bool GameWorking() 
{
bool ret = true;

if (game_status != 1) // переменная хранит текущее состояние игры
ret = false;

return ret;
}

Скрипт активации меток проверяет возможность перевести метку в активное состояние, вызывая функцию GameWorking в вашем основном скрипте.

Если вам такая логика не нужна, то функцию можно убрать.

Для работы с текстом я использую библиотеку TextMeshPro.

Установка TextMeshPro

Для установки пакета заходим во вкладку WindowPackage Manager, выбираем раздел Unity Registry и находим TextMeshPro, и затем скачиваем и добавляем в проект:

Сам текстовой элемент можно добавить правой кнопкой мыши в Hierarchy:

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

Для плавной смены значений переменных и анимации я использую DOTween.

Установка DOTween

Заходим во вкладку WindowAssets:

Или по прямой ссылке.


Теперь добавляем на сцену ваш объект, для которого будет использоваться метка.

Важно: нельзя масштабировать родителя метки, т.к. нарушится правильная работа с масштабом метки.

Создадим пустой объект:

А затем в него перенесем меш объекта. И на этом же уровне вложенности добавим префаб метки:

Для примера, создадим обычный куб, и перетащим центр метки чуть выше самого куба:

Чтобы сделать префаб нового объекта

Переносим родительский объект в любую папку, например Prefabs:

Все готово! Наконец-то

Чтобы проверить работоспособность на пустой сцене – двигаем камеру ближе или дальше от объекта в самом инспекторе.

Осталось настроить параметры метки на ваш вкус:

Mark type – Тип действия для метки. Например, у вас в игре много дверей, которые нужно открывать. И здесь вы задаете номер для такого однотипного действия.

Главное запомнить этот номер, дальше он будет использоваться в скрипте.

Mark status – если одна метка нужна для разных действий (например, открыть, а потом закрыть и т.д.), здесь задаем номер начального действия при запуске приложения. В скрипте в зависимости от этого значения нужно будет написать начальные положения объектов.

Cof scale – коэффициент масштаба метки. Значение напрямую влияет на видимый размер на экране.

Distanse active – расстояние от камеры до метки, при котором метка станет интерактивной.

Distanse show – расстояние от камеры до метки, при котором метка будет видимой.

Enable – дополнительный параметр, который позволяет не использовать эту метку (не удаляя сам компонент).

Angle active – в скрипте используется расчет угла обзора. Если угол камеры к метке в пределах этого значения, то метка перейдет из видимого состояния в активное.

Color Show – цвет включенной метки.

Остальные цвета можно настроить в скрипте, или вывести в виде переменной.

В следующей главе рассмотрим код проекта.

Глава 2. Изучаем скрипты

Здесь я не буду писать построчно весь код, а сразу начнем разбирать, как и что работает.

Интересно услышать ваше мнение, насколько понятно написан материал в этой главе).

1. Функция LateUpdate из скрипта Mark.cs

Эта функция проверяет каждый кадр изменение состояния метки (state = 0, выключена, 1 = видимая, 2 = интерактивная).

Код функции LateUpdate
private void LateUpdate()
{
  if (enable)
  {
      distanse = Vector3.Distance(main_cam.position, tr.position);
          if (distanse < distance_show) 
          {
              if (!show) 
              {                   
                  render.enabled = true;
                  show = true;
              }
  
              if (state == 1 || state == 2) 
              angle_to_object = Mathf.Abs(Vector3.SignedAngle(tr.position - main_cam.position, main_cam.forward, Vector3.right));  
              if (state != 1 && distanse <= distance_active && angle_to_object <= angle_active && (script_scene == null || (script_scene != null && script_scene.GameWorking()) ) )             
                  ChangeState(1);
              else if ((state != 2 && distanse > distance_active) || (state == 1 && angle_to_object > angle_active) || (state == 1 && disable_active)) 
                  ChangeState(2);
             
          }
          else if (state != 0 && distanse > distance_show + 1) 
              ChangeState(0);
  
          if (show) 
          {
              tr.rotation = main_cam.rotation;
              sprite_size = cof_scale * distanse;   
              tr.localScale = Vector3.Lerp(tr.localScale, new Vector3(sprite_size, sprite_size, sprite_size), Time.deltaTime * 10);
          }
  }
}

Сначала определяем текущее расстояние от камеры до метки:

distanse = Vector3.Distance(main_cam.position, tr.position);

Если это расстояние меньше дистанции видимости:
if (distanse < distance_show)

то включаем графическое отображение метки (если до этого была выключена):

if (!show)                
{                   
       render.enabled = true;
       show = true;
}

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

if (state == 1 || state == 2) 
angle_to_object = Mathf.Abs(Vector3.SignedAngle(tr.position - main_cam.position, main_cam.forward, Vector3.right)); 

Дальше проверяем условие перехода в активное состояние (state = 1):

if (state != 1 && distanse <= distance_active && angle_to_object <= angle_active && script_scene.GameWorking()) 
ChangeState(1); 

Если метка сейчас в другом состоянии, и расстояние до камеры входит в заданное расстояние активности (distance_active), и угол между направлением камеры и меткой в пределах заданного (angle_active), и скрипт сцены разрешает сейчас активировать метку (например, игра не на паузе), то запускаем функцию ChangeState с аргументом 1. Функцию ChangeState рассмотрим позже.

Если условие для перехода активное состояние метки не выполнено, проверяем условие перехода в состояние обычной видимости (state = 2):

else if ((state != 2 && distanse > distance_active) || (state == 1 && angle_to_object > angle_active) || (state == 1 && disable_active)) 
ChangeState(2); 

Если метка еще не в этом состоянии, и расстояние до камеры больше расстояние активности (distance_active), или метка сейчас в состоянии активности, но текущий угол обзора стал больше заданного пользователем (angle_active), или метка сейчас в состоянии активности, но это состояние принудительно нужно выключить общим скриптом меток ( например, в приоритете другая метка), то запускаем функцию ChangeState с аргументом 2.

Если вышестоящее условие if (distanse < distance_show) не было выполнено, то проверяем условие выключения метки:

else if (state != 0 && distanse > distance_show + 1) 
ChangeState(0); 

Если метка еще не в выключенном состоянии и дистанция до камеры больше расстояния видимости плюс 1 метр (Здесь добавляем + 1 метр к расчету, чтобы не было постоянного включения / выключения (смена 0 и 1 состояния), при легком смещении), то запускаем функцию ChangeState с аргументом 0.

2. Функция ChangeState из скрипта Mark.cs

Код функции ChangeState
private void ChangeState(int num) 
    {
        bool mark_access = true; 
        if (num == 1) 
            mark_access = script.NewActiveMark(this.gameObject, angle_to_object, action_type_txt[mark_type-1][mark_status]); 

        if (mark_access) 
        {
            disable_active = false; 
            if (state == 1 && num != 1) 
                script.RemoveActiveMark(this.gameObject); 

            state = num;           

            if (enable)
                ChangeColor();
        }          
       
    }

После проверки изменения метки, эта функция устанавливает новое состояние метки.

Задаем переменную для проверки смены состояния, по умолчанию разрешено:

bool mark_access = true;

Если нужно поменять состояние на активное (функция запущена с аргументом 1), то, дополнительно проверяем в общем скрипте:

if (num == 1) 
mark_access = script.NewActiveMark(this.gameObject, angle_to_object, action_type_txt[mark_type-1][mark_status]); 

Функция NewActiveMark будет рассмотрена позже. Она возвращает успешность активации метки. В нее передаем текущий объект метки, угол камеры к метке и текст подсказки.

Дальше в зависимости от этого состояния, выполняется блок кода:

if (mark_access) 
{
    disable_active = false; 
    if (state == 1 && num != 1) 
       script.RemoveActiveMark(this.gameObject); 
    state = num; 
    if (enable)
    ChangeColor();
}

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

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

После всех изменений устанавливаем новое значение метки и меняем ее цвет.

3. Функция NewActiveMark из скрипта MainScriptMark.cs

Код функции NewActiveMark
public bool NewActiveMark(GameObject obj, float angle, string type_str) 
{
        bool ret = false;
        bool check_game_active = true;

        if (scene_script != null && !scene_script.GameWorking())
            check_game_active = false;

        if (check_game_active) 
        {
            if (active_mark != obj) 
            {
                if (active_mark != null) 
                {
                    if (angle < active_mark.GetComponent<Mark>().GetAngle()) 
                    {
                        active_mark.GetComponent<Mark>().RemoveActiveState();        
                        ret = true;
                    }
                }
                else ret = true;
            }
            
            if (ret)
            {
                active_mark = obj;    
                if (notice_txt != null) 
                {
                    notice_txt.text = type_str + " - E";
                    notice_txt.enabled = true;
                    EffectScaleTxt(); 
                }                
            }
        }

    return ret; 
}

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

Если новая метка еще не является активной, то включаем ее. А если в этот момент была активна другая метка, то делаем запрос в скрипт этой метки для принудительного оповещения об ее отключении и штрафом :

if (active_mark != obj) 
{
    if (active_mark != null) 
    {
        if (angle < active_mark.GetComponent<Mark>().GetAngle()) 
        {
            active_mark.GetComponent<Mark>().RemoveActiveState(); 
            ret = true;
        }
    }
    else ret = true;
}

Если новая метка стала активной, записываем ссылку на ее объект в переменную (для будущего отслеживания) и обновляем текстовое поле с использованием анимации:

if (ret)
{
    active_mark = obj; 
    if (notice_txt != null) 
    {
        notice_txt.text = type_str + " - E";
        notice_txt.enabled = true;
        EffectScaleTxt(); 
    }                
}

4. Функция LoadStartValue из скрипта Mark.cs

Код функции LoadStartValue
private void LoadStartValue()  
{
    render.enabled = false;
    disable_active = false;
    show = false;
    arr_color = new Color32[] { new Color32(color_show.r, color_show.g, color_show.b, 0), color_active, color_show }; 

    MarkStart(); 

    render.color = arr_color[0];
    action_type_txt = new List>();

    action_type_txt.Add(new List());
    action_type_txt[0].Add("Уменьшить");
    action_type_txt[0].Add("Увеличить");
    action_type_txt.Add(new List());
    action_type_txt[1].Add("Сдвинуть вверх 1 раз");
    action_type_txt.Add(new List());
    action_type_txt[2].Add("Удалить");
    action_type_txt.Add(new List());
    action_type_txt[3].Add("Сдвинуть вверх");
    action_type_txt[3].Add("Сдвинуть вниз");
    action_type_txt.Add(new List());
    string[] piano_str = new string[] { "Ля #", "Си", "До", "До #", "Ре", "Ре #", "Ми", "Фа", "Фа #", "Соль", "Соль #", "Ля", "Ля #", "Си", "До", "До #" }; 
    
    for (int i=0; i < piano_str.Length;i++)
    {
        action_type_txt[4].Add(piano_str[i] + " сыграть ");
    }

}

В этой функции прописываем начальные параметры при запуске проекта.
Заполняем массив цвета метки значениями: выключенного состояния метки, видимого и активного, используя введенные значения цвета из инспектора:

arr_color = new Color32[] { new Color32(color_show.r, color_show.g, color_show.b, 0), color_active, color_show };

В функции MarkStart() прописываем начальные состояния ваших объектов (открыто / закрыто, включено / выключено, и т.д.).

action_type_txt – массив значений текстовых подсказок при работе с метками.
Этот массив в каждом индексе хранит текстовые подсказки (которые показываются на экран, когда метка активна), для нужного типа метки.

Чтобы добавить в массив новую подсказку, сначала добавляем новый индекс массива, а затем прописываем для него нужный текст.
Например,
action_type_txt.Add(new List<string>()); // инициализировали новую переменную

action_type_txt[0].Add("Сдвинуть вверх"); здесь 0 означает индекс массива для удобства, т.к. это значение будет видно сразу. Но можно все заполнить и в цикле, сократив код.

Если нужно добавлять разные действия на одну метку, то дублируем строку и прописываем новое значение action_type_txt[0].Add("Сдвинуть вниз");

5. Функция UseMark из скрипта Mark.cs

Код функции UseMark
public void UseMark() 
{
    //тип метки
    if (mark_type == 1) 
    {
        float new_scaleY = 1f;
        if (mark_status == 0)
            new_scaleY = 0.5f;

        this.gameObject.transform.parent.GetComponentInChildren<MeshRenderer>().transform.DOScale(new Vector3(1, new_scaleY, 1), 0.5f);

        if (mark_status == 0)
        {
            arr_color[1] = color_disable;
            mark_status = 1;
        }
        else
        {
            arr_color[1] = color_active;
            mark_status = 0;
        }

        ChangeColor();
        script.RefreshTxtNotice(this.gameObject, action_type_txt[mark_type - 1][mark_status]);

    }
    else if (mark_type == 2) 
    {
        this.gameObject.transform.parent.GetComponentInChildren<MeshRenderer>().transform.DOMoveY(1.2f, 0.5f);
        enable = false; 
        Destroy(this.gameObject); 
    }
    else if (mark_type == 3) 
    {
        Destroy(this.gameObject.transform.parent.gameObject);
        enable = false;
    }
    else if (mark_type == 4) 
    {
        float new_posY = 0.5f;
        if (mark_status == 0)
            new_posY = 1.5f;

        this.gameObject.transform.parent.transform.DOMoveY(new_posY, 1.5f);


        if (mark_status == 0) 
        {
            arr_color[1] = color_active;
            mark_status = 1;
        }
        else
        {
            arr_color[1] = color_disable;
            mark_status = 0;
        }

        ChangeColor();
        script.RefreshTxtNotice(this.gameObject, action_type_txt[mark_type - 1][mark_status]); 


    }
    else if (mark_type == 5) 
    {
        script_scene.PlayPiano(mark_status, tr.position + Vector3.up * 0.1f); 
    }
}

Функция запускается непосредственно после нажатия клавиши по активной метке.

В ней перебираем все значения mark_type и прописываем свое действие на каждый тип метки.

Вспомнить, где задавали mark_type

Например, для метки с mark_type = 1, будем менять масштаб объекта по оси Y:

if (mark_type == 1) 
{
    float new_scaleY = 1f;
    if (mark_status == 0)
        new_scaleY = 0.5f;
    this.gameObject.transform.parent.GetComponentInChildren<MeshRenderer>().transform.DOScale(new Vector3(1, new_scaleY, 1), 0.5f);

    if (mark_status == 0) 
    {
        arr_color[1] = color_disable;
        mark_status = 1;
    }
    else
    {
        arr_color[1] = color_active;
        mark_status = 0;
    }

    ChangeColor();
    script.RefreshTxtNotice(this.gameObject, action_type_txt[mark_type - 1][mark_status]);  


}

Для сохранения текущего состояния метки используем переменную Mark_status: 0 – объект уменьшен, 1 – обычный масштаб.

После выполнения действия, меняем цвет иконки и значение Mark_status на противоположное.

И затем, если нужно обновить текстовую подсказку после действия (если метка для одного действия, то можно не делать), запускаем функцию RefreshTxtNotice в общем скрипте. В нее передаем текущий объект метки, и новый текст подсказки (который уже есть в массиве action_type_txt).


Заключение: мы успешно разобрали функционал работы с интерактивными метками. Теперь вы сможете наполнить свой проект большим разнообразием интерактивного взаимодействия с предметами.

В качестве упражнения попробуйте разобрать, как в проекте работает пианино:).

Усложненную версию данного функционала меток (реализация иконок в виде системы частиц) можно посмотреть на видео:

Ссылка на исходник проекта.

Источник: https://habr.com/ru/articles/734192/


Интересные статьи

Интересные статьи

Статистика пробок по Москве и многим другим крупным городам России с каждым годом уменьшается, но пока остается высокой. По данным Центра организации дорожного движения правительства Москвы (ЦОДД), пр...
Добрый день, меня зовут Алексей, наша команда занимается разработкой игр и приложений.Текущий год внес большие перемены в игровую индустрию. Многие игровые компании покидают Россию, чтобы не лишиться ...
Я люблю сталкиваться с трудностями. Но с такими, которые можно решить, подумать над интересным решением, подобрать технологию. Люблю быть в потоке, а после решения чувствую себя настоящим профессионал...
Миллионы лет назад столкновение Земли с космическим объектом диаметром около 10 км привело к вымиранию динозавров. Поэтому сегодня людям хочется как можно раньше узнавать о приближении долгопериодичес...
Сегодня, в пятом уроке курса по Vue.js для начинающих, речь пойдёт о том, как обрабатывать события. → Vue.js для начинающих, урок 1: экземпляр Vue → Vue.js для начинающих, уро...