Directrices de codificación: MRTK2

En este documento se describen los principios de codificación y las convenciones que se deben seguir al contribuir a MRTK.


Filosofía

Ser conciso y esforzarse por simplificar

La solución más sencilla suele ser la mejor. Se trata de un objetivo invalidado de estas directrices y debe ser el objetivo de toda la actividad de codificación. Parte de ser simple es ser concisa y coherente con el código existente. Intente simplificar el código.

Los lectores solo deben encontrar artefactos que proporcionen información útil. Por ejemplo, los comentarios que restan lo que es obvio no proporcionan información adicional y aumentan la relación de ruido a señal.

Mantenga la lógica de código simple. Tenga en cuenta que esto no es una instrucción sobre el uso del menor número de líneas, minimizando el tamaño de los nombres de identificador o el estilo de llave, sino sobre la reducción del número de conceptos y la maximización de la visibilidad de los mismos a través de patrones conocidos.

Generar código coherente y legible

La legibilidad del código está correlacionada con bajas tasas de defectos. Esfuércese por crear código que sea fácil de leer. Intente crear código que tenga lógica simple y vuelva a usar los componentes existentes, ya que también ayudará a garantizar la corrección.

Todos los detalles del código que genera importan, desde el detalle más básico de la corrección hasta el estilo y el formato coherentes. Mantenga el estilo de codificación coherente con lo que ya existe, incluso si no coincide con sus preferencias. Esto aumenta la legibilidad del código base general.

Compatibilidad con la configuración de componentes en el editor y en tiempo de ejecución

MRTK admite un conjunto diverso de usuarios: personas que prefieren configurar componentes en el editor de Unity y cargar objetos prefabricados, y personas que necesitan crear instancias y configurar objetos en tiempo de ejecución.

Todo el código debe funcionar agregando un componente a gameObject en una escena guardada y creando instancias de ese componente en el código. Las pruebas deben incluir un caso de prueba para crear instancias de objetos prefabricados y crear instancias, configurando el componente en tiempo de ejecución.

Play-in-editor es su primera y principal plataforma de destino

Play-In-Editor es la forma más rápida de iterar en Unity. Proporcionar formas para que nuestros clientes iteren rápidamente les permita desarrollar soluciones más rápidamente y probar más ideas. En otras palabras, maximizar la velocidad de iteración permite a nuestros clientes lograr más.

Haga que todo funcione en el editor y, a continuación, haga que funcione en cualquier otra plataforma. Manténgalo trabajando en el editor. Es fácil agregar una nueva plataforma a Play-In-Editor. Es muy difícil conseguir que Play-In-Editor funcione si la aplicación solo funciona en un dispositivo.

Agregar nuevos campos públicos, propiedades, métodos y campos privados serializados con cuidado

Cada vez que se agrega un método público, campo, propiedad, se convierte en parte de la superficie de API pública de MRTK. Los campos privados marcados con [SerializeField] también exponen campos al editor y forman parte de la superficie de la API pública. Otras personas pueden usar ese método público, configurar objetos prefabricados personalizados con el campo público y depender de él.

Los nuevos miembros públicos deben examinarse cuidadosamente. Cualquier campo público deberá mantenerse en el futuro. Recuerde que si el tipo de un campo público (o campo privado serializado) cambia o se quita de un MonoBehaviour, podría romper a otras personas. El campo tendrá que quedar en desuso en primer lugar para una versión y el código para migrar los cambios de las personas que han tomado dependencias deberán proporcionarse.

Dar prioridad a las pruebas de escritura

MRTK es un proyecto de la comunidad, modificado por una amplia gama de colaboradores. Es posible que estos colaboradores no conozcan los detalles de la característica o la corrección de errores y interrumpan accidentalmente la característica. MRTK ejecuta pruebas de integración continuas antes de completar todas las solicitudes de incorporación de cambios. No se pueden comprobar los cambios que interrumpen las pruebas. Por lo tanto, las pruebas son la mejor manera de garantizar que otras personas no interrumpan la característica.

Cuando corrija un error, escriba una prueba para asegurarse de que no se devuelve en el futuro. Si agrega una característica, escriba pruebas que comprueben que la característica funciona. Esto es necesario para todas las características de la experiencia de usuario, excepto para las características experimentales.

