Рекомендации по написанию кода — MRTK2

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


Философия

Будьте лаконичными и стремитесь к простоте

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

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

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

Создание согласованного и удобочитаемого кода

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

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

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

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

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

Игра в редакторе — это ваша первая и основная целевая платформа

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

Сделайте все, чтобы все работало в редакторе, а затем — на любой другой платформе. Работайте в редакторе. В play-In-Editor легко добавить новую платформу. Если приложение работает только на устройстве, очень сложно обеспечить работу редактора.

Добавление новых открытых полей, свойств, методов и сериализованных частных полей с осторожностью

Каждый раз, когда вы добавляете открытый метод, поле или свойство, оно становится частью общедоступной области API MRTK. Частные поля, помеченные параметром , [SerializeField] также предоставляют поля редактору и являются частью общедоступной области API. Другие пользователи могут использовать этот общедоступный метод, настраивать пользовательские заготовки с помощью вашего открытого поля и принимать от него зависимость.

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

Определение приоритетов при написании тестов

MRTK — это проект сообщества, измененный различными участниками. Эти участники могут не знать сведения об исправлении ошибки или функции и случайно нарушить ее. MRTK выполняет непрерывные тесты интеграции перед выполнением каждого запроса на вытягивание. Изменения, которые прерывают тесты, не могут быть возвращены. Поэтому тесты — это лучший способ убедиться, что другие пользователи не нарушат вашу функцию.

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

Соглашения о написании кода на C#

Заголовки с информацией о лицензии сценария

Все сотрудники Корпорации Майкрософт, предоставляющие новые файлы, должны добавить следующий стандартный заголовок Лицензии в верхней части всех новых файлов, точно так, как показано ниже:

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

Сводные заголовки функций и методов

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

/// <summary>
/// The Controller definition defines the Controller as defined by the SDK / Unity.
/// </summary>
public struct Controller
{
    /// <summary>
    /// The ID assigned to the Controller
    /// </summary>
    public string ID;
}

Это гарантирует правильное создание и распространение документации для всех классов, методов и свойств.

Все файлы сценариев, отправленные без соответствующих тегов сводных сведений, будут отклонены.

Правила пространства имен MRTK

Набор средств Смешанная реальность использует модель пространства имен на основе функций, где все базовые пространства имен начинаются с Microsoft.MixedReality.Toolkit. Как правило, не нужно указывать уровень набора средств (например, Core, Providers, Services) в пространствах имен.

В настоящее время определены следующие пространства имен:

  • Microsoft.MixedReality.Toolkit
  • Microsoft.MixedReality.Toolkit.Boundary
  • Microsoft.MixedReality.Toolkit.Diagnostics
  • Microsoft.MixedReality.Toolkit.Editor
  • Microsoft.MixedReality.Toolkit.Input
  • Microsoft.MixedReality.Toolkit.SpatialAwareness
  • Microsoft.MixedReality.Toolkit.Teleport
  • Microsoft.MixedReality.Toolkit.Utilities

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

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

Добавление новых скриптов MonoBehaviour

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

В приведенном ниже примере в поле Пакет здесь должно быть указано расположение пакета компонента. При размещении элемента в папке MRTK/SDK пакет будет пакетом SDK.

[AddComponentMenu("Scripts/MRTK/{Package here}/MyNewComponent")]
public class MyNewComponent : MonoBehaviour

Добавление новых скриптов инспектора Unity

Как правило, старайтесь избегать создания пользовательских скриптов инспектора для компонентов MRTK. Это добавляет дополнительные издержки и управление базой кода, которую может обрабатывать подсистема Unity.

Если требуется класс inspector, попробуйте использовать класс Unity DrawDefaultInspector(). Это еще раз упрощает класс inspector и оставляет большую часть работы Unity.

public override void OnInspectorGUI()
{
    // Do some custom calculations or checks
    // ....
    DrawDefaultInspector();
}

Если в классе inspector требуется пользовательская отрисовка, попробуйте использовать SerializedProperty и EditorGUILayout.PropertyField. Это обеспечит правильную обработку Unity вложенных заготовок и измененных значений.

Если EditorGUILayout.PropertyField не удается использовать из-за требования в пользовательской логике, убедитесь, что все использование заключено в оболочку EditorGUI.PropertyScope. Это гарантирует, что Unity правильно отрисовывает инспектор для вложенных заготовок и измененных значений с заданным свойством.

