In diesem Artikel werde ich Ihnen erklären, wie Sie Portale in Unreal Engine 4 erstellen. Ich habe keine Quellen gefunden, die ein solches System detailliert beschreiben (Überwachung durch Portale und Weitergabe durch diese), daher habe ich beschlossen, meine eigenen zu schreiben.
Was ist ein Portal?
Beginnen wir mit Beispielen und Erklärungen, was ein Portal ist. Der einfachste Weg, die Portale als einen Weg von einem Raum zum anderen zu beschreiben. In einigen beliebten Spielen wird dieses Konzept für visuelle Effekte und sogar für die Spielmechanik verwendet:
Beispiele für Spielportale (GIF)Vorkammer (2013) und Portal (2007)Beute, 2006 Das berühmteste der drei Spiele ist wahrscheinlich Portal, aber ich persönlich habe Prey immer bewundert und sie wollte sie kopieren. Einmal habe ich versucht, meine eigene Version in Unreal Engine 4 zu implementieren, aber es gelang mir nicht wirklich, da der Engine die Funktionalität fehlte. Trotzdem gelang es mir, diese Experimente durchzuführen:
Erst in neuen Versionen von Unreal Engine gelang es mir jedoch, den gewünschten Effekt zu erzielen:
Portale - wie funktionieren sie?
Bevor wir mit den Einzelheiten fortfahren, schauen wir uns das allgemeine Bild der Funktionsweise von Portalen an.
Tatsächlich ist ein Portal ein Fenster, das nicht nach draußen geht, sondern an einen anderen Ort. Das heißt, wir legen lokal einen bestimmten Ansichtspunkt relativ zum Objekt fest und replizieren diesen Ansichtspunkt an einer anderen Stelle. Mit diesem Prinzip können wir zwei Räume verbinden, auch wenn sie sehr weit voneinander entfernt sind. Das Fenster ähnelt einer Maske, mit der wir herausfinden können, wo und wann ein anderer Bereich anstelle des ursprünglichen angezeigt werden soll. Da der Ausgangspunkt der Sichtweise an anderer Stelle wiederholt wird, entsteht die Illusion von Kontinuität.