Convenciones de codificación de C#

Encabezados de información de licencia de los scripts

Todos los empleados de Microsoft que contribuyen a nuevos archivos deben agregar el siguiente encabezado de licencia estándar en la parte superior de los nuevos archivos, exactamente como se muestra a continuación:

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

Encabezados de resumen de función y método

Todas las clases públicas, estructuras, enumeraciones, funciones, propiedades, campos publicados en MRTK deben describirse como su propósito y uso, exactamente como se muestra a continuación:

/// <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;
}

Esto garantiza que la documentación se genere y difunda correctamente para todas las clases, métodos y propiedades.

Todo archivo de script que se envíe sin las etiquetas de resumen adecuadas se rechazará.

Reglas de espacio de nombres de MRTK

El kit de herramientas de Mixed Reality usa un modelo de espacio de nombres basado en características, donde todos los espacios de nombres fundamentales comienzan por "Microsoft.MixedReality.Toolkit". En general, no es necesario especificar la capa del kit de herramientas (por ejemplo, Core, Providers, Services) en los espacios de nombres.

Los espacios de nombres definidos actualmente son:

  • 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

En el caso de los espacios de nombres con una gran cantidad de tipos, es aceptable crear un número limitado de subespacios de nombres para ayudar a determinar el uso.

Si se omite el espacio de nombres de una interfaz, clase o tipo de datos, el cambio se bloqueará.

Adición de nuevos scripts de MonoBehaviour

Al agregar nuevos scripts MonoBehaviour con una solicitud de incorporación de cambios, asegúrese de que el AddComponentMenu atributo se aplica a todos los archivos aplicables. Esto garantiza que el componente se pueda detectar fácilmente en el editor bajo el botón Agregar componente . La marca de atributo no es necesaria si el componente no se puede mostrar en el editor, como una clase abstracta.

En el ejemplo siguiente, el paquete aquí debe rellenarse con la ubicación del paquete del componente. Si coloca un elemento en la carpeta MRTK/SDK , el paquete será SDK.

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

Adición de nuevos scripts de inspector de Unity

En general, intente evitar la creación de scripts de inspector personalizados para los componentes de MRTK. Agrega sobrecarga adicional y administración del código base que el motor de Unity podría controlar.

Si es necesaria una clase inspector, intente usar la clase de DrawDefaultInspector()Unity. Esto simplifica de nuevo la clase inspector y deja gran parte del trabajo en Unity.

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

Si se requiere representación personalizada en la clase inspector, intente usar SerializedProperty y EditorGUILayout.PropertyField. Esto garantizará que Unity controle correctamente la representación de los objetos prefabricados anidados y los valores modificados.

Si EditorGUILayout.PropertyField no se puede usar debido a un requisito en la lógica personalizada, asegúrese de que todo el uso se encapsula alrededor de .EditorGUI.PropertyScope Esto garantizará que Unity represente el inspector correctamente para los objetos prefabricados anidados y los valores modificados con la propiedad especificada.

Además, intente decorar la clase de inspector personalizado con .CanEditMultipleObjects Esta etiqueta garantiza que se puedan seleccionar y modificar juntos varios objetos con este componente en la escena. Las nuevas clases de inspector deben probar que su código funciona en esta situación en la escena.

    // 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;
                    });
            }
        }
    }

Adición de nuevos ScriptableObjects

Al agregar nuevos scripts ScriptableObject, asegúrese de que el CreateAssetMenu atributo se aplica a todos los archivos aplicables. Esto garantiza que el componente se pueda detectar fácilmente en el editor a través de los menús de creación de recursos. La marca de atributo no es necesaria si el componente no se puede mostrar en el editor, como una clase abstracta.

En el ejemplo siguiente, la subcarpeta debe rellenarse con la subcarpeta MRTK, si procede. Si coloca un elemento en la carpeta MRTK/Providers , el paquete será Proveedores. Si coloca un elemento en la carpeta MRTK/Core , establézcalo en "Perfiles".

En el ejemplo siguiente, MyNewService | MyNewProvider debe rellenarse con el nombre de la nueva clase, si procede. Si coloca un elemento en la carpeta MixedRealityToolkit , deje esta cadena fuera.

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

