Na Epic Games, temos alguns padrões e convenções de codificação simples. Este documento reflete o estado dos padrões de codificação atuais da Epic Games. Seguir os padrões de codificação é obrigatório.
As convenções de códigos são importantes para os programadores por vários motivos:
-
80% do custo do tempo de vida de um software vai para a manutenção.
-
Quase nenhum software é mantido durante toda a sua vida pelo autor original.
-
As convenções de códigos melhoram a legibilidade do software, permitindo que o desenvolvedor entenda um novo código de forma rápida e completa.
-
Se decidirmos exportar código-fonte para o desenvolvedor da comunidade de modificadores, queremos que seja facilmente entendido.
-
Muitas dessas convenções são necessárias para compatibilidade entre compiladores.
Os padrões de codificação abaixo são focados em C++; no entanto, espera-se que o padrão seja seguido independentemente da linguagem que for usada. Uma seção pode fornecer regras equivalentes ou exceções para linguagens específicas onde for aplicável.
Organização de classes
As classes deves ser organizadas com o leitor em mente, não o autor. Como a maioria dos jogadores usará a interface pública da classe, a implementação pública deve ser declarada primeiro, seguida pela implementação privada da classe.
UCLASS()
class EXAMPLEPROJECT_API AExampleActor : public AActor
{
GENERATED_BODY()
Público:
// Define valores padrão para as propriedades deste ator
AExampleActor();
protected:
// Chamado quando o jogo começa ou quando gerado
virtual void BeginPlay() override;
};
Aviso de direitos autorais
Qualquer arquivo de origem (".h", ".cpp", ".xaml") fornecido pela Epic Games para distribuição pública deve conter um aviso de direitos autorais como a primeira linha do arquivo. O formato do aviso deve corresponder exatamente ao mostrado abaixo:
// Copyright Epic Games, Inc. Todos os direitos reservados.
Se essa linha estiver faltando ou não for formatada corretamente, o CIS gerará um erro e falhará.
Convenções de nomenclatura
Ao usar código de nomenclatura, todos os códigos e comentários devem usar a ortografia e a gramática do inglês estadunidense.
- A primeira letra de cada palavra em um nome (como nome de teclado ou nome de variável) fica em letras maiúsculas. Geralmente não há sublinhado entre as palavras. Por exemplo, "Health" e "UpprimitiveComponent" estão corretos, mas "lastMouseCoordinates" ou "delta_coordinates", não.
Esta é a formatação PascalCase para o usuário que pode estar familiarizado com outras linguagens de programação orientada a objetos
-
Ps nomes dos tipos são prefixados com uma letra maiúscula adicional para diferenciá-los dos nomes das variáveis. Por exemplo, "FSkin" é o nome de um tipo, e "Skin" é uma instância do tipo "FSkin".
-
A classe de modelo é prefixada por T.
modelo <typename ObjectType> classe TAttribute -
As classes que herdam de UObject são prefixadas por U.
classe UActorComponent -
As classes que herdam de AActor são prefixadas por A.
classe AActor -
As classes que herdam de SWidget são prefixadas por S.
classe SComponentWidget -
As classes que são interfaces abstratas são prefixadas por I.
classe IAnalyticsProvider -
Os tipos de estrutura semelhantes a um conceito da Epic são prefixados por C.
estrutura CStaticClassProvider { modelo <typename T> auto Requires(UClass*& ClassRef) -> decltype( ClassRef = T::StaticClass() ); }; -
As enumerações são prefixadas por E.
enum class EColorBits { ECB_Red, ECB_Green, ECB_Blue }; -
A variável booleana deve ser prefixada por b.
bPendingDestruction bHasFadedIn -
A maioria das outras classes é prefixada por F, embora alguns subsistemas usem outras letras.
-
Definições de tipo devem ser prefixadas com o que for apropriado para esse tipo, como:
-
F para a definição de tipo de uma estrutura
-
U para a definição de tipo de um "UObject"
-
-
Uma definição de tipo de uma instanciação de modelo específica não é mais um modelo e deve ser prefixada apropriadamente.
typedef TArray<FMytype> FArrayOfMyTypes; -
Prefixos são omitidos em C++.
-
A ferramenta Cabeçalho da Unreal requer o prefixo correto na maioria dos casos, por isso é importante fornecê-los.
-
Parâmetros de modelo de tipo e aliases de tipo aninhados com base nesses parâmetros de modelo não estão sujeitos às regras sobre prefixos acima, pois a categoria do tipo é desconhecida.
-
Prefira um sufixo de tipo após um termo descritivo.
-
Desambigue parâmetros de modelo de aliases usando um prefixo "In":
modelo <typename InElementType> classe TContainer { Público: using ElementType = InElementType; }; -
Os nomes de variáveis e tipos são substantivos.
-
Os nomes de métodos são verbos que descrevem o efeito do método ou o valor de retorno de um método sem efeito.
-
Nomes de macro devem estar em letras maiúsculas com palavras separadas por sublinhados e prefixados com "UE_".
#define UE_AUDIT_SPRITER_IMPORT
Os nomes de variáveis, métodos e classes devem ser:
-
Claros
-
Sem ambiguidades
-
Descritivos
Quanto maior o escopo do nome, maior a importância de um bom nome descritivo. Evite abreviações excessivas.
Todas as variáveis devem ser declaradas em sua linha para que você possa fornecer comentário sobre o significado de cada variável.
O estilo JavaDocs requer isso.
Você pode usar um comentário de várias linhas ou de uma única linha antes de uma variável. As linhas em branco são opcionais para a variável de agrupamento.
Todas as funções que retornam um booleano devem fazer uma pergunta verdadeiro/falso, como "IsVisible()" ou "ShouldClearBuffer()".
Um procedimento (uma função sem valor de retorno) deve usar um verbo forte seguido por um objeto. Uma exceção será quando o objeto do método for o objeto em que ele está. Nesse caso, o objeto é entendido a partir do contexto. Os nomes a evitar incluem aqueles que começam com "Handle"(Manipular) e "Process" (Processar), porque os verbos são ambíguos.
Recomendamos que você prefixe os nomes do parâmetro de função com "Out" se:
-
A função parâmetro é passada por referência.
-
Espera-se que a função grave nesse valor.
Isso torna óbvio que o valor transmitido nesse argumento é substituído pela função.
Se um parâmetro In ou Out também for um booleano, coloque "b" antes do prefixo In/Out, como "bOutResult".
Funções que retornam um valor devem descrever o valor de retorno. O nome deve deixar claro qual valor a função retorna. Isso é particularmente importante para funções booleanas. Considere os dois métodos de exemplo a seguir:
// o que significa "true" (verdadeiro)?
bool CheckTea(FTea Tea);
// o nome deixa claro que "true" (verdadeiro) indica que o chá está fresco
bool IsTeaFresh(FTea Tea);
float TeaWeight;
int32 TeaCount;
bool bDoesTeaStink;
FName TeaName;
FString TeaFriendlyName;
UClass* TeaClass;
USoundCue* TeaSound;
UTexture* TeaTexture;
Escolha de palavra inclusiva
Quando trabalhar na base de código da Unreal Engine, incentivamos que você se esforce para usar linguagem respeitosa, inclusiva e profissional.
A escolha de palavras se aplica quando você:
-
classes de nome.
-
funções.
-
estruturas de dados.
-
tipos.
-
variáveis.
-
arquivo e pastas.
-
plugins.
Aplica-se quando você escreve uma extensão de texto voltada para o usuário da IU, mensagens de erro e notificações. Também se aplica ao escrever sobre o código, como em comentários e descrições de listas de alterações.
As seções a seguir fornecem orientação e sugestões para ajudar a escolher palavras e nomes que sejam respeitosos e apropriados para todas as situações e públicos, além de serem uma forma de comunicação mais eficiente.
Inclusão racial, étnica e religiosa
-
Não use melodias ou semelhanças que reforcem estereótipos. Os exemplos incluem comparações entre pretos e brancos, ou lista negra e lista branca.
-
Não use palavras que se refiram a traumas históricos ou experiências de discriminação passadas. Os exemplos incluem escravo, mestre e bomba atômica.
Inclusão de gênero
-
Use linguagem neutra para referir-se a pessoas fictícias.
-
Você pode usar pronomes pessoais para referir-se até mesmo a elementos inanimados. Por exemplo, módulo, plugin, função, cliente, servidor ou qualquer outro componente de software ou hardware.
-
Evite marcas de gênero ao se referir ao usuário.
-
Não use substantivos coletivos, como caras, que presumem o gênero masculino.
-
Evite expressões coloquiais que contenham gêneros arbitrários, como o "X de um coitado".
Gíria
-
Lembre-se de que suas palavras estão sendo lidas por um público global que pode não compartilhar os mesmos idiomas e comportamentos e pode não entender a mesma referência natural.
-
Evite gírias e linguagem informal, mesmo que você ache engraçado ou inofensivo. Pessoas que não têm o inglês como primeira língua poderão achar as expressões difíceis de entender e ter dificuldade para traduzi-las.
-
Não use xingamentos.
Polissemia
- Muitos termos que usamos por seus significados técnicos também têm outros significados fora da área de tecnologia. Os exemplos incluem abortar, executar ou nativo. Ao usar palavras como essas, sempre priorize a exatidão e analise o contexto em que elas aparecem.
Lista de palavras
A lista a seguir identifica algumas terminologias que usamos na base de código da Unreal no passado, mas que achamos que devem ser substituídas por alternativas melhores:
| Nome da palavra | Nome alternativo para a palavra |
|---|---|
| Lista negra | "lista de proibições", "lista de bloqueios", "lista de exclusões" |
| Lista de permissões | lista de inclusões, Lista de preferências, lista de elementos aprovados |
| Mestre | primário, original, controlador, modelo, referência, principal, líder |
| Escravo | secundário, réplica, agente, seguidor, trabalhador, nó de cluster, bloqueado, vinculado, sincronizado |
Estamos trabalhando ativamente para alinhar nosso códigos com os princípios apresentados acima.
Código C++ portátil
Os tipos "int" e "int" sem sinal variam de tamanho conforme a plataforma. Eles devem ter pelo menos 32 bits de largura e são aceitáveis em código quando a largura inteira não for importante. Botão de tamanho explícito é usado em formatos serializados ou replicados.
Veja a seguir uma lista de tipos comuns:
-
"bool" para valores booleanos (nunca presuma o tamanho do bool). "Bool" não compilará.
-
"TCHAR" para um caractere (nunca presuma o tamanho do TCHAR).
-
"uint8" para byte sem sinal (1 byte).
-
"int8" para byte com sinal (1 byte).
-
"uint16" para shorts sem sinal (2 bytes).
-
"int16" para shorts com sinal (2 bytes).
-
"uint32" para ints sem sinal (4 bytes).
-
"int32" para ints com sinal (4 bytes).
-
"uint64" para palavras quad sem sinal (8 bytes).
-
"int64" para palavras quad com sinal (8 bytes).
-
"float" para ponto flutuante de precisão única (4 bytes).
-
doublepara ponto flutuante de precisão dupla (8 bytes). -
"PTRINT" para um inteiro que pode conter um ponteiro (nunca presuma o tamanho do PTRINT).
Uso de bibliotecas padrão
Tradicionalmente, a UE tem evitado o uso direto de bibliotecas padrão de C e C++ pelos seguintes motivos:
-
Substituição de implementações lentas por nosso proprietário que fornecem controles adicionais sobre a alocação de memória.
-
Adição de nova funcionalidade antes que ela esteja amplamente disponível, como:
-
Implementar mudanças de comportamento desejáveis, mas fora do padrão.
-
Ter uma sintaxe consistente em toda a base do código.
-
Evitar construções que são incompatíveis com os idiomas da UE.
-
No entanto, a biblioteca padrão evoluiu e inclui funcionalidades que não queremos encapsular com uma camada de abstração ou reimplementar por nós mesmos.
Quando houver uma escolha entre uma funcionalidade padrão de biblioteca em vez da nossa, você deve preferir a opção que gera resultados melhores. Também é importante lembrar que a consistência é valorizada. Se uma implementação herdada da UE não estiver mais servindo a uma finalidade, podemos optar por descontinuá-la e migrar todo o uso para a biblioteca padrão.
Evite misturar idiomas da UE e expressões de biblioteca padrão na mesma API. A tabela a seguir lista expressões idiomáticas comuns junto com recomendações sobre quando usá-las.
| Idioma | Descrição |
|---|---|
| " |
O idioma atômico deve ser usado no novo código e no antigo migrado quando alguém tocar. Espera-se que os atômicos sejam implementados de forma total e eficiente em todas as plataformas compatíveis. Nosso "TAtomic" está apenas parcialmente implementado e não é do nosso interesse mantê-lo e melhorá-lo. |
| " |
O idioma com características de tipo deve ser usado onde houver sobreposição entre uma característica legada da UE e uma característica padrão. Características são geralmente implementadas como intrínsecas do compilador para correção, e o compilador pode ter conhecimento das características padrão e selecionar o caminho de compilação mais rápido em vez de tratá-las como C++ simples. Uma preocupação é que nossas características normalmente têm um "valor" estático ou um tipo de definição "tipo". Essa é uma distinção importante, pois uma sintaxe específica é esperada pelas características da composição, por exemplo: "std::conjunction". As novas características que adicionamos devem ser escritas com "valor" ou "tipo" em letra minúscula para suportar a composição. As características existentes devem ser atualizadas para oferecer suporte para qualquer um dos casos. |
| " |
O idioma da lista de inicializadores deve ser usado para oferecer suporte para a sintaxe do inicializador com chaves. Este é um caso em que a linguagem e as bibliotecas padrão se sobrepõem. Não há alternativa se você quiser apoiar isso. |
| " |
O idioma regex pode ser usado diretamente, mas seu uso deve ser encapsulado no código somente do editor. Não temos planos para implementar nossa solução de regex de proprietário. |
<limits> |
std::numeric_limits pode ser usado em sua totalidade. |
| " |
Todas as funções de ponto flutuante deste cabeçalho podem ser usadas. |
| " |
Essas expressões podem ser usadas em vez de "FMemory::Memcpy" e "FMemory::Memset" respectivamente, quando elas têm um benefício de desempenho demonstrado. |
Strings e um contêineres padrão devem ser evitados, exceto no código de interoperabilidade.
Comentários
Comentários são comunicação, e a comunicação é essencial. A seção a seguir detalha algumas coisas importantes para ter em mente sobre comentários (A prática da programação, Kerrighan e Pike).
Diretrizes
-
Escreva código autodocumentado. Por exemplo:
// Ruim: t = s + l - b; // Bom: TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves; -
Escreva comentários úteis. Por exemplo:
// Ruim: // incrementa folhas ++Folhas; // Bom: // sabemos que há outra folha de chá ++Folhas; -
Não exagere no comentário de código ruim — reescreva-o em vez disso. Por exemplo:
// Ruim: // o número total de folhas é a soma das // folhas pequenas e grandes menos o // número de folhas que são ambas t = s + l - b; // Bom: TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves; -
Não contrarie o código. Por exemplo:
// Ruim: // nunca incrementa Folhas! ++Leaves; // Bom: // sabemos que há outra folha de chá ++Leaves;
Correção constante
Constante é uma documentação, tanto quanto é uma diretriz de compilador. Todo código deve se esforçar para ser constante e correto. Isso inclui as seguintes diretrizes:
-
Passar argumentos de função por ponteiro constante ou referência se esses argumentos não se destinarem a ser modificados pela função.
-
Sinalizar métodos como constante se não modificarem o objeto.
-
Use a iteração constante sobre o contêiner se o loop não tiver a intenção de modificar o contêiner.
Exemplo de constante:
void SomeMutatingOperation(FThing& OutResult, const TArray<Int32>& InArray)
{
// InArray não será modificado aqui, mas OutResult provavelmente será
}
void FThing::SomeNonMutatingOperation() const
{
// Este código não modificará o FThing em que está sendo invocado
}
TArray<FString> StringArray;
for (const FString& : StringArray)
{
// O corpo deste loop não modificará StringArray
}
A constante também é preferida para parâmetro por valor e local. Isso informa ao leitor que a variável não será modificada no corpo da função, o que facilita a compreensão. Se você fizer isso, certifique-se de que a declaração e a definição correspondam, pois isso pode afetar o processo JavaDoc.
void AddSomeThings(const int32 Count);
void AddSomeThings(const int32 Count)
{
const int32 CountPlusOne = Count + 1;
// Nem Count nem CountPlusOne podem ser alterados durante o corpo da função
}
Uma exceção a isso é o parâmetro de passagem por valor, que é movido para um contêiner. Para mais informações, consulte a seção "Semântica de movimentação" nesta página.
Exemplo:
void FBlah::SetMemberArray(TArray<FString> InNewArray)
{
MemberArray = MoveTemp(InNewArray);
}
Coloque a palavra-chave constante na extremidade ao tornar o próprio ponteiro constante (e não para o que ele aponta). As referências não podem ser "reatribuídas" de qualquer maneira e, portanto, não podem se tornar constante da mesma maneira.
Exemplo:
// Ponteiro constante para objeto não constante - o ponteiro não pode ser reatribuído, mas T ainda pode ser modificado
T* const Ptr = ...;
// Ilegal
T& const Ref = ...;
Nunca use constante em um tipo de retorno. Isso inibe semânticas de movimento para tipos complexos e dará avisos de compilação para tipo integrado. Esta regra só se aplica ao tipo de retorno propriamente dito, não ao tipo de destino de um ponteiro ou referência sendo retornado.
Exemplo:
// Ruim - retornando uma matriz constante
const TArray<FString> GetSomeArray();
// Bom - retornando uma referência para uma matriz constante
const TArray<FString>& GetSomeArray();
// Bom - retornando um ponteiro para uma matriz constante
const TArray<FString>* GetSomeArray();
// Ruim - retornando um ponteiro constante para uma matriz constante
const TArray<FString>* const GetSomeArray();
Exemplo de formatação
Usamos um sistema baseado em JavaDoc para extrair comentário do código e construir a documentação automaticamente, por isso recomendamos regras específicas de formatação de comentário.
O exemplo a seguir demonstra o formato de comentários sobre classe, método e variável**. Lembre-se de que o comentário deve complementar o código. O código documenta a implementação, enquanto comentário documenta a intenção. Certifique-se de atualizar o comentário ao alterar a intenção de um trecho de código.
Observe que dois estilos diferentes de comentário são suportados, mostrados pelos métodos "Steep" e "Sweeten". O estilo "@param" usado por "Steep" é o estilo multilinha tradicional. Para uma função simples, pode ser mais claro integrar a documentação sobre o parâmetro e o valor de retorno no comentário descritivo da função. Isso é demonstrado no exemplo "Sweeten". Tags de comentário especiais, como "@see ou "@return", só devem ser usadas para iniciar novas linhas após a descrição principal.
Os comentários de método devem ser incluídos apenas uma vez: onde o método é declarado publicamente. Os comentários do método devem conter apenas informações relevantes para o chamador do método, incluindo qualquer informação sobre a substituição do método que possa ser relevante para o chamador. Detalhes sobre a implementação do método e sua substituição, que não são relevantes para o chamador, devem ser comentados na implementação do método.
Os comentários de classe devem incluir:
-
Uma descrição do problema que esta classe resolve.
-
O motivo pelo qual esta classe foi criada.
Os comentários de método multilinha deve incluir:
-
Finalidade da função: documenta o problema que a função resolve. Conforme declarado anteriormente, os comentários documentam a intenção, e o código a implementação.
-
**Comentários de parâmetro: cada comentário de parâmetro deve incluir:
-
unidades de medida;
-
A faixa de valores esperada;
-
Valores "impossíveis";
-
E o significado dos códigos de status/erro.
-
-
*Comentário de retorno**: documenta o valor de retorno esperado, assim como uma variável de saída é documentada. Para evitar redundância, um comentário "@return" explícito não deve ser usado se o único objetivo da função for retornar esse valor e isso já estiver documentado na finalidade da função.
-
Informações extras: "@warning", "@note", "@see" e "@deprecated" podem opcionalmente ser usadas para documentar informações relevantes adicionais. Cada um deve ser declarado em sua linha de proprietário após o restante do comentário.
Sintaxe da linguagem C++ moderna
O Unreal Engine foi construído para ser massivamente portátil para muitos compiladores C++, por isso temos o cuidado de usar funcionalidades que sejam compatíveis com o compilador para o qual podemos estar oferecendo suporte. Às vezes, os recursos são tão úteis que os agrupamos em macros e os usamos de maneira abrangente. No entanto, geralmente queremos esperar até que todos os compiladores compatíveis estejam de acordo com o padrão mais recente.
O Unreal Engine compila com uma versão de linguagem de C++20 por padrão e requer uma versão mínima de C++20 para compilar. Usamos muitos recursos de linguagem modernas que são bem suportados em todo o compilador moderno. Em alguns casos, agrupamos o uso dessas funcionalidades nas condicionais do pré-processador. No entanto, às vezes resolvemos evitar totalmente certas funcionalidades da linguagem, por portabilidade ou outros motivos.
A menos que seja especificado abaixo, como uma funcionalidade de compilador C++ moderno que suportamos, você não deve usar funcionalidades de linguagem específicas de compilador, a menos que estejam agrupadas em macros de pré-processador ou condicionais e sejam usadas com moderação.
Asserção estática
A palavra-chave "static_assert" é válida para uso onde você precisa de uma asserção de tempo de compilação.
Substituição e final
As palavras-chave "override" e "final" são válidas para uso, e seu uso é altamente recomendável. Pode haver muitos lugares onde essas funções foram omitidas, mas elas serão fixadas com o tempo.
Nullptr
Você deve usar "nullptr" em vez da macro "Null" de estilo C em todos os casos.
Uma exceção a isso é o uso de "nullptr" em compilações C++/CX (como para Xbox One). Neste caso, o uso de "nullptr" é, na verdade, a referência de nulo gerenciada. Ela é compatível principalmente com "nullptr" do C++ nativo, exceto em seu tipo e alguns contextos de instanciação de modelo. Portanto, você deve usar a macro "TYPE_OF_NULLPTR" em vez do "decltype(nullptr)" mais comum para obter compatibilidade.
Automático
Você não deve usar "auto" no código C++, exceto pelas poucas exceções listadas abaixo. Sempre seja explícito sobre o tipo que você está inicializando. Isso significa que o tipo deve estar totalmente visível para o leitor. Essa regra também se aplica ao uso da palavra-chave "var" em C#.
O recurso de vinculação estruturada do C++20 também não deve ser usado, pois é efetivamente um "auto" variável.
Uso aceitável de auto:
-
Quando você precisa vincular um lambda a uma variável, pois os tipos de lambda não podem se expressados no código.
-
Para variáveis de iterador, mas somente onde o tipo do iterador for detalhado e prejudicaria a legibilidade.
-
No modelo de código, onde o tipo de uma expressão não pode ser facilmente discernido. Esse é um caso avançado.
É muito importante que o tipo esteja claramente visível para alguém que esteja lendo o código. Mesmo que alguns IDE sejam capazes de inferir o tipo, isso depende de o código estar em um estado compilável. Também não ajudará o usuário de ferramentas de mesclagem/comparação, ou ao visualizar o arquivo de origem individual isoladamente, como no GitHub.
Se tiver certeza de que está usando "auto" de uma maneira aceitável, lembre-se sempre de usar "const", "&" ou "*" corretamente, como faria com o nome do tipo. Com "auto", o tipo inferido será forçado a ser o que você quiser.
"for" baseado em intervalo
Isso é preferido para manter o código mais fácil de entender e mais sustentável. Quando você migrar código que usa iteradores antigos "TMap", esteja ciente de que as antigas funções "key()" e "value()", que eram métodos do tipo de iterador, agora são simplesmente os campos "key" e "value" do valor-chave subjacente "TPair".
Exemplo:
TMap<FString, int32> MyMap;
// Estilo antigo
for (auto It = MyMap.CreateIterator(); It; ++It)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), It.Key(), *It.Value());
}
// Novo estilo
for (TPair<FString, int32>& Kvp : MyMap)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), *Kvp.Key, Kvp.Value);
}
Também temos substituições de intervalo para alguns tipos de iterador independentes.
Exemplo:
// Estilo antigo
for (TFieldIterator<UProperty> PropertyIt(InStruct, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++PropertyIt)
{
UProperty* Property = *PropertyIt;
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
}
// Novo estilo
for (UProperty* Property : TFieldRange<UProperty>(InStruct, EFieldIteratorFlags::IncludeSuper))
{
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
}
Funções anônimas e lambdas
Os lambdas podem ser usados livremente, mas vêm com preocupações adicionais de segurança. Os melhores lambas não devem ter mais do que algumas instruções de tamanho, especialmente quando usados como parte de uma expressão ou instrução maior, por exemplo, como um predicado em um algoritmo genérico.
Exemplo:
// Encontrar a primeira coisa cujo nome contém a palavra "Olá"
Thing* HelloThing = ArrayOfThings.FindByPredicate([](const Thing& Th){ return Th.GetName().Contains(TEXT("Hello")); });
// Classifica a matriz na ordem inversa de nome
Algo::Sort(ArrayOfThings, [](const Thing& Lhs, const Thing& Rhs){ return Lhs.GetName() > Rhs.GetName(); });|
Esteja ciente de que lambdas com estado não podem ser atribuídos a ponteiros de função, que usamos com frequência. Lambdas não triviais devem ser documentados da mesma maneira que funções regulares. Lambdas também podem ser usados como Delegados para execução adiada usando funções como "BindWeakLambda" em que a variável capturada funciona como uma carga útil.
Capturas e tipos de retorno
Capturas explícitas devem ser usadas em vez de capturas automáticas ("[&]" e "[=]"). Isso é importante por motivos de legibilidade, capacidade de manutenção, segurança e desempenho, especialmente quando usado com pancadas grandes e execução adiada.
Capturas explícitas declaram a intenção do autor; portanto, os erros são detectados durante a revisão do código. Capturas incorretas podem causar travamentos e bugs sérios, que têm mais probabilidade de se tornar problematizadores à medida que o código é mantido ao longo do tempo. Aqui estão algumas coisas adicionais para se manter em mente sobre capturas de lambda:
-
A captura por referência e a captura por valor do ponteiro (incluindo o ponteiro "This") podem causar corrupção nos dados e travamentos se a execução do parece ser adiada. Variáveis de membros e locais nunca devem ser capturadas por referência para lambdas adiados.
-
A captura por valor pode ser um problema de desempenho se fizer cópias desnecessárias para um lambda não adiado.
-
Os ponteiros de UObjects capturados acidentalmente são invisíveis para o coletador de lixo. A captura automática captura "This" implicitamente se qualquer variável de membro for referenciada, mesmo que "[=]" dê a impressão de que o lambda tem suas cópias de tudo.
-
Wrappers de delegado, como "CreateWeakLambda" e "CreateSPLambda", devem ser usados para execução adiada, pois serão desvinculados automaticamente se o UObject ou o ponteiro compartilhado for liberado. Outro objeto compartilhado pode ser capturado como TWeakObjectPtr ou TWeakPtr e depois validado dentro do lambda.
-
Qualquer uso de lambda adiado que não siga essas diretrizes deve ter um comentário explicando por que a captura do lambda é segura.
O tipo de retorno explícito deve ser usado para loops grandes ou quando você estiver retornando o resultado de outra chamada de função. Elas devem ser consideradas da mesma forma que a palavra-chave "auto".
Enumerações fortemente tipadas
Classes enumeradas (enumeração) são uma substituição para enumerações com namespace de estilo antigo, tanto para enumerações regulares quanto "UENUMs". Por exemplo:
// Enumeração antiga
UENUM()
namespace EThing
{
enum Type
{
Thing1,
Thing2
};
}
// Nova enumeração
UENUM()
enum class EThing : uint8
{
Thing1,
Thing2
}
Enums são suportados como "UPropertys" e substituem a antiga solução alternativa "TenumAsByte<>". As propriedades da enumeração também podem ser de qualquer tamanho, não apenas byte:
// Propriedade antiga
UPROPERTY()
TEnumAsByte<EThing::Type> MyProperty;
// Nova propriedade
UPROPERTY()
EThing MyProperty;
Enumerações expostas a Blueprints devem continuar a ser baseadas em "uint8".
Classes de enumeração usadas como sinalizadores pode aproveitar a macro "ENUM_CLASS_FLAGS(EnumType)" para definir automaticamente todo o operador bit a bit:
enum class EFlags
{
Nenhum = 0x00,
Flag1 = 0x01,
Flag2 = 0x02,
Flag3 = 0x04
};
ENUM_CLASS_FLAGS(EFlags)
A única exceção a isso é o uso de sinalizadores em um contexto de truth, essa é uma limitação da linguagem. Em vez disso, todos os sinalizadores de enumeração devem ter um enumerador chamado "Non" que é definido como 0 para fins de comparação:
// Antigo
if (Flags & EFlags::Flag1)
// Novo
if ((Flags & EFlags::Flag1) != EFlags::None)
Semânticas de movimento
Todos os principais tipos de contêineres, "TArra", "TMap", "TSet", "FString" — têm construtores de movimento e operadores de atribuição de movimento. Eles geralmente são usados automaticamente ao passar ou retornar esses tipos por valor. Eles também podem ser invocados explicitamente usando "MoveTemp", o equivalente na UE a "std::move".
Retornar contêineres ou strings por valor pode ser útil para expressão, sem o custo normal de cópias temporárias. As regras sobre passagem por valor e o uso de "MoveTemp" ainda estão sendo estabelecidas, mas já podem ser encontradas em algumas áreas otimizadas da base do código.
Inicializadores de membro padrão
Inicializadores de membros padrão podem ser usados para definir os padrões de uma classe dentro da própria classe:
UCLASS()
class UTeaOptions : public UObject
{
GENERATED_BODY()
Público:
UPROPERTY()
int32 MaximumNumberOfCupsPerDay = 10;
UPROPERTY()
float CupWidth = 11.5f;
UPROPERTY()
FString TeaType = TEXT("Earl Grey");
UPROPERTY()
EDrinkingStyle DrinkingStyle = EDrinkingStyle::PinkyExtended;
};
O código escrito dessa forma tem os seguintes benefícios:
-
Não é necessário duplicar os inicializadores em vários construtores.
-
Não é possível misturar a ordem de inicialização e a ordem de declaração.
-
O tipo de membro, os sinalizadores de propriedade e o valor padrão estão todos em um só lugar. Isso ajuda na legibilidade e na manutenção.
No entanto, também existem algumas desvantagens:
-
Qualquer alteração nos padrões requer uma reconstrução de todos os arquivos dependentes.
-
Os cabeçalhos não podem ser alterados no restante do patch da engine, então esse estilo pode limitar os tipos de correções possíveis.
-
Algumas coisas não podem ser inicializadas dessa maneira, como classes-base, subobjetos "UObject", ponteiros para tipos declarados frontais, valores deduzidos de argumentos do construtor e membros inicializados em várias etapas.
-
Colocar alguns inicializadores no cabeçalho e o resto no construtor no arquivo .cpp pode reduzir a legibilidade e a manutenção.
Use seu melhor critério ao decidir se deve usar inicializadores de membros padrão. Como regra geral, os inicializadores de membro padrão fazem mais sentido sem o código de jogo do que com o código da engine. Considere o uso do arquivo de configuração para o valor padrão.
Código de terceiros
Sempre que você modificar o código para uma biblioteca que usamos na engine, certifique-se de marcar suas alterações com um comentário //@UE5, bem como uma explicação do por que você fez a alteração. Isso facilita a mesclagem das alterações em uma nova versão dessa biblioteca e garante que os licenciados poderão encontrar facilmente todas as modificações feitas.
Qualquer código de terceiros incluído na engine deve ser marcado com comentário formatado para ser facilmente pesquisável. Por exemplo:
// @código de terceiros - BEGIN PhysiX
#include <physx.h>
// @código de terceiros - END Physix
// @código de terceiros - BGIN MSDN setThreadName
// [http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx]
// Usado para definir o nome da thread no depurador
...
//@código de terceiros - END MSDN SetThreadName
Formatação de código
Chaves
Guerras de chaves são sujas. A Epic Games tem um padrão de uso de longa data de colocar as chaves em uma nova linha. Siga esse uso independentemente do tamanho da função ou do bloco. Por exemplo:
// Ruim
int32 GetSize() const { return Size; }
// Bom
int32 GetSize() const
{
return Size;
}
Sempre inclua chaves em blocos de instrução única. Por exemplo:
if (bThing)
{
return;
}
If - Else
Cada bloco de execução em uma instrução if-else deve estar entre chaves. Isso ajuda a evitar erros de edição. Quando chaves não são usadas, alguém pode inadvertidamente adicionar outra linha a um bloco if. A linha extra não seria controlada pela expressão if, o que seria ruim. Também é ruim quando compilar condicionalmente o item faz com que a instrução if/else seja interrompida. Portanto, use sempre chaves.
if (bHaveUnrealLicense)
{
InsertYourGameHere();
}
else
{
CallMarkRein();
}
Uma instrução if multidirecional deve ser recuada, com cada "else if" recuado na mesma quantidade que o primeiro "if"; isso torna a estrutura clara para o leitor:
if (TannicAcid < 10)
{
UE_LOG(LogCategory, Log, TEXT("Acidez baixa"));
}
else if (TannicAcid < 100)
{
UE_LOG(LogCategory, Log, TEXT("Acidez média"));
}
else
{
UE_LOG(LogCategory, Log, TEXT("Acidez alta"));
}
Separadores e recuo
Veja a seguir alguns padrões para recuo do seu código.
-
Recue o código por bloco de execução.
-
Use separadores para espaço em branco no início de uma linha, não espaços. Defina o tamanho do separador para 4 caracteres. Observe que os espaços às vezes são necessários e permitidos para manter o código alinhado, independentemente do número de espaços em um separador. Por exemplo, quando você está alinhando o código que está de acordo com caracteres que não são separadores.
-
Se você estiver escrevendo código em C++, use também separadores, não espaços. O motivo para isso é que os programadores costumam alternar entre C# e C++, e a maioria prefere usar uma configuração consistente para separadores. O Visual Studio usa espaços para arquivos C# por padrão, então você precisa se lembrar de alterar essa configuração ao trabalhar no código da Unreal Engine.
Instruções de alternância
Exceto para casos vazios (vários casos com código idêntico), as instruções de caso de alternância devem rotular explicitamente que um caso passará para o próximo. Inclua uma interrupção ou inclua um comentário de "passagem" em cada caso. Outros comandos de transferência de controle de código (return, continue e assim por diante) também funcionam.
Sempre tenha um caso padrão. Inclua uma interrupção apenas no caso de alguém adicionar um novo caso após o padrão.
switch (condition)
{
caso 1:
...
// passagem
case 2:
...
break;
case 3:
...
return;
caso 4:
case 5:
...
break;
default:
break;
}
Namespaces
Você pode usar namespaces para organizar suas classes, funções e variáveis quando apropriado. Se você usá-los, siga as regras abaixo.
-
A maioria dos códigos da UE não está atualmente encapsulada em um namespace global.
- Tenha cuidado para evitar colisões no escopo global, especialmente ao usar ou incluir código de terceiros.
-
Namespaces não são suportados pelo UnrealHeaderTool.
- Namespaces não devem ser usados ao definir "UCLASSes", "USTRUCTs" e assim por diante.
-
Novas APIs que não são "UCClasses", "USTRUCTs" etc. devem ser colocadas em um namespace "UE::" e, idealmente, um namespace aninhado, por exemplo, "UE Audio::".
- Namespaces que são usados para armazenar detalhes de implementação que não fazem parte da API voltada para o público devem ficar em um namespace "Private", por exemplo, "UE::Audio::Private::".
-
Declarações "Using":
- Não coloque declarações " no escopo global, mesmo em um arquivo ".cpp" (isso causará problemas com nosso sistema de compilação de "unidade".)
-
Não há problema em colocar a declaração "using" dentro de outro namespace ou dentro do corpo de uma função.
-
Se você colocar declarações "using" dentro de um namespace, ele será transportado para outras ocorrências desse namespace na mesma unidade de translação. Contanto que você seja consistente, tudo ficará bem.
-
Você só poderá usar a instrução "using" no arquivo de cabeçalho com segurança se seguir as regras acima.
-
Tipos declarados frontais precisam ser declarados em seu respectivo namespace.
- Se você não fizer isso, receberá erros de link.
-
Se você declarar muitas classes ou tipos em um namespace, pode ser difícil usar esses tipos em outras classes de escopo global (por exemplo, assinaturas de função precisarão usar namespace explícito ao aparecer em declarações de classe).
-
Você pode usar a declaração "using" para variáveis específicas apenas de alias dentro de um namespace em seu escopo.
- Por exemplo, usando "Foo::FBar". No entanto, geralmente não fazemos isso no código do Unreal.
-
As macros não podem residir em um namespace.
- Elas devem ter o prefixo "UE_" em vez de residir em um namespace, por exemplo, "log".
Dependências físicas
-
Nomes de arquivos não devem ser prefixados sempre que possível.
- Por exemplo, "Scene.cpp" em vez de "UScene.cpp". Isso facilita o uso de ferramentas como o Workspace Whiz ou o Open File in Solution do Visual Assist ao reduzir o número de letras necessárias para identificar o arquivo desejado.
-
Todos os cabeçalhos devem se proteger de inclusões múltiplas com a diretiva "#pragma once".
-
Observe que todos os compiladores que usamos oferecem suporte para "#pragma once".
#pragma once //<file contents>
-
-
Tente minimizar o encaixe físico.
- Em particular, evite incluir cabeçalhos de biblioteca padrão de outros cabeçalhos.
-
A declaração de deslocamento é preferível a incluir cabeçalhos.
-
Ao incluir um cabeçalho, seja o mais minucioso possível.
- Por exemplo, não inclua "Core.h". Em vez disso, você deve incluir os cabeçalhos específicos no núcleo dos quais precisa de definição.
-
Tente incluir todos os cabeçalhos necessários diretamente para facilitar a inclusão minuciosa.
-
Não confie em um cabeçalho que seja incluído indiretamente por outro cabeçalho que você incluiu.
-
Não confie que nada será incluído por meio de outro cabeçalho. Inclua tudo de que você precisa.
-
O módulo tem diretórios de origens privada e pública.
- Qualquer definição necessária para outro módulo deve estar em cabeçalhos no diretório público. Todo o resto deve estar no diretório privado. Em módulos mais antigos da Unreal, esses diretórios ainda podem ser chamados de "Src" e "Inc", mas esses diretórios destinam-se a separar os códigos privado e público da mesma maneira e não devem separar o arquivo de cabeçalho do arquivo de origem.
-
Não se preocupe em configurar seus cabeçalhos para geração de cabeçalho pré-compilado.
- O UnrealBuildTool pode fazer um trabalho melhor do que você.
-
Divida funções grandes em subfunções lógicas.
- Uma área de otimizações dos compiladores é a eliminação de subexpressões comuns. Quanto maiores forem suas funções, mais trabalho o compilador terá que fazer para identificá-las. Isso leva a tempos de construção muito inflados.
-
Não use um grande número de funções em linha.
- Funções em linha forçam novas compilações mesmo em arquivos que não as utiliza. As funções em linha devem ser usadas apenas para acessadores triviais e quando a criação de perfil indicar que há um benefício em fazê-lo.
-
Seja conservador no uso de "FORCEINLINE".
- Todos os códigos e variáveis locais serão expandidos para a função chamadora. Isso causará os mesmos problemas de tempo de compilação que os causados por funções grandes.
Encapsulamento
Aplique o encapsulamento com palavras-chave de proteção. Os membros da classe quase sempre devem ser declarados privados, a menos que façam parte da interface pública/protegida da classe. Use sua intuição, mas sempre esteja ciente de que a falta de acessadores dificulta a refatoração mais tarde sem interromper o plugin e os projetos existentes.
Se campos específicos se destinarem apenas a serem utilizados pela classe derivada, torne-os privados e forneça acessadores protegidos.
Use a final se sua classe não tiver sido projetada para ser derivada.
Problemas gerais de estilo
-
Minimize a distância de dependência.
- Quando o código depende de uma variável para ter um determinado valor, tente definir o valor dessa variável logo antes de usá-la. Inicializar uma variável no topo de um bloco de execução e não usá-la para cem linhas de código dá muito espaço para alguém alterar acidentalmente o valor sem perceber a dependência. Tê-lo na próxima linha faz com que fique claro por que a variável é inicializada da forma que é e onde é usada.
-
Divida os métodos em submétodos sempre que possível.
- É mais fácil para alguém olhar para um quadro geral e depois detalhar os detalhes interessantes do que começar com os detalhes e reconstruir o quadro geral a partir deles. Da mesma forma, é mais fácil entender um método simples, que chama uma sequência de vários submétodos bem nomeados, do que entender um método equivalente que simplesmente contém todo o código nesses submétodos.
-
Em declarações de função ou sites de chamada de função, não adicione um espaço entre o nome da função e os parênteses que precedem a lista de argumentos.
-
Avisos do compilador de endereço.
- Mensagens de aviso do compilador significam que algo está errado. Corrija o que foi indicado pelo aviso do compilador. Se você não conseguir resolver o problema, use "#pragma" para suprimir o aviso, mas isso só deve ser feito como último recurso.
-
Deixe uma linha em branco no final do arquivo.
- Todos os arquivos ".cpp" e ".h" devem incluir uma linha em branco, para coordenada com gcc.
-
O código de depuração deve ser útil e polido ou não deve ser registrado.
- Um código de depuração misturado com outro código dificulta a leitura de outro código.
-
Sempre use a macro "Text()" em torno dos literais das strings.
- Sem a macro "Text()", o código que constrói "FStrings" a partir do literal causará um processo de conversão de strings indesejado.
-
Evite repetir a mesma operação redundantemente em loop.
- Mova subexpressões comuns para fora do loop para evitar cálculos redundantes. Faça uso de estática em alguns casos, para evitar operações globalmente redundantes em chamadas de funções, como construir um "FName" a partir de uma string literal.
-
Cuide com a recarga dinâmica.
- Minimize a dependência para reduzir o tempo de iteração. Não use expansão em linha ou modelos para funções que têm chance de mudar durante um recarregamento. Use estática apenas para coisas que devem permanecer constantes durante um recarregamento.
-
Use variáveis intermediárias para simplificar expressões complicadas.
-
Se você tiver uma expressão complicada, ela poderá ser mais fácil de entender se a dividir em subexpressões, que são atribuídas a variáveis intermediárias, com nomes descrevendo o significado da subexpressão dentro da expressão-pai. Por exemplo:
if ((Blah->BlahP->WindowExists->Etc && Stuff) && !(bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday()))) { DoSomething(); }
Deve ser substituído por:
-
const bool bIsLegalWindow = Blah->BlahP->WindowExists->Etc && Stuff;
const bool bIsPlayerDead = bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday();
if (bIsLegalWindow && !bIsPlayerDead)
{
DoSomething();
}
-
Os ponteiros e referências devem ter apenas um espaço à direita do ponteiro ou referência.
-
Isso facilita o uso rápido de localizar nos arquivos para todo ponteiro ou referência a um determinado tipo. Por exemplo:
// Use isso FShaderType* Ptr // Não use estes: FShaderType *Ptr FShaderType * Ptr
-
-
Variáveis sombreadas não são permitidas.
-
O C++ permite que variável seja sombreada a partir de um escopo externo, mas isso torna o uso ambíguo para um leitor. Por exemplo, há três variáveis "Count" úteis nesta função de membro:
class FSomeClass { Público: void Func(const int32 Count) { for (int32 Count = 0; Count != 10; ++Count) { // Usar contagem } } private: int32 Count; }
-
-
Evite usar literais anônimos em chamadas de função.
-
Prefira as constantes nomeadas que descrevem seu significado. Isso torna a intenção mais óbvia para um leitor ocasional, pois evita a necessidade de procurar a declaração da função para entendê-la.
// Estilo antigo Trigger(TEXT("Soldier"), 5, true);. // Novo estilo const FName ObjectName = TEXT("Soldier"); const float CooldownInSeconds = 5; const bool bVulnerableDuringCooldown = true; Trigger(ObjectName, CooldownInSeconds, bVulnerableDuringCooldown);
-
-
Evite definir variáveis estáticas não triviais em cabeçalhos.
-
Variáveis estáticas não triviais fazem com que uma instância seja compilada em cada unidade de translação que inclui esse cabeçalho:
// SomeModule.h static const FString GUsefulNamedString = TEXT("String"); // *Substitua acima por:* // SomeModule.h extern SOMEMODULE_API const FString GUsefulNamedString; // SomeModule.cpp const FString GUsefulNamedString = TEXT("String");
-
-
Evite fazer alterações abrangentes que não alteram o comportamento do código (por exemplo: alterar um espaço em branco ou renomear em massa uma variável privada), pois causam ruído desnecessário no histórico de origem e interrompem a mesclagem.
-
Se essa alteração for importante, por exemplo, para corrigir o recuo causado por uma ferramenta de mesclagem automatizada, ela deve ser enviada por conta própria e não deve ser combinada com alterações de comportamento.
-
Prefira corrigir espaços em branco ou outras violações comuns de programação somente quando outras edições estiverem sendo feitas nas mesmas linhas ou em código próximo.
-
Diretrizes de design de API
-
Parâmetros de função booleanos devem ser evitados.
-
Em particular, parâmetros booleanos devem ser evitados para sinalizadores passados para funções. Eles têm o mesmo problema de literal anônimo mencionado anteriormente, mas também tendem a se multiplicar ao longo do tempo à medida que a API é estendida com mais comportamentos. Em vez disso, prefira uma enumeração (consulte o aviso sobre o uso de enumerações como sinalizadores na seção Enumerações fortemente tipadas):
// Estilo antigo FCup* MakeCupOfTea(FTea* Tea, bool bAddSugar = false, bool bAddMilk = false, bool bAddHoney = false, bool bAddLemon = false); FCup* Cup = MakeCupOfTea(Tea, false, true, true); // Novo estilo enum class ETeaFlags { None, Leite = 0x01, Açúcar = 0x02, Honey = 0x04, Limão = 0x08 }; ENUM_CLASS_FLAGS(ETeaFlags) FCup* MakeCupOfTea(FTea* Tea, ETeaFlags Flags = ETeaFlags::None); FCup* Cup = MakeCupOfTea(Tea, ETeaFlags::Milk | ETeaFlags::Honey);
-
-
Essa forma impede a transposição acidental de sinalizadores, evita a conversão acidental de argumentos de inteiros e ponteiros, remove a necessidade de repetir padrões redundantes e é mais eficiente.
-
É aceitável usar "bools" como argumento quando eles forem o estado concluído a ser passado para uma função como um definidor, como "void FWidget::SetEnabled(bool bEnabled)". Mas considere refatorar se isso mudar.
-
Evite listas de parâmetros de função muito longas.
-
Se uma função usar muitos parâmetros, considere passar uma estrutura dedicada:
// Estilo antigo TUniquePtr<FCup[]> MakeTeaForParty(const FTeaFlags* TeaPreferences, uint32 NumCupsToMake, FKettle* Kettle, ETeaType TeaType = ETeaType::EnglishBreakfast, float BrewingTimeInSeconds = 120.0f); // Novo estilo struct FTeaPartyParams { const FTeaFlags* TeaPreferences = nullptr; uint32 NumCupsToMake = 0; FKettle* Kettle = nullptr; ETeaType TeaType = ETeaType::EnglishBreakfast; float BrewingTimeInSeconds = 120.0f; }; TUniquePtr<FCup[]> MakeTeaForParty(const FTeaPartyParams& Params);
-
-
Evite sobrecarregar as funções com "bool" e "FString".
-
Isso pode ter um comportamento inesperado:
void Func(const FString& String); void Func(bool bBool); Func(TEXT("String")); // Chama a sobrecarga de booleanos!
-
-
As classes da interface sempre devem ser abstratas.
- As classes de interface tem o prefixo "I" e não devem ter variáveis de membro. As interfaces têm permissão para conter métodos que não são virtuais puros e podem conter métodos que não são virtuais ou estáticos, desde que sejam implementados em linha.
-
Use as palavras-chave "virtual' e "override" ao declarar um método de substituição.
Ao declarar uma função virtual em uma classe derivada que substitui uma função virtual na classe-pai, você deve usar as palavras-chave "virtual" e "substituir". Por exemplo:
class A
{
Público:
virtual void F() {}
};
class B : public A
{
Público:
virtual void F() override;
}
Há muitos códigos existentes que ainda não seguem isso, devido à adição recente da palavra-chave "override". A palavra-chave "override" deve ser adicionada a esse código quando conveniente.
-
UObjects devem ser passados por ponteiro, não por referência. Se nulo não for esperado por uma função, isso deverá ser documentado pela API ou tratado apropriadamente. Por exemplo:
// Ruim void AddActorToList(AActor& Obj); // Bom void AddActorToList(AActor* Obj);
Código específico da plataforma
O código específico da plataforma deve sempre ser abstraído e implementado nos arquivos de origem específicos dela, em subdiretórios nomeados apropriadamente, por exemplo:
Engine/Platforms/[PLATFORM]/Source/Runtime/Core/Private/[PLATFORM]PlatformMemory.cpp
Em geral, você deve evitar adicionar usos de "PLATFORM_[PLATFORM]". Por exemplo, evite adicionar "PLATFORM_XBOXONE" ao código fora de um diretório chamado "[PLATFORM]". Em vez disso, estenda a camada de abstração de hardware para adicionar uma função estática, por exemplo em FPlatformMisc:
FORCEINLINE static int32 GetMaxPathLength()
{
return 128;
}
As plataformas podem então substituir essa função, retornando um valor constante específico da plataforma ou até mesmo usando a API da plataforma para determinar o resultado. Se você forçar a função em linha, ela terá as mesmas características de desempenho que usando um definidor.
Nos casos em que um padrão é absolutamente necessário, crie novas diretrizes "#define" que descrevem propriedades específicas que podem ser aplicadas a uma plataforma, por exemplo, "PLATFORM_USE_PTHREADS". Defina o valor padrão em "Platform.h" e substitua para qualquer plataforma que exija isso no arquivo "Platform.h" específico.
Por exemplo, em "Platform.h" nós temos:
#ifndef PLATFORM_USE_PTHREADS
#define PLATFORM_USE_PTHREADS 1
#endif
"WindowsPlatform.h" tem:
#define PLATFORM_USE_PTHREADS 0
O código multiplataforma pode usar o padrão diretamente sem precisar conhecer a plataforma.
#if PLATFORM_USE_PTHREADS
#include "HAL/PThreadRunnableThread.h"
#endif
Centralizamos os detalhes específicos da plataforma da engine, o que permite que os detalhes sejam contidos totalmente no arquivo de origem específico da plataforma. Fazer isso facilita a manutenção da engine em várias plataformas. Além disso, você pode portar o código para plataformas novas sem a necessidade de percorrer a base do código em busca de definições específicas da plataforma.
Manter o código da plataforma em pastas específicas para ela também é um requisito para plataformas NDA, como PlayStation, Xbox e Nintendo Switch.
É importante garantir que o código compile e seja executado independentemente de o subdiretório "[PLATFORM]" estar presente. Em outras palavras, o código multiplataforma nunca deve depender do código específico da plataforma.