In diesem Bild befindet sich das Aufnahmegerät (SceneCapture in UE4) vor dem Raum, der dem Raum aus der Sicht des Spielers entspricht. Alles, was nach der Linie sichtbar ist, wird durch das ersetzt, was die Aufnahme sehen kann. Da sich das Erfassungsgerät zwischen der Tür und anderen Objekten befinden kann, ist es wichtig, die sogenannte „Clipping-Ebene“ zu verwenden. Im Fall des Portals möchten wir, dass die enge Schnittebene die vor dem Portal sichtbaren Objekte maskiert.
Zusammenfassend. Wir brauchen:
- Standort des Spielers
- Portal-Einstiegspunkt
- Portalausgangspunkt
- Clipping-Gerät mit Clipping-Ebene
Wie implementiere ich das in Unreal Engine?
Ich habe mein System auf der Grundlage von zwei Hauptklassen erstellt, die von
PlayerController und
Character verwaltet werden . Die
Portal- Klasse ist ein echter Portal-Einstiegspunkt, dessen Ansichts- / Austrittspunkt der Zielakteur ist. Es gibt auch einen
Portal-Manager , der vom PlayerController generiert und von Character aktualisiert wird, um jedes Portal auf der Ebene zu verwalten und zu aktualisieren sowie das SceneCapture-Objekt (das allen Portalen gemeinsam ist) zu bearbeiten.
Beachten Sie, dass das Lernprogramm erwartet, dass Sie über Code auf die Klassen Character und PlayerController zugreifen können. In meinem Fall heißen sie ExedreCharacter und ExedrePlayerController.
Erstellen einer Portal Actor Class
Beginnen wir mit dem Akteur des Portals, mit dem die „Fenster“ festgelegt werden, durch die wir die Ebene betrachten. Die Aufgabe des Schauspielers besteht darin, Informationen über den Spieler bereitzustellen, um verschiedene Positionen und Züge zu berechnen. Er wird auch erkennen, ob der Spieler das Portal überquert, und seine Teleportation.
Bevor ich mit einer ausführlichen Diskussion des Schauspielers beginne, möchte ich einige Konzepte erläutern, die ich zur Verwaltung des Portalsystems erstellt habe:
- Zur bequemen Ablehnung von Berechnungen hat das Portal einen Aktiv-Inaktiv-Status. Dieser Status wird vom Portal Manager aktualisiert.
- Das Portal hat Vorder- und Rückseite, die durch seine Position und Richtung (Vorwärtsvektor) bestimmt werden.
- Um herauszufinden, ob der Spieler das Portal überquert, speichert er die vorherige Position des Spielers und vergleicht sie mit der aktuellen. Wenn sich der Spieler im vorherigen Takt vor dem Portal und im aktuellen - hinter ihm - befand, glauben wir, dass der Spieler es überquert hat. Das umgekehrte Verhalten wird ignoriert.
- Das Portal verfügt über eine begrenzte Lautstärke, um keine Berechnungen und Überprüfungen durchzuführen, bis sich der Player in dieser Lautstärke befindet. Beispiel: Ignorieren Sie die Kreuzung, wenn der Player das Portal nicht berührt.
- Der Standort des Spielers wird aus dem Standort der Kamera berechnet, um ein korrektes Verhalten sicherzustellen, wenn der Blickwinkel das Portal überquert, nicht jedoch den Körper des Spielers.
- Das Portal erhält ein Renderziel, das in jeder Kennzahl einen anderen Blickwinkel anzeigt, falls die Textur beim nächsten Mal falsch ist und ersetzt werden muss.
- Das Portal speichert einen Link zu einem anderen Akteur namens Target, um zu wissen, wo der andere Bereich kontaktiert werden soll.
Mit diesen Regeln habe ich eine neue ExedrePortal-Klasse erstellt, die von AActor als Ausgangspunkt geerbt wurde. Hier ist der Titel:
#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;
Wie Sie sehen können, gibt es die meisten der hier beschriebenen Verhaltensweisen. Nun wollen wir sehen, wie sie im Körper verarbeitet werden (.cpp).
Der Designer hier bereitet die Stammkomponenten vor. Ich habe beschlossen, zwei Stammkomponenten zu erstellen, da der Portal-Akteur sowohl grafische Effekte als auch Kollisionen / Erkennung kombiniert. Daher brauchte ich eine einfache Methode, um festzustellen, wo sich die Fenster- / Portalebene befindet, ohne dass Bluetooth-Funktionen oder andere Tricks erforderlich sind. PortalRootComponent ist die Basis für alle Berechnungen im Zusammenhang mit dem Portal.
Das Portalstammverzeichnis ist auf dynamisch eingestellt, falls die Blueprint-Klasse es animiert (verwendet beispielsweise eine Open / Close-Animation).
Es gibt nur Get- und Set-Funktionen und nichts weiter. Wir werden den Aktivitätsstatus von einem anderen Ort aus verwalten.
bool AExedrePortal::IsActive() { return bIsActive; } void AExedrePortal::SetActive( bool NewActive ) { bIsActive = NewActive; }
Blueprint-Ereignisse, ich mache nichts in der C ++ - Klasse.
void AExedrePortal::ClearRTT_Implementation() { } void AExedrePortal::SetRTT_Implementation( UTexture* RenderTexture ) { } void AExedrePortal::ForceTick_Implementation() { }
Die Funktionen Get und Set für den Zielakteur. Auch in diesem Teil gibt es nichts Komplizierteres.
AActor* AExedrePortal::GetTarget() { return Target; } void AExedrePortal::SetTarget( AActor* NewTarget ) { Target = NewTarget; }
Mit dieser Funktion können wir leicht überprüfen, ob sich ein Punkt vor einer Ebene befindet, und in unserem Fall handelt es sich um ein Portal. Die Funktion verwendet die FPlane-Struktur der UE4-Engine, um Berechnungen durchzuführen.
bool AExedrePortal::IsPointInFrontOfPortal( FVector Point, FVector PortalLocation, FVector PortalNormal ) { FPlane PortalPlane = FPlane( PortalLocation, PortalNormal ); float PortalDot = PortalPlane.PlaneDot( Point );
Diese Funktion prüft, ob der Punkt die Portalebene überschritten hat. Hier verwenden wir die alte Position, um herauszufinden, wie sich der Punkt verhält. Diese Funktion ist üblich, damit sie mit jedem Schauspieler funktioniert, aber in meinem Fall wird sie nur mit dem Player verwendet.
Die Funktion erstellt eine Richtung / ein Segment zwischen dem vorherigen und dem aktuellen Standort und prüft dann, ob sie die Ebene schneiden. Wenn ja, prüfen wir, ob es sich in die richtige Richtung kreuzt (von vorne nach hinten?).
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 );
Schauspieler teleportieren
Der letzte Teil des Portal-Akteurs, den wir uns ansehen werden, ist die
TeleportActor () -Funktion.
Wenn Sie einen Schauspieler von Punkt A nach Punkt B teleportieren, müssen Sie seine Bewegung und Position nachbilden. Wenn zum Beispiel ein Spieler das Portal betritt, scheint es ihm in Kombination mit geeigneten visuellen Effekten, dass er durch eine gewöhnliche Tür gegangen ist.
Der Schnittpunkt des Portals fühlt sich an, als würde man sich in einer geraden Linie bewegen, aber in Wirklichkeit passiert etwas völlig anderes. Beim Verlassen des Portals befindet sich der Spieler möglicherweise in einem ganz anderen Kontext. Betrachten Sie ein Beispiel aus Portal:
Wie Sie sehen können, dreht sich die Kamera beim Überqueren des Portals relativ zu ihrem Vorwärtsvektor (dreht sich). Dies liegt daran, dass Start- und Endpunkt parallel zu verschiedenen Ebenen liegen:
Damit dies funktioniert, müssen wir die Bewegung des Spielers in den relativen Bereich des Portals umwandeln, um sie in den Zielbereich umzuwandeln. Durch diese Implementierung können wir sicher sein, dass der Spieler nach dem Betreten des Portals und dem Verlassen der anderen Seite korrekt in Bezug auf den Platz ausgerichtet ist. Dies gilt nicht nur für die Position und Rotation des Schauspielers, sondern auch für seine
Geschwindigkeit .
Wenn wir einen Schauspieler ohne Änderungen teleportieren und ihn in eine lokale Rotation umwandeln, kann sich der Schauspieler auf den Kopf stellen. Dies kann für Objekte geeignet sein, gilt jedoch nicht für die Charaktere oder den Spieler selbst. Sie müssen die Position des Schauspielers ändern, wie oben im Beispiel von Portal gezeigt.
void AExedrePortal::TeleportActor( AActor* ActorToTeleport ) { if( ActorToTeleport == nullptr || Target == nullptr ) { return; }
Wie Sie wahrscheinlich bemerkt haben, rufe ich externe Funktionen auf, um Rotation / Position aufzurufen. Sie werden von der UTool-Benutzerklasse aufgerufen, die statische Funktionen definiert, die von überall aufgerufen werden können (einschließlich Blaupausen). Ihr Code wird unten gezeigt. Sie können sie so implementieren, wie es Ihnen am besten erscheint (es ist wahrscheinlich einfacher, sie einfach in die Portal-Actor-Klasse einzufügen).
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; }
Die Transformation wird hier durchgeführt, indem das Skalarprodukt von Vektoren berechnet wird, um verschiedene Winkel zu bestimmen. Der Richtungsvektor ist nicht normalisiert, dh wir können das Punktergebnis erneut mit Zielvektoren multiplizieren, um die Position im lokalen Raum des Zielakteurs in genau derselben Entfernung zu erhalten.
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(); }
Die Transformation zu drehen war etwas schwieriger umzusetzen. Am Ende stellte sich heraus, dass die Verwendung von
Quaternionen die beste Lösung war, da dies viel genauer ist als die Arbeit mit normalen
Euler-Winkeln und nur wenige Codezeilen erfordert. Rotationen durch Quaternionen werden mithilfe der Multiplikation ausgeführt. In unserem Fall wird Inverse () auf die Rotation angewendet, die konvertiert werden soll, und in den lokalen Raum verschoben. Als nächstes müssen wir es nur noch einmal mit der Zielrunde multiplizieren, um die letzte Runde zu erhalten.
Erstellen eines Portalnetzes
Um aus der Sicht eines Spielers schön auszusehen, verwendet mein Portalsystem ein bestimmtes Netz. Das Netz ist in zwei verschiedene Ebenen unterteilt:
- Ebene 1 : Die Hauptebene, auf der das Renderziel des Portals angezeigt wird. Dieses Flugzeug hat ein eher ungewöhnliches Verhalten, da es die Aufgabe hat, sich ein wenig vom Spieler abzuheben, wenn er sich nähert, um ein Abschneiden durch die Kamera zu vermeiden. Da sich die Ränder des Flugzeugs nicht bewegen, sondern nur die mittleren Spitzen, kann der Spieler das Rendern des Portals ohne visuelle Artefakte überlagern. Die Kanten an den Kanten haben in der oberen Hälfte eine eigene UV-Strahlung, während die Innenkanten in der unteren Hälfte eine eigene UV-Strahlung haben, wodurch es einfach ist, sie im Shader zu maskieren.
- Ebene 2 : Diese Ebene wird nur zum Erweitern des Standardbegrenzungsrahmens des Netzes verwendet. Die Normalen der Scheitelpunkte sind nach unten gerichtet, sodass das Netz selbst auf nicht planarem Boden standardmäßig nicht sichtbar ist (da das Rendering-Material nicht zweiseitig ist).
Warum ein solches Netz verwenden?
Ich entschied, dass sich "Flugzeug 1" ausdehnen würde, wenn sich der Spieler näherte. Dadurch kann der Spieler das Portal überlappen und es passieren, ohne es zu beschneiden (zu schneiden). Dies kann beispielsweise passieren, wenn die Kamera die Ebene des Portals noch nicht überquert hat, die Füße des Spielers sie jedoch bereits berührt haben. Auf diese Weise können Sie den Player nicht abschneiden und das Netz andererseits duplizieren.
Die Aufgabe „Ebene 2“ besteht darin, den Standardbegrenzungsrahmen des Netzes zu erweitern. Da „Ebene 1“ flach ist, hat der Begrenzungsrahmen auf einer Achse eine Dicke von 0, und wenn sich die Kamera dahinter befindet, wird er vom Motor abgeschnitten (dh nicht gerendert). Ebene 1 hat eine Größe von 128 × 128, sodass sie mit der Engine einfach skaliert werden kann. Ebene 2 ist etwas größer und befindet sich unter dem Boden (unter 0).
Nachdem wir das Netz erstellt haben, exportieren wir es einfach aus einem 3D-Editor eines Drittanbieters und importieren es in Unreal. Es wird im nächsten Schritt verwendet.
Portalmaterial erstellen
Um die andere Seite des Portals anzuzeigen, müssen wir unser eigenes Material erstellen. Erstellen Sie neues Material im Inhaltsbrowser (ich habe es
MAT_PortalBase genannt ):
Öffnen Sie es nun und erstellen Sie das folgende Diagramm:
So funktioniert das Material:
- FadeColor ist die Farbe, die durch das Portal sichtbar ist, wenn es sehr weit entfernt ist. Dies ist erforderlich, da nicht immer alle Portale gerendert werden. Daher wird das Rendern verdeckt, wenn der Player / die Kamera weit entfernt ist.
- Um herauszufinden, wie weit der Player vom Portal entfernt ist, bestimme ich den Abstand zwischen Kameraposition und Schauspielerposition. Dann dividiere ich den Abstand durch den Maximalwert, mit dem ich einen Vergleich durchführen möchte. Wenn zum Beispiel das von mir festgelegte Maximum 2000 ist und die Entfernung zum Spieler 1000 beträgt, erhalten wir 0,5. Wenn der Spieler weiter ist, erhalte ich einen Wert größer als 1, daher verwende ich gesättigte Knoten, um ihn zu begrenzen. Als nächstes kommt der Smoothstep-Knoten, mit dem die Entfernung als Farbverlauf skaliert und die Portalschattierung genauer gesteuert wird. Wenn der Player beispielsweise in der Nähe ist, verschwindet der Schatten vollständig.
- Ich verwende die Entfernungsberechnung als Alpha-Kanalwert für den Lerp- Knoten, um die Schattierungsfarbe und die Textur zu mischen, die das Portalziel rendern.
- Schließlich isoliere ich die Y-Komponente der UV-Koordinaten, um eine Maske zu erstellen, mit der Sie wissen, welche Scheitelpunkte des Netzes verschoben werden. Ich multipliziere diese Maske mit der Abstoßungsmenge, die ich brauche. Ich verwende einen negativen Wert, damit sich die Normalen der Scheitelpunkte, wenn sie mit den Scheitelpunkten multipliziert werden, in die entgegengesetzte Richtung bewegen.
Nachdem wir dies alles getan haben, haben wir gebrauchsfertiges Material erstellt.
Erstellen eines Portal Actor in Blueprint
Lassen Sie uns eine neue Blueprint-Klasse einrichten, die vom Portal-Akteur erbt. Klicken Sie mit der rechten Maustaste auf den Inhaltsbrowser und wählen Sie die Blueprint-Klasse aus:
Geben Sie nun "Portal" in das Suchfeld ein, um die Portalklasse auszuwählen:
Öffnen Sie Bluetooth, falls es noch nicht geöffnet ist. In der Liste der Komponenten sehen Sie die folgende Hierarchie:
Wie erwartet gibt es eine Root-Komponente und ein Portal-Root. Fügen wir PortalRootComponent eine statische Netzkomponente hinzu und laden das im vorherigen Schritt erstellte Netz hinein:
Wir fügen auch die Kollisionsbox hinzu, mit der bestimmt wird, ob sich der Player innerhalb des Portal-Volumes befindet:
Das Feld Kollision befindet sich unter der Szenenkomponente, die dem Hauptstamm zugeordnet ist, und nicht unter dem Portalstamm. Ich habe auch ein Symbol (Werbetafel) und eine Pfeilkomponente hinzugefügt, um das Portal auf den Ebenen besser sichtbar zu machen. Dies ist natürlich nicht erforderlich.
Lassen Sie uns nun das Material in Blaupause einrichten.
Zunächst benötigen wir zwei Variablen - eine vom Typ
Actor und der Name
PortalTarget , die zweite vom Typ
Dynamic Material Instance und heißt
MaterialInstance . PortalTarget ist ein Verweis auf die Position, auf die das Portalfenster schaut (daher ist die Variable häufig mit einem Symbol für offenes Auge), damit wir sie ändern können, wenn der Akteur auf der Ebene platziert wird. MaterialInstance speichert einen Link zu dynamischem Material, damit wir in Zukunft das Renderziel des Portals im laufenden Betrieb zuweisen können.
Wir müssen auch unsere eigenen Ereignisknoten hinzufügen. Öffnen Sie am besten das Kontextmenü im
Ereignisdiagramm und suchen Sie die Namen der Ereignisse:
Und hier, um das folgende Diagramm zu erstellen:
- Spiel starten: Hier rufen wir die übergeordnete Funktion SetTarget () des Portals auf, um ihm einen Link zum Schauspieler zuzuweisen, der später für SceneCapture verwendet wird. Anschließend erstellen wir ein neues dynamisches Material und weisen ihm den Wert der MaterialInstance-Variablen zu. Mit diesem neuen Material können wir es der statischen Netzkomponente zuweisen. Ich habe dem Material auch eine Dummy-Textur gegeben, aber dies ist optional.
- RTT löschen: Mit dieser Funktion wird die dem Portalmaterial zugewiesene Renderziel-Textur gelöscht. Es wird vom Portal-Manager gestartet.
- RTT festlegen: Mit dieser Funktion wird das Renderzielmaterial des Portals festgelegt. Es wird vom Portal-Manager gestartet.
Bisher sind wir mit Bluetooth fertig, aber wir werden später darauf zurückkommen, um Tick-Funktionen zu implementieren.
Portal Manager
Jetzt haben wir alle grundlegenden Elemente, die zum Erstellen einer neuen Klasse erforderlich sind, die von AActor geerbt wurde, nämlich Portal Manager. Möglicherweise benötigen Sie die Portal Manager-Klasse nicht in Ihrem Projekt, aber in meinem Fall vereinfacht dies die Arbeit mit einigen Aspekten erheblich. Hier ist eine Liste der vom Portal-Manager ausgeführten Aufgaben:
- Der Portal-Manager ist ein vom Player-Controller erstellter und damit verbundener Akteur, um den Status und die Entwicklung des Spielers innerhalb der Spielebene zu verfolgen.
- Renderzielportal erstellen und zerstören. Die Idee ist, dynamisch eine Render-Zieltextur zu erstellen, die der Bildschirmauflösung des Players entspricht. Wenn Sie die Auflösung während des Spiels ändern, konvertiert der Manager sie automatisch in die gewünschte Größe.
- Der Portal-Manager findet und aktualisiert die Portal-Darstellerebene, um ihnen ein Renderziel zu geben. Diese Aufgabe wird so ausgeführt, dass die Kompatibilität mit Level-Streaming gewährleistet ist. Wenn ein neuer Schauspieler erscheint, sollte er eine Textur bekommen. Wenn sich das Renderziel ändert, kann der Manager außerdem automatisch ein neues Ziel zuweisen. Dies erleichtert die Verwaltung des Systems, anstatt dass jeder Portal-Akteur den Manager manuell kontaktiert.
- Die SceneCapture- Komponente ist an den Portal-Manager angehängt, um nicht für jedes Portal eine Kopie zu erstellen. Darüber hinaus können Sie es jedes Mal wiederverwenden, wenn wir zu einem bestimmten Portal-Akteur auf der Ebene wechseln.
- Wenn das Portal beschließt, den Player zu teleportieren , sendet es eine Anfrage an Portal Manager. Dies ist erforderlich, um sowohl das Quell- als auch das Zielportal (falls vorhanden) zu aktualisieren, sodass der Übergang ohne Verbindungen erfolgt.
- Der Portal-Manager wird am Ende der Funktion " Character tick ()" aktualisiert, sodass alles korrekt aktualisiert wird, einschließlich der Kamera des Players. Dies stellt sicher, dass alles auf dem Bildschirm synchronisiert ist, und vermeidet eine Verzögerung von einem Frame während des Renderns durch die Engine.
Werfen wir einen Blick auf den Portal Manager-Header:
#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "ExedrePortalManager.generated.h"
Bevor ich auf Details eingehe, werde ich zeigen, wie ein Akteur aus der Player Controller-Klasse erstellt wird, die von der BeginPlay () -Funktion aufgerufen wird:
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();
Also erstellen wir einen Akteur, hängen ihn an den Controller des Players an (diesen), speichern den Link und rufen die Funktion Init () auf.
Es ist auch wichtig zu beachten, dass wir den Akteur manuell aus der Zeichenklasse aktualisieren:
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 ); } }
Und hier ist der Konstruktor von Portal Manager. Beachten Sie, dass Tick wieder deaktiviert ist, da wir Portal Manager manuell über den Player aktualisieren.
AExedrePortalManager::AExedrePortalManager(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { PrimaryActorTick.bCanEverTick = false; PortalTexture = nullptr; UpdateDelay = 1.1f; PreviousScreenSizeX = 0; PreviousScreenSizeY = 0; }
Hier sind die Funktionen von get / set Portal Manager (danach werden wir zu interessanteren Dingen übergehen):
void AExedrePortalManager::SetControllerOwner( AExedrePlayerController* NewOwner ) { ControllerOwner = NewOwner; } FTransform AExedrePortalManager::GetCameraTransform() { if( SceneCapture != nullptr ) { return SceneCapture->GetComponentTransform(); } else { return FTransform(); } } UTexture* AExedrePortalManager::GetPortalTexture() {
Das erste, mit dem Sie beginnen, ist natürlich die Funktion
Init () .
Das Hauptziel dieser Funktion besteht darin, die SceneCapture-Komponente (dh das oben erwähnte Aufnahmegerät) zu erstellen und korrekt zu konfigurieren. Es beginnt mit der Erstellung eines neuen Objekts und seiner Registrierung als Bestandteil dieses Akteurs. Anschließend werden die Eigenschaften für diese Erfassung festgelegt.Zu erwähnende Eigenschaften:- bCaptureEveryFrame = false : Wir möchten nicht, dass die Erfassung aktiviert wird, wenn wir sie nicht benötigen. Wir werden es manuell verwalten.
- bEnableClipPlane = true : Eine ziemlich wichtige Eigenschaft zum korrekten Rendern der Portalaufnahme.
- bUseCustomProjectionMatrix = true : Dies ermöglicht es uns, die Capture-Projektion basierend auf dem Standpunkt des Players durch unsere eigene zu ersetzen.
- CaptureSource = ESceneCaptureSource :: SCS_SceneColorSceneDepth : Dieser Modus ist etwas teuer, aber erforderlich, um eine ausreichende Menge an Informationen zu rendern.
Die übrigen Eigenschaften beziehen sich hauptsächlich auf die Nachbearbeitungsparameter. Sie sind eine bequeme Möglichkeit, die Qualität zu kontrollieren und damit die Leistung zu erfassen.Der letzte Teil ruft die Funktion auf, mit der das Renderziel erstellt wird (siehe unten). void AExedrePortalManager::Init() {
GeneratePortalTexture () ist eine Funktion, die bei Bedarf aufgerufen wird, wenn Sie eine neue Renderziel-Textur für Portale erstellen müssen. Dies geschieht in der Initialisierungsfunktion, kann aber auch während des Portal Manager-Upgrades aufgerufen werden. Aus diesem Grund verfügt diese Funktion über eine interne Überprüfung zum Ändern der Auflösung des Ansichtsfensters. Ist dies nicht der Fall, wird das Update nicht durchgeführt.In meinem Fall habe ich eine Wrapper-Klasse für UCanvasRenderTarget2D erstellt. Ich habe es ExedreScriptedTexture genannt, es ist eine Komponente, die an einen Schauspieler angehängt werden kann. Ich habe diese Klasse erstellt, um Renderziele bequem mit Akteuren zu verwalten, die Renderaufgaben haben. Er führt die ordnungsgemäße Initialisierung des Renderziels durch und ist mit meinem eigenen UI-System kompatibel. Im Kontext von Portalen ist eine reguläre RenderTarget2D-Textur jedoch mehr als ausreichend. Daher können Sie es einfach verwenden. 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);
Wie oben erwähnt, habe ich meine eigene Klasse erstellt, daher müssen die hier festgelegten Eigenschaften an das übliche Renderziel angepasst werden.Es ist wichtig zu verstehen, wo die Aufnahme angezeigt wird. Da das Renderziel im Spiel angezeigt wird, bedeutet dies, dass dies vor der gesamten Nachbearbeitung geschieht. Daher müssen wir die Szene mit genügend Informationen rendern (um Werte über 1 zu speichern, um Bloom zu erstellen). Aus diesem Grund habe ich das RGBA16-Format gewählt (beachten Sie, dass es eine eigene Aufzählung hat, Sie müssen stattdessen ETextureRenderTargetFormat verwenden).Weitere Informationen finden Sie in den folgenden Quellen:
Weiter werden wir Update-Funktionen betrachten. Die Grundfunktion ist recht einfach und komplexer. Vor dem Aufrufen der Funktion GeneratePortalTexture () tritt eine Verzögerung auf, um zu vermeiden, dass das Renderziel beim Ändern der Größe des Ansichtsfensters neu erstellt wird (z. B. im Editor). Während der Veröffentlichung des Spiels kann diese Verzögerung beseitigt werden. void AExedrePortalManager::Update( float DeltaTime ) {
Wir rufen UpdatePortalsInWorld () auf, um alle in der aktuellen Welt vorhandenen Portale (einschließlich aller geladenen Ebenen) zu finden und zu aktualisieren. Die Funktion bestimmt auch, welche "aktiv" ist, d.h. für den Spieler sichtbar. Wenn wir ein aktives Portal finden, rufen wir UpdateCapture () auf , das die SceneCapture-Komponente steuert.
So funktioniert das Welt-Update in UpdatePortalsInWorld () :- ( )
- iterator ,
- , , ClearRTT() , . (, ).
- , , , , .
Die Überprüfung, die die Richtigkeit des Portals feststellt, ist einfach: Wir geben dem Portal, das dem Spieler am nächsten liegt, Priorität, da er aus seiner Sicht höchstwahrscheinlich am sichtbarsten ist. Um Verwandte, aber zum Beispiel Portale, die sich hinter dem Player befinden, fallen zu lassen, sind komplexere Überprüfungen erforderlich, aber ich wollte mich in meinem Tutorial nicht darauf konzentrieren, da dies ziemlich schwierig werden kann. AExedrePortal* AExedrePortalManager::UpdatePortalsInWorld() { if( ControllerOwner == nullptr ) { return nullptr; } AExedreCharacter* Character = ControllerOwner->GetCharacter();
Es ist Zeit, die UpdateCapture () - Funktion in Betracht zu ziehen .Dies ist eine Upgrade-Funktion, die die andere Seite des Portals erfasst. Aus den Kommentaren sollte alles klar sein, aber hier ist eine kurze Beschreibung:- Wir erhalten Links zu Character und Player Controller.
- Wir prüfen, ob alles korrekt ist (Portal, SceneCapture-Komponente, Player).
- Camera Target .
- , SceneCapture.
- SceneCapture Target.
- , SceneCapure , , .
- Render Target SceneCapture, .
- PlayerController.
- , Capture SceneCapture .
Wie wir sehen können, ist beim Teleportieren eines Spielers die korrekte Umwandlung der Position und Drehung des Portals in den lokalen Zielraum ein Schlüsselelement des natürlichen und fehlerfreien Verhaltens von SceneCapture.Die Definition von ConvertLocationToActorSpace () finden Sie unter „Teleportieren eines Akteurs“.
void AExedrePortalManager::UpdateCapture( AExedrePortal* Portal ) { if( ControllerOwner == nullptr ) { return; } AExedreCharacter* Character = ControllerOwner->GetCharacter();
Die Funktion GetCameraProjectionMatrix () ist in der PlayerController-Klasse standardmäßig nicht vorhanden. Ich habe sie selbst hinzugefügt. Es wird unten gezeigt: FMatrix AExedrePlayerController::GetCameraProjectionMatrix() { FMatrix ProjectionMatrix; if( GetLocalPlayer() != nullptr ) { FSceneViewProjectionData PlayerProjectionData; GetLocalPlayer()->GetProjectionData( GetLocalPlayer()->ViewportClient->Viewport, EStereoscopicPass::eSSP_FULL, PlayerProjectionData ); ProjectionMatrix = PlayerProjectionData.ProjectionMatrix; } return ProjectionMatrix; }
Schließlich müssen wir den Aufruf der Teleport-Funktion implementieren. Der Grund für die teilweise Verarbeitung der Teleportation über den Portal-Manager besteht darin, dass die Aktualisierung der erforderlichen Portale gewährleistet werden muss, da nur der Manager Informationen zu allen Portalen in der Szene hat.Wenn wir zwei verbundene Portale haben, müssen wir beim Wechsel von einem zum anderen beide in einem Tick aktualisieren. Andernfalls teleportiert sich der Spieler und befindet sich auf der anderen Seite des Portals, aber das Zielportal ist erst beim nächsten Frame / Takt aktiv. Dadurch entstehen visuelle Lücken mit dem versetzten Material des ebenen Netzes, das wir oben gesehen haben. void AExedrePortalManager::RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ) { if( Portal != nullptr && TargetToTeleport != nullptr ) { Portal->TeleportActor( TargetToTeleport );
Nun, das ist es, wir sind endlich fertig mit Portal Manager!Beenden Sie die Blaupause
Nach Abschluss des Portal-Managers müssen wir nur den Portal-Akteur selbst fertigstellen. Danach funktioniert das System. Das einzige, was hier fehlt, sind die Tick-Funktionen:So funktioniert es:- Wir aktualisieren das Material so, dass es nicht in einem aktiven Zustand bleibt.
- Wenn das Portal derzeit inaktiv ist , wird der Rest der Maßnahme verworfen.
- Wir bekommen die Zeichenklasse die für den Zugriff auf die Kamera die Lage .
- Der erste Teil prüft, ob sich die Kamera im Kollisionsfeld des Portals befindet. Wenn ja, versetzen wir das Portalnetz mit seinem Material .
- Der zweite Teil besteht darin, die Position innerhalb der Kollisionsbox erneut zu überprüfen. Wenn es ausgeführt wird, rufen wir eine Funktion auf, die prüft, ob wir das Portal überqueren .
- , Portal manager, Teleport .
Im Screenshot meines Diagramms sehen Sie zwei interessante Punkte: Is Point Inside Box und Get Portal Manager . Ich habe diese beiden Funktionen noch nicht erklärt. Dies sind statische Funktionen, die ich in meiner eigenen Klasse definiert habe, damit Sie sie von überall aus aufrufen können. Dies ist eine Art Helferklasse. Der Code dieser Funktionen wird unten angezeigt. Sie können selbst entscheiden, wo sie eingefügt werden sollen. Wenn Sie sie nicht außerhalb des Portalsystems benötigen, können Sie sie direkt in die Portal-Akteurklasse einfügen.Zuerst wollte ich das Kollisionssystem verwenden, um den Portal-Akteur in der Kollisionsbox zu finden, aber es schien mir nicht zuverlässig genug zu sein. Außerdem scheint mir diese Methode schneller anzuwenden zu sein und hat einen Vorteil: Sie berücksichtigt die Rotation des Schauspielers. bool IsPointInsideBox( FVector Point, UBoxComponent* Box ) { if( Box != nullptr ) {
AExedrePortalManager* GetPortalManager( AActor* Context ) { AExedrePortalManager* Manager = nullptr;
Der letzte Teil des Blueprint-Darstellers ist ForceTick . Denken Sie daran, dass Force Tick aufgerufen wird, wenn ein Spieler ein Portal überquert und sich neben einem anderen Portal befindet, für das Portal Manager ein Update erzwingt. Da wir gerade teleportiert haben, ist es nicht erforderlich, denselben Code zu verwenden, und Sie können die vereinfachte Version verwenden:Der Prozess beginnt ungefähr zur gleichen Zeit wie die Tick-Funktion, aber wir führen nur den ersten Teil der Sequenz aus, der das Material aktualisiert.Sind wir fertig?
Fast.Wenn wir das Portalsystem in dieser Form implementieren, werden wir höchstwahrscheinlich auf das folgende Problem stoßen:Was ist hier los?In diesem GIF ist die Framerate des Spiels auf 6 FPS begrenzt, um das Problem deutlicher darzustellen. In einem Frame verschwindet der Cube, weil das Unreal Engine- Clipping-System ihn als unsichtbar betrachtet.Dies liegt daran, dass die Ermittlung im aktuellen Frame durchgeführt und dann im nächsten verwendet wird. Dies erzeugt eine Verzögerung von einem Frame . Dies kann normalerweise behoben werden, indem der Begrenzungsrahmen des Objekts so erweitert wird, dass es registriert wird, bevor es sichtbar wird. Dies wird hier jedoch nicht funktionieren, da wir uns beim Überqueren des Portals von einem Ort zu einem völlig anderen teleportieren.Das Deaktivieren des Clipping-Systems ist ebenfalls nicht möglich, insbesondere weil dies bei Ebenen mit vielen Objekten die Leistung beeinträchtigt. Außerdem habe ich viele Teams der Unreal-Engine ausprobiert, aber keine positiven Ergebnisse erzielt: In allen Fällen blieb eine Verzögerung von einem Frame bestehen. Glücklicherweise konnte ich nach einer detaillierten Untersuchung des Quellcodes von Unreal Engine eine Lösung finden (der Weg war lang - es dauerte mehr als eine Woche)!Wie bei der SceneCapture-Komponente können Sie der Kamera des Players mitteilen, dass wir einen Sprungschnitt vorgenommen haben- Die Kameraposition ist zwischen zwei Bildern gesprungen, was bedeutet, dass wir uns nicht auf die Informationen des vorherigen Bildes verlassen können. Dieses Verhalten kann bei Verwendung von Matinee oder Sequencer beobachtet werden, z. B. beim Wechseln der Kamera: Bewegungsunschärfe oder Glättung können nicht auf Informationen aus dem vorherigen Frame beruhen.Dazu müssen wir zwei Aspekte berücksichtigen:- LocalPlayer : Diese Klasse verarbeitet verschiedene Informationen (z. B. das Ansichtsfenster des Players) und ist dem PlayerController zugeordnet. Hier können wir den Renderprozess der Kamera des Players beeinflussen.
- PlayerController : Wenn sich ein Spieler teleportiert, beginnt diese Klasse dank des Zugriffs auf LocalPlayer zu spleißen.
Der große Vorteil dieser Lösung besteht darin, dass der Eingriff in den Rendering-Prozess der Engine bei zukünftigen Updates von Unreal Engine minimal und einfach zu warten ist.
Beginnen wir mit der Erstellung einer neuen Klasse, die von LocalPlayer geerbt wurde. Im Folgenden finden Sie eine Überschrift, in der zwei Hauptkomponenten aufgeführt sind: Neudefinition der Berechnungen des Szenenansichtsfensters und eine neue Funktion zum Aufrufen des Kameraklebens. #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; };
So wird alles implementiert: #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 () startet Camera Cut nur mit einem booleschen Wert. Wenn die Engine die Funktion CalcSceneView () aufruft , führen wir zuerst die ursprüngliche Funktion aus. Dann prüfen wir, ob wir kleben müssen. In diesem Fall definieren wir die boolesche Variable "Camera Cut" innerhalb der FSceneView- Struktur neu , die vom Engine-Rendering-Prozess verwendet wird, und setzen dann die boolesche Variable zurück (verwenden Sie sie).
Auf der Player Controller-Seite sind die Änderungen minimal. Sie müssen dem Header eine Variable hinzufügen, um einen Link zur nativen LocalPlayer-Klasse zu speichern: UPROPERTY() UExedreLocalPlayer* LocalPlayer;
Dann in der BeginPlay () Funktion : LocalPlayer = Cast<UExedreLocalPlayer>( GetLocalPlayer() );
Ich habe auch eine Funktion hinzugefügt, um Cut schnell zu starten: void AExedrePlayerController::PerformCameraCut() { if( LocalPlayer != nullptr ) { LocalPlayer->PerformCameraCut(); } }
Schließlich können wir in der Portal Manager-Funktion RequestTeleportByPortal () während der Camera Cut-Teleportation Folgendes ausführen: void AExedrePortalManager::RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ) { if( Portal != nullptr && TargetToTeleport != nullptr ) { if( ControllerOwner != nullptr ) { ControllerOwner->PerformCameraCut(); } [...]
Und das ist alles!Camera Cut muss aufgerufen werden, bevor SceneCapture aktualisiert wird. Deshalb befindet es sich am Anfang der Funktion.Endergebnis
Jetzt haben wir gelernt, in Portalen zu denken.Wenn das System gut funktioniert, sollten wir in der Lage sein, folgende Dinge zu erstellen:Wenn Sie Probleme haben, überprüfen Sie Folgendes:- Stellen Sie sicher, dass Portal Manager ordnungsgemäß erstellt und initialisiert wurde.
- Das Renderziel wird korrekt erstellt (Sie können das im Inhaltsbrowser erstellte Ziel zum Starten verwenden).
- Portale sind korrekt aktiviert und deaktiviert.
- Bei Portalen ist der Zieldarsteller im Editor richtig eingestellt.
Fragen und Antworten
Die beliebtesten Fragen, die mir zu diesem Tutorial gestellt wurden:Ist es möglich, dies auf Blunts und nicht über C ++ zu implementieren?Der Großteil des Codes kann mit Ausnahme von zwei Aspekten in Blunts implementiert werden:- Die zum Abrufen der Projektionsmatrix verwendete LocalPlayer- Funktion GetProjectionData () ist in Blaupausen nicht verfügbar.
- Die LocalPlayer CalcSceneView () -Funktion , die für die Lösung des Clipping-Systemproblems von entscheidender Bedeutung ist, ist in Blaupausen nicht verfügbar.
Daher müssen Sie entweder eine C ++ - Implementierung verwenden, um auf diese beiden Funktionen zuzugreifen, oder den Quellcode der Engine ändern, um sie über Blaupausen zugänglich zu machen.Kann ich dieses System in VR verwenden?Ja, zum größten Teil. Einige Teile müssen jedoch angepasst werden, zum Beispiel:- Sie müssen zwei Renderziele verwenden (eines für jedes Auge) und diese im Portalmaterial maskieren, um sie nebeneinander im Bildschirmbereich anzuzeigen. Jedes Renderziel sollte halb so breit sein wie die Auflösung des VR-Geräts.
- Sie müssen zwei SceneCapture verwenden, um das Ziel mit dem richtigen Abstand (dem Abstand zwischen den Augen) zu rendern und stereoskopische Effekte zu erzielen.
Das Hauptproblem wird die Leistung sein, da die andere Seite des Portals zweimal gerendert werden muss.Kann ein anderes Objekt das Portal überqueren?Mein Code enthält keine. Es ist jedoch nicht so schwierig, es allgemeiner zu gestalten. Dazu muss das Portal weitere Informationen zu allen Objekten in der Nähe verfolgen, um zu überprüfen, ob sie diese überqueren.Unterstützt das System die Rekursion (Portal innerhalb des Portals)?Dieses Tutorial ist nicht. Für die Rekursion benötigen Sie zusätzliches Renderziel und SceneCapture. Außerdem muss festgelegt werden, welches RenderTarget zuerst gerendert werden soll, und so weiter. Das ist ziemlich schwierig und ich wollte das nicht, weil dies für mein Projekt nicht notwendig ist.Kann ich das Portal in der Nähe der Wand überqueren?Leider gibt es keine.
Ich sehe jedoch zwei Möglichkeiten, dies (theoretisch) umzusetzen:- , . , .
- , , . . , , . . , , , , level streaming .