Registro

Al agregar nuevas características o actualizar características existentes, considere la posibilidad de agregar registros DebugUtilities.LogVerbose a código interesante que puede ser útil para la depuración futura. Hay un equilibrio entre agregar registro y el ruido agregado y no suficiente registro (lo que dificulta el diagnóstico).

Un ejemplo interesante en el que tener el registro es útil (junto con una carga interesante):

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

Este tipo de registro puede ayudar a detectar problemas como https://github.com/microsoft/MixedRealityToolkit-Unity/issues/8016, que fueron causados por eventos detectados y perdidos de origen que no coinciden.

Evite agregar registros para los datos y eventos que se producen en cada fotograma; idealmente, el registro debe abarcar eventos "interesantes" controlados por entradas de usuario distintas (es decir, un "clic" por un usuario y el conjunto de cambios y eventos que proceden de eso son interesantes para registrar). El estado en curso de "el usuario sigue manteniendo un gesto" que registra cada fotograma no es interesante y sobrecargará los registros.

Tenga en cuenta que este registro detallado no está activado de forma predeterminada (debe estar habilitado en la configuración del sistema de diagnóstico).

Espacios frente a pestañas

Asegúrese de usar 4 espacios en lugar de pestañas al contribuir a este proyecto.

Espaciado

No agregar espacios adicionales entre corchetes y paréntesis:

Lo que debe evitar:

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

Lo que es necesario hacer:

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

Convenciones de nomenclatura

PascalCase Use siempre para las propiedades. Use camelCase para la mayoría de los campos, excepto para PascalCasestatic readonly los campos y const . La única excepción aplicable es para las estructuras de datos que requieren la serialización de los campos mediante JsonUtility.

Lo que debe evitar:

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

Lo que es necesario hacer:

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

Modificadores de acceso

Declare siempre un modificador de acceso para todos los campos, propiedades y métodos.

  • Todos los métodos de API de Unity deben ser private de forma predeterminada, a menos que tenga que invalidarlos en una clase derivada. En este caso, se debe usar protected.

  • Los campos siempre deben ser private, con los descriptores de acceso de propiedades public o protected.

  • Usar miembros con forma de expresión y propiedades automáticas siempre que sea posible

Lo que debe evitar:

// 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() { }

Lo que es necesario hacer:

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

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

Usar llaves

Use siempre llaves después de cada bloque de instrucciones y colóquelas en la línea siguiente.

Cosas que evitar

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

Lo que debe evitar:

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

Lo que es necesario hacer:

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

Todas las clases públicas, estructuras y enumeraciones deben ir en sus propios archivos.

Si la clase, la estructura o la enumeración se pueden hacer privadas, está bien incluirse en el mismo archivo. Esto evita problemas de compilaciones con Unity y garantiza que se produzca una abstracción de código adecuada, también reduce los conflictos y los cambios importantes cuando el código necesita cambiar.

Lo que debe evitar:

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

Lo que es necesario hacer:

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

Cosas que hacer

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;
}

Inicializar enumeraciones

Para asegurarse de que todas las enumeraciones se inicializan correctamente a partir de 0, .NET proporciona un acceso directo ordenado para inicializar automáticamente la enumeración agregando el primer valor (starter). (por ejemplo, el valor 1 = 0 No se requieren valores restantes)

Lo que debe evitar:

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

Lo que es necesario hacer:

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

Enumeraciones de orden para la extensión adecuada

Es fundamental que, si es probable que una enumeración se extienda en el futuro, para ordenar los valores predeterminados en la parte superior de la enumeración, esto garantiza que los índices de Enumeración no se vean afectados con nuevas adiciones.

Lo que debe evitar:

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

Lo que es necesario hacer:

/// <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
}

Revisión del uso de enumeración para los campos de bits

Si existe la posibilidad de que una enumeración requiera varios estados como un valor, por ejemplo, La entrega = Left & Right. A continuación, la enumeración debe estar decorada correctamente con BitFlags para permitir que se use correctamente.

El archivo Handedness.cs tiene una implementación concreta al respecto.

Lo que debe evitar:

public enum Handedness
{
    None,
    Left,
    Right
}

Lo que es necesario hacer:

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