Кроме того, попробуйте украсить пользовательский класс inspector с помощью CanEditMultipleObjects. Этот тег гарантирует, что несколько объектов с этим компонентом в сцене могут быть выбраны и изменены вместе. Любые новые классы инспектора должны проверять, работает ли их код в этой ситуации в сцене.

    // Example inspector class demonstrating usage of SerializedProperty & EditorGUILayout.PropertyField
    // as well as use of EditorGUI.PropertyScope for custom property logic
    [CustomEditor(typeof(MyComponent))]
    public class MyComponentInspector : UnityEditor.Editor
    {
        private SerializedProperty myProperty;
        private SerializedProperty handedness;

        protected virtual void OnEnable()
        {
            myProperty = serializedObject.FindProperty("myProperty");
            handedness = serializedObject.FindProperty("handedness");
        }

        public override void OnInspectorGUI()
        {
            EditorGUILayout.PropertyField(destroyOnSourceLost);

            Rect position = EditorGUILayout.GetControlRect();
            var label = new GUIContent(handedness.displayName);
            using (new EditorGUI.PropertyScope(position, label, handedness))
            {
                var currentHandedness = (Handedness)handedness.enumValueIndex;

                handedness.enumValueIndex = (int)(Handedness)EditorGUI.EnumPopup(
                    position,
                    label,
                    currentHandedness,
                    (value) => {
                        // This function is executed by Unity to determine if a possible enum value
                        // is valid for selection in the editor view
                        // In this case, only Handedness.Left and Handedness.Right can be selected
                        return (Handedness)value == Handedness.Left
                        || (Handedness)value == Handedness.Right;
                    });
            }
        }
    }

Добавление новых объектов ScriptableObject

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

В приведенном ниже примере вложенная папка должна быть заполнена вложенной папкой MRTK, если применимо. Если поместить элемент в папку MRTK/Providers , пакет будет поставщиком. При размещении элемента в папке MRTK/Core задайте для этого параметра значение "Профили".

В приведенном ниже примере myNewService | Если применимо, myNewProvider должен быть заполнен именем вашего нового класса. При размещении элемента в папке MixedRealityToolkit оставьте эту строку.

[CreateAssetMenu(fileName = "MyNewProfile", menuName = "Mixed Reality Toolkit/{Subfolder}/{MyNewService | MyNewProvider}/MyNewProfile")]
public class MyNewProfile : ScriptableObject

Ведение журнала

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

Интересный пример, в котором полезно ведение журнала (наряду с интересными полезными данными):

DebugUtilities.LogVerboseFormat("RaiseSourceDetected: Source ID: {0}, Source Type: {1}", source.SourceId, source.SourceType);

Этот тип ведения журнала может помочь перехватывать такие проблемы, как https://github.com/microsoft/MixedRealityToolkit-Unity/issues/8016, которые были вызваны несовпадаемыми обнаруженными источниками и событиями потери источника.

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

Обратите внимание, что это подробное ведение журнала не включено по умолчанию (оно должно быть включено в параметрах системы диагностики).

Пробелы и вкладки

При участии в этом проекте обязательно используйте 4 пробела вместо вкладок.

Интервал

Не добавляйте дополнительные пробелы между квадратными скобками и скобками:

Не рекомендуется

private Foo()
{
    int[ ] var = new int [ 9 ];
    Vector2 vector = new Vector2 ( 0f, 10f );
}

Рекомендуется

private Foo()
{
    int[] var = new int[9];
    Vector2 vector = new Vector2(0f, 10f);
}

Соглашения об именах

Всегда используйте PascalCase для свойств. Используется camelCase для большинства полей, за исключением использования PascalCase для static readonly полей и const . Единственным исключением из этого является структура данных, требующая сериализации полей с помощью JsonUtility.

Не рекомендуется

public string myProperty; // <- Starts with a lowercase letter
private string MyField; // <- Starts with an uppercase letter

Рекомендуется

public string MyProperty;
protected string MyProperty;
private static readonly string MyField;
private string myField;

Модификаторы доступа

Всегда объявляйте модификатор доступа для всех полей, свойств и методов.

  • Все методы API Unity должны быть по умолчанию равны private, если их не нужно переопределять в производном классе. В этом случае следует использовать protected.

  • Поля всегда должны иметь значение private, с методами доступа к свойствам public или protected.

  • По возможности используйте элементы, воплощаемые в выражениях, и автоматические свойства

Не рекомендуется

// protected field should be private
protected int myVariable = 0;

