Extender UObject en Unreal Engine 4

Hola a todos! Mi nombre es Alexander, he estado trabajando con Unreal Engine durante más de 5 años, y casi todo este tiempo, con proyectos de red.

Dado que los proyectos de red difieren en sus requisitos de desarrollo y rendimiento, a menudo es necesario trabajar con objetos más simples, como las clases UObject, pero su funcionalidad se trunca inicialmente, lo que puede crear un marco sólido. En este artículo, hablaré sobre cómo activar varias funciones en la clase base UObject en Unreal Engine 4.



De hecho, escribí el artículo más como referencia. La mayoría de la información es extremadamente difícil de encontrar en la documentación o en la comunidad, y aquí puede abrir rápidamente el enlace y copiar el código deseado. ¡Decidí al mismo tiempo compartir contigo! El artículo está dirigido a aquellos que ya están un poco familiarizados con UE4. Se considerará el código C ++, aunque no es necesario saberlo. Simplemente puede seguir las instrucciones si necesita algo de qué hablar. Además, no es necesario copiar todo, puede pegar el código de la sección con las propiedades necesarias y debería funcionar.



Un poco sobre UObject


UObject es la clase base para casi todo lo que está en Unreal Engine 4. La gran mayoría de los objetos que se crean en su mundo o solo en la memoria se heredan de él: objetos en el escenario (AActor), componentes (UActorComponent), diferentes tipos para trabajar con datos y otros.

La clase en sí misma, aunque es más fácil que las derivadas, es al mismo tiempo bastante funcional. Por ejemplo, contiene muchos eventos útiles, como cambiar los valores de las variables en el editor y las funciones básicas de la red, que no están activas de forma predeterminada.

Los objetos creados por esta clase no pueden estar en el escenario y existen exclusivamente en la memoria. No se pueden agregar como componentes a los actores, aunque puede ser un tipo de componente si implementa usted mismo la funcionalidad necesaria.

¿Por qué necesito UObject si AActor ya admite todo lo que necesito? En general, hay muchos ejemplos de uso. Lo más fácil son los artículos de inventario. En el escenario, en algún lugar del cielo, almacenarlos no es práctico, por lo que puede almacenarlos en la memoria sin cargar el renderizado y sin crear propiedades innecesarias. Para aquellos a quienes les gustan las comparaciones técnicas, AActor toma un kilobyte (1016 bytes) y un UObject vacío tiene solo 56 bytes.



¿Qué es un problema de UObject?


No hay problemas en general, bueno, o simplemente no los encontré. Todo lo que molesta a UObject es la falta de varias funciones que están disponibles por defecto en AActor o en componentes. Estos son los problemas que he identificado para mi práctica:

  • Los UObjects no se replican a través de la red;
  • debido al primer punto, no podemos activar eventos RPC;
  • No puede utilizar un amplio conjunto de funciones que requieren un enlace al mundo en Blueprints;
  • no tienen eventos estándar como BeginPlay y Tick;
  • no puede agregar componentes de UObjects a AActor en Blueprints.

La mayoría de las cosas se pueden resolver fácilmente. Pero algunos tendrán que jugar.



Creando UObject


Antes de expandir nuestra clase con características, necesitamos crearla. Usemos el editor para que el generador escriba automáticamente todo lo que se necesita para trabajar en el encabezado (.h).

Podemos crear una nueva clase en el editor de Content Browser haciendo clic en el botón Nuevo y seleccionando Nueva clase C ++ .



A continuación, debemos elegir la clase en sí. Es posible que no esté en la lista general, por lo tanto, ábralo y seleccione UObject.



Asigne un nombre a su clase y seleccione en qué carpeta se almacenará. Cuando creamos la clase, puede ir al estudio, encontrarla allí y comenzar a incorporar todas las funciones necesarias.

Principiantes, tenga en cuenta que se crean dos archivos: .h y .ccp. En .h, declarará variables y funciones, y en .cpp definirá su lógica. Encuentra ambos archivos en tu proyecto. Si no cambió la ruta, entonces deberían estar en Proyecto / Fuente / Proyecto /.

