En este artículo, le diré cómo crear portales en Unreal Engine 4. No encontré ninguna fuente que describiera tal sistema en detalle (monitoreando a través de los portales y pasando por ellos), así que decidí escribir el mío.
¿Qué es un portal?
Comencemos con ejemplos y explicaciones de lo que es un portal. La forma más fácil de describir los portales como una forma de paso de un espacio a otro. En algunos juegos populares, este concepto se usa para efectos visuales e incluso para la mecánica del juego:
Ejemplos de portal de juegos (GIF)Antichamber (2013) y Portal (2007)Presa, 2006 De los tres juegos, el más famoso es probablemente Portal, pero personalmente siempre he admirado a Prey y era a ella a quien quería copiar. Una vez intenté implementar mi propia versión en Unreal Engine 4, pero realmente no tuve éxito, porque el motor carecía de funcionalidad. Sin embargo, logré realizar estos experimentos:
Sin embargo, solo en las nuevas versiones de Unreal Engine finalmente logré lograr el efecto deseado:
Portales: ¿cómo funcionan?
Antes de continuar con los detalles, veamos la imagen general de cómo funcionan los portales.
De hecho, un portal es una ventana que no sale, pero a otro lugar, es decir, establecemos localmente un punto de vista específico relativo al objeto y replicamos este punto de vista en otro lugar. Usando este principio, podemos conectar dos espacios, incluso si están muy lejos el uno del otro. La ventana se asemeja a una máscara que nos permite averiguar dónde y cuándo mostrar otro espacio en lugar del original. Dado que el punto de partida de la vista se replica en otro lugar, esto nos da la ilusión de continuidad.

