En Epic Games, tenemos unas cuantas normas y convenciones de programación sencillas. Este documento refleja el estado de las normas de programación actuales de Epic Games, las cuales es obligatorio seguir.
Las convenciones de código son importantes para los programadores por varios motivos:
-
El 80 % del coste de la duración de un contenido de software se destina a mantenimiento.
-
Durante toda su vida útil, casi ningún software recibe mantenimiento de su autor original.
-
Las convenciones de código mejoran la legibilidad del software, lo que permite a los ingenieros comprender los nuevos códigos de forma rápida y exhaustiva.
-
Si decidimos exponer el código fuente a la comunidad de desarrolladores, es conveniente que se entienda fácilmente.
-
Muchas de estas convenciones son necesarias para la compatibilidad entre compiladores.
Las normas de programación que se indican a continuación están basadas en C++; sin embargo, las normas deben seguirse igualmente independientemente del lenguaje que se utilice. Una sección puede incluir reglas o excepciones equivalentes para lenguajes específicos cuando proceda.
Organización de las clases
Las clases deben organizarse pensando en quien las lee y no en quien las escribe. Como la mayoría de los lectores utilizarán la interfaz pública de la clase, la implementación pública debe declararse primero, seguida de la implementación privada de la clase.
UCLASS()
class EXAMPLEPROJECT_API AExampleActor : public AActor
{
GENERATED_BODY()
public:
// Establece el valor predeterminado para la propiedad de este actor.
AExampleActor();
protected:
// Se llama cuando se inicia el juego o cuando el actor aparece.
virtual void BeginPlay() override;
};
Aviso de copyright
Cualquier archivo fuente (.h, .cpp o .xaml) proporcionado por Epic Games para su distribución pública debe contener un aviso de copyright en la primera línea del archivo. El formato de la notificación debe ser exactamente igual al que se muestra a continuación:
// Copyright Epic Games, Inc. Todos los derechos reservados.
Si falta esta línea o no tiene el formato correcto, CIS generará un error y fallará.
Convenciones de nomenclatura
Cuando se utilicen convenciones de nomenclatura, todos los códigos y comentarios deben utilizar la ortografía y la gramática del inglés estadounidense.
- La primera letra de cada palabra de un nombre (como el nombre de tipo o de variable) va en mayúscula. Normalmente no hay guion bajo entre las palabras. Por ejemplo,
HealthyUPrmitiveComponentson correctos, perolastMouseCoordinatesodelta_coordinatesno lo son.
Este es el formato PascalCase para un usuario que puede estar familiarizado con otros lenguajes de programación orientados a objetos.
-
Los nombres de tipos van precedidos de una letra mayúscula adicional para distinguirlos de los nombres de variables. Por ejemplo,
FSkines un nombre de tipo, ySkines una instancia del tipoFSkin. -
Las clases de plantilla van precedidas de T.
template <typename ObjectType> clase TAttribute -
Las clases que heredan de UObject van precedidas de U.
class UActorComponent -
Las clases que heredan de AActor van precedidas de A.
class AActor -
Las clases que heredan de SWidget van precedidas de S.
clase SCompoundWidget -
Las clases que son interfaces abstractas van precedidas de I.
clase IAnalyticsProvider -
Los tipos de estructuras conceptuales de Epic van precedidas de C.
struct CStaticClassProvider { template <typename T> auto Requires(UClass*& ClassRef) -> decltype( ClassRef = T::StaticClass() ); }; -
Las enumeraciones van precedidas de E.
enum clase EColorBits { ECB_Red, ECB_Green, ECB_Blue }; -
Las variables booleanas deben ir precedidas de b.
bPendingDestruction bHasFadedIn -
La mayoría del resto de clases van precedidas de F, aunque algunos subsistemas utilizan otras letras.
-
Las definiciones de tipo deben ir precedidas de lo que sea apropiado para ese tipo, como:
-
F para la definición de tipo de una estructura.
-
U para definición de tipo de un
UObject.
-
-
Una definición de tipo de una creación de una instancia concreta de plantilla deja de ser una plantilla y debe ir precedida del prefijo correspondiente.
typedef TArray<FMytype> FArrayOfMyTypes; -
Los prefijos se omiten en C#.
-
UnrealHeaderTool requiere el prefijo correcto en la mayoría de los casos, por lo que es importante incluirlo.
-
Los parámetros de plantilla de tipo y los alias de tipo anidados basados en ellos no están sujetos a las reglas de prefijos anteriores, ya que se desconoce la categoría del tipo.
-
Se recomienda un sufijo
Typedespués de un término descriptivo. -
Se debe eliminar la ambigüedad de los parámetros de plantilla utilizando un prefijo
In:template <typename InElementType> clase TContainer { public: using ElementType = InElementType; }; -
Los nombres de los tipos y las variables son sustantivos.
-
Los nombres de los métodos son verbos que describen el efecto del método o el valor de retorno de un método sin efecto.
-
Los nombres de las macros deben estar completamente en mayúsculas, con las palabras separadas por guiones bajos, y con el prefijo
UE_.#define UE_AUDIT_SPRITER_IMPORT
Los nombres de las variables, los métodos y las clases deben ser:
-
Claros
-
Sin ambigüedades
-
Descriptivo
Cuanto más amplio sea el ámbito del nombre, mayor será la importancia de un buen nombre descriptivo. Evita abreviar en exceso.
Todas las variables deben declararse en su propia línea para que puedas hacer comentarios sobre el significado de cada variable.
El estilo JavaDocs lo requiere.
Puedes utilizar un comentario de varias líneas o de una sola antes de una variable. Las líneas en blanco son opcionales para agrupar variables.
Todas las funciones que devuelven un valor booleano deben hacer una pregunta de verdadero/falso, como IsVisible() o ShouldClearBuffer().
Un procedimiento (una función sin valor de retorno) debe utilizar un verbo fuerte seguido de un objeto. Una excepción es si el objeto del método es el objeto en el que está. En este caso, el objeto se entiende a partir del contexto. Entre los nombres que se deben evitar, se incluyen los que empiezan por «gestionar» y «procesar», porque se trata de verbos ambiguos.
Te animamos a añadir el prefijo «Out» a los nombres de parámetros de función si:
-
Los parámetros de función se pasan como referencia.
-
Se espera que la función escriba en ese valor.
Esto hace que sea obvio que el valor transferido en este argumento se sustituye por la función.
Si un parámetro de entrada o de salida también es un booleano, pon una «b» antes del prefijo de entrada/salida, por ejemplo, bOutResult.
Las funciones que devuelven un valor deben describir el valor de retorno. El nombre debe dejar claro qué valor devuelve la función. Esto es especialmente importante para la función booleana. Considera los siguientes dos métodos de ejemplo:
// ¿Qué significa `true`?
bool CheckTea(FTea Tea);
// El nombre deja claro que `true` significa que el té está recién hecho.
bool IsTeaFresh(FTea Tea);
float TeaWeight;
int32 TeaCount;
bool bDoesTeaStink;
FName TeaName;
FString TeaFriendlyName;
UClass* TeaClass;
USoundCue* TeaSound;
UTexture* TeaTexture;
Elección inclusiva de palabras
Cuando trabajes en el código base de Unreal Engine, intenta esforzarte por utilizar un lenguaje respetuoso, inclusivo y profesional.
La elección de palabras se aplica a:
-
Nombres de clases
-
Funciones
-
estructuras de datos.
-
Tipos
-
Variables
-
Archivos y carpetas
-
Complementos
Se aplica cuando escribes un fragmento de texto orientado al usuario para la IU, mensajes de error y notificaciones. También se aplica al escribir sobre un código, como en las descripciones de los comentarios y de las listas de cambios.
La siguiente sección ofrece orientación y sugerencias que te ayudarán a elegir palabras y nombres respetuosos y apropiados para todas las situaciones y públicos, y a ser un comunicador más eficaz.
Inclusión racial, étnica y religiosa
-
No utilices metáforas ni símiles que refuercen los estereotipos. Por ejemplo, el contraste de blanco y negro o términos como listanegra y listablanca.
-
No utilices palabras que hagan referencia a un trauma histórico o a experiencias vividas de discriminación. Por ejemplo, esclavo o maestro.
Inclusión de género
-
Puedes referirte a las personas hipotéticas de forma general en plural y en singular.
-
Puedes referirte a todo lo que no sea una persona de forma impersonal. Por ejemplo, un módulo, un complemento, una función, un cliente, un servidor o cualquier otro componente de software o hardware.
-
No asignes género a nada que no lo tenga.
-
No utilices términos colectivos como todos que asumen el género.
-
Evita frases coloquiales que contengan géneros arbitrarios, como «no pasa nada, hombre».
Argot
-
Recuerda que tus palabras las está leyendo un público global que puede no compartir las mismas expresiones idiomáticas y actitudes, y que puede que no comprenda las mismas referencias culturales.
-
Evita la jerga y los coloquialismos, aunque pienses que son divertidos o inofensivos. Pueden ser difíciles de entender para las personas cuya primera lengua no sea la tuya, y pueden no entenderse bien.
-
No utilices lenguaje ofensivo.
Palabras sobrecargadas
- Muchos términos que utilizamos por sus significados técnicos también tienen otros significados fuera del ámbito de la tecnología. Por ejemplo abortar, ejecutar o nativo. Cuando utilices palabras como estas, hazlo siempre con precisión y analiza el contexto en el que aparecen.
Lista de palabras
La siguiente lista identifica terminología que creemos que debería sustituirse por alternativas mejores en Unreal Engine:
| Nombre de palabra | Nombre alternativo de palabra |
|---|---|
| Lista negra | _lista de denegados_, _lista de bloqueados_, _lista de excluidos_, _lista de evitados_, _lista sin aprobar_, _lista prohibida_, _lista de permitidos_ |
| Lista blanca | lista de permitidos, lista de incluidos, lista de confianza, lista segura, lista de preferidos, lista aprobada, lista con permiso |
| Maestro | primario, origen, controlador, plantilla, referencia, principal, líder, original, base |
| Esclavo | secundario, réplica, agente, seguidor, trabajador, nodo de clúster, bloqueado, vinculado, sincronizado |
Estamos trabajando activamente para adaptar nuestro código con los principios establecidos anteriormente.
Código de C++ portátil
Los tipos int e int sin signo varían en tamaño dependiendo de la plataforma. Se garantiza que tienen al menos 32 bits de ancho y son aceptables en código cuando el ancho de entero no es importante. Los tipos de tamaño explícito se utilizan en formatos serializados o replicados.
A continuación tienes una lista de tipos más comunes:
-
boolpara el valor booleano (nunca asumas el tamaño de bool).BOOLno se compilará. -
TCHARpara un carácter (nunca asumas el tamaño de TCHAR). -
uint8para bytes sin signo (1 byte). -
int8para bytes con signo (1 byte). -
uint16para abreviaturas sin signo (2 bytes). -
int16para abreviaturas con signo (2 bytes). -
uint32para ints sin signo (4 bytes). -
int32para ints con signo (4 bytes). -
uint64para palabras cuad sin signo (8 bytes). -
int64para palabras cuad con signo (8 bytes). -
floatpara coma flotante de precisión simple (4 bytes). -
doublepara coma flotante de precisión doble (8 bytes). -
PTRINTpara un entero que puede contener un puntero (nunca asumas el tamaño de PTRINT).
Uso de las bibliotecas estándar
Históricamente, UE ha evitado el uso directo de bibliotecas estándar C y C++ por los siguientes motivos:
-
Reemplazar las implementaciones lentas por las nuestras, que proporcionan una asignación adicional de control sobre memoria.
-
Añadir nuevas funciones antes de que estén ampliamente disponibles, como:
-
Hacer cambios de comportamiento deseables, pero no estándar.
-
Tener una sintaxis coherente en todo el código base.
-
Evitar construcciones incompatibles con las expresiones de UE.
-
Sin embargo, la biblioteca estándar ha mejorado mucho e incluye una función que no nos interesa envolver en una capa de abstracción o reimplementar nosotros mismos.
Cuando tengas que elegir entre una funcionalidad de la biblioteca estándar en lugar de la nuestra, debes preferir la opción que ofrezca un resultado superior. También es importante recordar que se valora la coherencia. Si una implementación heredada de UE ya no sirve, podemos optar por dejarla en desuso y migrar todo su uso a la biblioteca estándar.
Evita mezclar expresiones de UE con expresiones de biblioteca estándar en la misma API. La siguiente tabla enumera las expresiones más comunes junto con recomendaciones sobre cuándo utilizarlas.
| Expresión | Descripción |
|---|---|
<atomic> |
La expresión atomic debe utilizarse en el código nuevo y el antiguo migrado si se modifica. Se espera implementar atomicsde forma completa y eficiente en todas las plataformas compatibles. Nuestro propio TAtomic solo está parcialmente implementado, y no nos interesa mantenerlo y mejorarlo. |
<type_traits> |
Debe utilizarse la expresión type_traits cuando haya solapamiento entre un rasgo heredado de UE y un rasgo estándar. Los rasgos suelen implementarse como elementos intrínsecos del compilador para garantizar su corrección, y el compilador puede tener conocimiento de los rasgos estándar y seleccionar una ruta de compilación más rápida en lugar de tratarlos como C++ simple. Un problema es que nuestros rasgos suelen tener un valor estático Value o un tipo de definición Type en mayúsculas, mientras que se espera que los rasgos estándar utilicen value y type. Se trata de una distinción importante, ya que los rasgos de composición, por ejemplo, std::conjunction, esperan una sintaxis determinada. Los nuevos rasgos que añadamos deben escribirse con value o type en minúsculas para facilitar la composición. Los rasgos existentes deben actualizarse para admitir ambos casos. |
<initializer_list> |
La expresión initializer_list debe utilizarse para admitir la sintaxis reforzada de inicializadores. Este es un caso en el que el lenguaje y la biblioteca estándar se solapan. No hay alternativa si quieres compatibilidad. |
<regex> |
La expresión regex puede utilizarse directamente, pero su uso debe encapsularse dentro del código del editor. No tenemos planes de implementar nuestra propia solución de expresiones regulares. |
<limits> |
std::numeric_limits puede utilizarse en su totalidad. |
<cmath> |
Se pueden utilizar todas la funciones de coma flotante de este encabezado. |
<cstring>: memcpy() y memset() |
Estas expresiones pueden utilizarse en lugar de FMemory::Memcpy y FMemory::Memset respectivamente, cuando tengan una ventaja demostrable en el rendimiento. |
Deben evitarse contenedores y cadenas estándar, excepto en el código de interoperabilidad.
Comentarios
Los comentarios son comunicación y la comunicación es vital. En las siguientes secciones se detallan aspectos importantes que debes tener en cuenta acerca de los comentarios (contenido extraído de La práctica de la programación, de Kernighan y Pike).
Directrices
-
Escribe código autodocumentado. Por ejemplo:
// Incorrecto: t = s + l - b; // Correcto: TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves; -
Escribe comentarios útiles. Por ejemplo:
// Incorrecto: // incrementa las hojas ++Leaves; // Correcto: // sabemos que hay otra hoja de té ++Leaves; -
No comentes mucho si el código es incorrecto: vuelve a escribirlo. Por ejemplo:
// Incorrecto: // el número total de hojas es la suma de // hojas pequeñas y grandes menos las // número de hojas que son ambas. t = s + l - b; // Correcto: TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves; -
No contradigas el código. Por ejemplo:
// Incorrecto: // ¡Nunca incrementes las hojas! ++Hojas; // Correcto: // sabemos que hay otra hoja de té ++Leaves;
Cómo usar la palabra clave const correctamente
Const es tanto una documentación como una directiva del compilador. Todo el código debe esforzarse por ser correcto en const. Esto incluye las siguientes directrices:
-
Pasa argumentos de funciones con punteros o referencias
constsi esos argumentos no van a ser modificados por la función. -
Marca métodos como
constsi no modifican el objeto. -
Utiliza iteraciones
consten contenedores si la intención del bucle no es modificar el contenedor.
Ejemplo de const:
void SomeMutatingOperation(FThing& OutResult, const TArray<Int32>& InArray)
{
// InArray no se modificará aquí, pero OutResult probablemente sí.
}
void FThing::SomeNonMutatingOperation() const
{
// Este código no modificará el FThing al que invoca.
}
TArray<FString> StringArray;
for (const FString& : StringArray)
{
// El cuerpo de este bucle no modificará StringArray
}
const también se prefiere para configuraciones locales y parámetros de funciones por valor. Esto indica al lector que la variable no se modificará en el cuerpo de la función, lo que facilita su comprensión. Si haces esto, asegúrate de que la declaración y la definición coincidan, ya que esto puede afectar al proceso de JavaDoc.
void AddSomeThings(const int32 Count);
void AddSomeThings(const int32 Count)
{
const int32 CountPlusOne = Count + 1;
// Ni `Count` ni `CountPlusOne` pueden modificarse durante el cuerpo de la función.
}
Una excepción a esto son los parámetros pasados por valor, que se mueven a un contenedor. Para obtener más información, consulta la sección «Semántica de move» en esta misma página.
Ejemplo:
void FBlah::SetMemberArray(TArray<FString> InNewArray)
{
MemberArray = MoveTemp(InNewArray);
}
Pon la palabra clave const al final cuando hagas que un puntero sea constante (en lugar de hacia lo que apunta). De todos modos, las referencias no se pueden «reasignar» y, por tanto, no se pueden hacer constantes del mismo modo.
Ejemplo:
// Puntero `const` a objeto no constante. El puntero no se puede reasignar, pero `T` se puede modificar.
T* const Ptr = ...;
// No permitido
T& const Ref = ...;
Nunca utilices const en un tipo de retorno. Esto inhibe la semántica de move para tipos complejos y dará avisos de compilación para tipos integrados. Esta regla solo se aplica al propio tipo de retorno, no al tipo de objetivo de un puntero ni referencias devueltas.
Ejemplo:
// Incorrecto: devuelve una matriz `const`.
const TArray<FString> GetSomeArray();
// Correcto: devuelve una referencia a una matriz `const`.
const TArray<FString>& GetSomeArray();
// Correcto: devuelve un puntero a una matriz `const`.
const TArray<FString>* GetSomeArray();
// Incorrecto: devuelve un puntero `const` a una matriz `const`.
const TArray<FString>* const GetSomeArray();
Ejemplo de formato
Utilizamos un sistema basado en JavaDoc para extraer comentarios del código y compilar la documentación automáticamente, por lo que recomendamos reglas específicas de formato para los comentarios.
El siguiente ejemplo muestra el formato de comentarios para las clases, los métodos y las variables. Recuerda que los comentarios deben complementar al código. El código documenta la implementación, mientras que los comentarios documentan la intención. Asegúrate de actualizar los comentarios cuando cambies la intención de un fragmento de código.
Observa que son compatibles dos estilos de comentarios de parámetros diferentes, mostrados por los métodos Steep y Sweeten. El estilo @param utilizado por Steep es el estilo tradicional de varias líneas. En el caso de las funciones simples, puede resultar más claro integrar la documentación sobre parámetros y valores de retorno en el comentario descriptivo de la función. Esto se demuestra en el ejemplo Sweeten. Las etiquetas de comentarios especiales como @see o @return solo deben utilizarse para comenzar nuevas líneas a continuación de la descripción principal.
Los comentarios de los métodos solo deben incluirse una vez: cuando el método se declara públicamente. Los comentarios de los métodos solo deben contener información relevante para el autor de la llamada del método, incluida cualquier información sobre las anulaciones del método que pueda ser relevante para el autor de la llamada. Los detalles sobre la implementación del método y sus anulaciones que no sean relevantes para el autor de la llamada deben ser comentados dentro de la implementación del método.
Los comentarios de las clases deben incluir:
-
Una descripción del problema que resuelve la clase.
-
El motivo por el que se creó la clase.
Los comentarios de varias líneas de los métodos deben incluir:
-
Finalidad de la función: documenta el problema que resuelve esta función. Como hemos dicho anteriormente, los comentarios documentan la intención y el código documenta la implementación.
-
Comentarios de los parámetros: cada comentario de parámetro debe incluir:
-
unidades de medida;
-
el rango de valores esperados;
-
valores «imposibles»
-
y el significado de los códigos de estado/error.
-
-
Comentarios de retorno: documenta el valor de retorno esperado, igual que se documenta una variable de salida. Para evitar la redundancia, no se debe utilizar un comentario
@returnexplícito si el único propósito de la función es devolver este valor y eso ya está documentado en la finalidad de la función. -
Información adicional:
@warning,@note,@seey@deprecatedpueden utilizarse opcionalmente para documentar información adicional relevante. Cada uno debe declararse en su propia línea a continuación del resto de los comentarios.
Sintaxis del lenguaje C++ moderno
Unreal Engine está diseñado para llevarse de forma masiva a muchos compiladores de C++, por lo que tenemos cuidado de utilizar funciones que sean compatibles con el compilador que estemos admitiendo. A veces, las funciones son tan útiles que las envolvemos en macros y las utilizamos de forma generalizada. Sin embargo, solemos esperar a que todos los compiladores que utilizamos estén actualizados.
Unreal Engine compila con una versión del lenguaje C++20 de forma predeterminada y requiere una versión mínima de C++20 para hacerlo. Utilizamos muchas características del lenguaje moderno que son compatibles con todos los compiladores modernos. En algunos casos, envolvemos el uso de estas funciones en condicionales de preprocesador. Sin embargo, a veces decidimos evitar ciertas características del lenguaje por completo, por portabilidad u otros motivos.
A menos que se especifique más adelante, como característica del compilador de C++ moderno que admitimos, no debes utilizar características específicas del lenguaje del compilador a menos que vayan envueltas en macros o condicionales de preprocesador y se utilicen con moderación.
Static Assert
La palabra clave static_assert es válida cuando se necesita una aserción de tiempo de compilación.
Override y Final
Las palabras clave override y final son válidas y se recomienda encarecidamente su uso. Es posible que se haya omitido en muchas partes, pero se solucionará con el tiempo.
Nullptr
Debes utilizar nullptr en lugar de la macro de estilo C NULL en todos los casos.
Una excepción es el uso de nullptr en compilaciones C++/CX (por ejemplo, para Xbox One). En este caso, el uso de nullptr es en realidad el tipo de referencia nula gestionada. Es mayoritariamente compatible con nullptr de C++ nativo, excepto en su tipo y en algunos contextos de creación de instancias de plantilla, por lo que debes utilizar la macro type en lugar del más habitual decltype(nullptr) por motivos de compatibilidad.
Auto
No debes utilizar auto en código C++, excepto en las pocas excepciones que se indican a continuación. Sé siempre explícito sobre el tipo que vas a inicializar. Esto significa que el tipo debe ser claramente visible para el lector. Esta regla también se aplica al uso de la palabra clave var en C#.
La funcionalidad de vinculación estructurada de C++20 tampoco debe utilizarse, ya que se trata en realidad de una variante de auto.
Uso aceptable de auto:
-
Cuando necesites vincular una lambda a una variable, ya que los tipos lambda no se pueden expresar en código.
-
Para las variables de iterador, pero solo donde el tipo del iterador sea detallado y pueda dificultar la legibilidad.
-
En código de plantillas, donde el tipo de una expresión no se pueda discernir fácilmente. Este es un caso avanzado.
Es muy importante que los tipos sean claramente visibles para cualquier persona que lea el código. Aunque algunos IDE pueden inferir el tipo, hacerlo depende de que el código tenga un estado compilable. Tampoco ayudará a los usuarios de herramientas de combinación/comparación, ni al visualizar archivos fuente individuales de forma aislada, como en GitHub.
Si sabes con certeza que estás utilizando auto de forma aceptable, recuerda siempre usar correctamente const, & o *, tal y como lo harías con el nombre del tipo. Con auto, esto forzará que el tipo inferido sea lo que tú quieres.
For basado en rango
Esto es preferible para que el código sea más fácil de entender y mantener. Cuando migres un código que utilice los antiguos iteradores TMap, ten en cuenta que las antiguas funciones Key() y Value(), que eran un método del tipo de iterador, ahora son simplemente campos Key y Value del TPair clave-valor subyacente.
Ejemplo:
TMap<FString, int32> MyMap;
// Estilo antiguo
for (auto It = MyMap.CreateIterator(); It; ++It)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), It.Key(), *It.Value());
}
// Nuevo estilo
for (TPair<FString, int32>& Kvp : MyMap)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), *Kvp.Key, Kvp.Value);
}
También tenemos reemplazos de rango para algunos tipos de iteradores independientes.
Ejemplo:
// Estilo antiguo
for (TFieldIterator<UProperty> PropertyIt(InStruct, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++PropertyIt)
{
UProperty* Property = *PropertyIt;
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
}
// Nuevo estilo
for (UProperty* Property : TFieldRange<UProperty>(InStruct, EFieldIteratorFlags::IncludeSuper))
{
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
}
Lambdas y funciones anónimas
Las lambdas pueden utilizarse libremente, pero conllevan problemas de seguridad adicionales. Las mejores expresiones lambda no deben tener más de un par declaraciones de longitud, sobre todo cuando se utilizan como parte de una expresión o declaración mayor, por ejemplo como predicado en un algoritmo genérico.
Ejemplo:
// Encuentra la primera cosa cuyo nombre contenga la palabra "Hola"
Thing* HelloThing = ArrayOfThings.FindByPredicate([](const Thing& Th){ return Th.GetName().Contains(TEXT("Hola")); });
// Ordena la matriz por nombre en orden inverso.
Algo::Sort(ArrayOfThings, [](const Thing& Lhs, const Thing& Rhs){ return Lhs.GetName() > Rhs.GetName(); });|
Ten en cuenta que las expresiones lambda con estado no se pueden asignar a punteros de funciones (que solemos utilizar mucho). Las expresiones lambda no triviales deben documentarse del mismo modo que las funciones normales. Las lambdas también pueden utilizarse como delegados para ejecución diferida mediante funciones como BindWeakLambda, donde se capturaba la función de las variables como una carga.
Capturas y tipos de retorno
Se deben utilizar capturas explícitas en lugar de la captura automática ([&] y [=]). Esto es importante por razones de legibilidad, facilidad de mantenimiento, seguridad y rendimiento, sobre todo cuando se utiliza con lambdas grandes y ejecución diferida.
Las capturas explícitas declaran la intención del autor; por lo tanto, los errores se detectan durante la revisión del código. Las capturas incorrectas pueden provocar errores graves y fallos, que es más probable que se vuelvan problemáticos a medida que el código se mantiene con el paso del tiempo. Aquí tienes algunos aspectos adicionales que debes tener en cuenta sobre las capturas lambda:
-
La captura por referencia y por valor de los punteros (incluido el puntero
this) puede provocar daños en los datos y fallos si se aplaza la ejecución de la lambda. Las variables locales y miembro nunca deben capturarse por referencia para lambdas diferidas. -
La captura por valor puede suponer un problema de rendimiento si hace copias innecesarias para una lambda no diferida.
-
Los punteros UObject capturados accidentalmente son invisibles para el recolector de basura. La captura automática atrapa
thisimplícitamente si se hace referencia a alguna de las variables miembro, aunque[=]da la impresión de que la lambda tiene sus propias copias de todo. -
Los envoltorios delegados, como
CreateWeakLambdayCreateSPLambda, deben utilizarse para la ejecución diferida, ya que se desvincularán automáticamente si se liberan UObject o el puntero compartido. Otros objetos compartidos pueden capturarse como TWeakObjectPtr o TWeakPtr y luego validarse dentro de la lambda. -
Cualquier uso de lambda diferida que no siga estas directrices debe tener un comentario que explique por qué la captura de lambda es segura.
Los tipos de retorno explícitos deben utilizarse para lambdas grandes o cuando devuelvas el resultado de otra llamada a función. Deben considerarse del mismo modo que la palabra clave auto.
Enumeraciones de tipado fuerte
Las clases enumeradas (Enum) sustituyen a las enumeraciones con espacio de nombres antiguas, tanto para las enumeraciones normales como para las UENUM. Por ejemplo:
// Enumeración antigua
UENUM()
namespace EThing
{
enum Type
{
Thing1,
Thing2
};
}
// Enumeración nueva
UENUM()
enum class EThing : uint8
{
Thing1,
Thing2
}
Las enumeraciones son compatibles como UPROPERTY y sustituyen a la antigua solución TEnumAsByte<>. Las propiedades Enum también pueden ser de cualquier tamaño, no solo de bytes:
// Propiedad antigua
UPROPERTY()
TEnumAsByte<EThing::Type> MyProperty;
// Propiedad nueva
UPROPERTY()
EThing MyProperty;
Las enumeraciones expuestas a blueprints deben seguir basándose en uint8.
Las clases Enum utilizadas como indicador pueden aprovechar la macro ENUM_CLASS_FLAGS(EnumType) para definir automáticamente todos los operadores bit a bit:
enum clase EFlags
{
Ninguno = 0x00,
Flag1 = 0x01,
Flag2 = 0x02,
Flag3 = 0x04
};
ENUM_CLASS_FLAGS(EFlags)
La única excepción a esto es el uso de indicador en un contexto fiable (es una limitación del lenguaje). En su lugar, todo indicador de enumeración debe tener un enumerador llamado None que se establece en 0 para las comparaciones:
// Antiguo
if (Flags & EFlags::Flag1)
// Nuevo
if ((Flags & EFlags::Flag1) != EFlags::None)
Semántica de move
Todos los tipos principales de contenedores (TArray, TMap, TSet y FString) tienen un constructor move y operadores de asignación move. Suelen utilizarse automáticamente al pasar o devolver estos tipos por valor. También se pueden invocar explícitamente utilizando MoveTemp, el equivalente en UE de std::move.
La devolución de contenedores o cadenas por valor puede ser beneficiosa para la expresividad, sin el coste habitual de las copias temporales. Las reglas sobre el paso por valor y el uso de MoveTemp aún se están definiendo, pero ya pueden encontrarse en algunas partes optimizadas del código base.
Inicializadores predeterminados de miembros
Los inicializadores de miembros predeterminados pueden utilizarse para definir los valores predeterminados de una clase dentro de la propia clase:
UCLASS()
class UTeaOptions : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
int32 MaximumNumberOfCupsPerDay = 10;
UPROPERTY()
float CupWidth = 11.5f;
UPROPERTY()
FString TeaType = TEXT("Earl Grey");
UPROPERTY()
EDrinkingStyle DrinkingStyle = EDrinkingStyle::PinkyExtended;
};
El código escrito así tiene las siguientes ventajas:
-
No es necesario duplicar los inicializadores en varios constructores.
-
No es posible mezclar el orden de inicialización y el orden de declaración.
-
El tipo de miembro, los indicadores de propiedad y los valores predeterminados se encuentran en el mismo sitio. Esto ayuda a la legibilidad y la facilidad de mantenimiento.
Sin embargo, también hay algunas desventajas:
-
Cualquier cambio en los valores predeterminados requiere una reconstrucción de todo el archivo dependiente.
-
Los encabezados no pueden cambiar en la publicación de parches del motor, por lo que este estilo puede limitar los tipos de correcciones posibles.
-
Algunas cosas no se pueden inicializar de esta forma, como las clases base, los subobjetos
UObject, el puntero para tipos de declaración directa, los valores deducidos de argumentos de constructores y miembros inicializados en múltiples pasos. -
Poner algunos inicializadores en el encabezado y el resto en los constructores del archivo .cpp. puede reducir la legibilidad y la facilidad de mantenimiento.
Usa tu criterio a la hora de decidir si vas a utilizar los inicializadores predeterminados de miembros. Como regla general, los inicializadores predeterminados de miembros tienen más sentido en el código del juego que en el código del motor. Considera utilizar archivos config como valores predeterminados.
Código de terceros
Siempre que modifiques el código de una biblioteca que utilizamos en el motor, asegúrate de etiquetar los cambios con un comentario //@UE5, e incluye una explicación de por qué has realizado el cambio. Esto facilita la fusión de los cambios en una nueva versión de esa biblioteca, y garantiza que los titulares de la licencia puedan encontrar fácilmente cualquier modificación que hayamos hecho.
Cualquier código de terceros incluido en el motor debe marcarse con comentarios con formato que permita buscarlos fácilmente. Por ejemplo:
// @código de terceros - BEGIN PhysX
#include <physx.h>
// @código de terceros - END PhysX
// @código de terceros - BEGIN MSDN SetThreadName
// [http://msdn.microsoft.com/es-es/library/xcb2z8hs.aspx]
// Sirve para establecer el nombre del subproceso en el depurador.
…
// @código de terceros - END MSDN SetThreadName
Formato del código
Llaves
Usar llaves es casi una guerra perdida. Epic Games tiene desde hace mucho tiempo la costumbre de incluir llaves en las líneas nuevas. Respeta este uso, independientemente del tamaño de la función o del bloque. Por ejemplo:
// Incorrecto
int32 GetSize() const { return Size; }
// Bueno
int32 GetSize() const
{
return Size;
}
Incluye siempre llaves en bloques de una sola declaración. Por ejemplo:
if (bThing)
{
return;
}
If - Else
Cada bloque de ejecución de una declaración if-else debe estar entre llaves para así evitar errores de edición. Si no se utilizan llaves, alguien podría añadir sin querer otra línea a un bloque if. La línea adicional no se controlaría con la expresión if, y esto no sería correcto. Tampoco es correcto que la compilación condicional de elementos haga que se rompan las declaraciones if/else. Así que utiliza siempre llaves.
if (bHaveUnrealLicense)
{
InsertYourGameHere();
}
else
{
CallMarkRein();
}
Una declaración if multidireccional debe tener una sangría con cada else if a la misma altura que el primer if; esto hace que la estructura sea clara para el lector:
if (TannicAcid < 10)
{
UE_LOG(LogCategory, Log, TEXT("Acidez baja"));
}
else if (TannicAcid < 100)
{
UE_LOG(LogCategory, Log, TEXT("Acidez media"));
}
else
{
UE_LOG(LogCategory, Log, TEXT("Acidez alta"));
}
Tabuladores y sangría
A continuación se indican algunas normas para aplicar sangría al código.
-
Aplica sangría al código por bloque de ejecución.
-
Utiliza tabuladores para los espacios en blanco al principio de una línea, no espacios. Establece el tamaño del tabulador en 4 caracteres. Ten en cuenta que los espacios a veces son necesarios y están permitidos para mantener el código alineado independientemente del número de espacios en un tabulador. Por ejemplo, cuando alineas un código que sigue a un carácter que no es un tabulador.
-
Si escribes el código en C++, utiliza también tabuladores, no espacios. El motivo es que los programadores suelen cambiar entre C# y C++, y la mayoría prefiere utilizar una configuración coherente para los tabuladores. Visual Studio utiliza de forma predeterminada espacios para archivos de C#, por lo que debes acordarte de cambiar esta configuración cuando trabajes en el código de Unreal Engine.
Declaraciones switch
Excepto en los casos vacíos (varios casos con un código idéntico), la declaración switch debe etiquetar explícitamente que un caso pasa al siguiente. Incluye un comando break o un comentario de «fallo» en cada caso. Otros comandos de transferencia de control de código (return, continue, etc.) también sirven.
Ten siempre un caso predeterminado. Incluye un comando break por si alguien añade un nuevo caso después del predeterminado.
switch (condition)
{
caso 1:
…
// pasa
caso 2:
…
break;
caso 3:
…
return;
caso 4:
caso 5:
…
break;
default:
break;
}
Espacios de nombres
Puedes utilizar espacios de nombres para organizar tus clases, funciones y variables cuando proceda. Si los utilizas, sigue las reglas que aparecen a continuación.
-
Actualmente, la mayor parte del código de UE no está envuelto en un espacio de nombres global.
- Ten cuidado de evitar colisiones en el ámbito global, especialmente cuando utilices o incluyas un código de terceros.
-
Los espacios de nombres no son compatibles con UnrealHeaderTool.
- Los espacios de nombres no deben utilizarse al definir
UCLASS,USTRUCT, etc.
- Los espacios de nombres no deben utilizarse al definir
-
Las nuevas API que no sean
UCLASS,USTRUCT, etc., deben colocarse en un espacio de nombresUE::, idóneamente en un espacio de nombres anidado, p. ej.UE::Audio::.- Los espacios de nombres que se utilizan para contener detalles de implementación que no forman parte de la API pública deben ir en un espacio de nombres
Private, p. ej.UE::Audio::Private::.
- Los espacios de nombres que se utilizan para contener detalles de implementación que no forman parte de la API pública deben ir en un espacio de nombres
-
Declaraciones
using:- No pongas la declaración
usingen el ámbito global, ni siquiera en un archivo.cpp(causaría problemas con nuestro sistema de compilación «unity»).
- No pongas la declaración
-
Puedes poner declaraciones
usingdentro de otro espacio de nombres o dentro de un cuerpo de función. -
Si colocas declaraciones
usingdentro de un espacio de nombres, se aplicarán a otras apariciones de ese espacio de nombres en la misma unidad de traslación. Mientras seas coherente, todo funcionará bien. -
Solo puedes usar declaraciones
usingen archivos de encabezado de forma segura si sigues las reglas anteriores. -
Los tipos directos declarados deben declararse dentro de sus respectivos espacios de nombres.
- Si no lo haces, obtendrás errores de enlaces.
-
Si declaras muchas clases o tipos dentro de un espacio de nombres, puede ser difícil utilizar esos tipos en otras clases de ámbito global (por ejemplo, la firma de la función necesitará utilizar un espacio de nombres explícito cuando aparezca en declaraciones de clase).
-
Puedes utilizar declaraciones
usingsolo para añadir a tu ámbito una variable específica dentro de un espacio de nombres.- Por ejemplo,
Foo::FBar. Sin embargo, no solemos hacer eso en el código de UE.
- Por ejemplo,
-
Las macros no pueden estar en un espacio de nombres.
- Deben tener el prefijo
UE_en lugar de estar en un espacio de nombres, por ejemploUE_LOG.
- Deben tener el prefijo
Dependencias físicas
-
Los nombres de los archivos no deben llevar prefijo siempre que sea posible.
- Por ejemplo,
Scene.cppen lugar deUScene.cpp. Esto facilita el uso de herramientas como Workspace Whiz o Abrir archivo en la solución, de Visual Assist, al reducir el número de letras necesarias para identificar el archivo que quieres.
- Por ejemplo,
-
Todos los encabezados deben proteger frente a inclusiones múltiples con la directiva
#pragma once.-
Ten en cuenta que todos los compiladores que utilizamos admiten
#pragma once.#pragma once //<file contents>
-
-
Intenta minimizar el acoplamiento físico.
- En concreto, evita incluir encabezados de biblioteca estándar desde otros encabezados.
-
Es preferible reenviar declaraciones a incluir encabezados.
-
Cuando incluyas un encabezado, hazlo con el máximo detalle posible.
- Por ejemplo, no incluyas
Core.h. En su lugar, debes incluir los encabezados específicos en Core de los que necesitas definiciones.
- Por ejemplo, no incluyas
-
Intenta incluir directamente todos los encabezados que necesites para facilitar la inclusión detallada.
-
No confíes en un encabezado que esté incluido indirectamente en otro encabezado que incluyas.
-
No confíes en que nada se incluya a través de otro encabezado. Incluye todo lo que necesites.
-
Los módulos tienen directorios fuente Private y Public.
- Cualquier definición que necesiten otros módulos debe estar en los encabezados del directorio Public. Todo lo demás debe estar en el directorio Private. En módulos antiguos de UE, estos directorios pueden seguir llamándose «Src» e «Inc», pero están pensados para separar el código privado y el público del mismo modo, y no para separar archivos de encabezado de archivos fuente.
-
No te preocupes por configurar los encabezados para la generación de encabezados precompilados.
- UnrealBuildTool puede hacerlo mejor que tú.
-
Divide funciones grandes en subfunciones lógicas.
- Un área de optimización del compilador es la eliminación de las subexpresiones comunes. Cuanto mayor sean tus funciones, más trabajo tendrá que hacer el compilador para identificarlas. Esto provoca tiempos de compilación muy altos.
-
No utilices un gran número de funciones insertadas.
- La potencia de las funciones insertadas se aplica incluso a archivos que no las utilizan. Las funciones insertadas solo deben usarse para descriptores de acceso triviales y cuando el perfilado muestre que hacerlo es beneficioso.
-
Sé conservador en el uso de
FORCEINLINE.- Todos los códigos y variables locales se expandirán en la función que llama. Esto provocará los mismos problemas de tiempo de compilación que los causados por funciones grandes.
Encapsulado
Aplica el encapsulado con las palabras clave de protección. Los miembros de las clases casi siempre deben declararse privados a menos que formen parte de la interfaz pública/protegida de la clase. Usa tu criterio, pero ten siempre en cuenta que la falta de descriptores de acceso dificulta la refactorización posterior sin romper los complementos y los proyectos existentes.
Si un campo concreto solo puede ser utilizado por clases derivadas, hazlo privado y proporciona descriptores de acceso protegidos.
Utiliza la versión final si tu clase no está diseñada para ser derivada.
Problemas generales de estilo
-
Minimizar la distancia de dependencia.
- Cuando el código dependa del valor determinado de una variable, intenta establecer el valor de esa variable justo antes de usarla. Inicializar una variable en la parte superior de un bloque de ejecución, y no utilizarla durante cien líneas de código, deja mucho espacio para que alguien cambie accidentalmente el valor sin darse cuenta de la dependencia. Al tenerla en la siguiente línea, deja claro por qué la variable se inicializa de esta forma y dónde se utiliza.
-
Divide el método en submétodos siempre que sea posible.
- Para una persona es más fácil observar una visión global y luego profundizar en los detalles interesantes, antes que empezar por los detalles y reconstruir la visión global a partir de ellos. Del mismo modo, es más fácil entender un método simple, que llama a una secuencia de varios submétodos bien nombrados, que entender un método equivalente que simplemente contiene todo el código de esos submétodos.
-
En sitios con declaraciones o llamadas a función, no añadas un espacio entre el nombre de la función y los paréntesis que preceden a la lista de argumentos.
-
Aborda los avisos del compilador.
- Los mensajes de aviso del compilador indican que algo va mal. Corrige los avisos del compilador. Si no puedes solucionarlos, utiliza
#pragmapara suprimir la advertencia, pero esto solo debe hacerse como último recurso.
- Los mensajes de aviso del compilador indican que algo va mal. Corrige los avisos del compilador. Si no puedes solucionarlos, utiliza
-
Deja una línea en blanco al final del archivo.
- Todos los archivos
.cppy.hdeben incluir una línea en blanco para coordinarse con gcc.
- Todos los archivos
-
El código de depuración debe ser útil y estar perfeccionado, o no estar registrado.
- Un código de depuración mezclado con otro código hace que este sea más difícil de leer.
-
Utiliza siempre la macro
TEXT()alrededor de literales de cadena.- Sin la macro
TEXT(), el código que construyeFStringsa partir de literales provocará un proceso de conversión de cadena no deseado.
- Sin la macro
-
Evita repetir la misma operación de forma redundante en bucle.
- Saca las subexpresiones comunes fuera del bucle para evitar cálculos redundantes. Utiliza la estática en algunos casos para evitar operaciones con redundancia global en las llamadas a la función, como construir un
FNamea partir de un literal de cadena.
- Saca las subexpresiones comunes fuera del bucle para evitar cálculos redundantes. Utiliza la estática en algunos casos para evitar operaciones con redundancia global en las llamadas a la función, como construir un
-
Ten en cuenta la recarga dinámica.
- Minimiza la dependencia para reducir el tiempo de iteración. No utilices estructuras insertadas ni plantillas para funciones, ya que es probable que cambien con una recarga. Solo utiliza la estática para cosas que se espera que permanezcan constantes tras una recarga.
-
Utiliza variables intermedias para simplificar expresiones complicadas.
-
Si tienes una expresión complicada, puede ser más fácil de entender si la divides en subexpresiones (que se asignan a las variables intermedias) con nombres que describen el significado de la subexpresión dentro de la expresión padre. Por ejemplo:
if ((Blah->BlahP->WindowExists->Etc && Stuff) && !(bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday()))) { DoSomething(); }
Debería reemplazarse por:
-
const bool bIsLegalWindow = Blah->BlahP->WindowExists->Etc && Stuff;
const bool bIsPlayerDead = bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday();
if (bIsLegalWindow && !bIsPlayerDead)
{
DoSomething();
}
-
Los punteros y las referencias solo deben tener un espacio a la derecha del puntero o la referencia.
-
Esto facilita el uso rápido de Buscar en archivos para todos los punteros o referencias a un tipo determinado. Por ejemplo:
// Utiliza esto: FShaderType* Ptr // No utilices estos: FShaderType *Ptr FShaderType * Ptr
-
-
Las variables sombreadas no están permitidas.
-
C++ permite sombrear las variables desde un ámbito exterior, pero esto hace que su uso sea ambiguo para el lector. Por ejemplo, hay tres variables
Countque se pueden utilizar en esta función miembro:clase FSomeClass { public: void Func(const int32 Count) { for (int32 Count = 0; Count != 10; ++Count) { // Utiliza `Count` } } private: int32 Count; }
-
-
Evita utilizar el literal anónimo en las llamadas a función.
-
Da preferencia a las constantes con nombre que describen su significado. Esto hace que la intención sea más obvia para un lector casual, ya que evita la necesidad de buscar la declaración de la función para entenderla.
// Estilo antiguo Trigger(TEXT("Soldado"), 5, true);. // Nuevo estilo const FName ObjectName = TEXT("Soldado"); const float CooldownInSeconds = 5; const bool bVulnerableDuringCooldown = true; Trigger(ObjectName, CooldownInSeconds, bVulnerableDuringCooldown);
-
-
Evita definir variables estáticas no triviales en los encabezados.
-
Una variable estática no trivial hace que se compile una instancia en cada unidad de traslación que incluya ese encabezado:
// SomeModule.h static const FString GUsefulNamedString = TEXT("Cadena"); // *Reemplaza lo anterior por:* // SomeModule.h extern SOMEMODULE_API const FString GUsefulNamedString; // SomeModule.cpp const FString GUsefulNamedString = TEXT("Cadena");
-
-
Evita hacer grandes cambios que no modifiquen el comportamiento del código (por ejemplo: cambiar los espacios en blanco o renombrar en bloque variables privadas), ya que provocan un ruido innecesario en el historial y entorpecen la fusión.
-
Si un cambio de este tipo es importante, por ejemplo, corregir la sangría incorrecta causada por una herramienta de fusión automatizada, debe enviarse solo y no mezclarse con cambios de comportamiento.
-
Es preferible corregir los espacios en blanco u otras infracciones menores de las normas de programación solo cuando se realicen otras ediciones en las mismas líneas o en un código cercano.
-
Directrices de diseño de API
-
Deben evitarse parámetros booleanos de función.
-
En concreto, debe evitarse el uso de parámetros booleanos para indicadores pasados a funciones. Tienen el mismo problema del literal anónimo mencionado anteriormente, pero también tienden a multiplicarse con el tiempo a medida que las API se amplían con más comportamiento. Es preferible usar un enum (consulta los consejos sobre el uso de enums como indicadores en la sección Enumeradores de tipado fuerte):
// Estilo antiguo FCup* MakeCupOfTea(FTea* Tea, bool bAddSugar = false, bool bAddMilk = false, bool bAddHoney = false, bool bAddLemon = false); FCup* Cup = MakeCupOfTea(Tea, false, true, true); // Nuevo estilo enum clase ETeaFlags { None, Milk = 0x01, Sugar = 0x02, Honey = 0x04, Lemon = 0x08 }; ENUM_CLASS_FLAGS(ETeaFlags) FCup* MakeCupOfTea(FTea* Tea, ETeaFlags Flags = ETeaFlags::None); FCup* Cup = MakeCupOfTea(Tea, ETeaFlags::Milk | ETeaFlags::Honey);
-
-
Esto evita la transposición accidental de indicadores, así como la conversión accidental de punteros y argumentos enteros, elimina la necesidad de repetir valores predeterminados redundantes y es más eficiente.
-
Es aceptable utilizar
boolscomo argumentos cuando son el estado completado que se va a pasar a una función como unsetter, por ejemplo,void FWidget::SetEnabled(bool bEnabled). Aunque debes plantearte refactorizar si esto cambia. -
Evita las listas de parámetros de funciones demasiado largas.
-
Si una función toma muchos parámetros, considera pasarle una estructura dedicada:
// Estilo antiguo TUniquePtr<FCup[]> MakeTeaForParty(const FTeaFlags* TeaPreferences, uint32 NumCupsToMake, FKettle* Kettle, ETeaType TeaType = ETeaType::EnglishBreakfast, float BrewingTimeInSeconds = 120.0f); // Nuevo 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);
-
-
Evita sobrecargar las funciones mediante
boolyFString.-
Esto puede tener un comportamiento inesperado:
void Func(const FString& String); void Func(bool bBool); Func(TEXT("Cadena")); // Llama a la sobrecarga de bool.
-
-
Las clases de interfaz deben ser siempre abstractas.
- Las clases de interfaz llevan el prefijo «I» y no deben tener variables miembro. Las interfaces pueden contener métodos que no sean exclusivamente virtuales, y pueden contener métodos que no sean virtuales o estáticos, siempre que se implementen en línea.
-
Utiliza las palabras clave
virtualyoverridecuando declares un método de anulación.
Al declarar una función virtual en una clase derivada que sobrescribe una función virtual en la clase padre, debes utilizar las palabras clave virtual y override. Por ejemplo:
clase A
{
public:
virtual void F() {}
};
class B : public A
{
public:
virtual void F() override;
}
Hay mucho código existente que todavía no sigue esta norma debido a la reciente incorporación de la palabra clave override. La palabra clave override debe añadirse a ese código cuando sea conveniente.
-
Los UObjects deben pasarse con puntero, no con referencias. Si una función no espera valores nulos, la API debe documentarlo o tratarlo adecuadamente. Por ejemplo:
// Incorrecto void AddActorToList(AActor& Obj); // Bueno void AddActorToList(AActor* Obj);
Código específico de la plataforma
El código específico de la plataforma siempre debe abstraerse e implementarse en el archivo fuente específico de la plataforma en subdirectorios con el nombre adecuado, por ejemplo:
Engine/Platforms/[PLATFORM]/Source/Runtime/Core/Private/[PLATFORM]PlatformMemory.cpp
En general, se debe evitar añadir cualquier uso de PLATFORM_[PLATFORM]. Por ejemplo, evita añadir PLATFORM_XBOXONE a código fuera de un directorio denominado [PLATFORM]. En su lugar, amplía la capa de abstracción del hardware para añadir una función estática, por ejemplo en FPlatformMisc:
FORCEINLINE static int32 GetMaxPathLength()
{
return 128;
}
Las plataformas pueden anular esta función, devolviendo un valor constante específico de la plataforma o incluso utilizando la API de plataforma para determinar el resultado. Si fuerzas la inserción de la función, tiene las mismas características de rendimiento que si utilizas una directiva define.
En los casos en los que una definición sea absolutamente necesaria, crea nuevas directivas #define que describan una propiedad concreta que se pueda aplicar a una plataforma, por ejemplo PLATFORM_USE_PTHREADS. Establece el valor predeterminado en Platform.h y anula para cualquier plataforma que lo requiera en el archivo Platform.h específico de la plataforma.
Por ejemplo, en Platform.h tenemos:
#ifndef PLATFORM_USE_PTHREADS
#define PLATFORM_USE_PTHREADS 1
#endif
WindowsPlatform.h tiene:
#define PLATFORM_USE_PTHREADS 0
El código multiplataforma puede utilizar entonces la definición directamente sin necesidad de conocer la plataforma.
#if PLATFORM_USE_PTHREADS
#include "HAL/PThreadRunnableThread.h"
#endif
Centralizamos los detalles específicos de la plataforma del motor, lo que permite que los detalles queden completamente contenidos en el archivo fuente específico de la plataforma. Esto facilita el mantenimiento del motor en varias plataformas y, además, permite portar el código a nuevas plataformas sin necesidad de buscar en el código base las definiciones específicas de la plataforma.
Mantener el código de plataforma en carpetas específicas de la plataforma también es un requisito para plataformas NDA como PlayStation, Xbox y Nintendo Switch.
Es importante asegurarse de que el código se compile y ejecute independientemente de si el subdirectorio [PLATFORM] está presente. Dicho de otro modo, el código multiplataforma nunca debe depender del código específico de la plataforma.