Directrices de codificación: MRTK2

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


Filosofía

Ser conciso y esforzarse por la simplicidad

La solución más sencilla suele ser la mejor. Este es un objetivo primordial de estas directrices y debe ser el objetivo de toda la actividad de codificación. Parte de ser simple es ser conciso y coherente con el código existente. Intente que el código sea sencillo.

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

Mantenga la lógica de código simple. Tenga en cuenta que no se trata de 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 ellas a través de patrones conocidos.

Generar código coherente y legible

La legibilidad del código se correlaciona con tasas de defectos bajas. Esfuércese por crear código que sea fácil de leer. Esfuércese por crear código que tenga lógica sencilla 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 son importantes, desde el detalle más básico de corrección hasta el estilo y el formato coherentes. Mantenga el estilo de codificación coherente con lo que ya existe, aunque no coincida con sus preferencias. Esto aumenta la legibilidad del código base general.

Compatibilidad con la configuración de componentes tanto en el editor como 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 un Objeto GameObject en una escena guardada y creando una instancia de ese componente en el código. Las pruebas deben incluir un caso de prueba para crear instancias de prefabs y crear instancias, configurando el componente en tiempo de ejecución.

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

Play-In-Editor es la forma más rápida de iterar en Unity. Proporcionar formas para que nuestros clientes iteren rápidamente les permite 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. Siga funcionando en el editor. Es fácil agregar una nueva plataforma a Play-In-Editor. Es muy difícil 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 LA API pública de MRTK. Los campos privados marcados con [SerializeField] también exponen campos al editor y forman parte de la superficie de API pública. Otras personas pueden usar ese método público, configurar objetos prefabricados personalizados con el campo público y tomar una dependencia de él.

Los nuevos miembros públicos deben examinarse cuidadosamente. Cualquier campo público tendrá que 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 interrumpir a otras personas. El campo tendrá que estar en desuso en primer lugar para una versión y deberá proporcionarse código para migrar los cambios de las personas que han tomado dependencias.

Priorización de 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 corrección de errores o la característica y que interrumpan accidentalmente la característica. MRTK ejecuta pruebas de integración continuas antes de completar cada solicitud de incorporación de cambios. Los cambios que interrumpen las pruebas no se pueden registrar. Por lo tanto, las pruebas son la mejor manera de asegurarse de que otras personas no interrumpen la característica.

Cuando corrija un error, escriba una prueba para asegurarse de que no retrocede 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 las características experimentales.

Convenciones de codificación de C#

Encabezados de información de licencia de script

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 archivos nuevos, exactamente como se muestra a continuación:

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

Encabezados de resumen de función o método

Todas las clases públicas, structs, enumeraciones, funciones, propiedades, campos publicados en MRTK deben describirse en función de su finalidad y uso, tal y 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.

Se rechazarán los archivos de script enviados sin etiquetas de resumen adecuadas.

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 espacios de nombres secundarios para ayudar a determinar el ámbito del uso.

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

Adición de nuevos scripts 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 en 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 una sobrecarga y administración adicionales del código base que podría controlar el motor de Unity.

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

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

Si se requiere la representación personalizada en la clase inspector, intente usar SerializedProperty y EditorGUILayout.PropertyField. Esto garantizará que Unity controla correctamente la representación de objetos prefabricados anidados y 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 ajuste alrededor de .EditorGUI.PropertyScope Esto garantizará que Unity represente el inspector correctamente para los objetos prefab 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 pueden seleccionar y modificar 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 ScriptsableObjects

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á Providers. 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 el registro y el ruido agregado y no hay suficiente registro (lo que dificulta el diagnóstico).

Un ejemplo interesante en el que 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 se deben a que se detectó un origen no coincidente y a eventos de pérdida de origen.

Evite agregar registros para los datos y eventos que se producen en cada fotograma: lo ideal es registrar eventos "interesantes" controlados por entradas de usuario distintas (es decir, un "clic" de un usuario y el conjunto de cambios y eventos que proceden de que son interesantes para registrar). El estado en curso de "el usuario sigue manteniendo un gesto" registrado cada fotograma no es interesante y saturará 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.

Spacing

No agregue espacios adicionales entre corchetes y paréntesis:

Incorrecto

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

Correcto

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 PascalCase para static readonly los campos y const . La única excepción a esto es para las estructuras de datos que requieren que los campos se serializan mediante JsonUtility.

Incorrecto

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

Correcto

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 protected , se debe usar.

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

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

Incorrecto

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

Correcto

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

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

Uso de llaves

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

Incorrecto

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

Incorrecto

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

Correcto

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

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

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

Incorrecto

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

Correcto

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

Correcto

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 los valores restantes)

Incorrecto

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

Correcto

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

Ordenación de enumeraciones 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.

Incorrecto

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

Correcto

/// <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 campos bit a bit

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

El archivo Handedness.cs tiene una implementación concreta para este

Incorrecto

public enum Handedness
{
    None,
    Left,
    Right
}

Correcto

[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 acceso de cadena codificadas de forma rígida, haga lo siguiente:

  1. Use las API dePath 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.

Incorrecto

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

string filePath = myVarRootPath + myRelativePath;

Correcto

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 deben tener en cuenta el rendimiento. Con esto en mente, tenga siempre cuidado al asignar memoria en el código denominado con frecuencia en bucles de actualización o algoritmos estrechos.

Encapsulación

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

Nota:

La única excepción a esto es para las estructuras de datos que requieren que los campos se serializan mediante JsonUtility, donde se requiere una clase de datos para tener todos los campos públicos para que funcione la serialización.

Incorrecto

private float myValue1;
private float myValue2;

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

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

Correcto

// 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 prefab siempre que sea posible

Teniendo en cuenta HoloLens, es mejor optimizar el rendimiento y las referencias de caché en la escena o prefab para limitar las asignaciones de memoria en tiempo de ejecución.

Incorrecto

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

Correcto

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

Almacenar en caché referencias a materiales, no llamar al ".material" cada vez

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

Incorrecto

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

Correcto

// 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 hace referencia a él.

Uso de la compilación dependiente de la plataforma para asegurarse de que el kit de herramientas no interrumpirá la compilación en otra plataforma

  • Úsese WINDOWS_UWP para usar API específicas de UWP que no sean de Unity. Esto impedirá que intenten ejecutarse en el Editor o en plataformas no admitidas. Esto es equivalente a UNITY_WSA && !UNITY_EDITOR y debe usarse a favor de .
  • Usa UNITY_WSA para usar API de Unity específicas de UWP, como el espacio de UnityEngine.XR.WSA nombres. 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 qué #if usar, en función de los casos de uso y la configuración de compilación que espera.

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

Preferir DateTime.UtcNow sobre DateTime.Now

DateTime.UtcNow es más rápido que DateTime.Now. En investigaciones de rendimiento anteriores hemos descubierto que el uso de DateTime.Now agrega una sobrecarga significativa, especialmente cuando se usa en el bucle Update(). Otros han tenido el mismo problema.

Prefiere usar DateTime.UtcNow a menos que realmente necesite las horas localizadas (es posible que desee 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 canalizaciones y varios scripts y utilidades. El nuevo código de PowerShell debe seguir el estilo PoshCode.

Vea también

Convenciones de codificación de C# de MSDN