Diretrizes de codificação – MRTK2

Este documento descreve os princípios de codificação e as convenções a seguir ao contribuir para o MRTK.


Filosofia

Seja conciso e esforce-se pela simplicidade

A solução mais simples é muitas vezes a melhor. Este é um objetivo primordial destas diretrizes e deve ser o objetivo de toda a atividade de codificação. Parte de ser simples é ser conciso e consistente com o código existente. Tente manter o código simples.

Os leitores só devem encontrar artefactos que forneçam informações úteis. Por exemplo, os comentários que reformulam o que é óbvio não fornecem informações adicionais e aumentam a proporção de ruído para sinal.

Mantenha a lógica de código simples. Tenha em atenção que não se trata de uma afirmação sobre a utilização do menor número de linhas, minimizando o tamanho dos nomes de identificadores ou estilo de chaveta, mas sim sobre a redução do número de conceitos e a maximização da visibilidade das mesmas através de padrões familiares.

Produzir código consistente e legível

A legibilidade do código está correlacionada com taxas de defeitos baixas. Esforce-se para criar código que seja fácil de ler. Esforce-se por criar código com lógica simples e reutilizar componentes existentes, uma vez que também ajudará a garantir a correção.

Todos os detalhes do código que produz são importantes, desde o detalhe mais básico da correção até ao estilo e formatação consistentes. Mantenha o seu estilo de codificação consistente com o que já existe, mesmo que não corresponda à sua preferência. Isto aumenta a legibilidade da base de código geral.

Suporte para configurar componentes no editor e no tempo de execução

O MRTK suporta um conjunto diversificado de utilizadores – pessoas que preferem configurar componentes no editor do Unity e carregar pré-fabricados e pessoas que precisam de instanciar e configurar objetos em tempo de execução.

Todo o seu código deve funcionar ao adicionar um componente a um GameObject numa cena guardada e ao instanciar esse componente no código. Os testes devem incluir um caso de teste para instanciar pré-fabricados e instanciar, configurando o componente no runtime.

O play-in-editor é a sua primeira e principal plataforma de destino

O Play-In-Editor é a forma mais rápida de iterar no Unity. Fornecer formas para os nossos clientes iterarem rapidamente permite-lhes desenvolver soluções mais rapidamente e experimentar mais ideias. Por outras palavras, maximizar a velocidade da iteração permite que os nossos clientes consigam mais.

Faça com que tudo funcione no editor e, em seguida, faça com que funcione em qualquer outra plataforma. Mantenha-o a funcionar no editor. É fácil adicionar uma nova plataforma ao Play-In-Editor. É muito difícil pôr o Play-In-Editor a funcionar se a sua aplicação só funcionar num dispositivo.

Adicionar novos campos públicos, propriedades, métodos e campos privados serializados com cuidado

Sempre que adiciona um método público, campo, propriedade, este torna-se parte da superfície da API pública do MRTK. Os campos privados marcados com [SerializeField] também expõem campos ao editor e fazem parte da superfície da API pública. Outras pessoas podem utilizar esse método público, configurar pré-fabricados personalizados com o seu campo público e assumir uma dependência do mesmo.

Os novos membros públicos devem ser cuidadosamente examinados. Qualquer campo público terá de ser mantido no futuro. Lembre-se de que se o tipo de campo público (ou campo privado serializado) for alterado ou removido de um MonoBehaviour, isso poderá quebrar outras pessoas. Primeiro, o campo terá de ser preterido para uma versão e o código para migrar as alterações para as pessoas que assumiram dependências teria de ser fornecido.

Priorizar testes de escrita

O MRTK é um projeto comunitário, modificado por uma variedade de contribuidores. Estes contribuidores podem não saber os detalhes da correção/funcionalidade de erros e interromper acidentalmente a funcionalidade. O MRTK executa testes de integração contínua antes de concluir todos os pedidos Pull. Não é possível dar entrada das alterações que interrompem os testes. Por conseguinte, os testes são a melhor forma de garantir que outras pessoas não quebram a sua funcionalidade.

Quando corrigir um erro, escreva um teste para garantir que não é regredido no futuro. Se adicionar uma funcionalidade, escreva testes que verifiquem se a funcionalidade funciona. Isto é necessário para todas as funcionalidades do UX, exceto funcionalidades experimentais.