En esta imagen, el dispositivo de captura (SceneCapture en UE4) está ubicado frente al espacio que corresponde al espacio visto desde el punto de vista del jugador. Todo lo que es visible después de la línea se reemplaza por lo que la captura puede ver. Dado que el dispositivo de captura puede ubicarse entre la puerta y otros objetos, es importante utilizar el llamado "plano de recorte". En el caso del portal, queremos que el plano de recorte cercano enmascare los objetos visibles en frente del portal.
Para resumir. Necesitamos:
- Ubicación del jugador
- Punto de entrada del portal
- Punto de salida del portal
- Dispositivo de recorte con plano de recorte
¿Cómo implementar esto en Unreal Engine?
Construí mi sistema sobre la base de dos clases principales administradas por
PlayerController y
Character . La clase
Portal es un verdadero punto de entrada del portal, cuyo punto de vista / salida es el actor Target. También hay un
Administrador de portal , que es generado por PlayerController y actualizado por Character para administrar cada portal en el nivel y actualizarlos, así como para manipular el objeto SceneCapture (que es común a todos los portales).
Ten en cuenta que el tutorial espera que tengas acceso a las clases Character y PlayerController desde el código. En mi caso, se llaman ExedreCharacter y ExedrePlayerController.
Crear una clase de actor de portal
Comencemos con el actor del portal, que se utilizará para establecer las "ventanas" a través de las cuales veremos el nivel. La tarea del actor es proporcionar información sobre el jugador para calcular varias posiciones y turnos. También se dedicará a reconocer si el jugador cruza el portal y su teletransportación.
Antes de comenzar una discusión detallada sobre el actor, permítanme explicar algunos conceptos que creé para administrar el sistema del portal:
- Para el rechazo conveniente de los cálculos, el portal tiene un estado activo-inactivo. Este estado lo actualiza Portal Manager.
- El portal tiene lados frontal y posterior determinados por su posición y dirección (vector hacia adelante).
- Para averiguar si el jugador cruza el portal, almacena la posición anterior del jugador y la compara con la actual. Si en la medida anterior el jugador estaba delante del portal y en la corriente, detrás de él, entonces creemos que el jugador lo cruzó. El comportamiento inverso se ignora.
- El portal tiene un volumen límite, para no realizar cálculos y comprobaciones hasta que el jugador esté en este volumen. Ejemplo: ignore la intersección si el jugador no está tocando el portal.
- La ubicación del jugador se calcula a partir de la ubicación de la cámara para garantizar un comportamiento correcto cuando el punto de vista cruza el portal pero no el cuerpo del jugador.
- El portal recibe un Objetivo de procesamiento, que muestra un punto de vista diferente en cada medida en caso de que la textura la próxima vez sea incorrecta y deba reemplazarse.
- El portal almacena un enlace a otro actor llamado Target, para saber dónde debe contactarse con el otro espacio.
Usando estas reglas, creé una nueva clase ExedrePortal heredada de AActor como punto de partida. Aquí está su título:
#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "ExedrePortal.generated.h" UCLASS() class EXEDRE_API AExedrePortal : public AActor { GENERATED_UCLASS_BODY() protected: virtual void BeginPlay() override; public: virtual void Tick(float DeltaTime) override;
Como puede ver, hay la mayoría de los comportamientos descritos aquí. Ahora veamos cómo se procesan en el cuerpo (.cpp).
El diseñador aquí está preparando los componentes raíz. Decidí crear dos componentes raíz, porque el actor del portal combinará efectos gráficos y colisiones / reconocimiento. Así que necesitaba una forma simple de determinar dónde está el plano de la ventana / portal, sin la necesidad de funciones de Bluetooth u otros trucos. PortalRootComponent será la base para todos los cálculos relacionados con el portal.
La raíz del portal se establece en dinámica, en caso de que la clase Blueprint la anime (por ejemplo, use una animación de apertura / cierre).
Solo hay funciones Get y Set, y nada más. Gestionaremos el estado de la actividad desde otro lugar.
bool AExedrePortal::IsActive() { return bIsActive; } void AExedrePortal::SetActive( bool NewActive ) { bIsActive = NewActive; }
Eventos de planos, no estoy haciendo nada en la clase C ++.
void AExedrePortal::ClearRTT_Implementation() { } void AExedrePortal::SetRTT_Implementation( UTexture* RenderTexture ) { } void AExedrePortal::ForceTick_Implementation() { }
Las funciones Get y Set para el actor Target. No hay nada más complicado en esta parte tampoco.
AActor* AExedrePortal::GetTarget() { return Target; } void AExedrePortal::SetTarget( AActor* NewTarget ) { Target = NewTarget; }
Con esta función, podemos verificar fácilmente si un punto está frente a un plano, y en nuestro caso es un portal. La función utiliza la estructura FPlane del motor UE4 para realizar cálculos.
bool AExedrePortal::IsPointInFrontOfPortal( FVector Point, FVector PortalLocation, FVector PortalNormal ) { FPlane PortalPlane = FPlane( PortalLocation, PortalNormal ); float PortalDot = PortalPlane.PlaneDot( Point );
Esta función verifica si el punto ha cruzado el plano del portal. Es aquí donde usamos la posición anterior para descubrir cómo se comporta el punto. Esta función es común para que pueda funcionar con cualquier actor, pero en mi caso solo se usa con el jugador.
La función crea una dirección / segmento entre la ubicación anterior y la actual, y luego verifica si se cruzan con el plano. Si es así, verificamos si se cruza en la dirección correcta (¿de adelante hacia atrás?).
bool AExedrePortal::IsPointCrossingPortal( FVector Point, FVector PortalLocation, FVector PortalNormal ) { FVector IntersectionPoint; FPlane PortalPlane = FPlane( PortalLocation, PortalNormal ); float PortalDot = PortalPlane.PlaneDot( Point ); bool IsCrossing = false; bool IsInFront = PortalDot >= 0; bool IsIntersect = FMath::SegmentPlaneIntersection( LastPosition, Point, PortalPlane, IntersectionPoint );
Teleport Actor
La última parte del actor del portal que veremos es la función
TeleportActor () .
Al teletransportar a un actor del punto A al punto B, debe replicar su movimiento y posición. Por ejemplo, si un jugador pasa al portal, en combinación con los efectos visuales adecuados, le parecerá que atravesó una puerta ordinaria.
La intersección del portal se siente como moverse en línea recta, pero en realidad sucede algo completamente diferente. Al salir del portal, el jugador puede encontrarse en un contexto muy diferente. Considere un ejemplo de Portal:
Como puede ver, al cruzar el portal, la cámara gira en relación con su vector hacia adelante (gira). Esto se debe a que los puntos inicial y final son paralelos a diferentes planos:
Por lo tanto, para que esto funcione, necesitamos transformar el movimiento del jugador en el espacio relativo del portal para convertirlo en el espacio Objetivo. Al implementar esto, podemos estar seguros de que después de ingresar al portal y salir del otro lado, el jugador estará correctamente alineado con respecto al espacio. Esto se aplica no solo a la posición y rotación del actor, sino también a su
velocidad .
Si teletransportamos a un actor sin cambios, convirtiéndolo en una rotación local, entonces, como resultado, el actor puede encontrarse al revés. Esto puede ser adecuado para objetos, pero no aplicable a los personajes o al jugador mismo. Debe cambiar la posición del actor, como se muestra arriba en el ejemplo de Portal.
void AExedrePortal::TeleportActor( AActor* ActorToTeleport ) { if( ActorToTeleport == nullptr || Target == nullptr ) { return; }
Como probablemente haya notado, para llamar a rotación / posición, llamo funciones externas. Se llaman desde la clase de usuario UTool, que define funciones estáticas que se pueden llamar desde cualquier lugar (incluidos los planos). Su código se muestra a continuación, puede implementarlos de la manera que le parezca mejor (probablemente sea más fácil colocarlos en la clase de actor Portal).
FVector ConvertLocationToActorSpace( FVector Location, AActor* Reference, AActor* Target ) { if( Reference == nullptr || Target == nullptr ) { return FVector::ZeroVector; } FVector Direction = Location - Reference->GetActorLocation(); FVector TargetLocation = Target->GetActorLocation(); FVector Dots; Dots.X = FVector::DotProduct( Direction, Reference->GetActorForwardVector() ); Dots.Y = FVector::DotProduct( Direction, Reference->GetActorRightVector() ); Dots.Z = FVector::DotProduct( Direction, Reference->GetActorUpVector() ); FVector NewDirection = Dots.X * Target->GetActorForwardVector() + Dots.Y * Target->GetActorRightVector() + Dots.Z * Target->GetActorUpVector(); return TargetLocation + NewDirection; }
La transformación aquí se realiza calculando el producto escalar de vectores para determinar diferentes ángulos. El vector de dirección no está normalizado, es decir, podemos multiplicar nuevamente el resultado de los puntos por vectores de destino para obtener la posición exactamente a la misma distancia en el espacio local del actor de destino.
FRotator ConvertRotationToActorSpace( FRotator Rotation, AActor* Reference, AActor* Target ) { if( Reference == nullptr || Target == nullptr ) { return FRotator::ZeroRotator; } FTransform SourceTransform = Reference->GetActorTransform(); FTransform TargetTransform = Target->GetActorTransform(); FQuat QuatRotation = FQuat( Rotation ); FQuat LocalQuat = SourceTransform.GetRotation().Inverse() * QuatRotation; FQuat NewWorldQuat = TargetTransform.GetRotation() * LocalQuat; return NewWorldQuat.Rotator(); }
Convertir la transformación fue un poco más difícil de implementar. Al final, la mejor solución resultó ser el uso de
cuaterniones , porque esto es mucho más preciso que trabajar con
ángulos normales de
Euler y requiere solo unas pocas líneas de código. Las rotaciones por cuaterniones se realizan mediante multiplicación, por lo que en nuestro caso, aplicando Inverse () a la rotación que queremos convertir, la trasladaremos al espacio local. A continuación, solo tenemos que multiplicarlo nuevamente por el turno Objetivo para obtener el turno final.
Crear una malla de portal
Para lucir bella desde el punto de vista de un jugador, mi sistema de portal utiliza una malla específica. La malla se divide en dos planos diferentes:
- Plano 1 : el plano principal en el que se muestra el objetivo de representación del portal. Este avión tiene un comportamiento bastante inusual, porque su tarea es alejarse un poco del jugador cuando se acerca para evitar que la cámara lo recorte. Dado que los bordes del avión no se mueven, sino que solo se mueven sus picos medios, esto le permite al jugador superponer al renderizar el portal sin artefactos visuales. Los bordes en los bordes tienen su propio UV en la mitad superior, mientras que los bordes internos tienen su propio UV en la mitad inferior, lo que facilita enmascararlos en el sombreador.
- Plano 2 : este plano solo se usa para extender el cuadro delimitador estándar de la malla. Las normales de los vértices se dirigen hacia abajo, por lo que incluso en terreno no plano la malla no será visible por defecto (porque el material de renderizado no será de dos lados).
¿Por qué usar una malla como esta?
Decidí que el "avión 1" se estiraría a medida que el jugador se acercara. Esto permite que el jugador se superponga al portal y lo atraviese sin recortar (cortar). Esto puede suceder, por ejemplo, si la cámara aún no ha cruzado el plano del portal, pero los pies del jugador ya la han tocado. Esto le permite no cortar el reproductor y duplicar la malla por otro lado.
La tarea del "plano 2" es extender el cuadro delimitador estándar de la malla. Dado que el "plano 1" es plano, el cuadro delimitador en un eje tiene un grosor de 0, y si la cámara está detrás, el motor lo cortará (es decir, no lo renderizará). El avión 1 tiene un tamaño de 128 × 128, por lo que se puede escalar fácilmente con el motor. El plano 2 es ligeramente más grande y está debajo del piso (debajo de 0).
Una vez creada la malla, simplemente la exportamos desde un editor 3D de terceros y la importamos a Unreal. Se usará en el siguiente paso.
Crear material del portal
Para mostrar el otro lado del portal, necesitamos crear nuestro propio material. Cree nuevo material en el navegador de contenido (lo llamé
MAT_PortalBase ):
Ahora ábralo y cree el siguiente gráfico:
Así es como funciona el material:
- FadeColor es el color que será visible a través del portal cuando esté muy lejos. Es necesario porque no siempre renderizamos todos los portales, por lo que oscurecemos el renderizado cuando el reproductor / cámara está muy lejos.
- Para averiguar qué tan lejos está el jugador del portal, determino la distancia entre la posición de la cámara y la posición del actor. Luego divido la distancia por el valor máximo con el que quiero realizar una comparación. Por ejemplo, si el máximo que configuré es 2000, y la distancia al jugador es 1000, entonces obtenemos 0.5. Si el jugador está más lejos, obtendré un valor mayor que 1, así que uso nodos saturados para limitarlo. Luego viene el nodo Smoothstep, usado para escalar la distancia como un gradiente y controlar con mayor precisión el sombreado del portal. Por ejemplo, cuando el jugador está cerca, quiero que la sombra desaparezca por completo.
- Utilizo el cálculo de distancia como el valor del canal alfa para el nodo Lerp para mezclar el color de sombreado y la textura que representará el objetivo del portal.
- Finalmente, aíslo el componente Y de las coordenadas UV para crear una máscara que le permita saber qué vértices de la malla serán empujados. Multiplico esta máscara por la cantidad de repulsión que necesito. Utilizo un valor negativo para que cuando las normales de los vértices se multipliquen por los vértices, se muevan en la dirección opuesta.
Una vez hecho todo esto, creamos material listo para usar.
Crear un actor de portal en Blueprint
Configuremos una nueva clase de blueprint heredada del actor Portal. Haga clic derecho en el navegador de contenido y seleccione la clase Blueprint:
Ahora ingrese "portal" en el campo de búsqueda para seleccionar la clase de portal:
Abra bluetooth si aún no está abierto. En la lista de componentes verá la siguiente jerarquía:
Como esperábamos, hay un componente raíz y una raíz de portal. Agreguemos un componente de malla estática a PortalRootComponent y carguemos la malla creada en el paso anterior:
También agregamos el cuadro de colisión, que se utilizará para determinar si el jugador está dentro del volumen del portal:
El cuadro Colisión se encuentra debajo del componente de escena asociado con la raíz principal, y no debajo de la raíz del Portal. También agregué un ícono (cartelera) y un componente de flecha para hacer que el portal sea más visible en los niveles. Por supuesto, esto no es necesario.
Ahora configuremos el material en plano.
Para empezar, necesitamos dos variables: una será del tipo
Actor y el nombre es
PortalTarget , la segunda es del tipo
Dynamic Material Instance y se llama
MaterialInstance . PortalTarget será una referencia a la posición que está mirando la ventana del portal (por lo tanto, la variable es común, con un icono de ojo abierto) para que podamos cambiarla cuando el actor se coloca en el nivel. MaterialInstance almacenará un enlace a material dinámico para que en el futuro podamos asignar el objetivo de renderizado del portal sobre la marcha.
También necesitamos agregar nuestros propios nodos de eventos. Es mejor abrir el menú derecho del mouse en el
Gráfico de eventos y encontrar los nombres de los eventos:
Y aquí para crear el siguiente diagrama:
- Comenzar reproducción : aquí llamamos a la función principal SetTarget () del portal para asignarle un enlace al actor, que luego se usará para SceneCapture. Luego creamos un nuevo material dinámico y le asignamos el valor de la variable MaterialInstance. Con este nuevo material, podemos asignarlo al componente de malla estática. También le di al material una textura ficticia, pero esto es opcional.
- Borrar RTT : el propósito de esta función es borrar la textura de Render Target asignada al material del portal. Es lanzado por el administrador del portal.
- Establecer RTT : el propósito de esta función es establecer el material de destino del portal. Es lanzado por el administrador del portal.
Hasta ahora hemos terminado con bluetooth, pero volveremos más tarde para implementar las funciones de Tick.
Administrador del portal
Entonces, ahora tenemos todos los elementos básicos necesarios para crear una nueva clase heredada de AActor, que será Portal Manager. Es posible que no necesite la clase Portal Manager en su proyecto, pero en mi caso, simplifica enormemente el trabajo con algunos aspectos. Aquí hay una lista de tareas realizadas por el administrador del portal:
- El administrador del portal es un actor creado por el controlador del jugador y conectado a él para rastrear el estado y la evolución del jugador dentro del nivel del juego.
- Crear y destruir el portal de destino de renderizado . La idea es crear dinámicamente una textura de destino de renderizado que coincida con la resolución de pantalla del jugador. Además, al cambiar la resolución durante el juego, el administrador la convertirá automáticamente al tamaño deseado.
- El administrador del portal encuentra y actualiza el nivel de actor del portal para darles un objetivo de representación. Esta tarea se realiza de manera que se garantice la compatibilidad con el nivel de transmisión. Cuando aparece un nuevo actor, debe obtener una textura. Además, si el objetivo Render cambia, el administrador también puede asignar uno nuevo automáticamente. Esto facilita la administración del sistema, en lugar de hacer que cada actor del Portal se comunique manualmente con el administrador.
- El componente SceneCapture está conectado al administrador del portal, para no crear una copia para cada portal. Además, le permite reutilizarlo cada vez que cambiemos a un actor de portal específico en el nivel.
- Cuando el portal decide teletransportar al jugador, envía una solicitud al Administrador del portal. Esto es necesario para actualizar los portales de origen y de destino (si los hay), para que la transición ocurra sin uniones.
- El administrador del portal se actualiza al final de la función tick () del personaje para que todo se actualice correctamente, incluida la cámara del jugador. Esto asegura que todo en la pantalla esté sincronizado y evita un retraso de un cuadro durante el renderizado por el motor.
Echemos un vistazo al encabezado de Portal Manager:
#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "ExedrePortalManager.generated.h"
Antes de entrar en detalles, mostraré cómo se crea un actor a partir de la clase Player Controller, llamada desde la función BeginPlay ():
FActorSpawnParameters SpawnParams; PortalManager = nullptr; PortalManager = GetWorld()->SpawnActor<AExedrePortalManager>( AExedrePortalManager::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams); PortalManager->AttachToActor( this, FAttachmentTransformRules::SnapToTargetIncludingScale); PortalManager->SetControllerOwner( this ); PortalManager->Init();
Entonces, creamos un actor, lo adjuntamos al controlador del jugador (esto), y luego guardamos el enlace y llamamos a la función Init ().
También es importante tener en cuenta que actualizamos el actor manualmente desde la clase Character:
void AExedreCharacter::TickActor( float DeltaTime, enum ELevelTick TickType, FActorTickFunction& ThisTickFunction ) { Super::TickActor( DeltaTime, TickType, ThisTickFunction ); if( UGameplayStatics::GetPlayerController(GetWorld(), 0) != nullptr ) { AExedrePlayerController* EPC = Cast<AExedrePlayerController>( UGameplayStatics::GetPlayerController(GetWorld(), 0) ); EPC->PortalManager->Update( DeltaTime ); } }
Y aquí está el constructor de Portal Manager. Tenga en cuenta que Tick está deshabilitado, nuevamente porque actualizaremos manualmente Portal Manager a través del reproductor.
AExedrePortalManager::AExedrePortalManager(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { PrimaryActorTick.bCanEverTick = false; PortalTexture = nullptr; UpdateDelay = 1.1f; PreviousScreenSizeX = 0; PreviousScreenSizeY = 0; }
Estas son las funciones de get / set Portal Manager (después de eso pasaremos a cosas más interesantes):
void AExedrePortalManager::SetControllerOwner( AExedrePlayerController* NewOwner ) { ControllerOwner = NewOwner; } FTransform AExedrePortalManager::GetCameraTransform() { if( SceneCapture != nullptr ) { return SceneCapture->GetComponentTransform(); } else { return FTransform(); } } UTexture* AExedrePortalManager::GetPortalTexture() {
Obviamente, lo primero para comenzar es la función
Init () .
El objetivo principal de esta función es crear el componente SceneCapture (es decir, el dispositivo de captura mencionado anteriormente) y configurarlo correctamente. Comienza con la creación de un nuevo objeto y su registro como componente de este actor. Luego pasamos a establecer propiedades relacionadas con esta captura.Propiedades a mencionar:- bCaptureEveryFrame = false : no queremos que la captura se active cuando no la necesitamos. Lo gestionaremos manualmente.
- bEnableClipPlane = true : una propiedad bastante importante para representar la captura del portal correctamente.
- bUseCustomProjectionMatrix = true : esto nos permite reemplazar la proyección de Captura por la nuestra, según el punto de vista del jugador.
- CaptureSource = ESceneCaptureSource :: SCS_SceneColorSceneDepth : este modo es un poco costoso, pero necesario para representar una cantidad suficiente de información.
Las propiedades restantes están relacionadas principalmente con los parámetros de postprocesamiento. Son una forma conveniente de controlar la calidad y, por lo tanto, capturar el rendimiento.La última parte llama a la función que crea el Destino de renderizado, que veremos a continuación. void AExedrePortalManager::Init() {
GeneratePortalTexture () es una función que se llama cuando es necesario cuando necesita crear una nueva textura Render Target para portales. Esto sucede en la función de inicialización, pero también se puede invocar durante la actualización de Portal Manager. Es por eso que esta función tiene una verificación interna para cambiar la resolución de la ventana gráfica. Si no sucedió, entonces la actualización no se realiza.En mi caso, creé una clase de contenedor para UCanvasRenderTarget2D. Lo llamé ExedreScriptedTexture, es un componente que se puede conectar a un actor. Creé esta clase para administrar convenientemente los objetivos de renderizado con actores que tienen tareas de renderizado. Realiza la inicialización adecuada del Render Target y es compatible con mi propio sistema de interfaz de usuario. Sin embargo, en el contexto de los portales, una textura RenderTarget2D normal es más que suficiente. Por lo tanto, simplemente puede usarlo. void AExedrePortalManager::GeneratePortalTexture() { int32 CurrentSizeX = 1920; int32 CurrentSizeY = 1080; if( ControllerOwner != nullptr ) { ControllerOwner->GetViewportSize(CurrentSizeX, CurrentSizeY); } CurrentSizeX = FMath::Clamp( int(CurrentSizeX / 1.7), 128, 1920);
Como se mencionó anteriormente, creé mi propia clase, por lo que las propiedades establecidas aquí deben adaptarse al objetivo de renderizado habitual.Es importante comprender dónde se mostrará la captura. Dado que el objetivo de renderizado se mostrará en el juego, esto significa que esto sucederá antes de todo el procesamiento posterior y, por lo tanto, necesitamos renderizar la escena con suficiente información (para almacenar valores superiores a 1 para crear Bloom). Es por eso que elegí el formato RGBA16 (tenga en cuenta que tiene su propia Enum, necesitará usar ETextureRenderTargetFormat en su lugar).Para obtener más información, consulte las siguientes fuentes:
Además consideraremos las funciones de actualización. La función básica es bastante simple y causa más compleja. Hay una demora antes de llamar a la función GeneratePortalTexture () para evitar volver a crear el destino de representación al cambiar el tamaño de la ventana gráfica (por ejemplo, en el editor). Durante la publicación del juego, este retraso puede eliminarse. void AExedrePortalManager::Update( float DeltaTime ) {
Llamamos a UpdatePortalsInWorld () para encontrar todos los portales presentes en el mundo actual (incluidos todos los niveles cargados) y actualizarlos. La función también determina cuál está "activo", es decir visible para el jugador. Si encontramos un portal activo, llamamos UpdateCapture () , que controla el componente SceneCapture.
Así es como funciona la actualización mundial dentro de UpdatePortalsInWorld () :- ( )
- iterator ,
- , , ClearRTT() , . (, ).
- , , , , .
La verificación que determina la corrección del portal es simple: le damos prioridad al portal más cercano al jugador, porque lo más probable es que sea el más visible desde su punto de vista. Para descartar parientes, pero, por ejemplo, portales ubicados detrás del jugador, se requerirán controles más complejos, pero no quería centrarme en esto en mi tutorial, porque puede ser bastante difícil. AExedrePortal* AExedrePortalManager::UpdatePortalsInWorld() { if( ControllerOwner == nullptr ) { return nullptr; } AExedreCharacter* Character = ControllerOwner->GetCharacter();
Es hora de considerar la función UpdateCapture () .Esta es una función de actualización que captura el otro lado del portal. De los comentarios todo debe quedar claro, pero aquí hay una breve descripción:- Obtenemos enlaces a Controlador de personaje y jugador.
- Verificamos si todo es correcto (Portal, componente SceneCapture, Reproductor).
- Camera Target .
- , SceneCapture.
- SceneCapture Target.
- , SceneCapure , , .
- Render Target SceneCapture, .
- PlayerController.
- , Capture SceneCapture .
Como podemos ver, al teletransportar a un jugador, un elemento clave del comportamiento natural e impecable de SceneCapture es la correcta transformación de la posición y la rotación del portal en el espacio Target local.Para la definición de ConvertLocationToActorSpace (), consulte "Teletransportación de un actor".
void AExedrePortalManager::UpdateCapture( AExedrePortal* Portal ) { if( ControllerOwner == nullptr ) { return; } AExedreCharacter* Character = ControllerOwner->GetCharacter();
La función GetCameraProjectionMatrix () no existe por defecto en la clase PlayerController, la agregué yo mismo. Se muestra a continuación: FMatrix AExedrePlayerController::GetCameraProjectionMatrix() { FMatrix ProjectionMatrix; if( GetLocalPlayer() != nullptr ) { FSceneViewProjectionData PlayerProjectionData; GetLocalPlayer()->GetProjectionData( GetLocalPlayer()->ViewportClient->Viewport, EStereoscopicPass::eSSP_FULL, PlayerProjectionData ); ProjectionMatrix = PlayerProjectionData.ProjectionMatrix; } return ProjectionMatrix; }
Finalmente, necesitamos implementar la llamada a la función Teleport. La razón para el procesamiento parcial de la teletransportación a través del administrador del Portal es que es necesario garantizar la actualización de los portales necesarios, porque solo el Administrador tiene información sobre todos los portales en la escena.Si tenemos dos portales conectados, entonces, al cambiar de uno a otro, necesitamos actualizar ambos en un Tick. De lo contrario, el jugador se teletransportará y estará al otro lado del portal, pero el Portal de destino no estará activo hasta el próximo cuadro / medida. Esto creará huecos visuales con el material desplazado de la malla plana que vimos arriba. void AExedrePortalManager::RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ) { if( Portal != nullptr && TargetToTeleport != nullptr ) { Portal->TeleportActor( TargetToTeleport );
Bueno, eso es todo, ¡finalmente hemos terminado con Portal Manager!Termina el plano
Una vez completado el Administrador del portal, solo necesitamos completar el actor del Portal, después de lo cual el sistema funcionará. Lo único que falta aquí son las características de Tick:Así es como funciona:- Estamos actualizando el material para que no permanezca en un estado activo.
- Si el portal está actualmente inactivo , el resto de la medida se descarta.
- Obtenemos la clase de personaje para acceder a la ubicación de la cámara .
- La primera parte verifica si la cámara está en el cuadro de colisión del portal. Si es así, compensamos la malla del portal con su Material .
- La segunda parte es volver a verificar la ubicación dentro del cuadro de colisión. Si se ejecuta, llamamos a una función que verifica si cruzamos el portal .
- , Portal manager, Teleport .
En la captura de pantalla de mi gráfico, puede observar dos puntos interesantes: Is Point Inside Box y Get Portal Manager . Todavía no he explicado estas dos funciones. Estas son funciones estáticas que definí en mi propia clase para que pueda llamarlas desde cualquier lugar. Este es un tipo de clase auxiliar. El código de estas funciones se muestra a continuación, usted mismo puede decidir dónde insertarlas. Si no los necesita fuera del sistema de portal, puede insertarlos directamente en la clase de actor Portal.Al principio quería usar el sistema de colisión para determinar si el actor del portal dentro del cuadro de colisión está en el portal, pero me pareció que no era lo suficientemente confiable. Además, me parece que este método es más rápido de usar y tiene una ventaja: tiene en cuenta la rotación del actor. bool IsPointInsideBox( FVector Point, UBoxComponent* Box ) { if( Box != nullptr ) {
AExedrePortalManager* GetPortalManager( AActor* Context ) { AExedrePortalManager* Manager = nullptr;
La última parte del actor Blueprint es ForceTick . Recuerda que se llama Force Tick cuando un jugador cruza un portal y está al lado de otro portal para el que Portal Manager está forzando una actualización. Como nos teletransportamos, no es necesario usar el mismo código, y puede usar su versión simplificada:El proceso comienza aproximadamente al mismo tiempo que la función Tick, pero solo ejecutamos la primera parte de la secuencia, que actualiza el material.Hemos terminado?
Casi.Si implementamos el sistema de portal de esta forma, lo más probable es que encontremos el siguiente problema:¿Qué está pasando aquí?En este gif, la velocidad de fotogramas del juego se limita a 6 FPS para mostrar el problema con mayor claridad. En un cuadro, el cubo desaparece porque el sistema de recorte Unreal Engine lo considera invisible.Esto se debe a que el descubrimiento se realiza en el marco actual y luego se usa en el siguiente. Esto crea un retraso de un cuadro . Esto generalmente se puede resolver expandiendo el cuadro delimitador del objeto para que se registre antes de que sea visible. Sin embargo, esto no funcionará aquí, porque cuando cruzamos el portal, nos teletransportamos de un lugar a otro completamente diferente.Desactivar el sistema de recorte también es imposible, especialmente porque a niveles con muchos objetos esto reducirá el rendimiento. Además, probé muchos equipos del motor Unreal, pero no obtuve resultados positivos: en todos los casos, se mantuvo un retraso de un cuadro. Afortunadamente, después de un estudio detallado del código fuente de Unreal Engine, logré encontrar una solución (el camino fue largo, ¡tomó más de una semana)!Al igual que con el componente SceneCapture, puedes decirle a la cámara del jugador que hicimos un corte de salto- la posición de la cámara saltó entre dos cuadros, lo que significa que no podemos confiar en la información del cuadro anterior. Este comportamiento se puede observar cuando se usa Matinee o Sequencer, por ejemplo, al cambiar de cámara: el desenfoque de movimiento o el suavizado no pueden depender de la información del fotograma anterior.Para hacer esto, debemos considerar dos aspectos:- LocalPlayer : esta clase procesa diversa información (por ejemplo, la vista del jugador) y está asociada con el PlayerController. Aquí es donde podemos influir en el proceso de renderizado de la cámara del jugador.
- PlayerController : cuando un jugador se teletransporta, esta clase comienza a empalmarse gracias al acceso a LocalPlayer.
La gran ventaja de esta solución es que la intervención en el proceso de renderizado del motor es mínima y fácil de mantener en futuras actualizaciones de Unreal Engine.
Comencemos creando una nueva clase heredada de LocalPlayer. A continuación se muestra un encabezado que identifica dos componentes principales: redefinir los cálculos de la vista de escena y una nueva función para invocar el pegado de la cámara. #pragma once #include "CoreMinimal.h" #include "Engine/LocalPlayer.h" #include "ExedreLocalPlayer.generated.h" UCLASS() class EXEDRE_API UExedreLocalPlayer : public ULocalPlayer { GENERATED_BODY() UExedreLocalPlayer(); public: FSceneView* CalcSceneView( class FSceneViewFamily* ViewFamily, FVector& OutViewLocation, FRotator& OutViewRotation, FViewport* Viewport, class FViewElementDrawer* ViewDrawer, EStereoscopicPass StereoPass) override; void PerformCameraCut(); private: bool bCameraCut; };
Así es como se implementa todo: #include "Exedre.h" #include "ExedreLocalPlayer.h" UExedreLocalPlayer::UExedreLocalPlayer() { bCameraCut = false; } FSceneView* UExedreLocalPlayer::CalcSceneView( class FSceneViewFamily* ViewFamily, FVector& OutViewLocation, FRotator& OutViewRotation, FViewport* Viewport, class FViewElementDrawer* ViewDrawer, EStereoscopicPass StereoPass) {
PerformCameraCut () simplemente inicia Camera Cut con un valor booleano. Cuando el motor llama a la función CalcSceneView () , primero ejecutamos la función original. Luego verificamos, tenemos que realizar el pegado. Si es así, redefinimos la variable booleana Camera Cut dentro de la estructura FSceneView , que será utilizada por el proceso de renderizado del motor, y luego restablecemos la variable booleana ( úsela ).
En el lado del controlador del jugador, los cambios son mínimos. Debe agregar una variable al encabezado para almacenar un enlace a la clase nativa LocalPlayer: UPROPERTY() UExedreLocalPlayer* LocalPlayer;
Luego, en la función BeginPlay () : LocalPlayer = Cast<UExedreLocalPlayer>( GetLocalPlayer() );
También agregué una función para iniciar rápidamente Cut: void AExedrePlayerController::PerformCameraCut() { if( LocalPlayer != nullptr ) { LocalPlayer->PerformCameraCut(); } }
Finalmente, en la función de Administrador de portal RequestTeleportByPortal (), podemos ejecutar durante la teletransportación de Camera Cut: void AExedrePortalManager::RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ) { if( Portal != nullptr && TargetToTeleport != nullptr ) { if( ControllerOwner != nullptr ) { ControllerOwner->PerformCameraCut(); } [...]
¡Y eso es todo!Se debe llamar a Camera Cut antes de actualizar SceneCapture, razón por la cual se encuentra al comienzo de la función.Resultado final
Ahora hemos aprendido a pensar en portales.Si el sistema funciona bien, entonces deberíamos poder crear estas cosas:Si tiene problemas, compruebe lo siguiente:- Verifique que Portal Manager se haya creado e inicializado correctamente.
- El objetivo de representación se crea correctamente (puede usar el creado en el navegador de contenido para comenzar).
- Los portales están correctamente activados y desactivados.
- Los portales tienen el actor Target configurado correctamente en el editor.
Preguntas y respuestas
Las preguntas más populares que me hicieron sobre este tutorial:¿Es posible implementar esto en blunts y no a través de C ++?La mayor parte del código se puede implementar en romos, con la excepción de dos aspectos:- La función GetProjectionData () de LocalPlayer utilizada para obtener la matriz de proyección no está disponible en planos.
- La función LocalPlayer CalcSceneView () , que es crítica para resolver el problema del sistema de recorte, no está disponible en planos.
Por lo tanto, debe usar una implementación de C ++ para acceder a estas dos funciones o modificar el código fuente del motor para que sea accesible a través de planos.¿Puedo usar este sistema en VR?Sí, en su mayor parte. Sin embargo, algunas partes deberán adaptarse, por ejemplo:- Debe usar dos objetivos de renderizado (uno para cada ojo) y enmascararlos en el material del portal para mostrar uno al lado del otro en el espacio de la pantalla. Cada objetivo de renderizado debe tener la mitad del ancho de la resolución del dispositivo VR.
- Debe usar dos SceneCapture para representar el objetivo con la distancia correcta (la distancia entre los ojos) para crear efectos estereoscópicos.
El principal problema será el rendimiento, ya que el otro lado del portal deberá representarse dos veces.¿Puede otro objeto cruzar el portal?No hay en mi código. Sin embargo, hacerlo más general no es tan difícil. Para hacer esto, el portal necesita rastrear más información sobre todos los objetos cercanos para verificar si lo cruzan.¿El sistema admite recursividad (portal dentro del portal)?Este tutorial no lo es. Para la recursividad, necesita un destino de renderizado adicional y SceneCapture. También será necesario determinar qué RenderTarget se representará primero, y así sucesivamente. Esto es bastante difícil y no quería hacerlo, porque para mi proyecto esto no es necesario.¿Puedo cruzar el portal cerca de la pared?Lamentablemente no. Sin embargo, veo dos formas de implementar esto (teóricamente):- Desactiva las colisiones del jugador para que pueda atravesar las paredes. Es fácil de implementar, pero generará muchos efectos secundarios.
- Hackea un sistema de colisión para crear un agujero de forma dinámica, lo que permitirá al jugador pasar. Para hacer esto, debe modificar el sistema físico del motor. Sin embargo, por lo que sé, después de cargar el nivel, la física estática no se puede actualizar. Por lo tanto, admitir esta función requerirá mucho trabajo. Si sus portales son estáticos, entonces probablemente pueda solucionar este problema utilizando la transmisión de nivel para cambiar entre diferentes colisiones.