Diretrizes de codificação

Este documento descreve os princípios e convenções de codificação a seguir quando contribuírem 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 orientações 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 seu código simples.

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

Mantenha a lógica do código simples. Note que esta não é uma afirmação sobre o uso do menor número de linhas, minimizando o tamanho dos nomes dos identificadores ou estilo de cinta, mas sobre a redução do número de conceitos e maximizando a visibilidade daqueles através de padrões familiares.

Produzir código consistente e legível

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

Todos os detalhes do código que produz importam, desde o detalhe mais básico da correção 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 global.

Suporte componentes configurantes tanto no editor como no tempo de execução

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

Todo o seu código deve funcionar adicionando um componente a um GameObject numa cena guardada e instantaneamente esse componente em código. Os testes devem incluir uma caixa de teste tanto para os pré-fabricados instantâneos como para a instantânea, configurando o componente em tempo de execução.

Play-in-editor é a sua primeira e principal plataforma alvo

Play-In-Editor é a forma mais rápida de iterar em Unidade. Fornecer formas de iterar rapidamente os nossos clientes permite-lhes desenvolver soluções mais rapidamente e experimentar mais ideias. Por outras palavras, maximizar a velocidade da iteração capacita os nossos clientes a alcançarem mais.

Faça tudo funcionar como editor, e depois faça-o funcionar em qualquer outra plataforma. Mantenha-o a trabalhar no editor. É fácil adicionar uma nova plataforma à Play-In-Editor. É muito difícil fazer com que a Play-In-Editor funcione se a sua aplicação funcionar apenas num dispositivo.

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

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

Os novos membros públicos devem ser cuidadosamente examinados. Qualquer campo público terá de ser mantido no futuro. Lembre-se que se o tipo de campo público (ou campo privado serializado) mudar ou for removido de um MonoBehaviour, isso pode quebrar outras pessoas. O campo terá primeiro de ser depreciado para uma libertação, e o código para migrar mudanças para as pessoas que tomaram dependências teria de ser fornecido.

Priorizar testes de escrita

O MRTK é um projeto comunitário, modificado por uma gama diversificada de colaboradores. Estes colaboradores podem não conhecer os detalhes da sua correção/funcionalidade de bug e quebrar acidentalmente a sua funcionalidade. O MRTK realiza testes de integração contínua antes de completar todos os pedidos de puxão. As alterações que quebram os testes não podem ser verificadas. Por isso, os testes são a melhor forma de garantir que outras pessoas não quebram a sua funcionalidade.

Quando corrigir um bug, escreva um teste para garantir que não regrediu no futuro. Se adicionar uma funcionalidade, escreva testes que verifiquem o funcionamento da sua funcionalidade. Isto é necessário para todas as funcionalidades UX, exceto características experimentais.

C# Convenções de codificação

Cabeçalhos de informação de licença de script

Todos os colaboradores da Microsoft que contribuem com novos ficheiros devem adicionar o seguinte cabeçalho padrão de licença no topo de quaisquer novos ficheiros, exatamente como mostrado abaixo:

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

Função / cabeçalhos sumários do método

Todas as classes públicas, structs, enums, funções, propriedades, campos destacados para o MRTK devem ser descritos como para o seu propósito e utilização, exatamente como 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 é devidamente gerada e disseminada para todas as classes, métodos e propriedades.

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

Regras do espaço de nome MRTK

O Mixed Reality Toolkit utiliza um modelo de espaço de nome baseado em funcionalidades, onde todos os espaços de nomes fundamentais começam com "Microsoft.MixedReality.Toolkit". Em geral, não precisa de especificar a camada de conjunto de ferramentas (ex: Core, Fornecedores, Serviços) nos seus espaços de nome.

Os espaços de nome 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 com nomes com uma grande quantidade de tipos, é aceitável criar um número limitado de sub-nomes para ajudar na utilização de scoping.

Omitir o espaço de nome para uma interface, classe ou tipo de dados fará com que a sua alteração seja bloqueada.

Adicionar novos scripts monoBehour

Ao adicionar novos scripts MonoBehourour com um pedido de puxar, 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. A bandeira do atributo não é necessária 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 da embalagem do componente. Se colocar um item na pasta MRTK/SDK, então a embalagem será SDK.

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

Adicionar novos scripts de inspetor da Unidade

Em geral, tente evitar a criação de scripts de inspetores personalizados para componentes MRTK. Adiciona uma sobrecarga adicional e uma gestão da base de código que poderia ser manuseada pelo motor Unidade.

Se for necessária uma classe de inspetores, tente utilizar a DrawDefaultInspector() unidade. Isto simplifica mais uma vez a classe de inspetores e deixa grande parte do trabalho para a Unidade.

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

Se for necessária uma prestação personalizada na classe de inspetores, tente utilizar SerializedProperty e EditorGUILayout.PropertyField . Isto garantirá que a Unidade lida corretamente com pré-fabricados aninhados e valores modificados.

Se EditorGUILayout.PropertyField não puder ser utilizado devido a um requisito na lógica personalizada, certifique-se de que toda a utilização está enrolada em torno de um EditorGUI.PropertyScope . Isto garantirá que a Unidade torna o inspetor corretamente para pré-fabricados aninhados e valores modificados com o imóvel dado.

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. Qualquer nova classe de inspetor deve 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 ativos. A bandeira do atributo não é necessária se o componente não puder aparecer no editor como uma classe abstrata.

