Neste artigo, mostrarei como criar portais no Unreal Engine 4. Não encontrei nenhuma fonte que descreva esse sistema em detalhes (monitorando através de portais e passando por eles), por isso decidi escrever meus próprios.
O que é um portal?
Vamos começar com exemplos e explicações sobre o que é um portal. A maneira mais fácil de descrever os portais como um caminho de passagem de um espaço para outro. Em alguns jogos populares, esse conceito é usado para efeitos visuais e até para a mecânica de jogo:
Exemplos de portal de jogos (GIF)Antichamber (2013) e Portal (2007)Presa, 2006 Dos três jogos, o mais famoso é provavelmente o Portal, mas pessoalmente sempre admirei a Prey e era ela que eu queria copiar. Uma vez tentei implementar minha própria versão no Unreal Engine 4, mas não consegui, porque o mecanismo não tinha funcionalidade. No entanto, eu consegui realizar essas experiências:
No entanto, somente em novas versões do Unreal Engine eu finalmente consegui alcançar o efeito desejado:
Portais - como eles funcionam?
Antes de prosseguir com os detalhes, vejamos a imagem geral de como os portais funcionam.
De fato, um portal é uma janela que não sai, mas para outro local, ou seja, definimos localmente um ponto de vista específico em relação ao objeto e replicamos esse ponto de vista em outro lugar. Usando esse princípio, podemos conectar dois espaços, mesmo que estejam muito distantes um do outro. A janela se assemelha a uma máscara que nos permite descobrir onde e quando exibir outro espaço em vez do original. Como o ponto de partida da visão é replicado em outro lugar, isso nos dá a ilusão de continuidade.
Nesta imagem, o dispositivo de captura (SceneCapture no UE4) está localizado na frente do espaço que corresponde ao espaço visto do ponto de vista do jogador. Tudo o que é visível após a linha é substituído pelo que a captura pode ver. Como o dispositivo de captura pode estar localizado entre a porta e outros objetos, é importante usar o chamado "plano de corte". No caso do portal, queremos que o plano de recorte próximo mascara os objetos visíveis na frente do portal.
Para resumir. Precisamos de:
- Localização do Jogador
- Ponto de entrada do portal
- Ponto de Saída do Portal
- Dispositivo de recorte com plano de recorte
Como implementar isso no Unreal Engine?
Eu construí meu sistema com base em duas classes principais gerenciadas pelo
PlayerController e
Character . A classe
Portal é um verdadeiro ponto de entrada do portal, cujo ponto de visualização / saída é o ator de Destino. Há também um
Portal Manager , que é gerado pelo PlayerController e atualizado pelo Character para gerenciar cada portal no nível e atualizá-los, além de manipular o objeto SceneCapture (que é comum a todos os portais).
Lembre-se de que o tutorial espera que você tenha acesso às classes Character e PlayerController a partir do código. No meu caso, eles são chamados ExedreCharacter e ExedrePlayerController.
Criando uma classe de ator do portal
Vamos começar com o ator do portal, que será usado para definir as "janelas" pelas quais veremos o nível. A tarefa do ator é fornecer informações sobre o jogador para calcular várias posições e turnos. Ele também se empenhará em reconhecer se o jogador atravessa o portal e seu teletransporte.
Antes de iniciar uma discussão detalhada do ator, deixe-me explicar alguns conceitos que eu criei para gerenciar o sistema do portal:
- Para uma recusa conveniente de cálculos, o portal possui um status ativo-inativo. Este estado é atualizado pelo Portal Manager.
- O portal possui lados dianteiro e traseiro determinados por sua posição e direção (vetor para frente).
- Para descobrir se o jogador atravessa o portal, ele armazena a posição anterior do jogador e a compara com a atual. Se na medida anterior o jogador estava na frente do portal e na corrente - atrás dele, acreditamos que o jogador o atravessou. O comportamento inverso é ignorado.
- O portal possui um volume limitador, para não realizar cálculos e verificações até que o player esteja nesse volume. Exemplo: ignore o cruzamento se o jogador não estiver realmente tocando o portal.
- A localização do jogador é calculada a partir da localização da câmera para garantir o comportamento correto quando o ponto de vista cruza o portal, mas não o corpo do jogador.
- O portal recebe um destino de renderização, que exibe um ponto de vista diferente em cada medida, caso a textura da próxima vez esteja incorreta e precise ser substituída.
- O portal armazena um link para outro ator chamado Target, para saber onde o outro espaço deve ser contatado.
Usando essas regras, criei uma nova classe ExedrePortal herdada do AActor como ponto de partida. Aqui está o seu 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 você pode ver, existem muitos dos comportamentos descritos aqui. Agora vamos ver como eles são processados no corpo (.cpp).
O designer aqui está preparando os componentes raiz. Decidi criar dois componentes raiz, porque o ator do portal combinaria efeitos gráficos e colisões / reconhecimento. Então, eu precisava de uma maneira simples de determinar onde está o plano da janela / portal, sem a necessidade de recursos bluetooth ou outros truques. PortalRootComponent será a base para todos os cálculos relacionados ao portal.
A raiz do portal é configurada como dinâmica, caso a classe Blueprint a anime (por exemplo, use uma animação de abrir / fechar).
Existem apenas funções Get e Set, e nada mais. Gerenciaremos o estado da atividade de outro local.
bool AExedrePortal::IsActive() { return bIsActive; } void AExedrePortal::SetActive( bool NewActive ) { bIsActive = NewActive; }
Eventos do Blueprint, não estou fazendo nada na classe C ++.
void AExedrePortal::ClearRTT_Implementation() { } void AExedrePortal::SetRTT_Implementation( UTexture* RenderTexture ) { } void AExedrePortal::ForceTick_Implementation() { }
As funções Get e Set para o ator Target. Também não há nada mais complicado nesta parte.
AActor* AExedrePortal::GetTarget() { return Target; } void AExedrePortal::SetTarget( AActor* NewTarget ) { Target = NewTarget; }
Com essa função, podemos verificar facilmente se um ponto está na frente de um avião e, no nosso caso, é um portal. A função usa a estrutura FPlane do mecanismo UE4 para executar cálculos.
bool AExedrePortal::IsPointInFrontOfPortal( FVector Point, FVector PortalLocation, FVector PortalNormal ) { FPlane PortalPlane = FPlane( PortalLocation, PortalNormal ); float PortalDot = PortalPlane.PlaneDot( Point );
Esta função verifica se o ponto cruzou o plano do portal. É aqui que usamos a posição antiga para descobrir como o ponto se comporta. Essa função é comum para que possa funcionar com qualquer ator, mas no meu caso é usada apenas com o player.
A função cria uma direção / segmento entre o local anterior e o atual e verifica se eles cruzam o plano. Nesse caso, verificamos se cruza na direção certa (da frente para trá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 );
Ator de teletransporte
A última parte do ator do portal que veremos é a função
TeleportActor () .
Ao teleportar um ator do ponto A para o ponto B, você precisa replicar seu movimento e posição. Por exemplo, se um jogador entra no portal, em combinação com efeitos visuais adequados, parece-lhe que ele passou por uma porta comum.
A interseção do portal parece mover-se em uma linha reta, mas, na realidade, algo completamente diferente acontece. Ao sair do portal, o jogador pode estar em um contexto muito diferente. Considere um exemplo do Portal:
Como você pode ver, ao atravessar o portal, a câmera gira em relação ao seu vetor para frente (gira). Isso ocorre porque os pontos inicial e final são paralelos a diferentes planos:
Portanto, para que isso funcione, precisamos transformar o movimento do jogador no espaço relativo do portal para convertê-lo no espaço Alvo. Ao implementar isso, podemos ter certeza de que, após entrar no portal e sair do outro lado, o player estará alinhado corretamente com relação ao espaço. Isso se aplica não apenas à posição e rotação do ator, mas também à sua
velocidade .
Se teleportarmos um ator sem alterações, convertendo-o em uma rotação local, então, como resultado, o ator poderá se ver de cabeça para baixo. Isso pode ser adequado para objetos, mas não aplicável aos personagens ou ao próprio jogador. Você precisa alterar a posição do ator, conforme mostrado acima no exemplo do Portal.
void AExedrePortal::TeleportActor( AActor* ActorToTeleport ) { if( ActorToTeleport == nullptr || Target == nullptr ) { return; }
Como você provavelmente notou, para chamar rotação / posição, eu chamo funções externas. Eles são chamados da classe de usuário UTool, que define funções estáticas que podem ser chamadas de qualquer lugar (incluindo plantas). O código deles é mostrado abaixo; você pode implementá-los da maneira que lhe parecer melhor (provavelmente é mais fácil colocá-los na classe de ator do 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; }
A transformação aqui é realizada calculando o produto escalar de vetores para determinar ângulos diferentes. O vetor Direction não é normalizado, ou seja, podemos multiplicar novamente o resultado Dots por vetores Target para obter a posição exatamente na mesma distância no espaço local do ator Target.
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(); }
Transformar a transformação foi um pouco mais difícil. No final, a melhor solução acabou sendo o uso de
quaternions , porque isso é muito mais preciso do que trabalhar com
ângulos de Euler normais e requer apenas algumas linhas de código. As rotações por quaternions são realizadas usando multiplicação; portanto, no nosso caso, aplicando Inverse () na rotação que queremos converter, a moveremos para o espaço local. Em seguida, basta multiplicá-lo novamente pelo turno Alvo para obter o turno final.
Criando uma malha do portal
Para ficar bonita do ponto de vista de um jogador, meu sistema de portal usa uma malha específica. A malha é dividida em dois planos diferentes:
- Plano 1 : O plano principal no qual o destino de renderização do portal é exibido. Este avião tem um comportamento bastante incomum, porque sua tarefa é se afastar um pouco do jogador enquanto ele se aproxima para evitar o corte pela câmera. Como as bordas do avião não se movem, mas apenas seus picos médios se movem, isso permite ao jogador se sobrepor na renderização do portal sem artefatos visuais. As bordas nas bordas têm seu próprio UV na metade superior, enquanto as bordas internas têm seu próprio UV na metade inferior, o que facilita mascará-las no shader.
- Plano 2 : Este plano é usado apenas para estender a caixa delimitadora padrão da malha. As normais dos vértices são direcionadas para baixo, portanto, mesmo em terreno não plano, a malha não será visível por padrão (porque o material de renderização não terá dois lados).
Por que usar uma malha como esta?
Eu decidi que o "avião 1" esticaria quando o jogador se aproximasse. Isso permite que o player se sobreponha ao portal e passe por ele sem aparar (cortar). Isso pode acontecer, por exemplo, se a câmera ainda não atravessou o plano do portal, mas os pés do jogador já o tocaram. Isso permite que você não corte o reprodutor e duplique a malha, por outro lado.
A tarefa "plano 2" é estender a caixa delimitadora padrão da malha. Como o "plano 1" é plano, a caixa delimitadora de um eixo tem uma espessura de 0 e, se a câmera estiver atrás dela, o mecanismo a interromperá (isto é, não a renderizará). O plano 1 tem um tamanho de 128 × 128, para que possa ser facilmente escalado usando o mecanismo. O plano 2 é um pouco maior e abaixo do piso (abaixo de 0).
Após criar a malha, simplesmente a exportamos de um editor 3D de terceiros e importamos para a Unreal. Será usado na próxima etapa.
Criando material do portal
Para exibir o outro lado do portal, precisamos criar nosso próprio material. Crie um novo material no navegador de conteúdo (eu o chamei
MAT_PortalBase ):
Agora abra-o e crie o seguinte gráfico:
Veja como o material funciona:
- FadeColor é a cor que será visível através do portal quando estiver muito longe. É necessário porque nem sempre renderizamos todos os portais, por isso obscurecemos a renderização quando o player / câmera está longe.
- Para descobrir a que distância o jogador está do portal, eu determino a distância entre a posição da câmera e a posição do ator. Em seguida, divido a distância pelo valor máximo com o qual quero realizar uma comparação. Por exemplo, se o número máximo definido é 2000 e a distância para o jogador é 1000, obtemos 0,5. Se o player estiver mais longe, receberei um valor maior que 1, então utilizo nós saturados para limitá-lo. Em seguida, vem o nó Smoothstep, usado para dimensionar a distância como um gradiente e controlar com mais precisão o sombreamento do portal. Por exemplo, quando o jogador está perto, a sombra desaparece completamente.
- Uso o cálculo da distância como o valor do canal alfa do nó Lerp para misturar a cor do sombreamento e a textura que renderizará o destino do portal.
- Por fim, isolei o componente Y das coordenadas UV para criar uma máscara que permite saber quais vértices da malha serão empurrados. Eu multiplico essa máscara pela quantidade de repulsa que preciso. Uso um valor negativo para que, quando as normais dos vértices sejam multiplicadas pelos vértices, elas se movam na direção oposta.
Depois de tudo isso, criamos material pronto para uso.
Criando um ator do portal no Blueprint
Vamos configurar uma nova classe de blueprint herdada do ator do Portal. Clique com o botão direito do mouse no navegador de conteúdo e selecione a classe Blueprint:
Agora digite "portal" no campo de pesquisa para selecionar a classe do portal:
Abra o bluetooth, se ainda não estiver aberto. Na lista de componentes, você verá a seguinte hierarquia:
Como esperávamos, há um componente raiz e uma raiz do portal. Vamos adicionar um componente de malha estática ao PortalRootComponent e carregar nela a malha criada na etapa anterior:
Também adicionamos a Collision Box, que será usada para determinar se o player está dentro do volume do portal:
A caixa Colisão está localizada abaixo do componente da cena associado à raiz principal e não sob a raiz do Portal. Também adicionei um ícone (outdoor) e um componente de seta para tornar o portal mais visível nos níveis. Claro, isso não é necessário.
Agora vamos configurar o material no blueprint.
Para começar, precisamos de duas variáveis - uma será do tipo
Actor e o nome é
PortalTarget , a segunda é do tipo
Dynamic Material Instance e é chamada
MaterialInstance . O PortalTarget será uma referência à posição que a janela do portal está visualizando (portanto, a variável é comum, com um ícone de olho aberto), para que possamos alterá-la quando o ator for colocado no nível. O MaterialInstance armazenará um link para material dinâmico para que, no futuro, possamos atribuir o destino de renderização do portal rapidamente.
Também precisamos adicionar nossos próprios nós de eventos. É melhor abrir o menu do botão direito do mouse no
Gráfico de Eventos e encontrar os nomes dos eventos:
E aqui para criar o seguinte diagrama:
- Begin Play : aqui chamamos a função pai SetTarget () do portal para atribuir um link ao ator, que será usado mais tarde no SceneCapture. Em seguida, criamos um novo material dinâmico e atribuímos a ele o valor da variável MaterialInstance. Com este novo material, podemos atribuí-lo ao componente de malha estática. Também dei uma textura fictícia ao material, mas isso é opcional.
- Limpar RTT : O objetivo deste recurso é limpar a textura Target Target atribuída ao material do portal. É lançado pelo gerente do portal.
- Definir RTT : o objetivo desta função é definir o material de destino da renderização do portal. É lançado pelo gerente do portal.
Até agora, terminamos o Bluetooth, mas retornaremos mais tarde para implementar as funções de Tick.
Gerenciador de portal
Portanto, agora temos todos os elementos básicos necessários para criar uma nova classe herdada do AActor, que será o Portal Manager. Você pode não precisar da classe Portal Manager no seu projeto, mas no meu caso, isso simplifica bastante o trabalho com alguns aspectos. Aqui está uma lista de tarefas executadas pelo gerente do Portal:
- O gerente do Portal é um ator criado pelo Player Controller e anexado a ele para rastrear o estado e a evolução do jogador no nível do jogo.
- Crie e destrua o portal de destino de renderização . A ideia é criar dinamicamente uma textura de destino de renderização que corresponda à resolução da tela do jogador. Além disso, ao alterar a resolução durante o jogo, o gerente a converterá automaticamente no tamanho desejado.
- O gerenciador do Portal localiza e atualiza o nível de ator do Portal para fornecer a eles um destino de renderização. Essa tarefa é executada de forma a garantir a compatibilidade com o nível de streaming. Quando um novo ator aparece, ele deve ter uma textura. Além disso, se o destino de renderização for alterado, o gerente também poderá atribuir um novo automaticamente. Isso facilita o gerenciamento do sistema, em vez de cada agente do Portal entrar em contato manualmente com o gerente.
- O componente SceneCapture é anexado ao gerenciador do Portal, para não criar uma cópia para cada portal. Além disso, permite reutilizá-lo sempre que mudarmos para um ator específico do portal no nível.
- Quando o portal decide teleportar o player, ele envia uma solicitação ao Portal Manager. Isso é necessário para atualizar os portais de origem e de destino (se houver), para que a transição ocorra sem juntas.
- O gerente do portal é atualizado no final da função tick () do personagem, para que tudo seja atualizado corretamente, incluindo a câmera do jogador. Isso garante que tudo na tela seja sincronizado e evite um atraso de um quadro durante a renderização pelo mecanismo.
Vamos dar uma olhada no cabeçalho do Portal Manager:
#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "ExedrePortalManager.generated.h"
Antes de entrar em detalhes, mostrarei como um ator é criado a partir da classe Player Controller, chamada a partir da função 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();
Portanto, criamos um ator, anexamos ao controlador do jogador (this) e salvamos o link e chamamos a função Init ().
Também é importante observar que atualizamos o ator manualmente a partir da classe 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 ); } }
E aqui está o construtor do Portal Manager. Observe que Tick está desativado, novamente porque atualizaremos manualmente o Portal Manager através do player.
AExedrePortalManager::AExedrePortalManager(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { PrimaryActorTick.bCanEverTick = false; PortalTexture = nullptr; UpdateDelay = 1.1f; PreviousScreenSizeX = 0; PreviousScreenSizeY = 0; }
Aqui estão as funções do get / set Portal Manager (depois disso, passaremos para coisas mais interessantes):
void AExedrePortalManager::SetControllerOwner( AExedrePlayerController* NewOwner ) { ControllerOwner = NewOwner; } FTransform AExedrePortalManager::GetCameraTransform() { if( SceneCapture != nullptr ) { return SceneCapture->GetComponentTransform(); } else { return FTransform(); } } UTexture* AExedrePortalManager::GetPortalTexture() {
Obviamente, a primeira coisa a começar é a função
Init () .
O principal objetivo desta função é criar o componente SceneCapture (ou seja, o dispositivo de captura mencionado acima) e configurá-lo corretamente. Começa com a criação de um novo objeto e seu registro como componente desse ator. Em seguida, passamos a definir propriedades relacionadas a esta captura.Propriedades a mencionar:- bCaptureEveryFrame = false : não queremos que a captura seja ativada quando não precisamos dela. Nós o gerenciaremos manualmente.
- bEnableClipPlane = true : uma propriedade muito importante para renderizar a captura do portal corretamente.
- bUseCustomProjectionMatrix = true : isso nos permite substituir a projeção do Capture pela nossa, com base no ponto de vista do jogador.
- CaptureSource = ESceneCaptureSource :: SCS_SceneColorSceneDepth : esse modo é um pouco caro, mas necessário para renderizar uma quantidade suficiente de informações.
As demais propriedades estão relacionadas principalmente aos parâmetros de pós-processamento. Eles são uma maneira conveniente de controlar a qualidade e, portanto, capturar o desempenho.A última parte chama a função que cria o Target Render, que veremos abaixo. void AExedrePortalManager::Init() {
GeneratePortalTexture () é uma função chamada quando necessário quando você precisa criar uma nova textura de destino de renderização para portais. Isso acontece na função de inicialização, mas também pode ser chamada durante a atualização do Portal Manager. É por isso que esta função possui uma verificação interna para alterar a resolução da janela de visualização. Se isso não aconteceu, a atualização não é executada.No meu caso, criei uma classe de wrapper para UCanvasRenderTarget2D. Eu chamei de ExedreScriptedTexture, é um componente que pode ser anexado a um ator. Criei esta classe para gerenciar convenientemente os destinos de renderização com atores que têm tarefas de renderização. Ele faz a inicialização adequada do destino de renderização e é compatível com meu próprio sistema de interface do usuário. No entanto, no contexto de portais, uma textura RenderTarget2D regular é mais que suficiente. Portanto, você pode simplesmente usá-lo. 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 mencionado acima, criei minha própria classe, portanto, as propriedades definidas aqui devem ser adaptadas ao destino de renderização usual.É importante entender onde a captura será exibida. Como o destino da renderização será exibido no jogo, isso significa que isso acontecerá antes de todo o pós-processamento e, portanto, precisamos renderizar a cena com informações suficientes (para armazenar valores acima de 1 para criar Bloom). Foi por isso que escolhi o formato RGBA16 (observe que ele possui seu próprio Enum, você precisará usar ETextureRenderTargetFormat).Para mais informações, consulte as seguintes fontes:
Além disso, consideraremos as funções de atualização. A função básica é bastante simples e causa mais complexidade. Há um atraso antes de chamar a função GeneratePortalTexture () para evitar a recriação do destino de renderização ao redimensionar a viewport (por exemplo, no editor). Durante a publicação do jogo, esse atraso pode ser removido. void AExedrePortalManager::Update( float DeltaTime ) {
Chamamos UpdatePortalsInWorld () para encontrar todos os portais presentes no mundo atual (incluindo todos os níveis carregados) e atualizá-los. A função também determina qual deles é "ativo", ou seja, visível para o jogador. Se encontrarmos um portal ativo, chamamos UpdateCapture () , que controla o componente SceneCapture.
Veja como a atualização mundial funciona em UpdatePortalsInWorld () :- ( )
- iterator ,
- , , ClearRTT() , . (, ).
- , , , , .
A verificação que determina a correção do portal é simples: priorizamos o portal mais próximo do jogador, porque ele provavelmente será o mais visível do ponto de vista dele. Para descartar parentes, mas, por exemplo, portais localizados atrás do player, serão necessárias verificações mais complexas, mas eu não queria me concentrar nisso no meu tutorial, porque pode se tornar bastante difícil. AExedrePortal* AExedrePortalManager::UpdatePortalsInWorld() { if( ControllerOwner == nullptr ) { return nullptr; } AExedreCharacter* Character = ControllerOwner->GetCharacter();
É hora de considerar a função UpdateCapture () .Este é um recurso de atualização que captura o outro lado do portal. A partir dos comentários, tudo deve ficar claro, mas aqui está uma breve descrição:- Nós temos links para o Character e o Player Controller.
- Verificamos se está tudo correto (Portal, componente SceneCapture, Player).
- Camera Target .
- , SceneCapture.
- SceneCapture Target.
- , SceneCapure , , .
- Render Target SceneCapture, .
- PlayerController.
- , Capture SceneCapture .
Como podemos ver, ao teleportar um jogador, um elemento-chave do comportamento natural e sem falhas do SceneCapture é a transformação correta da posição e rotação do portal no espaço de destino local.Para a definição de ConvertLocationToActorSpace (), consulte “Teleportando um ator”.
void AExedrePortalManager::UpdateCapture( AExedrePortal* Portal ) { if( ControllerOwner == nullptr ) { return; } AExedreCharacter* Character = ControllerOwner->GetCharacter();
A função GetCameraProjectionMatrix () não existe por padrão na classe PlayerController, eu mesmo a adicionei. É mostrado abaixo: 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, precisamos implementar a chamada para a função Teleport. O motivo do processamento parcial do teletransporte através do gerenciador do Portal é que é necessário garantir a atualização dos portais necessários, pois somente o Manager possui informações sobre todos os portais em cena.Se tivermos dois portais conectados, ao mudar de um para outro, precisamos atualizar ambos em um Tick. Caso contrário, o jogador se teletransportará e estará do outro lado do portal, mas o Portal de Alvos não estará ativo até o próximo quadro / medida. Isso criará lacunas visuais com o material de deslocamento da malha plana que vimos acima. void AExedrePortalManager::RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ) { if( Portal != nullptr && TargetToTeleport != nullptr ) { Portal->TeleportActor( TargetToTeleport );
Bem, é isso, finalmente terminamos o Portal Manager!Concluir o projeto
Depois de concluir o Portal Manager, precisamos apenas concluir o próprio agente do Portal, após o qual o sistema funcionará. A única coisa que falta aqui são os recursos do Tick:Veja como funciona:- Estamos atualizando o material para que ele não permaneça ativo.
- Se o portal estiver inativo no momento , o restante da medida será descartado.
- Nós obtemos a classe Character para acessar o local da câmera .
- A primeira parte verifica se a câmera está na caixa de colisão do portal. Nesse caso, compensamos a malha do portal com seu Material .
- A segunda parte é verificar novamente o local dentro da caixa de colisão. Se for executado, chamamos uma função que verifica se atravessamos o portal .
- , Portal manager, Teleport .
Na captura de tela do meu gráfico, você pode observar dois pontos interessantes: É Point Inside Box e Get Portal Manager . Ainda não expliquei essas duas funções. Essas são funções estáticas que defini na minha própria classe para que você possa chamá-las de qualquer lugar. Este é um tipo de classe auxiliar. O código dessas funções é mostrado abaixo, você mesmo pode decidir onde inseri-las. Se você não precisar deles fora do sistema do portal, poderá inseri-los diretamente na classe de ator do Portal.No começo, eu queria usar o sistema de colisão para encontrar o ator do portal dentro da caixa de colisão, mas me pareceu não ser confiável o suficiente. Além disso, parece-me que esse método é mais rápido de usar e tem uma vantagem: leva em consideração a rotação do ator. bool IsPointInsideBox( FVector Point, UBoxComponent* Box ) { if( Box != nullptr ) {
AExedrePortalManager* GetPortalManager( AActor* Context ) { AExedrePortalManager* Manager = nullptr;
A última parte do ator do Blueprint é o ForceTick . Lembre-se de que o Force Tick é chamado quando um jogador atravessa um portal e fica ao lado de outro portal para o qual o Portal Manager está forçando uma atualização. Como acabamos de nos teletransportar, não é necessário usar o mesmo código e você pode usar sua versão simplificada:O processo inicia aproximadamente ao mesmo tempo que a função Tick, mas apenas executamos a primeira parte da sequência, que atualiza o material.Nós terminamos?
Quase.Se implementarmos o sistema do portal neste formulário, provavelmente encontraremos o seguinte problema:O que está acontecendo aqui?Neste gif, a taxa de quadros do jogo é limitada a 6 FPS para mostrar o problema mais claramente. Em um quadro, o cubo desaparece porque o sistema de recorte do Unreal Engine o considera invisível.Isso ocorre porque a descoberta é realizada no quadro atual e usada no próximo. Isso cria um atraso de um quadro . Isso geralmente pode ser resolvido expandindo a caixa delimitadora do objeto para que seja registrado antes de se tornar visível. No entanto, isso não funcionará aqui, porque quando atravessamos o portal, nos teletransportamos de um lugar para outro completamente diferente.Desabilitar o sistema de recorte também é impossível, principalmente porque em níveis com muitos objetos isso reduz o desempenho. Além disso, tentei muitas equipes do mecanismo Unreal, mas não obtive resultados positivos: em todos os casos, permaneceu um atraso de um quadro. Felizmente, depois de um estudo detalhado do código fonte do Unreal Engine, consegui encontrar uma solução (o caminho foi longo - levou mais de uma semana)!Assim como no componente SceneCapture, você pode dizer à câmera do jogador que fizemos um corte rápido- a posição da câmera saltou entre dois quadros, o que significa que não podemos confiar nas informações do quadro anterior. Esse comportamento pode ser observado ao usar o Matinee ou o Sequencer, por exemplo, ao alternar as câmeras: desfoque ou suavização de movimento não podem confiar nas informações do quadro anterior.Para fazer isso, precisamos considerar dois aspectos:- LocalPlayer : essa classe processa várias informações (por exemplo, a janela de visualização do jogador) e está associada ao PlayerController. É aqui que podemos influenciar o processo de renderização da câmera do jogador.
- PlayerController : quando um jogador se teleporta, essa classe começa a se unir graças ao acesso ao LocalPlayer.
A grande vantagem dessa solução é que a intervenção no processo de renderização do mecanismo é mínima e fácil de manter em futuras atualizações do Unreal Engine.
Vamos começar criando uma nova classe herdada do LocalPlayer. Abaixo está um cabeçalho que identifica dois componentes principais: redefinindo os cálculos do Scene Viewport e uma nova função para chamar a colagem da câmera. #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; };
Veja como tudo é implementado: #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 () apenas inicia o Camera Cut com um valor booleano. Quando o mecanismo chama a função CalcSceneView () , primeiro executamos a função original. Então verificamos, precisamos realizar colagem. Nesse caso, redefinimos a variável booleana Camera Cut dentro da estrutura FSceneView , que será usada pelo processo de renderização do mecanismo, e redefinimos a variável booleana (use-a).
No lado do Player Controller, as alterações são mínimas. Você precisa adicionar uma variável ao cabeçalho para armazenar um link para a classe nativa LocalPlayer: UPROPERTY() UExedreLocalPlayer* LocalPlayer;
Em seguida, na função BeginPlay () : LocalPlayer = Cast<UExedreLocalPlayer>( GetLocalPlayer() );
Também adicionei uma função para iniciar rapidamente o Cut: void AExedrePlayerController::PerformCameraCut() { if( LocalPlayer != nullptr ) { LocalPlayer->PerformCameraCut(); } }
Por fim, na função RequestTeleportByPortal () do Portal Manager , podemos executar durante o teletransporte do Camera Cut: void AExedrePortalManager::RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ) { if( Portal != nullptr && TargetToTeleport != nullptr ) { if( ControllerOwner != nullptr ) { ControllerOwner->PerformCameraCut(); } [...]
E isso é tudo!O corte da câmera deve ser chamado antes que o SceneCapture seja atualizado, e é por isso que está no início da função.Resultado final
Agora aprendemos a pensar em portais.Se o sistema funcionar bem, poderemos criar o seguinte:Se você estiver tendo problemas, verifique o seguinte:- Verifique se o Portal Manager está criado e inicializado corretamente.
- O destino de renderização foi criado corretamente (você pode usar o criado no navegador de conteúdo para começar).
- Portais estão corretamente ativados e desativados.
- Os portais têm o ator Alvo definido corretamente no editor.
Perguntas e Respostas
As perguntas mais populares que me foram feitas sobre este tutorial:É possível implementar isso em blunts, e não através de C ++?A maior parte do código pode ser implementada em blunts, com exceção de dois aspectos:- A função LocalPlayer GetProjectionData () usada para obter a matriz de projeção não está disponível nos blueprints.
- A função LocalPlayer CalcSceneView () , essencial para resolver o problema do sistema de recorte, não está disponível nos blueprints.
Portanto, você precisa usar uma implementação C ++ para acessar essas duas funções ou modificar o código-fonte do mecanismo para torná-las acessíveis através de blueprints.Posso usar este sistema em VR?Sim, na maior parte. No entanto, algumas partes terão que ser adaptadas, por exemplo:- Você precisa usar dois Destinos de renderização (um para cada olho) e mascará-los no material do portal para exibir lado a lado no espaço da tela. Cada destino de renderização deve ter metade da largura da resolução do dispositivo VR.
- Você precisa usar dois SceneCapture para renderizar o alvo com a distância correta (a distância entre os olhos) para criar efeitos estereoscópicos.
O principal problema será o desempenho, porque o outro lado do portal precisará ser renderizado duas vezes.Outro objeto pode atravessar o portal?Não existe no meu código. No entanto, torná-lo mais geral não é tão difícil. Para fazer isso, o portal precisa rastrear mais informações sobre todos os objetos próximos, a fim de verificar se eles o atravessam.O sistema suporta recursão (portal dentro do portal)?Este tutorial não é. Para recursão, você precisa de um destino de renderização adicional e do SceneCapture. Também será necessário determinar qual RenderTarget renderizar primeiro e assim por diante. Isso é bastante difícil e eu não queria fazer isso, porque para o meu projeto isso não é necessário.Posso atravessar o portal perto da parede?Infelizmente não. No entanto, vejo duas maneiras de implementar isso (teoricamente):- Desative as colisões do jogador para que ele possa atravessar as paredes. É fácil de implementar, mas leva a muitos efeitos colaterais.
- Invada um sistema de colisão para criar um buraco dinamicamente, o que permitirá ao jogador passar. Para fazer isso, você precisa modificar o sistema físico do mecanismo. No entanto, pelo que sei, depois de carregar o nível, a física estática não pode ser atualizada. Portanto, para oferecer suporte a esse recurso, será necessário muito trabalho. Se seus portais são estáticos, provavelmente você pode solucionar esse problema usando o nível de streaming para alternar entre diferentes colisões.