Декали (decal)

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

Прежде всего, в Unity (доступно на выбор) три вида визуализации/рендеринга:

  1. Built-in rendering pipeline, или встроенный\стандартный\обычный;
  2. Universal Render Pipeline (URP) — универсальный — хорош для разных устройств;
  3. High Definition Render Pipeline (HDRP) — высокого разрешения — выжать всю мощь графического процессора;

Когда создаётся новый проект, тип рендеринга выбирается из этих трёх.

 

Декали в HDRP

Тут никаких проблем. В меню выбираем GameObject > Rendering > Decal Projector и настраиваем, как нужно. В документации всё хорошо описано, полно видео. Останавливаться нет смысла. У меня ноут не тянет это великолепие.

Декали в URP

Здесь грустнее. Встроенных возможностей («из коробки») нет, поэтому придётся настраивать материалы, шейдеры и пр. К счастью, есть умные люди, которые сделали крутой шейдер и выложили в сеть.

  • Качаем с гитхаба сам шейдер:
    https://github.com/ColinLeung-NiloCat/UnityURPUnlitScreenSpaceDecalShader
    добавляем в проект URP (в стандартном не будет работать).
  • На основании этого шейдера делаем материал (кликаем по шейдеру правой кнопкой и Create-Material). В качестве изображения можно использовать это:

    _SrcBlend устанавливаем в SrcAlfa — там так и написано (default = SrcAlfa);
    _DscBlend устанавливаем в OneMinusSrcAlfa — подсказка присутствует.
  • Создаём на сцене куб (Cube), кидаем на него материал. Из куба создаём префаб (закидываем его в Assets) и удаляем со сцены.
  • Добавляем на камеру скрипт, который будет раздавать дырки. Как альтернатива — можно создать пустой объект и на него накинуть скрипт.
    using UnityEngine;
    
    public class BulletHole : MonoBehaviour
    {
        public GameObject HolePrefab;   // префаб дырки
        private Camera cameraMain;      // кешируем камеру
    
        private void Start()
        {
            cameraMain = Camera.main;       
        }
    
        void Update()
        {
            if (Input.GetMouseButtonDown(0))
            {
                RaycastHit hit;
                Ray ray = cameraMain.ScreenPointToRay(Input.mousePosition);
    
                if (Physics.Raycast(ray, out hit, 100))
                {
                    Instantiate(HolePrefab, hit.point, Quaternion.LookRotation(hit.normal));
                }
            }
        }
    }
    

    В HolePrefab закидываем префаб, который создали ранее.

Теперь, когда запустим проект, дырки будут.

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

Декали в Buil-in

Их нет. Всё печально. И шейдера крутого нет. По крайней мере я не нашёл. Но можно имитировать.

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

  • Копируем себе в файл с сайта доков шейдер Shader «Example/Decal». Там много примеров, нужен именно Example/Decal, выделяем, копипастим в новый шейдер проекта.
  • На основании этого шейдера делаем материал (кликаем по шейдеру правой кнопкой и Create-Material). В качестве изображения можно использовать это:
  • Создаём на сцене четырёхугольник (Quad), кидаем на него материал. Cоздаём префаб (закидываем его в Assets) и удаляем со сцены.
  • Добавляем на камеру скрипт, который будет раздавать дырки. Как альтернатива — можно создать пустой объект и на него накинуть скрипт.
    using UnityEngine;
    
    public class BulletHole : MonoBehaviour
    {
        public GameObject HolePrefab;   // префаб дырки
        private Camera cameraMain;      // кешируем камеру
    
        private void Start()
        {
            cameraMain = Camera.main;       
        }
    
        void Update()
        {
            if (Input.GetMouseButtonDown(0))
            {
                RaycastHit hit;
                Ray ray = cameraMain.ScreenPointToRay(Input.mousePosition);
    
                if (Physics.Raycast(ray, out hit, 100))
                {
                    GameObject bh = Instantiate(HolePrefab, hit.point, Quaternion.LookRotation(-hit.normal));
                }
            }
        }
    }
    

    ! Разница со скриптом для URP в -hit.normal, или можно написать hit.normal * -1f
    В HolePrefab закидываем префаб, который создали ранее.
    На ровных поверхностях работает нормально, но вылезает за меш и неправильно отображается на кривых поверхностях, особенно в углах.