C# Coding conventions (Convenções de codificação C#)

Cabeçalhos de informações da licença de script

Todos os funcionários da Microsoft que contribuem com novos ficheiros devem adicionar o seguinte cabeçalho de Licença padrão na parte superior de quaisquer novos ficheiros, exatamente conforme mostrado abaixo:

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

Cabeçalhos de resumo da função/método

Todas as classes públicas, estruturas, enums, funções, propriedades, campos publicados no MRTK devem ser descritos como para o seu objetivo e utilização, exatamente conforme mostrado abaixo:

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

Isto garante que a documentação é corretamente gerada e divulgada para todas as classes, métodos e propriedades.

Todos os ficheiros de script submetidos sem etiquetas de resumo adequadas serão rejeitados.

Regras do espaço de nomes do MRTK

O Mixed Reality Toolkit utiliza um modelo de espaço de nomes baseado em funcionalidades, onde todos os espaços de nomes fundamentais começam por "Microsoft.MixedReality.Toolkit". Em geral, não precisa de especificar a camada do toolkit (por exemplo: Núcleo, Fornecedores, Serviços) nos seus espaços de nomes.

Os espaços de nomes atualmente definidos são:

  • 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

Para espaços de nomes com uma grande quantidade de tipos, é aceitável criar um número limitado de sub-espaços de nomes para ajudar na utilização do âmbito.

O omitir o espaço de nomes de uma interface, classe ou tipo de dados fará com que a alteração seja bloqueada.

Adicionar novos scripts MonoBehaviour

Ao adicionar novos scripts MonoBehaviour com um pedido Pull, certifique-se de que o AddComponentMenu atributo é aplicado a todos os ficheiros aplicáveis. Isto garante que o componente é facilmente detetável no editor sob o botão Adicionar Componente . O sinalizador de atributo não é necessário se o componente não puder aparecer no editor, como uma classe abstrata.

No exemplo abaixo, o Pacote aqui deve ser preenchido com a localização do pacote do componente. Se colocar um item na pasta MRTK/SDK , o pacote será SDK.

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

Adicionar novos scripts do inspetor do Unity

Em geral, tente evitar a criação de scripts de inspetor personalizados para componentes MRTK. Adiciona sobrecarga e gestão adicionais da base de código que podem ser processadas pelo motor do Unity.

Se for necessária uma classe de inspetor, tente utilizar o Unity' s DrawDefaultInspector(). Isto simplifica mais uma vez a classe de inspector e deixa grande parte do trabalho para o Unity.

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

Se for necessária composição personalizada na classe inspetor, tente utilizar SerializedProperty e EditorGUILayout.PropertyField. Isto irá garantir que o Unity processa corretamente a composição de pré-fabricados aninhados e valores modificados.

Se EditorGUILayout.PropertyField não for possível utilizar devido a um requisito na lógica personalizada, certifique-se de que toda a utilização está encapsulada em torno de um EditorGUI.PropertyScope. Isto garantirá que o Unity compõe corretamente o inspector para pré-fabricados aninhados e valores modificados com a propriedade especificada.

Além disso, tente decorar a classe de inspetor personalizado com um CanEditMultipleObjects. Esta etiqueta garante que vários objetos com este componente na cena podem ser selecionados e modificados em conjunto. Quaisquer novas classes de inspetor devem testar que o seu código funciona nesta situação no local.

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

Adicionar novos ScriptableObjects

Ao adicionar novos scripts ScriptableObject, certifique-se de que o CreateAssetMenu atributo é aplicado a todos os ficheiros aplicáveis. Isto garante que o componente é facilmente detetável no editor através dos menus de criação de recursos. O sinalizador de atributo não é necessário se o componente não puder aparecer no editor, como uma classe abstrata.

No exemplo abaixo, a Subpasta deve ser preenchida com a subpasta MRTK, se aplicável. Se colocar um item na pasta MRTK/Fornecedores , o pacote será Fornecedores. Se colocar um item na pasta MRTK/Core , defina-o como "Perfis".

No exemplo abaixo, o MyNewService | MyNewProvider deve ser preenchido com o nome da nova classe, se aplicável. Se colocar um item na pasta MixedRealityToolkit , deixe esta cadeia de carateres de fora.

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