No exemplo abaixo, o sub-dobrador deve ser preenchido com a sub-dobradeira MRTK, se aplicável. Se colocar um item na pasta MRTK/Fornecedores, então o pacote será Provedores. Se colocar um item na pasta MRTK/Core, desa um ponto para "Perfis".

No exemplo abaixo, o MyNewService | MyNewProvider deve ser preenchido com o nome da sua nova classe, se aplicável. Se colocar um item na pasta MixedRealityToolkit, deixe este fio para 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 registos DebugUtilities.LogVerbose a código interessante que pode ser útil para futuras depurações. Há aqui uma troca entre a adição de madeira e o ruído adicionado e a falta de registo (o que dificulta o diagnóstico).

Um exemplo interessante onde ter registo é útil (juntamente com uma carga útil interessante):

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

Este tipo de exploração madeireira pode ajudar a detetar problemas como https://github.com/microsoft/MixedRealityToolkit-Unity/issues/8016 , que foram causados por fontes desajustadas detetadas e eventos perdidos na fonte.

Evite adicionar registos para dados e eventos que ocorram em cada quadro - idealmente, o registo deve cobrir eventos "interessantes" impulsionados por entradas distintas do utilizador (ou seja, um "clique" por um utilizador e o conjunto de alterações e eventos que vêm de que são interessantes de registar). O estado em curso de "utilizador ainda está a segurar um gesto" registado em cada frame não é interessante e irá sobrecarregar os registos.

Note que esta marcação verbose não é ligada por predefinição (deve ser ativada nas definiçõesdo Sistema de Diagnóstico )

Espaços vs separadores

Por favor, certifique-se de usar 4 espaços em vez de separadores ao contribuir para este projeto.

Espaçamento

Não adicione espaços adicionais entre parênteses quadrados 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

Utilize sempre PascalCase para propriedades. Use camelCase para a maioria dos campos, exceto para uso e PascalCase static readonly const campos. A única exceção a isso é para as 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 unidade API devem ser private por defeito, a menos que você precise sobrepouí-los em uma classe derivada. Neste protected caso, deve ser utilizado.

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

  • Utilize membros com expressões e propriedades de automóveis 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() { }

Use aparelhos

Utilize sempre aparelhos depois de cada bloco de declaração e coloque-os 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();
    }
}

Classes públicas, structs e enums devem ir todos nos seus próprios arquivos

Se a classe, a estrutura ou o enum podem ser privados, então não há problema em ser incluído no mesmo ficheiro. Isto evita compilações problemas com unidade e garante que a abstração de código adequada ocorre, também reduz conflitos e quebra alterações 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 enums

Para garantir que todos os enums são inicializados corretamente a partir de 0, .NET dá-lhe um atalho arrumado para inicializar automaticamente o enum apenas adicionando o primeiro valor (starter). (por exemplo, o valor 1 = 0 valores remanescentes não são necessários)

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
}

Ordem enums para extensão adequada

É fundamental que, se um Enum for provavelmente prolongado no futuro, para ordenar incumprimentos no topo do Enum, isso garante que os índices Enum não sejam afetados com novos acréscimos.

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 o uso enum para bitfields

Se houver a possibilidade de um enum exigir vários estados como valor, por exemplo, mão -, esquerda & Direita. Em seguida, o Enum precisa ser decorado corretamente com BitFlags para permitir que seja usado corretamente

O ficheiro .cs Handedness 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 ficheiros codificados

Ao gerar caminhos de ficheiros de cordas e, em particular, escrever caminhos de cordas codificados, faça o seguinte:

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

Estas medidas garantem que o MRTK trabalha tanto em sistemas Windows como em sistemas baseados na 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);

Boas práticas, incluindo recomendações de unidade

Algumas das plataformas-alvo deste projeto requerem ter em consideração o desempenho. Com isto em mente, tenha sempre cuidado ao atribuir a memória em código frequentemente chamado em loops ou algoritmos de atualização apertados.

Encapsulação

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

Nota

A única exceção a isso é que as estruturas de dados que exigem que os campos sejam serializados pelo JsonUtility , onde uma classe de dados é obrigada a ter todos os campos públicos para a serialização funcionar.

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

Cache valores e serializá-los na cena/pré-fabricada sempre que possível

Com o HoloLens em mente, o melhor é otimizar para referências de desempenho e cache na cena ou pré-fabricada para limitar a atribuição de memória em 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 referências a materiais, não chame o ".material" de cada vez

A unidade criará um novo material cada vez 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" da Unidade que não cria um novo material cada vez que é referenciado.

Utilize a compilação dependente da plataforma para garantir que o Toolkit não quebre a construção noutra plataforma

  • Utilizar WINDOWS_UWP para utilizar APIs específicos do UWP, não-Unidade. Isto irá impedi-los de tentar correr no Editor ou em plataformas não apoiadas. Isto é equivalente a UNITY_WSA && !UNITY_EDITOR e deve ser usado a favor de.
  • Utilize UNITY_WSA para utilizar APIs de unidade específicos da UWP, como o UnityEngine.XR.WSA espaço de nome. Isto será executado no Editor quando a plataforma estiver definida para uWP, bem como em aplicações UWP construídas.

Este gráfico pode ajudá-lo a decidir qual #if usar, dependendo dos casos de uso e das definições de construção que espera.

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

Prefere DateTime.UtcNow em tempo de data.agora

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

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

Convenções de codificação PowerShell

Um subconjunto da base de código MRTK utiliza o PowerShell para infraestruturas de gasodutos e vários scripts e utilitários. O novo código PowerShell deve seguir o estilo PoshCode.

Ver também

C# convenções de codificação da MSDN