Hasta que continuemos, escribamos el parámetro Blueprintable en la macro UCLASS () sobre la declaración de clase. Deberías obtener algo como esto:

.h

UCLASS(Blueprintable) class MYPROPJECT_API UMyObject : public UObject { GENERATED_BODY() } 

Gracias a esto, puede crear Blueprints que heredarán todo lo que hacemos con este objeto.



Replicación de UObject


Por defecto, los UObjects no se replican a través de la red. Como describí anteriormente, se crean una serie de restricciones cuando necesita sincronizar datos o lógica entre las partes, pero no almacena basura en el mundo.

En Unreal Engine 4, la replicación tiene lugar precisamente debido a los objetos del mundo. Significa que simplemente crear un objeto en la memoria y replicarlo fallará. En cualquier caso, necesitará un propietario que gestione la transferencia de datos de objetos entre el servidor y los clientes. Por ejemplo, si su objeto es la habilidad de un personaje, entonces el personaje en sí debería convertirse en el propietario. También será un conductor para transmitir información a través de la red.

Prepare nuestro objeto para la replicación. Hasta ahora en el encabezado necesitamos establecer solo una función:

.h

 UCLASS(Blueprintable) class MYPROPJECT_API UMyObject : public UObject { GENERATED_BODY() public: virtual bool IsSupportedForNetworking () const override { return true; }; } 

IsSupportedForNetworking () determinará que el objeto es compatible con la red y se puede replicar.

Sin embargo, no todo es tan simple. Como escribí anteriormente, necesita un propietario que controle la transferencia del objeto. Para la pureza del experimento, cree un AActor que lo replicará. Esto se puede hacer exactamente de la misma manera que UObject, solo la clase padre, naturalmente, AActor.

Principiantes, si necesita replicar un objeto en un personaje, controlador u otro lugar, cree la clase base adecuada a través del editor, agregue la lógica necesaria y ya herede de esta clase en Blueprints.

Dentro necesitamos 3 funciones: un constructor, una función para replicar subobjetos, una función que determina lo que se replica dentro de este AActor (variables, referencias de objetos, etc.) y el lugar donde creamos nuestro objeto.

No olvides crear una variable por la cual nuestro objeto será almacenado:

.h

 class MYPROPJECT_API AMyActor : public AActor { GENERATED_BODY() public: AMyActor(); virtual bool ReplicateSubobjects (class UActorChannel *Channel, class FOutBunch *Bunch, FReplicationFlags *RepFlags) override; void GetLifetimeReplicatedProps (TArray<FLifetimeProperty>& OutLifetimeProps) const override; virtual void BeginPlay (); UPROPERTY(Replicated, BlueprintReadOnly, Category="Object") class UMyObject* MyObject; } 

Dentro del archivo fuente tenemos que escribir todo:

.cpp

 //  #include "MyActor.h" #include "Net/UnrealNetwork.h" #include "Engine/World.h" #include "Engine/ActorChannel.h" #include "   UObject/MyObject.h" AMyActor::AMyActor() { //  Actor  . bReplicates = true // . NetCullDistanceSquared = 99999; //  (  ). NetUpdateFrequency = 1.f; } void AMyActor::GetLifetimeReplicatedProps (TArray<FLifetimeProperty>& OutLifetimeProps) { Super::GetLifetimeReplicatedProps(OutLifetimeProps); //       .           . DOREPLIFETIME(AMyActor, MyObject); } bool AMyActor::ReplicateSubobjects(UActorChannel * Channel, FOutBunch * Bunch, FReplicationFlags * RepFlags) { bool WroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags); //   . if (MyObject ) WroteSomething |= Channel->ReplicateSubobject(MyObject , *Bunch, *RepFlags); return WroteSomething; } AMyActor::BeginPlay() { /*       (  )  .    this.        . ,       ,     . */ if(HasAuthority()) { MyObject = NewObject<UMyObject>(this); //       if(MyObject) UE_LOG(LogTemp, Log, TEXT("%s created"), *MyObject->GetName()); } } 