Registo

Ao adicionar novas funcionalidades ou atualizar funcionalidades existentes, considere adicionar DebugUtilities.LogVerbose a código interessante que pode ser útil para depuração futura. Há aqui uma desvantagem entre adicionar registos e o ruído adicionado e registo insuficiente (o que dificulta o diagnóstico).

Um exemplo interessante em que ter registos é útil (juntamente com payload interessante):

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

Este tipo de registo pode ajudar a detetar problemas como https://github.com/microsoft/MixedRealityToolkit-Unity/issues/8016, que foram causados por origem detetada desajustada e eventos perdidos de origem.

Evite adicionar registos para dados e eventos que estão a ocorrer em cada frame . Idealmente, o registo deve abranger eventos "interessantes" impulsionados por entradas de utilizadores distintas (ou seja, um "clique" de um utilizador e o conjunto de alterações e eventos provenientes desse tipo são interessantes para registar). O estado contínuo de "o utilizador ainda está a segurar um gesto" registado em cada frame não é interessante e irá sobrecarregar os registos.

Tenha em atenção que este registo verboso não está ativado por predefinição (tem de estar ativado nas definições do Sistema de Diagnóstico)

Espaços vs separadores

Certifique-se de que utiliza 4 espaços em vez de separadores ao contribuir para este projeto.

Espaçamento

Não adicione espaços adicionais entre parênteses retos e parênteses:

O que não deve fazer

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

O que deve fazer

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

Convenções de nomenclatura

PascalCase Utilize sempre para propriedades. Utilize camelCase para a maioria dos campos, exceto para static readonly campos PascalCase e const . A única exceção é para estruturas de dados que exigem que os campos sejam serializados pelo JsonUtility.

O que não deve fazer

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

O que deve fazer

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

Modificadores de acesso

Declare sempre um modificador de acesso para todos os campos, propriedades e métodos.

  • Todos os Métodos de API do Unity devem ser private por predefinição, a menos que precise de os substituir numa classe derivada. Neste caso, protected deve ser utilizado.

  • Os campos devem ser privatesempre , com public ou protected acessórios de propriedade.

  • Utilize os membros encorpados de expressão e as propriedades automáticas sempre que possível

O que não deve fazer

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

O que deve fazer

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

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

Utilizar chavetas

Utilize sempre chavetas após cada bloco de instrução e coloque-as na linha seguinte.

O que não deve fazer

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

O que não deve fazer

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

O que deve fazer

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

As classes públicas, as estruturas e as enumerações devem entrar nos seus próprios ficheiros

Se a classe, a estrutura ou a enumeração puderem ser tornadas privadas, não há problema em ser incluída no mesmo ficheiro. Isto evita problemas de compilação com o Unity e garante que ocorre uma abstração de código adequada, também reduz conflitos e alterações interruptivas quando o código precisa de ser alterado.

O que não deve fazer

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

O que deve fazer

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

O que deve fazer

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 enumerações

Para garantir que todas as enumerações são inicializadas corretamente a partir de 0, o .NET dá-lhe um atalho organizado para inicializar automaticamente a enumeração ao adicionar apenas o primeiro valor (starter). (por exemplo, Valor 1 = 0 Não são necessários valores restantes)

O que não deve fazer

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

O que deve fazer

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

Enumerações de encomendas para a extensão adequada

É fundamental que, se for provável que uma Enumeração seja expandida no futuro, para ordenar as predefinições na parte superior da Enumeração, isto garante que os índices de Enum não são afetados com novas adições.

O que não deve fazer

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

O que deve fazer

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

Rever a utilização de enum para campos de bits

Se existir a possibilidade de uma enumeração exigir vários estados como um valor, por exemplo, Handedness = Left & Right. Em seguida, a Enumeração tem de ser corretamente decorada com BitFlags para permitir que seja utilizada corretamente

O ficheiro Handedness.cs tem uma implementação concreta para este

O que não deve fazer

public enum Handedness
{
    None,
    Left,
    Right
}

O que deve fazer

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

Caminhos de ficheiro hard-coded

