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 se esforce pela simplicidade

A solução mais simples geralmente é a melhor. Esse é um objetivo principal dessas 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 seu código simples.

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

Mantenha a lógica de código simples. Observe que essa não é uma instrução sobre como usar o menor número de linhas, minimizando o tamanho de nomes de identificador ou estilo de chave, mas sobre reduzir o número de conceitos e maximizar a visibilidade dessas linhas por meio 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 fácil de ler. Esforce-se para criar um código que tenha lógica simples e reutilize componentes existentes, pois ele também ajudará a garantir a correção.

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

Suporte à configuração de componentes no editor e em tempo de execução

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

Todo o código deve funcionar adicionando um componente a um GameObject em uma cena salva e instanciando esse componente no código. Os testes devem incluir um caso de teste para instanciar pré-fabricados e instanciar, configurando o componente em runtime.

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

O Play-In-Editor é a maneira mais rápida de iterar no Unity. Fornecer maneiras de nossos clientes iterarem rapidamente permite que ambos desenvolvam soluções mais rapidamente e experimentem mais ideias. Em outras palavras, maximizar a velocidade da iteração capacita nossos clientes a obter mais.

Faça tudo funcionar no editor e faça com que funcione em qualquer outra plataforma. Mantenha-o funcionando no editor. É fácil adicionar uma nova plataforma ao Play-In-Editor. É muito difícil fazer o Play-In-Editor funcionar se o aplicativo funcionar apenas em um dispositivo.

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

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

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

Priorizar testes de gravação

O MRTK é um projeto de comunidade, modificado por uma variedade diversificada de colaboradores. Esses colaboradores podem não saber os detalhes da correção/recurso do bug e interromper acidentalmente o recurso. O MRTK executa testes de integração contínua antes de concluir cada solicitação de pull. As alterações que interrompem testes não podem ser verificadas. Portanto, os testes são a melhor maneira de garantir que outras pessoas não interrompa o recurso.

Ao corrigir um bug, escreva um teste para garantir que ele não regreda no futuro. Se adicionar um recurso, faça testes de gravação que verifiquem se o recurso funciona. Isso é necessário para todos os recursos de EXPERIÊNCIA, exceto recursos experimentais.

Convenções de codificação em C#

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

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

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

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

Todas as classes públicas, structs, enums, funções, propriedades, campos postados no MRTK devem ser descritos quanto à sua finalidade e uso, 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;
}

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

Todos os arquivos de script enviados sem marcas de resumo adequadas serão rejeitados.

Regras de namespace do MRTK

O Realidade Misturada Toolkit usa um modelo de namespace baseado em recursos, em que todos os namespaces fundamentais começam com "Microsoft.MixedReality.Toolkit". Em geral, você não precisa especificar a camada do kit de ferramentas (por exemplo: Núcleo, Provedores, Serviços) em seus namespaces.

Os namespaces definidos no momento são:

  • Microsoft.MixedReality. Toolkit
  • Microsoft.MixedReality. Toolkit. Limite
  • Microsoft.MixedReality. Toolkit. Diagnostics
  • Microsoft.MixedReality. Toolkit. Editor
  • Microsoft.MixedReality. Toolkit. Entrada
  • Microsoft.MixedReality. Toolkit. SpatialAwareness
  • Microsoft.MixedReality. Toolkit. Teleport
  • Microsoft.MixedReality. Toolkit. Utilitários

Para namespaces com uma grande quantidade de tipos, é aceitável criar um número limitado de subpaspas para ajudar no uso de escopo.

A omissão do namespace para uma interface, classe ou tipo de dados fará com que a alteração seja bloqueada.

Adicionando novos scripts MonoBehaviour

Ao adicionar novos scripts MonoBehaviour com uma solicitação de pull, verifique se o AddComponentMenu atributo é aplicado a todos os arquivos aplicáveis. Isso garante que o componente seja facilmente detectável no editor no botão Adicionar Componente . O sinalizador de atributo não será 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 o local do pacote do componente. Se você colocar um item na pasta MRTK/SDK , o pacote será SDK.

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

Adicionando novos scripts de inspetor do Unity