// property should have protected setter
public int MyVariable => myVariable;

// No public / private access modifiers
void Foo() { }
void Bar() { }

Рекомендуется

public int MyVariable { get; protected set; } = 0;

private void Foo() { }
public void Bar() { }
protected virtual void FooBar() { }

Использование фигурных скобок

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

Не рекомендуется

private Foo()
{
    if (Bar==null) // <- missing braces surrounding if action
        DoThing();
    else
        DoTheOtherThing();
}

Не рекомендуется

private Foo() { // <- Open bracket on same line
    if (Bar==null) DoThing(); <- if action on same line with no surrounding brackets
    else DoTheOtherThing();
}

Рекомендуется

private Foo()
{
    if (Bar==true)
    {
        DoThing();
    }
    else
    {
        DoTheOtherThing();
    }
}

Открытые классы, структуры и перечисления должны находиться в собственных файлах

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

Не рекомендуется

public class MyClass
{
    public struct MyStruct() { }
    public enum MyEnumType() { }
    public class MyNestedClass() { }
}

Рекомендуется

 // Private references for use inside the class only
public class MyClass
{
    private struct MyStruct() { }
    private enum MyEnumType() { }
    private class MyNestedClass() { }
}

Рекомендуется

MyStruct.cs

// Public Struct / Enum definitions for use in your class.  Try to make them generic for reuse.
public struct MyStruct
{
    public string Var1;
    public string Var2;
}

MyEnumType.cs

public enum MuEnumType
{
    Value1,
    Value2 // <- note, no "," on last value to denote end of list.
}

MyClass.cs

public class MyClass
{
    private MyStruct myStructReference;
    private MyEnumType myEnumReference;
}

Инициализация перечислений

Чтобы обеспечить правильную инициализацию всех перечислений, начиная с 0, .NET предоставляет аккуратный ярлык для автоматической инициализации перечисления, просто добавив первое (начальное) значение. (например, значение 1 = 0 оставшихся значений не требуется)

Не рекомендуется

public enum Value
{
    Value1, <- no initializer
    Value2,
    Value3
}

Рекомендуется

public enum ValueType
{
    Value1 = 0,
    Value2,
    Value3
}

Перечисления порядка для соответствующего расширения

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

Не рекомендуется

public enum SDKType
{
    WindowsMR,
    OpenVR,
    OpenXR,
    None, <- default value not at start
    Other <- anonymous value left to end of enum
}

Рекомендуется

/// <summary>
/// The SDKType lists the VR SDKs that are supported by the MRTK
/// Initially, this lists proposed SDKs, not all may be implemented at this time (please see ReleaseNotes for more details)
/// </summary>
public enum SDKType
{
    /// <summary>
    /// No specified type or Standalone / non-VR type
    /// </summary>
    None = 0,
    /// <summary>
    /// Undefined SDK.
    /// </summary>
    Other,
    /// <summary>
    /// The Windows 10 Mixed reality SDK provided by the Universal Windows Platform (UWP), for Immersive MR headsets and HoloLens.
    /// </summary>
    WindowsMR,
    /// <summary>
    /// The OpenVR platform provided by Unity (does not support the downloadable SteamVR SDK).
    /// </summary>
    OpenVR,
    /// <summary>
    /// The OpenXR platform. SDK to be determined once released.
    /// </summary>
    OpenXR
}

Проверка использования перечисления для битовых полей

Если существует возможность для перечисления требовать несколько состояний в качестве значения, например, Handedness = Left & Right. Затем перечисление должно быть правильно декорировано с помощью BitFlags, чтобы обеспечить его правильное использование.

Файл Handedness.cs имеет конкретную реализацию для этого.

Не рекомендуется

public enum Handedness
{
    None,
    Left,
    Right
}

Рекомендуется

[Flags]
public enum Handedness
{
    None = 0 << 0,
    Left = 1 << 0,
    Right = 1 << 1,
    Both = Left | Right
}

Жестко заданные пути к файлам

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

  1. По возможности используйте API C#, Path например Path.Combine или Path.GetFullPath.
  2. Используйте / или Path.DirectorySeparatorChar вместо \ или \\.

Эти действия гарантируют, что MRTK работает в системах Windows и Unix.

Не рекомендуется

private const string FilePath = "MyPath\\to\\a\\file.txt";
private const string OtherFilePath = "MyPath\to\a\file.txt";

string filePath = myVarRootPath + myRelativePath;

Рекомендуется