Ao gerar caminhos de ficheiros de cadeia e, em particular, ao escrever caminhos de cadeia hard-coded, faça o seguinte:

  1. Utilize as APIs dePath C#sempre que possível, como Path.Combine ou Path.GetFullPath.
  2. Utilize / ou Path.DirectorySeparatorChar em vez de \ ou \\.

Estes passos garantem que o MRTK funciona em sistemas baseados em Windows e Unix.

O que não deve fazer

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

string filePath = myVarRootPath + myRelativePath;

O que deve fazer

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

Melhores práticas, incluindo recomendações do Unity

Algumas das plataformas de destino deste projeto necessitam de ter em consideração o desempenho. Com isto em mente, tenha sempre cuidado ao alocar memória em código frequentemente denominado em ciclos ou algoritmos de atualização apertados.

Encapsulamento

Utilize sempre campos privados e propriedades públicas se for necessário acesso ao campo fora da classe ou estrutura. Certifique-se de que colocalize o campo privado e a propriedade pública. Isto torna mais fácil ver rapidamente o que suporta a propriedade e que o campo é modificável por script.

Nota

A única exceção é para estruturas de dados que exigem que os campos sejam serializados pelo JsonUtility, em que é necessária uma classe de dados para ter todos os campos públicos para que a serialização funcione.

O que não deve fazer

private float myValue1;
private float myValue2;

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

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

O que deve fazer

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

Colocar valores em cache e serializá-los na cena/pré-visualização sempre que possível

Com o HoloLens em mente, é melhor otimizar para referências de desempenho e cache no cenário ou pré-fabricado para limitar as alocações de memória de tempo de execução.

O que não deve fazer

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

O que deve fazer

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

Cache references to materials, do not call the ".material" each time

O Unity criará um novo material sempre que utilizar ".material", o que causará uma fuga de memória se não for limpo corretamente.

O que não deve fazer

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

O que deve fazer

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

Em alternativa, utilize a propriedade "SharedMaterial" do Unity, que não cria um novo material sempre que é referenciado.

Utilizar a compilação dependente da plataforma para garantir que o Toolkit não interrompe a compilação noutra plataforma

  • Utilize WINDOWS_UWP para utilizar APIs específicas do UWP e não do Unity. Isto impedirá que tentem ser executados no Editor ou em plataformas não suportadas. Isto é equivalente a UNITY_WSA && !UNITY_EDITOR e deve ser utilizado a favor de.
  • Utilize UNITY_WSA para utilizar APIs do Unity específicas do UWP, como o UnityEngine.XR.WSA espaço de nomes. Esta ação será executada no Editor quando a plataforma estiver definida como UWP, bem como em aplicações UWP incorporadas.

Este gráfico pode ajudá-lo a decidir qual #if utilizar, consoante os seus casos de utilização e as definições de compilação esperadas.

Plataforma UWP IL2CPP UWP .NET Editor
UNITY_EDITOR Falso Falso Verdadeiro
UNITY_WSA Verdadeiro Verdadeiro Verdadeiro
WINDOWS_UWP Verdadeiro Verdadeiro Falso
UNITY_WSA && !UNITY_EDITOR Verdadeiro Verdadeiro Falso
ENABLE_WINMD_SUPPORT Verdadeiro Verdadeiro Falso
NETFX_CORE Falso Verdadeiro Falso

Preferir DateTime.UtcNow em vez de DateTime.Now

DateTime.UtcNow é mais rápido do que DateTime.Now. Em investigações de desempenho anteriores, descobrimos que a utilização de DateTime.Now adiciona uma sobrecarga significativa especialmente quando utilizada no ciclo Update(). Outros tiveram o mesmo problema.

Prefira utilizar DateTime.UtcNow, a menos que precise realmente das horas localizadas (uma razão legítima pode ser querer mostrar a hora atual no fuso horário do utilizador). Se estiver a lidar com tempos relativos (ou seja, o delta entre alguma última atualização e agora), é melhor utilizar DateTime.UtcNow para evitar a sobrecarga de fazer conversões de fuso horário.

Convenções de codificação do PowerShell

Um subconjunto do código base do MRTK utiliza o PowerShell para a infraestrutura de pipeline e vários scripts e utilitários. O novo código do PowerShell deve seguir o estilo PoshCode.

Ver também

Convenções de codificação C# do MSDN