Em geral, tente evitar a criação de scripts de inspetor personalizados para componentes do MRTK. Ele adiciona sobrecarga adicional e gerenciamento da base de código que pode ser tratada pelo mecanismo do Unity.

Se uma classe de inspetor for necessária, tente usar a do DrawDefaultInspector()Unity. Isso simplifica novamente a classe de inspetor e deixa grande parte do trabalho para o Unity.

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

Se a renderização personalizada for necessária na classe inspector, tente utilizar SerializedProperty e EditorGUILayout.PropertyField. Isso garantirá que o Unity manipule corretamente a renderização de pré-fabricados aninhados e valores modificados.

Se EditorGUILayout.PropertyField não puder ser usado devido a um requisito na lógica personalizada, verifique se todo o uso é encapsulado em torno de um EditorGUI.PropertyScope. Isso garantirá que o Unity renderize o inspetor corretamente para pré-fabricados aninhados e valores modificados com a propriedade fornecida.

Além disso, tente decorar a classe de inspetor personalizado com um CanEditMultipleObjects. Essa marca garante que vários objetos com esse componente na cena possam ser selecionados e modificados juntos. Todas as novas classes de inspetor devem testar se o código deles funciona nessa situação na cena.

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

Adicionando novos ScriptableObjects

Ao adicionar novos scripts ScriptableObject, verifique se o CreateAssetMenu atributo é aplicado a todos os arquivos aplicáveis. Isso garante que o componente seja facilmente detectável no editor por meio dos menus de criação de ativos. O sinalizador de atributo não será 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 você colocar um item na pasta MRTK/Provedores , o pacote será Provedores. Se você 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 você colocar um item na pasta MixedRealityToolkit , deixe essa cadeia de caracteres de fora.

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

Log

Ao adicionar novos recursos ou atualizar recursos existentes, considere adicionar logs DebugUtilities.LogVerbose a um código interessante que pode ser útil para depuração futura. Há uma compensação aqui entre adicionar registro em log e o ruído adicionado e não há registro em log suficiente (o que dificulta o diagnóstico).

Um exemplo interessante em que ter log é útil (juntamente com conteúdo interessante):

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

Esse tipo de registro em log pode ajudar a capturar problemas como https://github.com/microsoft/MixedRealityToolkit-Unity/issues/8016, que foram causados pela origem incompatível detectada e eventos perdidos de origem.

Evite adicionar logs para dados e eventos que estão ocorrendo em cada quadro – o ideal é que o registro em log cubra eventos "interessantes" impulsionados por entradas de usuário distintas (ou seja, um "clique" de um usuário e o conjunto de alterações e eventos provenientes disso são interessantes de registrar). O estado contínuo de "o usuário ainda está segurando um gesto" registrado em cada quadro não é interessante e sobrecarregará os logs.

Observe que esse log detalhado não está ativado por padrão (ele deve estar habilitado nas configurações do Sistema de Diagnóstico)

Espaços versus guias

Use 4 espaços em vez de guias ao contribuir para este projeto.

Espaçamento

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

O que não fazer

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

O que fazer

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

Convenções de nomenclatura

Sempre use PascalCase para propriedades. Use para a maioria dos camelCase campos, exceto para static readonly uso PascalCase e const campos. As estruturas de dados que exigem que os campos sejam serializados por JsonUtility são a única exceção.

O que não fazer

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

O que fazer

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

Modificadores de acesso

Sempre declare um modificador de acesso para todos os campos, propriedades e métodos.

  • Todos os métodos de API do Unity devem ser private por padrão, a menos seja necessário substituí-los em uma classe derivada. Nesse caso, protected deve ser usado.

  • Os campos devem ser sempre private, com public ou protected acessadores de propriedade.

  • Usar membros com corpo de expressão e propriedades automáticas sempre que possível

O que não 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 fazer

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

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

Usar chaves

Sempre use chaves após cada bloco de instrução e coloque-as na linha seguinte.

O que não fazer

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

O que não 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 fazer

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

Classes públicas, structs e enumerações devem estar todos em seus próprios arquivos

Se a classe, o struct ou a enumeração puderem ser tornados privados, tudo bem ser incluído no mesmo arquivo. Isso evita problemas de compilações com o Unity e garante que a abstração de código adequada ocorra, também reduz conflitos e alterações significativas quando o código precisa ser alterado.