Rutas de acceso de archivo codificadas de forma rígida

Al generar rutas de acceso de archivo de cadena y, en particular, escribir rutas de cadena codificadas de forma rígida, haga lo siguiente:

  1. Use las API de Path C#siempre que sea posible, como Path.Combine o Path.GetFullPath.
  2. Use / o Path.DirectorySeparatorChar en lugar de \ o \\.

Estos pasos garantizan que MRTK funciona en sistemas basados en Windows y Unix.

Lo que debe evitar:

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

string filePath = myVarRootPath + myRelativePath;

Lo que es necesario hacer:

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);

Procedimientos recomendados, incluidas las recomendaciones de Unity

Algunas de las plataformas de destino de este proyecto requieren tener en cuenta el rendimiento. Teniendo esto en cuenta siempre debe tener cuidado al asignar memoria en código llamado con frecuencia en bucles o algoritmos de actualización ajustados.

Encapsulación

Use siempre campos privados y propiedades públicas si se requiere acceso al campo desde fuera de la clase o estructura. Asegúrese de coubicar el campo privado y la propiedad pública. Esto facilita la visualización, de un vistazo, de lo que respalda la propiedad y que el campo es modificable por script.

Nota

La única excepción a esto son las estructuras de datos que requieren que JsonUtility serialice los campos, donde una clase de datos debe tener todos los campos públicos para que la serialización funcione.

Lo que debe evitar:

private float myValue1;
private float myValue2;

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

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

Lo que es necesario hacer:

// 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
    }
}

Almacenar en caché los valores y serializarlos en la escena o el objeto prefabricado siempre que sea posible

Con HoloLens en mente, los mejor es optimizar el rendimiento y las referencias de caché en la escena o elemento prefabricado para limitar las asignaciones de memoria en tiempo de ejecución.

Lo que debe evitar:

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

Lo que es necesario hacer:

[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);
}

Las referencias de caché a materiales, no llamen al ".material" cada vez

Unity creará un nuevo material cada vez que use ".material", lo que provocará una fuga de memoria si no se limpia correctamente.

Lo que debe evitar:

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

Lo que es necesario hacer:

// 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);
    }
}

Nota

Como alternativa, use la propiedad "SharedMaterial" de Unity, que no crea un nuevo material cada vez que se le hace referencia.

Usar la compilación dependiente de la plataforma para garantizar que el kit de herramientas no interrumpa la compilación en otra plataforma.

  • Utilice WINDOWS_UWP para usar API que no son de Unity y no son específicas de UWP. Esto impedirá que intenten ejecutarse en el Editor o en plataformas no admitidas. Esto es equivalente a UNITY_WSA && !UNITY_EDITOR y debe usarse en favor de.
  • Utilice UNITY_WSA para usar las API de Unity específicas de UWP, como el espacio de nombres UnityEngine.XR.WSA. Esto se ejecutará en el Editor cuando la plataforma esté establecida en UWP, así como en aplicaciones para UWP integradas.

Este gráfico puede ayudarle a decidir el elemento #if que debe utilizar, en función de los casos de uso y de la configuración de compilación esperada.

Plataforma UWP IL2CPP UWP .NET Editor
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 False
NETFX_CORE False True False

Preferencia de DateTime.UtcNow frente a DateTime.Now

DateTime.UtcNow es más rápido que DateTime.Now. En investigaciones de rendimiento anteriores se descubrió que el uso de DateTime.Now agrega una sobrecarga considerable, sobre todo cuando se usa en el bucle Update(). Otros han tenido el mismo problema.

Es preferible usar DateTime.UtcNow, a menos que realmente necesite las horas localizadas (un motivo válido puede ser que quiera mostrar la hora actual en la zona horaria del usuario). Si trabaja con horas relativas (es decir, la diferencia entre alguna última actualización y ahora), es mejor usar DateTime.UtcNow para evitar la sobrecarga de realizar conversiones de zona horaria.

Convenciones de codificación de PowerShell

Un subconjunto del código base de MRTK usa PowerShell para la infraestructura de canalización y varios scripts y utilidades. El nuevo código de PowerShell debe seguir el estilo PoshCode.

Consulte también

Convenciones de codificación de C# desde MSDN