Compartilhar via


Diretrizes de codificação – MRTK2

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


Filosofia

Seja conciso e esforce-se pela simplicidade

A solução mais simples geralmente é a melhor. Esse é um objetivo principal dessas diretrizes e deve ser o objetivo de todas as atividades 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 os 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 para nossos clientes iterarem rapidamente permite que eles desenvolvam soluções mais rapidamente e experimentem mais ideias. Em outras palavras, maximizar a velocidade da iteração capacita nossos clientes a alcançar mais.

Faça tudo funcionar no editor e faça funcionar em qualquer outra plataforma. Mantenha-o funcionando no editor. É fácil adicionar uma nova plataforma ao Play-In-Editor. É muito difícil fazer com que o Play-In-Editor funcione se seu 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] também expõem campos 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 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 alterações para pessoas que assumiram 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 os testes não podem ser verificadas. Portanto, os testes são a melhor maneira de garantir que outras pessoas não interrompa seu recurso.

Ao corrigir um bug, escreva um teste para garantir que ele não regreda no futuro. Se adicionar um recurso, escreva testes que verifiquem se o recurso funciona. Isso é necessário para todos os recursos de UX, 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 todos os 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, enumerações, funções, propriedades, campos postados no MRTK devem ser descritos quanto à sua finalidade e uso, 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;
}

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 recurso, 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.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 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 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 manipulada pelo mecanismo do Unity.

Se uma classe de inspetor for necessária, tente usar o do DrawDefaultInspector()Unity. Isso simplifica novamente a classe inspector 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 está 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 personalizada 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 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, 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

Registro em log

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

Um exemplo interessante em que ter registro em 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 por eventos de origem detectados incompatíveis e perda 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" controlados 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 ser 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 os PascalCase campos e const . 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, não há problema em 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 interruptivas 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 todas as enumerações sejam inicializadas 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 uma Enumeração provavelmente for estendida no futuro, para ordenar padrões na parte superior da Enumeração, isso garante que os índices de enumeração 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 campos de bits

Se houver uma possibilidade de uma enumeração exigir vários estados como um valor, por exemplo, Handedness = Left & Right. Em seguida, a enumeração precisa ser decorada corretamente com BitFlags para habilitá-la a ser usada 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 embutidos em código

Ao gerar caminhos de arquivo de cadeia de caracteres e, em particular, escrever caminhos de cadeia de caracteres embutidos 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 ou algoritmos apertados.

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 para materiais, não chame o ".material" todas as vezes

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 True
UNITY_WSA True True True
WINDOWS_UWP True True Falso
UNITY_WSA && !UNITY_EDITOR True True Falso
ENABLE_WINMD_SUPPORT True True Falso
NETFX_CORE Falso True 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 a ú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 do 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.

Confira também

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