O que não fazer

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

O que fazer

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

O que 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 todos os enumerações sejam inicializados corretamente a partir de 0, o .NET fornece um atalho arrumado para inicializar automaticamente a enumeração apenas adicionando o primeiro valor (inicial). (por exemplo, valor 1 = 0 Valores restantes não são necessários)

O que não fazer

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

O que fazer

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

Ordenar enumerações para a extensão apropriada

É fundamental que, se um Enum provavelmente for estendido no futuro, para ordenar padrões na parte superior do Enum, isso garante que os índices Enum não sejam afetados com novas adições.

O que não fazer

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

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

Examinar o uso de enumeração para bitfields

Se houver uma possibilidade de uma enumeração exigir vários estados como um valor, por exemplo, Handedness = Left & Right. Em seguida, o Enum precisa ser decorado corretamente com BitFlags para habilitá-lo a ser usado corretamente

O arquivo Handedness.cs possui uma implementação concreta para esse caso

O que não fazer

public enum Handedness
{
    None,
    Left,
    Right
}

O que fazer

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

Caminhos de arquivo codificados em código

Ao gerar caminhos de arquivo de cadeia de caracteres e, em particular, escrever caminhos de cadeia de caracteres codificados em código, faça o seguinte:

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

Essas etapas garantem que o MRTK funcione em sistemas baseados em Windows e Unix.

O que não 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 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);

Práticas recomendadas, incluindo recomendações do Unity

Algumas plataformas de destino deste projeto precisam levar o desempenho em consideração. Com isso em mente, sempre tenha cuidado ao alocar memória em código frequentemente chamado em loops de atualização apertados ou algoritmos.

Encapsulamento

Sempre use campos privados e propriedades públicas se o acesso ao campo for necessário de fora da classe ou struct. Colocalize o campo privado e a propriedade pública. Isso torna mais fácil ver, rapidamente, o que apoia a propriedade e que o campo é modificável por script.

Observação

As estruturas de dados que exigem que os campos sejam serializados pelo JsonUtility são a única exceção a essa regra, onde uma classe de dados precisa ter todos os campos públicos para que a serialização funcione.

O que não 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 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
    }
}

Armazenar em cache valores e serializá-los na cena/pré-fabricado sempre que possível

Com o HoloLens em mente, é melhor otimizar as referências de desempenho e cache na cena ou pré-fabricação para limitar as alocações de memória de runtime.

O que não fazer

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

O que 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);
}

Referências de cache a materiais, não chame o ".material" cada vez

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

O que não fazer

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

O que 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);
    }
}

Observação

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

Use a compilação dependente de plataforma para garantir que o kit de ferramentas não interrompa a compilação em outra plataforma

  • Use WINDOWS_UWP para usar APIs não Unity específicas da UWP. Isso impedirá que eles tentem executar no Editor ou em plataformas sem suporte. Isso é equivalente a UNITY_WSA && !UNITY_EDITOR e deve ser usado em favor de.
  • Use UNITY_WSA para usar APIs do Unity específicas da UWP, como o namespace UnityEngine.XR.WSA. Isso será executado no Editor quando a plataforma estiver definida como UWP, bem como em aplicativos UWP internos.

Este gráfico pode ajudá-lo a decidir qual #if usar, dependendo dos casos de uso e das configuraçõ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

Prefira DateTime.UtcNow em vez de DateTime.Now

DateTime.UtcNow é mais rápido que DateTime.Now. Verificamos em investigações de desempenho anteriores que o uso de DateTime.Now adiciona sobrecarga significativa, especialmente quando usado no loop Update(). Outras pessoas encontraram o mesmo problema.

Prefira usar DateTime.UtcNow, a menos que você realmente precise dos horários localizados (um dos possíveis motivos pode ser quando você quiser mostrar a hora atual no fuso horário do usuário). Se você estiver lidando com tempos relativos (ou seja, o delta entre alguma última atualização e agora), é melhor usar DateTime.UtcNow para evitar a sobrecarga de fazer conversões de fuso horário.

Convenções de codificação do PowerShell

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

Veja também

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