Ahora su objeto se replicará con este actor. Puede mostrar su nombre en la marca, pero ya en el cliente. Tenga en cuenta que en Begin Play es poco probable que un objeto llegue antes que el cliente, por lo que no tiene sentido escribir un registro en él.



Replicación de variables en UObject


En la mayoría de los casos, no tiene sentido replicar un objeto si no contiene información que también se sincronizará entre el servidor y los clientes. Como nuestro objeto ya está replicado, pasar variables no es difícil. Esto se hace de la misma manera que dentro de nuestro Actor:

.h

 UCLASS(Blueprintable) class MYPROPJECT_API UMyObject : public UObject { GENERATED_BODY() public: virtual bool IsSupportedForNetworking () const override { return true; }; void GetLifetimeReplicatedProps (TArray<FLifetimeProperty>& OutLifetimeProps) const override; UPROPERTY(Replicated, BlueprintReadWrite, Category="Object") int MyInteger; //   } 

.cpp

 //  #include "MyObject.h" #include "Net/UnrealNetwork.h" UMyObject ::UMyObject () { //  Object  .     . bReplicates = true //       ,     . } void UMyObject ::GetLifetimeReplicatedProps (TArray<FLifetimeProperty>& OutLifetimeProps) { Super::GetLifetimeReplicatedProps(OutLifetimeProps); //   Integer  . DOREPLIFETIME(UMyObject, MyInteger); } } 

Al agregar una variable y marcarla para su replicación, podemos replicarla. Todo es simple y lo mismo que en AActor.

Sin embargo, hay una pequeña trampa que no es visible de inmediato, pero puede ser engañosa. Esto será especialmente notable si está creando su UObject no para trabajar en C ++, sino para prepararlo para la herencia y el trabajo en Blueprints.

La conclusión es que las variables creadas en el heredero de Blueprints no se replicarán. El motor no los marca automáticamente y cambiar un parámetro en el servidor en el BP no cambia nada en el valor del cliente. Pero hay una cura para esto. Para la replicación correcta de las variables de BP, debe marcarlas de antemano. Agregue un par de líneas a GetLifetimeReplicatedProps ():

.cpp

 void UMyObject ::GetLifetimeReplicatedProps (TArray<FLifetimeProperty>& OutLifetimeProps) { Super::GetLifetimeReplicatedProps(OutLifetimeProps); //   Integer  . DOREPLIFETIME(UMyObject, MyInteger); //       UBlueprintGeneratedClass* BPClass = Cast<UBlueprintGeneratedClass>(GetClass()); if (BPClass) BPClass->GetLifetimeBlueprintReplicationList(OutLifetimeProps); } 

Las variables en las clases secundarias de Blueprint ahora se replicarán como se esperaba.



Eventos RPC en UObject


Los eventos RPC (Llamada a procedimiento remoto) son funciones especiales que se llaman al otro lado de la interacción de red de un proyecto. Utilizándolos, puede llamar a la función desde el servidor en otros clientes y desde el cliente en el servidor. Muy útil y de uso frecuente al escribir proyectos de red.

Si no está familiarizado con ellos, le recomiendo leer un artículo. Describe el uso en C ++ y en Blueprints .

Si bien no hay problemas en Actor o en los componentes con su llamada, en UObject los eventos se disparan en el mismo lado donde fueron llamados, lo que hace que sea imposible hacer una llamada remota cuando sea necesario.

Mirando el código del componente (UActorComponent), podemos encontrar varias funciones que le permiten transferir llamadas a través de la red. Como UActorComponent se hereda de UObject, simplemente podemos copiar las secciones de código necesarias y pegarlas en nuestro objeto para que funcione como debería:

.h

 //   #include "Engine/EngineTypes.h" UCLASS(Blueprintable) class MYPROPJECT_API UMyObject : public UObject { GENERATED_BODY() public: virtual bool CallRemoteFunction (UFunction * Function, void * Parms, struct FOutParmRec * OutParms, FFrame * Stack) override; virtual int32 GetFunctionCallspace (UFunction* Function, void* Parameters, FFrame* Stack) override; //   } 

.cpp

 //   #include "Engine/NetDriver.h" //       . bool UMyObject::CallRemoteFunction(UFunction * Function, void * Parms, FOutParmRec * OutParms, FFrame * Stack) { if (!GetOuter()) return false; UNetDriver* NetDriver = GetOuter()->GetNetDriver(); if (!NetDriver) return false; NetDriver->ProcessRemoteFunction(GetOuter(), Function, Parms, OutParms, Stack, this); return true; } int32 UMyObject::GetFunctionCallspace(UFunction * Function, void * Parameters, FFrame * Stack) { return (GetOuter() ? GetOuter()->GetFunctionCallspace(Function, Parameters, Stack) : FunctionCallspace::Local); } 

Con estas funciones, podremos activar eventos RPC no solo en código, sino también en planos.

Tenga en cuenta que para activar eventos de Cliente o Servidor, necesita un propietario cuyo Propietario sea nuestro jugador. Por ejemplo, el objeto es propiedad del personaje del usuario o del objeto en el que el Propietario es el Controlador del jugador.



Características globales en planos


Si alguna vez ha creado un Blueprint de objetos, es posible que haya notado que no puede llamar a funciones globales (estáticas, pero por razones de claridad lo llamamos) que están disponibles en otras clases, por ejemplo, GetGamemode (). Parece que simplemente no puede hacer clases en las clases Object, por lo que debe pasar todos los enlaces al crear o pervertir de alguna manera, y a veces la elección recae por completo en la clase Actor que se crea en el escenario y Soporta todo.

Pero en C ++, por supuesto, no hay tales problemas. Sin embargo, el diseñador del juego, que juega con la configuración y agrega diferentes cosas pequeñas, no puede decir que necesita abrir Visual Studio, encontrar la clase adecuada y obtener el modo de juego en la función doSomething () cambiando los puntos en él. Por lo tanto, es imperativo que el diseñador pueda iniciar sesión en Bluprint y que con dos clics haga su trabajo. Ahorre tanto su tiempo como el suyo. Sin embargo, los planos fueron inventados para esto.

La conclusión es que cuando busca o llama a funciones en el menú contextual de Bluprint, esas mismas funciones globales que requieren una referencia al mundo intentan llamar a una función dentro de su objeto que se refiere a él. Y si el editor ve que no hay una función, entiende que no puede usarla y no la muestra en la lista.



Sin embargo, hay una cura para esto. Incluso dos.

Consideremos primero una opción para un uso más conveniente en el editor. Tendremos que redefinir una función que devuelva un enlace al mundo y luego el editor comprenderá que en el juego en sí puede funcionar:

.h

 UCLASS(Blueprintable) class MYPROPJECT_API UMyObject : public UObject { GENERATED_BODY() //  GetWorld()    . virtual UWorld* GetWorld() const override; //   } 

.cpp

 UWorld* UMyObject::GetWorld() const { //       ,    . if (GIsEditor && !GIsPlayInEditorWorld) return nullptr; else if (GetOuter()) return GetOuter()->GetWorld(); else return nullptr; } 

Ahora está definido y el editor comprenderá que, en general, el objeto puede obtener el puntero deseado (aunque no es válido) y usar funciones globales en el BP.

Tenga en cuenta que el propietario (GetOuter ()) también debe tener acceso al mundo. Podría ser otro UObject con un objeto GetWorld (), componente u actor específico en la escena.



Hay otra manera Es suficiente agregar una etiqueta a la macro UCLASS () al declarar la clase que el parámetro WorldContextObject se agregará a las funciones estáticas en el BP, en el cual cualquier objeto que sirve como conductor del "mundo" y las funciones globales del motor se alimentan. Esta opción es adecuada para aquellos que en el proyecto pueden tener varios mundos al mismo tiempo (por ejemplo, el mundo del juego y el mundo para el espectador):

.h

 //   WorldContext      UCLASS(Blueprintable, meta=(ShowWorldContextPin)) class MYPROPJECT_API UMyObject : public UObject { GENERATED_BODY() //   } 

Si ingresa GetGamemode en la búsqueda en el BP, aparecerá en la lista, al igual que otras funciones similares, y el parámetro será WorldContextObject, en el que debe pasar un enlace a Actor.



Por cierto, puede presentar el propietario de nuestra propiedad allí. Recomiendo crear una función en Actor, siempre será útil para el objeto:

.h

 UCLASS(Blueprintable, meta=(ShowWorldContextPin)) class MYPROPJECT_API UMyObject : public UObject { GENERATED_BODY() //      ,     . public: UFUNCTION(BlueprintPure) AActor* GetOwner() const {return Cast<AActor>(GetOuter());}; //   } 

Ahora puede simplemente usar las funciones globales en combinación con nuestra función Pure para obtener el propietario.



Si también declara GetWorld () en la segunda variante como en la primera variante, puede enviar una referencia a usted mismo (Self o This) en el parámetro WorldContextObject.



BeginPlay y eventos Tick


Otro problema que pueden encontrar los desarrolladores de Blueprint es que no hay eventos BeginPlay y Tick en la clase Object. Por supuesto, puede crearlos usted mismo y llamar desde otra clase. Pero debe admitir que es mucho más conveniente cuando todo sale de la caja.

Comencemos por comprender cómo hacer Begin Play. Podemos crear una función disponible para reescribir en el BP y llamarla en el constructor de la clase, pero hay una serie de problemas, ya que en el momento del constructor su objeto aún no está completamente inicializado.

En todas las clases, existe la función PostInitProperties (), que se llama después de la inicialización de la mayoría de los parámetros y el registro del objeto en varios sistemas internos, por ejemplo, para el recolector de basura. En él, puede llamar a nuestro evento, que se utilizará en los Blueprints:

.h

 UCLASS(Blueprintable) class MYPROPJECT_API UMyObject : public UObject { GENERATED_BODY() //      . virtual void PostInitProperties() override; // ,      . UFUNCTION(BlueprintImplementableEvent) void BeginPlay(); //   } 

.cpp

 void UMyObject::PostInitProperties() { Super::PostInitProperties(); //   ,   .   BeginPlay    if(GetOuter() && GetOuter()->GetWorld()) BeginPlay(); } 

En lugar de if (GetOuter () && GetOuter () -> GetWorld ()) simplemente puede poner if (GetWorld ()) si ya lo ha redefinido.

Ten cuidado Por defecto, PostInitProperties () también se llama en el editor.

Ahora podemos entrar en nuestro objeto BP y llamar al evento BeginPlay. Se llamará cuando se cree el objeto.

Pasemos al Evento Tick. No hay una función simple para nosotros. Los objetos de marca en el motor llaman a un administrador especial, al que debe seleccionar de alguna manera. Sin embargo, aquí hay un truco muy conveniente: herencia adicional de FTickableGameObject. Esto le permitirá hacer automáticamente todo lo que necesita, y luego será suficiente solo para recoger las funciones necesarias:

.h

 //   #include "Tickable.h" //   c FTickableGameObject UCLASS(Blueprintable) class MYPROPJECT_API UMyObject : public UObject, public FTickableGameObject { GENERATED_BODY() public: //   virtual void Tick(float DeltaTime) override; virtual bool IsTickable() const override; virtual TStatId GetStatId() const override; protected: //     UFUNCTION(BlueprintImplementableEvent) void EventTick(float DeltaTime); //   } 

.cpp

 void UMyObject::Tick(float DeltaTime) { //       . EventTick(DeltaTime); //     . } //     bool UMyObject::IsTickable() const { return true; } TStatId UMyObject::GetStatId() const { return TStatId(); } 

Si hereda de su objeto y crea una clase BP, estará disponible un evento EventTick, lo que causará lógica para cada marco.



Agregar componentes desde UObjects


En UObject Blueprints, no puede generar componentes para actores. El mismo problema es inherente a los planos de ActorComponent. La lógica de Epic Games no está muy clara, ya que en C ++ esto se puede hacer. Además, puede agregar un componente de Actor a otro objeto de Actor simplemente especificando un enlace. Pero esto no se puede hacer.

Desafortunadamente, no pude resolver este artículo. Si alguien tiene instrucciones sobre cómo hacer esto, estaré encantado de publicarlo aquí.

La única opción que puedo ofrecer en este momento es hacer un contenedor en la clase UObject, proporcionando acceso a una simple adición de componentes. Por lo tanto, será posible agregar componentes al Actor, pero no habrá creado dinámicamente los parámetros de entrada del engendro. A menudo, esto puede ser descuidado.



Configurar una instancia a través del editor


En UE4, hay otra "característica" conveniente para trabajar con objetos: esta es la capacidad de crear una instancia durante la inicialización y cambiar sus parámetros a través del editor, estableciendo así sus propiedades, sin crear una clase secundaria solo por el bien de la configuración. Especialmente útil para diseñadores de juegos.

Suponga que tiene un administrador de modificadores para un personaje y los modificadores mismos están representados por clases que describen los efectos superpuestos. El diseñador del juego creó un par de modificadores e indica en el administrador cuáles se utilizan.

En una situación normal, se vería así:

.h

 class MYPROPJECT_API AMyActor : public AActor { GENERATED_BODY() public: UPROPERTY(EditAnywhere) TSubclassOf<class UMyObject> MyObjectClass; } 



Sin embargo, existe el problema de que no puede configurar modificadores y debe crear una clase adicional para otros valores. De acuerdo, no es muy conveniente tener docenas de clases en el Navegador de contenido que difieran solo en valores. Arreglar esto es fácil. Puede agregar un par de campos dentro de USTRUCT (), y también indicar en el objeto contenedor que nuestros objetos serán instancias, y no solo referencias a objetos o clases inexistentes:

.h

 UCLASS(Blueprintable, DefaultToInstanced, EditInlineNew) //  -        class MYPROPJECT_API UMyObject : public UObject { GENERATED_BODY() UPROPERTY(EditAnywhere) //       uint8 MyValue; // ,    //   } 

Esto por sí solo no es suficiente, ahora es necesario indicar que la misma variable con la clase será una instancia. Esto ya se hace donde almacena el objeto, por ejemplo, en el administrador de modificadores de caracteres:

.h

 class MYPROPJECT_API AMyActor : public AActor { GENERATED_BODY() public: UPROPERTY(EditAnywhere, Instanced) //   Instanced    class UMyObject* MyObject; //    } 

Tenga en cuenta que usamos la referencia al objeto, y no a la clase, ya que la instancia se creará inmediatamente después de la inicialización. Ahora podemos ir a la ventana del editor para seleccionar una clase y ajustar los valores dentro de la instancia. Es mucho más conveniente y más flexible.

imagen



Informacion


Hay otra clase interesante en Unreal Engine. Este es AInfo. Una clase heredada de AActor que no tiene una representación visual en el mundo. Info utiliza clases como: modo de juego, GameState, PlayerState y otros. Es decir, las clases que admiten diferentes chips de AActor, por ejemplo, la replicación, pero no se colocan en la escena.

Si necesita crear un administrador global adicional que debería admitir la red y todas las clases de actores resultantes, puede usarlo. No tiene que manipular la clase UObject como se describió anteriormente para forzarla, por ejemplo, a replicar datos.

Sin embargo, tenga en cuenta que aunque el objeto no tiene coordenadas, ni componentes visuales, y no se muestra en la pantalla, sigue siendo un descendiente de la clase Actor, lo que significa que es tan pesado como el padre. Razonablemente utilizado en pequeñas cantidades y por conveniencia.



Conclusión


UObject se necesita con mucha frecuencia, y le aconsejo que lo use siempre que el Actor no sea realmente necesario. Es una pena que sea un poco limitado, pero también es una ventaja. A veces tiene que jugar cuando necesita usar una plantilla personalizada, pero lo más importante es que se pueden eliminar todas las restricciones principales.

, , UObject, , , .

, , Unreal Engine 4. - , . , - , UObject.

Source: https://habr.com/ru/post/475622/


All Articles