В итоге, я не получил того, что ожидал. Плюс к этому столкнулся с неправильным отображением материалов при конвертировании одного проекта в другой, например, стандартного в UPR. Все материалы стали розового цвета, пункт меню «Edit » Render Pipeline » Universal Render Pipeline » Upgrade Project Materials to UniversalRP Materials» не помог. То же самое было и с HDPR. Видимо, что-то я делал неправильно, но посмотрел, что в интернете многие сталкиваются с подобным, и не стал углубляться.

P.S. Дырки — это пример, забава. В качестве декали могут быть различные текстуры — повреждения, подтёки, надписи и пр., что очень улучшает восприятие игры.

adb

Бывает, что андроид игра, установленная на телефон (планшет) не запускается вовсе или неожиданно завершает работу. И не понятно, что с этим делать.

Одним из быстрых вариантов «куда копать» — посмотреть лог устройства с помощью утилиты adb.exe

Для этого нужно:

  1. Подключить устройство к компьютеру с Unity.
  2. Найти, где adb.exe располагается на диске компьютера. Тут либо просто запускаем в проводнике поиск, либо смотрим в «Настройках» Unity:


    Копируем путь «Android SDK Tools installed…» (нажимаем «Copy Path»)

  3. Открываем консоль Windows. Нажимаем «Пуск» — выполнить — cmd
    В открывшемся окне консоли, пишем «cd «, вставляем скопированный путь (можно через правую кнопку мыши «Вставить») и добавляем папку «\platform-tools». Жмём Enter.
  4. Запускаем adb logcat. Видим весь лог устройства. Чтобы посмотреть те строки, которые касаются только нашей игры (с названием, например, MyGame), пишем adb | grep MyGame.

  5. Чтобы вывести в файл, добавляем в конце «> c:\log.txt». А чтобы избежать буферизации:
    adb logcat | grep --line-buffered MyGame > c:\log.txt

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

Подробнее о всех возможностях adb можно почитать в документации (погуглить).

Расширяем инспектор

Мне не нравится, как сейчас в инспекторе редактора Unity реализован выбор объекта. Это когда в скрипте, например,

public GameObject MyPrefab;

и в инспекторе появляется поле, рядом кружочек с точкой, нажав на которые выскакивает окошко со списком объектов. Для программиста сойдёт, но для гейм-дизайнера — это пытка.

К счастью, Unity можно расширять своими скриптами. Например, добавить выпадающий список для поля.

Пример.

Создадим пустой объект, на него повесим простенький скрипт TestClass:

using UnityEngine;

public class TestClass : MonoBehaviour
{
    public GameObject myPrefab;
}

В инспекторе появилось то самое поле myPrefab с возможностью выбрать объект.

В папке Editor создадим скрипт-расширение (я пробовал — если создать в другом месте, тоже будет работать, но порядок лучше соблюдать)

using UnityEditor;
using UnityEngine;
using System.Linq;  

[CustomEditor(typeof(TestClass))]
public class TestEditor : Editor
{
    // папка в Assets/Resources, откуда будем читать
    private string resFolder = "Prefabs/";
    // массив имён объектов (заголовков)
    private string[] choices;
    // сейчас выбрано это
    private int selectIndex;

    private void OnEnable()
    {
        // читаем из папки 
        GameObject[] found = Resources.LoadAll<GameObject>(resFolder);
        // создаём массив, если в заголовке есть "/", то выпадающий список будет многоуровневым
        choices = found.Select(x => x.name).ToArray();

        /* второй вариант без linq, если нужна изысканная настройка
        int i = 0;
        foreach (GameObject go in found)
        {
            choices[i] = go.name;
            i++;
        }*/
    }

    public override void OnInspectorGUI()
    {
        // базовые настройки класса, если закомментировать, не будут показаны в инспекторе
        DrawDefaultInspector();
        // красивая линия
        EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);
        // не обязательно
        GUILayout.Label("Красивый заголовок:");
        // список
        int choiceIndex = EditorGUILayout.Popup("Выбираем:", selectIndex, choices);
        // если что-то выбрали
        if (selectIndex != choiceIndex)
        {
            // наш класс
            TestClass myTarget = (TestClass)target;
            // наше поле
            myTarget.myPrefab = Resources.Load<GameObject>(resFolder + choices[choiceIndex]);
/           // сообщаем редактору об изменениях, потом всё нужно будет сохранить 
            EditorUtility.SetDirty(myTarget);
            selectIndex = choiceIndex;
        }
    }
}

Теперь, если посмотрим в инспектор, увидим следующую картину:

При выборе из выпадающего списка, поле MyPrefab автоматически заполняется.

