Diretrizes de codificação


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 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 configurados 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 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 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 divulgada 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 MonoBehour 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 de 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 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á embrulhada 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 esta a definição é "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 abranger 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 contínuo 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 PascalCasestatic readonlyconst campos. A única exceção a esta questão é 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 sobrepõe-os 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 após 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 suscetível de ser 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 a utilização enum para bitfields

Se houver a possibilidade de um enum exigir vários estados como valor, por exemplo, mão-de-mão = & Direita Esquerda. 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 apis de C#sempre que possível, tais como Path.Combine ou Path.GetFullPath . .
  2. Use / ou Path.DirectorySeparatorChar em vez de \ ou \\.

Estas medidas asseguram 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 conta o desempenho. Com isto em mente, tenha sempre cuidado ao alocar a memória em código frequentemente chamado em loops ou algoritmos de atualização apertados.

Encapsulamento

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 e UNITY_WSA && !UNITY_EDITOR deve ser usado a favor.
  • 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 seus 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

Este documento descreve os princípios e convenções de codificação a seguir quando contribuírem para o MRTK.