private const string FilePath = "MyPath/to/a/file.txt";
private const string OtherFilePath = "folder{Path.DirectorySeparatorChar}file.txt";

string filePath = Path.Combine(myVarRootPath,myRelativePath);

// Path.GetFullPath() will return the full length path of provided with correct system directory separators
string cleanedFilePath = Path.GetFullPath(unknownSourceFilePath);

Рекомендации, включая рекомендации Unity

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

Инкапсуляция

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

Примечание

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

Не рекомендуется

private float myValue1;
private float myValue2;

public float MyValue1
{
    get{ return myValue1; }
    set{ myValue1 = value }
}

public float MyValue2
{
    get{ return myValue2; }
    set{ myValue2 = value }
}

Рекомендуется

// Enable field to be configurable in the editor and available externally to other scripts (field is correctly serialized in Unity)
[SerializeField]
[ToolTip("If using a tooltip, the text should match the public property's summary documentation, if appropriate.")]
private float myValue; // <- Notice we co-located the backing field above our corresponding property.

/// <summary>
/// If using a tooltip, the text should match the public property's summary documentation, if appropriate.
/// </summary>
public float MyValue
{
    get => myValue;
    set => myValue = value;
}

/// <summary>
/// Getter/Setters not wrapping a value directly should contain documentation comments just as public functions would
/// </summary>
public float AbsMyValue
{
    get
    {
        if (MyValue < 0)
        {
            return -MyValue;
        }

        return MyValue
    }
}

Кэширование значений и их сериализация в сцене или заготовке, когда это возможно

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

Не рекомендуется

void Update()
{
    gameObject.GetComponent<Renderer>().Foo(Bar);
}

Рекомендуется

[SerializeField] // To enable setting the reference in the inspector.
private Renderer myRenderer;

private void Awake()
{
    // If you didn't set it in the inspector, then we cache it on awake.
    if (myRenderer == null)
    {
        myRenderer = gameObject.GetComponent<Renderer>();
    }
}

private void Update()
{
    myRenderer.Foo(Bar);
}

Кэшируйте ссылки на материалы, не вызывайте ".material" каждый раз

Unity будет создавать новый материал каждый раз при использовании ".material", что приведет к утечке памяти при неправильной очистке.

Не рекомендуется

public class MyClass
{
    void Update()
    {
        Material myMaterial = GetComponent<Renderer>().material;
        myMaterial.SetColor("_Color", Color.White);
    }
}

Рекомендуется

// Private references for use inside the class only
public class MyClass
{
    private Material cachedMaterial;

    private void Awake()
    {
        cachedMaterial = GetComponent<Renderer>().material;
    }

    void Update()
    {
        cachedMaterial.SetColor("_Color", Color.White);
    }

    private void OnDestroy()
    {
        Destroy(cachedMaterial);
    }
}

Примечание

Кроме того, можно использовать свойство Unity под названием SharedMaterial, которое не создает новый материал при каждом обращении.

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

  • Используйте WINDOWS_UWP, чтобы использовать API, относящихся к UWP и не относящимся к Unity. Это предотвратит попытку запуска в редакторе или на неподдерживаемых платформах. Это эквивалентно UNITY_WSA && !UNITY_EDITOR и следует использовать в пользу .
  • Используйте UNITY_WSA, чтобы использовать API Unity, зависящие от UWP, например пространство имен UnityEngine.XR.WSA. Он будет выполняться в редакторе, когда платформа имеет значение UWP, а также во встроенных приложениях UWP.

Эта таблица поможет вам выбрать нужный вариант #if в зависимости от ваших сценариев использования и ожидаемых параметров сборки.

Платформа UWP IL2CPP UWP .NET Редактор
UNITY_EDITOR False False True
UNITY_WSA True True True
WINDOWS_UWP True True False
UNITY_WSA && !UNITY_EDITOR True True False
ENABLE_WINMD_SUPPORT True True Неверно
NETFX_CORE False True False

Предпочитайте использовать DateTime.UtcNow вместо DateTime.Now

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

Используйте DateTime.UtcNow, если только вам не требуется локализованное время (согласно требованиям законодательства может понадобиться отобразить текущее время в часовом поясе пользователя). Если вы работаете с относительным временем (т. е. разницей между последним обновлением и сейчас), лучше использовать DateTime.UtcNow, чтобы избежать издержек при преобразовании часового пояса.

Соглашения о написании кода PowerShell

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

См. также раздел

Соглашения о написании кода на C# из MSDN