Конечно, выбирать можно что угодно, не только GameObject, тут как душа ляжет. Бонусом — это работает в ScriptableObject.

Events в аниматоре

В аниматоре, в любой анимации можно поставить событие — Event, которое будет вызвать определённый метод.

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

 

private Animator animator;   

private void PrepareAnimator()
{
    RuntimeAnimatorController rac = animator.runtimeAnimatorController;
    AnimationClip[] clips = rac.animationClips;

    foreach (AnimationClip clip in clips)
    {
        if (clip != null && !clip.isLooping)
        {
            AnimationEvent evt = new AnimationEvent();
            evt.time = clip.length;
            evt.stringParameter = clip.name;
            evt.functionName = "OnEndAnimation";
            clip.AddEvent(evt);
        }
    }
}

Теперь можно вызвать метод PrepareAnimator() и в скрипте на объекте с анимацией при завершении любой незацикленной анимации будет дёргаться метод OnEndAnimation(string):

 

private void Start()
{
    ...
    PrepareAnimator();
}

public void OnEndAnimation(string name) 
{
    Debug.Log("Анимация " + name + " закончилась");
}

 

Десериализация JSON

Например, мы получаем JSON-строку из php-скрипта, что-то вроде этого:

...
$res = $db->query("SELECT * FROM MyTable WHERE id = 1");
if ($row = $res->fetch_assoc($check)) 
{
  $ret = array ('name' => $row["name"], 'years' => $row["years"]);
  die(json_encode($value));
}
...

Вполне вероятно, что эта JSON-строка получится такой:

{«name»: «Иван Иванович», «years»:»45″}

В Unity принимаем её следующим образом.

Создаём класс с полями, соответствующими данным JSON-строки. Обязательно устанавливаем для него атрибут сериализации.

[Serializable]
public class Man
{
  public string name;
  public int years;

  private IEnumerator Start()
  {
     WWWForm form = new WWWForm();
     // добавляем поля запроса, если нужно
     //form.AddField("поле", значение);
 
     using UnityWebRequest www = UnityWebRequest.Post("http://мой_сервер/form.php", form);
     yield return www.SendWebRequest();

     // тут должны быть ещё проверки, но ограничимся примером
     if (www.result != UnityWebRequest.Result.ConnectionError && www.result != UnityWebRequest.Result.ProtocolError)
     {
      JsonUtility.FromJson<Man>(www.downloadHandler.text);
     }
  }
}

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

public class Man
{
  public string FullName;
  public int YearsOld;

  private IEnumerator Start()
  {
     WWWForm form = new WWWForm();
     // добавляем поля запроса, если нужно
     //form.AddField("поле", значение);
 
     using UnityWebRequest www = UnityWebRequest.Post("http://мой_сервер/form.php", form);
     yield return www.SendWebRequest();

     // тут должны быть ещё проверки, но ограничимся примером
     if (www.result != UnityWebRequest.Result.ConnectionError && www.result != UnityWebRequest.Result.ProtocolError)
     {
      ManJSON man = JsonUtility.FromJson<ManJSON>(www.downloadHandler.text);
      FullName = man.name;
      YearsOld = man.years;
    }
  }

  [Serializable]
  private class ManJSON 
  {   
    public string name;   
    public int years;
  }
}

Клик по объекту

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

Например, есть класс Examle, наследованный от MonoBehaviour. Имплементируем в нём этот интерфейс, добавляя «IPointerClickHandler» через запятую в объявлении:

public class Example : MonoBehaviour, IPointerClickHandler
{
...
}

После этого в классе Examle нужно реализовать метод OnPointerClick:

public class Example : MonoBehaviour, IPointerClickHandler
{
  public void OnPointerClick(PointerEventData eventData)
  {
    Debug.Log("По мне кликнули!");
  }
...
}

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

public class Example : MonoBehaviour, IPointerClickHandler, IPointerUpHandler, IDragHandler

Соответственно необходимо реализовать соответствующие методы: OnPointerUp и OnDrag.

Простейший класс перетаскивая может выглядеть так:

using UnityEngine;
using UnityEngine.EventSystems;

public class Example : MonoBehaviour, IPointerClickHandler, IPointerUpHandler, IDragHandler
{
    public void OnPointerClick(PointerEventData eventData) 
    { 
        Debug.Log("По мне кликнули!"); 
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        Debug.Log("Меня отпустили!");
    }

    public void OnDrag(PointerEventData eventData)
    {
        transform.position = eventData.position;
    }
}